feat: Passwordless cross-device authentication

- Arkitektur: docs/auth/passwordless-architecture.md
- Backend: iom/quixzoom-auth-service/ (FastAPI + Redis)
- Webb: quixzoom-market-pages/se/login/ (QR-kod + polling)
- App: iom/quixzoom-app/src/features/auth/ (push + deep links)

Flöde: QR-kod → app-godkännande → webb-inloggad
This commit is contained in:
Bernt
2026-07-07 07:11:50 +00:00
parent 4aa984ad74
commit 6989a98d75
61843 changed files with 5491611 additions and 872231 deletions
+213 -213
View File
@@ -32,7 +32,7 @@
.nav-logo { font-weight: 700; font-size: 18px; color: #6366f1; letter-spacing: -0.5px; }
.nav-links { display: flex; gap: 4px; }
.nav-link {
padding: 6px 14px; border-radius: 8px; color: #94a3b8;
padding: 6px 14px; border-radius: var(--radius-md); color: #94a3b8;
font-size: 13px; font-weight: 500; cursor: pointer;
transition: all 0.15s; background: none; border: none;
}
@@ -40,7 +40,7 @@
.nav-link.active { background: #1e2737; color: #6366f1; }
.nav-spacer { flex: 1; }
.nav-status { display: flex; align-items: center; gap: 8px; font-size: 12px; color: #64748b; }
.status-dot { width: 8px; height: 8px; border-radius: 50%; background: #22c55e; }
.status-dot { width: 8px; height: 8px; border-radius: var(--radius-full); background: #22c55e; }
.main-container { max-width: 1400px; margin: 0 auto; padding: 24px 24px 48px; }
.page-header { margin-bottom: 24px; }
@@ -49,7 +49,7 @@
.company-tabs { display: flex; gap: 4px; margin-bottom: 24px; flex-wrap: wrap; }
.company-tab {
padding: 8px 16px; border-radius: 10px; border: 1px solid #1e2737;
padding: 8px 16px; border-radius: var(--radius-md); border: 1px solid #1e2737;
background: #0d1117; color: #94a3b8; font-size: 13px; font-weight: 500;
cursor: pointer; transition: all 0.15s;
}
@@ -70,7 +70,7 @@
.tab-panel { display: none; }
.tab-panel.visible { display: block; }
.card { background: #0d1117; border: 1px solid #1e2737; border-radius: 12px; padding: 24px; }
.card { background: #0d1117; border: 1px solid #1e2737; border-radius: var(--radius-md); padding: 24px; }
.card + .card { margin-top: 16px; }
.card-title { font-size: 15px; font-weight: 600; color: #f1f5f9; margin-bottom: 16px; }
@@ -79,14 +79,14 @@
.form-group label { font-size: 12px; font-weight: 500; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.5px; }
.form-group input, .form-group select, .form-group textarea {
padding: 9px 12px; background: #030712; border: 1px solid #1e2737;
border-radius: 8px; color: #e2e8f0; font-size: 13px; width: 100%;
border-radius: var(--radius-md); color: #e2e8f0; font-size: 13px; width: 100%;
transition: border-color 0.15s; outline: none;
}
.form-group input:focus, .form-group select:focus, .form-group textarea:focus { border-color: #6366f1; }
.form-group input::placeholder { color: #374151; }
.btn {
padding: 9px 18px; border-radius: 8px; border: none; font-size: 13px;
padding: 9px 18px; border-radius: var(--radius-md); border: none; font-size: 13px;
font-weight: 600; cursor: pointer; transition: all 0.15s;
display: inline-flex; align-items: center; gap: 6px;
}
@@ -100,14 +100,14 @@
.btn-danger:hover { background: #b91c1c; }
.btn-warning { background: #d97706; color: #fff; }
.btn-warning:hover { background: #b45309; }
.btn-ghost { background: none; border: none; color: #64748b; cursor: pointer; padding: 4px 8px; border-radius: 6px; font-size: 13px; transition: all 0.15s; }
.btn-ghost { background: none; border: none; color: #64748b; cursor: pointer; padding: 4px 8px; border-radius: var(--radius-sm); font-size: 13px; transition: all 0.15s; }
.btn-ghost:hover { background: #1e2737; color: #e2e8f0; }
.btn-sm { padding: 5px 12px; font-size: 12px; }
.btn-teal { background: #0d9488; color: #fff; }
.btn-teal:hover { background: #0f766e; }
.filter-row { display: flex; gap: 8px; align-items: center; margin-bottom: 16px; flex-wrap: wrap; }
.filter-btn { padding: 6px 14px; border-radius: 20px; border: 1px solid #1e2737; background: none; color: #64748b; font-size: 12px; cursor: pointer; transition: all 0.15s; }
.filter-btn { padding: 6px 14px; border-radius: var(--radius-xl); border: 1px solid #1e2737; background: none; color: #64748b; font-size: 12px; cursor: pointer; transition: all 0.15s; }
.filter-btn:hover { border-color: #6366f1; color: #818cf8; }
.filter-btn.active { background: #1e1f4e; border-color: #6366f1; color: #818cf8; }
.refresh-info { margin-left: auto; font-size: 12px; color: #475569; }
@@ -119,7 +119,7 @@
justify-content: space-between;
background: #0f2818;
border: 1px solid #166534;
border-radius: 10px;
border-radius: var(--radius-md);
padding: 12px 18px;
margin-bottom: 16px;
gap: 12px;
@@ -149,7 +149,7 @@
/* Larger, clearer badges */
.badge {
display: inline-flex; align-items: center; gap: 4px;
padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 700;
padding: 4px 12px; border-radius: var(--radius-xl); font-size: 12px; font-weight: 700;
white-space: nowrap; letter-spacing: 0.2px;
}
.badge-running { background: #1e3a5f; color: #60a5fa; border: 1px solid #1d4ed8; }
@@ -166,7 +166,7 @@
.badge-timeout { background: #1c1917; color: #78716c; border: 1px solid #44403c; }
.screenshot-thumb {
max-width: 80px; max-height: 60px; border-radius: 6px; object-fit: cover;
max-width: 80px; max-height: 60px; border-radius: var(--radius-sm); object-fit: cover;
border: 1px solid #1e2737; cursor: pointer;
transition: transform 0.15s, border-color 0.15s;
}
@@ -175,26 +175,26 @@
/* Inline credentials for PENDING_MANUAL / MANUAL_REQUIRED */
.inline-creds {
background: #0a1628; border: 1px solid #1e3a5f; border-radius: 8px;
background: #0a1628; border: 1px solid #1e3a5f; border-radius: var(--radius-md);
padding: 10px 12px; margin-top: 8px; display: flex; flex-direction: column; gap: 6px;
}
.inline-creds-title { font-size: 11px; font-weight: 700; color: #60a5fa; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px; }
.cred-inline-row { display: flex; align-items: center; gap: 6px; font-size: 12px; flex-wrap: wrap; }
.cred-inline-label { color: #475569; min-width: 70px; }
.cred-inline-val { font-family: 'Monaco','Menlo',monospace; font-size: 12px; color: #cbd5e1; }
.pw-field { background: #030712; border: 1px solid #1e2737; border-radius: 4px; padding: 3px 8px; color: #fbbf24; font-family: monospace; font-size: 12px; width: 160px; }
.copy-btn { background: #6366f1; color: #fff; border: none; border-radius: 4px; padding: 3px 10px; cursor: pointer; font-size: 11px; font-weight: 600; transition: background 0.15s; }
.pw-field { background: #030712; border: 1px solid #1e2737; border-radius: var(--radius-sm); padding: 3px 8px; color: #fbbf24; font-family: monospace; font-size: 12px; width: 160px; }
.copy-btn { background: #6366f1; color: #fff; border: none; border-radius: var(--radius-sm); padding: 3px 10px; cursor: pointer; font-size: 11px; font-weight: 600; transition: background 0.15s; }
.copy-btn:hover { background: #4f46e5; }
.reveal-btn { background: #1e2737; color: #94a3b8; border: 1px solid #2d3748; border-radius: 4px; padding: 3px 8px; cursor: pointer; font-size: 11px; transition: all 0.15s; }
.reveal-btn { background: #1e2737; color: #94a3b8; border: 1px solid #2d3748; border-radius: var(--radius-sm); padding: 3px 8px; cursor: pointer; font-size: 11px; transition: all 0.15s; }
.reveal-btn:hover { background: #2d3748; color: #e2e8f0; }
/* Fetch-mail-code */
.mail-code-box {
background: #0a1628; border: 1px solid #1e3a5f; border-radius: 8px;
background: #0a1628; border: 1px solid #1e3a5f; border-radius: var(--radius-md);
padding: 10px 12px; margin-top: 8px;
}
.mail-code-result {
display: none; background: #1e293b; border-radius: 6px; padding: 8px 12px;
display: none; background: #1e293b; border-radius: var(--radius-sm); padding: 8px 12px;
margin-top: 8px; font-size: 14px; font-weight: 700; color: #6ee7b7; letter-spacing: 2px;
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
}
@@ -204,7 +204,7 @@
.creds-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; flex-wrap: wrap; gap: 8px; }
.creds-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.creds-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 16px; }
.cred-card { background: #030712; border: 1px solid #1e2737; border-radius: 10px; padding: 16px; }
.cred-card { background: #030712; border: 1px solid #1e2737; border-radius: var(--radius-md); padding: 16px; }
.cred-platform { font-size: 14px; font-weight: 700; color: #f1f5f9; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
.cred-rows { display: flex; flex-direction: column; gap: 8px; }
.cred-row { display: flex; justify-content: space-between; align-items: center; gap: 8px; font-size: 12px; }
@@ -212,7 +212,7 @@
.cred-value { color: #cbd5e1; font-family: 'Monaco', 'Menlo', monospace; font-size: 12px; word-break: break-all; flex: 1; text-align: right; }
.pw-hidden { color: #e2e8f0; }
.security-banner {
background: #1a1f2e; border: 1px solid #2d3748; border-radius: 8px;
background: #1a1f2e; border: 1px solid #2d3748; border-radius: var(--radius-md);
padding: 12px 16px; margin-bottom: 16px; display: flex; align-items: center; gap: 10px;
font-size: 12px; color: #94a3b8;
}
@@ -225,7 +225,7 @@
.platform-checkbox { display: flex; align-items: center; gap: 6px; }
.platform-checkbox input { width: auto; }
.platform-checkbox label { font-size: 13px; color: #cbd5e1; text-transform: none; font-weight: 400; letter-spacing: 0; }
.post-preview { background: #030712; border: 1px solid #1e2737; border-radius: 10px; padding: 16px; }
.post-preview { background: #030712; border: 1px solid #1e2737; border-radius: var(--radius-md); padding: 16px; }
.preview-platform { font-size: 12px; font-weight: 600; color: #6366f1; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }
.preview-content { font-size: 13px; color: #cbd5e1; white-space: pre-wrap; word-break: break-word; }
.post-queue-table { width: 100%; border-collapse: collapse; margin-top: 16px; }
@@ -237,7 +237,7 @@
.char-counter.over { color: #ef4444; }
.schedule-row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin-top: 8px; }
.schedule-toggle { display: flex; gap: 8px; }
.sched-btn { padding: 6px 14px; border-radius: 8px; border: 1px solid #1e2737; background: none; color: #64748b; font-size: 12px; cursor: pointer; transition: all 0.15s; }
.sched-btn { padding: 6px 14px; border-radius: var(--radius-md); border: 1px solid #1e2737; background: none; color: #64748b; font-size: 12px; cursor: pointer; transition: all 0.15s; }
.sched-btn.active { background: #1e1f4e; border-color: #6366f1; color: #818cf8; }
/* Action required column */
@@ -248,11 +248,11 @@
.state-box { text-align: center; padding: 48px 24px; color: #475569; font-size: 14px; }
.state-box .icon { font-size: 36px; margin-bottom: 12px; }
.state-box p { margin-top: 4px; font-size: 13px; }
.spinner { display: inline-block; width: 24px; height: 24px; border: 3px solid #1e2737; border-top-color: #6366f1; border-radius: 50%; animation: spin 0.8s linear infinite; margin-bottom: 12px; }
.spinner { display: inline-block; width: 24px; height: 24px; border: 3px solid #1e2737; border-top-color: #6366f1; border-radius: var(--radius-full); animation: spin 0.8s linear infinite; margin-bottom: 12px; }
@keyframes spin { to { transform: rotate(360deg); } }
.toast-container { position: fixed; bottom: 24px; right: 24px; display: flex; flex-direction: column; gap: 8px; z-index: 999; }
.toast { padding: 10px 16px; border-radius: 10px; font-size: 13px; font-weight: 500; max-width: 320px; animation: slideIn 0.2s ease; }
.toast { padding: 10px 16px; border-radius: var(--radius-md); font-size: 13px; font-weight: 500; max-width: 320px; animation: slideIn 0.2s ease; }
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: none; opacity: 1; } }
.toast-success { background: #14532d; color: #4ade80; border: 1px solid #16a34a; }
.toast-error { background: #450a0a; color: #f87171; border: 1px solid #dc2626; }
@@ -262,12 +262,12 @@
/* Modal */
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 500; display: none; align-items: center; justify-content: center; }
.modal-overlay.visible { display: flex; }
.modal { background: #0d1117; border: 1px solid #1e2737; border-radius: 16px; padding: 32px; max-width: 480px; width: 90%; }
.modal { background: #0d1117; border: 1px solid #1e2737; border-radius: var(--radius-lg); padding: 32px; max-width: 480px; width: 90%; }
.modal h3 { font-size: 18px; font-weight: 700; color: #f1f5f9; margin-bottom: 12px; }
.modal p { color: #94a3b8; font-size: 14px; margin-bottom: 24px; }
.modal-actions { display: flex; gap: 12px; justify-content: flex-end; }
.rotation-log { background: #030712; border: 1px solid #1e2737; border-radius: 8px; padding: 16px; margin-top: 16px; max-height: 300px; overflow-y: auto; }
.rotation-log { background: #030712; border: 1px solid #1e2737; border-radius: var(--radius-md); padding: 16px; margin-top: 16px; max-height: 300px; overflow-y: auto; }
.rotation-log-item { font-size: 12px; color: #64748b; padding: 6px 0; border-bottom: 1px solid #0f172a; display: flex; justify-content: space-between; gap: 8px; }
.rotation-log-item:last-child { border-bottom: none; }
</style>
@@ -275,7 +275,7 @@
<body>
<nav class="top-nav">
<div class="nav-logo">🦅 ARGUS Social</div>
<div class="nav-logo"> ARGUS Social</div>
<div class="nav-links">
<button class="nav-link active">Admin</button>
</div>
@@ -291,11 +291,11 @@
<!-- Rotate Confirm Modal -->
<div class="modal-overlay" id="rotateModal">
<div class="modal">
<h3>🔄 Rotera lösenord</h3>
<h3><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 8C2 11.3 4.7 14 8 14C10.5 14 12.7 12.5 13.7 10.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M14 8C14 4.7 11.3 2 8 2C5.5 2 3.3 3.5 2.3 5.7" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M13.5 7.5L14 10L11.5 9.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.5 8.5L2 6L4.5 6.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> Rotera lösenord</h3>
<p id="rotateModalText">Generera nya starka lösenord för alla plattformar? Detta kan inte ångras.</p>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="closeRotateModal()">Avbryt</button>
<button class="btn btn-warning" id="rotateConfirmBtn" onclick="confirmRotate()">🔄 Rotera nu</button>
<button class="btn btn-warning" id="rotateConfirmBtn" onclick="confirmRotate()"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 8C2 11.3 4.7 14 8 14C10.5 14 12.7 12.5 13.7 10.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M14 8C14 4.7 11.3 2 8 2C5.5 2 3.3 3.5 2.3 5.7" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M13.5 7.5L14 10L11.5 9.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.5 8.5L2 6L4.5 6.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> Rotera nu</button>
</div>
</div>
</div>
@@ -303,11 +303,11 @@
<!-- Employee Offboard Modal -->
<div class="modal-overlay" id="offboardModal">
<div class="modal">
<h3>⚠️ Anställd slutar — Lösenordsrotation</h3>
<h3><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Anställd slutar — Lösenordsrotation</h3>
<p>Detta roterar ALLA lösenord för ALLA bolag och ALLA plattformar. Använd vid anställds avslut.</p>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="document.getElementById('offboardModal').classList.remove('visible')">Avbryt</button>
<button class="btn btn-danger" onclick="confirmOffboard()">⚠️ Rotera ALLA nu</button>
<button class="btn btn-danger" onclick="confirmOffboard()"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Rotera ALLA nu</button>
</div>
</div>
</div>
@@ -323,23 +323,23 @@
<!-- Company Tabs -->
<div class="company-tabs">
<button class="company-tab active" data-company="landvex" onclick="selectCompany('landvex')">🏢 LandveX</button>
<button class="company-tab" data-company="quixzoom" onclick="selectCompany('quixzoom')"> QuiXzoom</button>
<button class="company-tab" data-company="apifly" onclick="selectCompany('apifly')">🚀 ApiFly</button>
<button class="company-tab" data-company="corpfitt" onclick="selectCompany('corpfitt')">💪 CorpFitt</button>
<button class="company-tab" data-company="vyra" onclick="selectCompany('vyra')"> VYRA</button>
<button class="company-tab" data-company="aamos" onclick="selectCompany('aamos')">🧠 AAMOS</button>
<button class="company-tab active" data-company="landvex" onclick="selectCompany('landvex')"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#666" stroke-width="1.5"/><path d="M6 5H7M6 8H7M6 11H7M9 5H10M9 8H10M9 11H10" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg> LandveX</button>
<button class="company-tab" data-company="quixzoom" onclick="selectCompany('quixzoom')"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg> QuiXzoom</button>
<button class="company-tab" data-company="apifly" onclick="selectCompany('apifly')"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg> ApiFly</button>
<button class="company-tab" data-company="corpfitt" onclick="selectCompany('corpfitt')"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M5 7C5 7 3 8 3 10C3 12 5 13 7 13H11C13 13 14 11 14 9C14 7 12 6 11 6H9V4C9 3 8 2 7 2C6 2 5 3 5 4V7Z" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/></svg> CorpFitt</button>
<button class="company-tab" data-company="vyra" onclick="selectCompany('vyra')"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V4M8 12V14M2 8H4M12 8H14" stroke="#f59e0b" stroke-width="1.5" stroke-linecap="round"/><circle cx="8" cy="8" r="2" fill="#f59e0b"/></svg> VYRA</button>
<button class="company-tab" data-company="aamos" onclick="selectCompany('aamos')"> AAMOS</button>
</div>
<!-- Main Tabs -->
<div class="main-tabs">
<button class="main-tab active" data-tab="register" onclick="switchTab('register')">📝 Registrera</button>
<button class="main-tab" data-tab="jobs" onclick="switchTab('jobs')">⚙️ Jobb</button>
<button class="main-tab" data-tab="credentials" onclick="switchTab('credentials')">🔐 Credentials</button>
<button class="main-tab" data-tab="publishing" onclick="switchTab('publishing')">📢 Publishing</button>
<button class="main-tab active" data-tab="register" onclick="switchTab('register')"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 13V10L10 3L13 6L6 13H3Z" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M9 4L12 7" stroke="#666" stroke-width="1.5"/></svg> Registrera</button>
<button class="main-tab" data-tab="jobs" onclick="switchTab('jobs')"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="3" stroke="#666" stroke-width="1.5"/><path d="M8 2V4M8 12V14M2 8H4M12 8H14M4.3 4.3L5.7 5.7M10.3 10.3L11.7 11.7M4.3 11.7L5.7 10.3M10.3 5.7L11.7 4.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg> Jobb</button>
<button class="main-tab" data-tab="credentials" onclick="switchTab('credentials')"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="7" width="10" height="7" rx="1" stroke="#666" stroke-width="1.5"/><path d="M5 7V5C5 3.3 6.3 2 8 2C9.7 2 11 3.3 11 5V7" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="10.5" r="1" fill="#666"/></svg> Credentials</button>
<button class="main-tab" data-tab="publishing" onclick="switchTab('publishing')"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 6V10H5L10 14V2L5 6H2Z" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M12 5C13 6 13 10 12 11" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg> Publishing</button>
</div>
<!-- ─── PANEL: Register ──────────────────────────────────── -->
<!-- PANEL: Register -->
<div id="panel-register" class="tab-panel visible">
<div class="card">
<div class="card-title">Registrera nytt konto</div>
@@ -347,12 +347,12 @@
<div class="form-group">
<label>Bolag</label>
<select id="regCompany" onchange="onRegCompanyChange()">
<option value="landvex">🏢 LandveX</option>
<option value="quixzoom"> QuiXzoom</option>
<option value="apifly">🚀 ApiFly</option>
<option value="corpfitt">💪 CorpFitt</option>
<option value="vyra"> VYRA</option>
<option value="aamos">🧠 AAMOS</option>
<option value="landvex"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#666" stroke-width="1.5"/><path d="M6 5H7M6 8H7M6 11H7M9 5H10M9 8H10M9 11H10" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg> LandveX</option>
<option value="quixzoom"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg> QuiXzoom</option>
<option value="apifly"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg> ApiFly</option>
<option value="corpfitt"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M5 7C5 7 3 8 3 10C3 12 5 13 7 13H11C13 13 14 11 14 9C14 7 12 6 11 6H9V4C9 3 8 2 7 2C6 2 5 3 5 4V7Z" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/></svg> CorpFitt</option>
<option value="vyra"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V4M8 12V14M2 8H4M12 8H14" stroke="#f59e0b" stroke-width="1.5" stroke-linecap="round"/><circle cx="8" cy="8" r="2" fill="#f59e0b"/></svg> VYRA</option>
<option value="aamos"> AAMOS</option>
</select>
</div>
<div class="form-group">
@@ -386,24 +386,24 @@
<input type="text" id="regUsername" placeholder="@bolag">
</div>
</div>
<div style="margin-top:12px; padding:10px 14px; background:#0a1628; border:1px solid #1e3a5f; border-radius:8px; font-size:12px; color:#60a5fa; display:flex; gap:8px; align-items:center;">
<span>🔒</span>
<div style="margin-top:12px; padding:10px 14px; background:#0a1628; border:1px solid #1e3a5f; border-radius: var(--radius-md); font-size:12px; color:#60a5fa; display:flex; gap:8px; align-items:center;">
<span></span>
<span>Lösenord genereras automatiskt av servern (32+ tecken). Aldrig synligt i UI.</span>
</div>
<div style="margin-top:20px; display:flex; gap:12px; flex-wrap:wrap;">
<button class="btn btn-primary" onclick="startRegistration()">🚀 Starta automation</button>
<button class="btn btn-secondary" onclick="clearRegForm()">🗑 Rensa</button>
<button class="btn btn-primary" onclick="startRegistration()"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg> Starta automation</button>
<button class="btn btn-secondary" onclick="clearRegForm()"> Rensa</button>
</div>
</div>
</div>
<!-- ─── PANEL: Jobs ──────────────────────────────────────── -->
<!-- PANEL: Jobs -->
<div id="panel-jobs" class="tab-panel">
<div class="card">
<!-- Approve-all banner (only shown when awaiting jobs exist) -->
<div class="approve-all-banner" id="approveAllBanner">
<span class="approve-all-banner-text" id="approveAllText">⏳ Jobb väntar på godkännande</span>
<button class="btn btn-success btn-sm" onclick="approveAll()"> Godkänn alla</button>
<button class="btn btn-success btn-sm" onclick="approveAll()"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Godkänn alla</button>
</div>
<div class="filter-row">
@@ -412,8 +412,8 @@
<button class="filter-btn" data-filter="done" onclick="setJobFilter('done')">Färdiga</button>
<button class="filter-btn" data-filter="error" onclick="setJobFilter('error')">Fel</button>
<button class="filter-btn" data-filter="awaiting" onclick="setJobFilter('awaiting')">⏳ Väntar</button>
<button class="filter-btn" data-filter="manual" onclick="setJobFilter('manual')">🖐 Manuell</button>
<button class="btn btn-secondary btn-sm" onclick="loadJobs()" style="margin-left:4px;">🔄 Ladda om</button>
<button class="filter-btn" data-filter="manual" onclick="setJobFilter('manual')"> Manuell</button>
<button class="btn btn-secondary btn-sm" onclick="loadJobs()" style="margin-left:4px;"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 8C2 11.3 4.7 14 8 14C10.5 14 12.7 12.5 13.7 10.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M14 8C14 4.7 11.3 2 8 2C5.5 2 3.3 3.5 2.3 5.7" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M13.5 7.5L14 10L11.5 9.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.5 8.5L2 6L4.5 6.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> Ladda om</button>
<span class="refresh-info" id="refreshInfo"></span>
</div>
<div id="jobsContent">
@@ -422,19 +422,19 @@
</div>
</div>
<!-- ─── PANEL: Credentials ──────────────────────────────── -->
<!-- PANEL: Credentials -->
<div id="panel-credentials" class="tab-panel">
<div class="card">
<div class="creds-header">
<div class="card-title" style="margin-bottom:0">Credentials — <span id="credsCompanyLabel">LandveX</span></div>
<div class="creds-actions">
<button class="btn btn-warning btn-sm" onclick="openRotateModal()">🔄 Rotera lösenord</button>
<button class="btn btn-danger btn-sm" onclick="document.getElementById('offboardModal').classList.add('visible')">⚠️ Anställd slutar</button>
<button class="btn btn-secondary btn-sm" onclick="loadCredentials()">🔄 Ladda om</button>
<button class="btn btn-warning btn-sm" onclick="openRotateModal()"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 8C2 11.3 4.7 14 8 14C10.5 14 12.7 12.5 13.7 10.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M14 8C14 4.7 11.3 2 8 2C5.5 2 3.3 3.5 2.3 5.7" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M13.5 7.5L14 10L11.5 9.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.5 8.5L2 6L4.5 6.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> Rotera lösenord</button>
<button class="btn btn-danger btn-sm" onclick="document.getElementById('offboardModal').classList.add('visible')"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Anställd slutar</button>
<button class="btn btn-secondary btn-sm" onclick="loadCredentials()"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 8C2 11.3 4.7 14 8 14C10.5 14 12.7 12.5 13.7 10.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M14 8C14 4.7 11.3 2 8 2C5.5 2 3.3 3.5 2.3 5.7" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M13.5 7.5L14 10L11.5 9.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.5 8.5L2 6L4.5 6.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> Ladda om</button>
</div>
</div>
<div class="security-banner">
<span class="icon">🛡️</span>
<span class="icon"></span>
<span>Lösenord lagras krypterat (AES-256). Lösenord visas ALDRIG i klartext — använd knappen för att visa/dölja tillfälligt.</span>
</div>
<div id="credsContent">
@@ -444,22 +444,22 @@
</div>
</div>
<!-- ─── PANEL: Publishing ────────────────────────────────── -->
<!-- PANEL: Publishing -->
<div id="panel-publishing" class="tab-panel">
<!-- Compose -->
<div class="card">
<div class="card-title">📝 Skapa inlägg</div>
<div class="card-title"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 13V10L10 3L13 6L6 13H3Z" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M9 4L12 7" stroke="#666" stroke-width="1.5"/></svg> Skapa inlägg</div>
<div class="publish-grid">
<div>
<div class="form-group" style="margin-bottom:16px;">
<label>Bolag</label>
<select id="pubCompany" onchange="onPubCompanyChange()">
<option value="landvex">🏢 LandveX</option>
<option value="quixzoom"> QuiXzoom</option>
<option value="apifly">🚀 ApiFly</option>
<option value="corpfitt">💪 CorpFitt</option>
<option value="vyra"> VYRA</option>
<option value="aamos">🧠 AAMOS</option>
<option value="landvex"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#666" stroke-width="1.5"/><path d="M6 5H7M6 8H7M6 11H7M9 5H10M9 8H10M9 11H10" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg> LandveX</option>
<option value="quixzoom"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg> QuiXzoom</option>
<option value="apifly"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg> ApiFly</option>
<option value="corpfitt"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M5 7C5 7 3 8 3 10C3 12 5 13 7 13H11C13 13 14 11 14 9C14 7 12 6 11 6H9V4C9 3 8 2 7 2C6 2 5 3 5 4V7Z" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/></svg> CorpFitt</option>
<option value="vyra"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2V4M8 12V14M2 8H4M12 8H14" stroke="#f59e0b" stroke-width="1.5" stroke-linecap="round"/><circle cx="8" cy="8" r="2" fill="#f59e0b"/></svg> VYRA</option>
<option value="aamos"> AAMOS</option>
</select>
</div>
<div class="form-group" style="margin-bottom:16px;">
@@ -477,22 +477,22 @@
<label>Schemaläggning</label>
<div class="schedule-row">
<div class="schedule-toggle">
<button class="sched-btn active" id="schedNowBtn" onclick="setScheduleMode('now')"> Nu</button>
<button class="sched-btn" id="schedLaterBtn" onclick="setScheduleMode('later')">🕐 Schemalägg</button>
<button class="sched-btn active" id="schedNowBtn" onclick="setScheduleMode('now')"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg> Nu</button>
<button class="sched-btn" id="schedLaterBtn" onclick="setScheduleMode('later')"> Schemalägg</button>
</div>
<input type="datetime-local" id="pubScheduleTime" style="display:none; flex:1; background:#030712; border:1px solid #1e2737; border-radius:8px; color:#e2e8f0; padding:7px 12px; font-size:13px;" onchange="updatePreview()">
<input type="datetime-local" id="pubScheduleTime" style="display:none; flex:1; background:#030712; border:1px solid #1e2737; border-radius: var(--radius-md); color:#e2e8f0; padding:7px 12px; font-size:13px;" onchange="updatePreview()">
</div>
</div>
<div style="margin-top:20px; display:flex; gap:12px; flex-wrap:wrap;">
<button class="btn btn-primary" onclick="submitPost()">📤 Publicera</button>
<button class="btn btn-secondary" onclick="saveDraft()">💾 Spara utkast</button>
<button class="btn btn-ghost" onclick="clearPost()">🗑 Rensa</button>
<button class="btn btn-primary" onclick="submitPost()"> Publicera</button>
<button class="btn btn-secondary" onclick="saveDraft()"> Spara utkast</button>
<button class="btn btn-ghost" onclick="clearPost()"> Rensa</button>
</div>
</div>
<div>
<div style="font-size:12px; font-weight:600; color:#475569; text-transform:uppercase; letter-spacing:0.5px; margin-bottom:12px;">Förhandsvisning</div>
<div id="pubPreviews">
<div style="color:#475569; font-size:13px; padding:16px; text-align:center; border:1px dashed #1e2737; border-radius:10px;">Välj plattformar och skriv innehåll för att se förhandsvisning</div>
<div style="color:#475569; font-size:13px; padding:16px; text-align:center; border:1px dashed #1e2737; border-radius: var(--radius-md);">Välj plattformar och skriv innehåll för att se förhandsvisning</div>
</div>
</div>
</div>
@@ -501,8 +501,8 @@
<!-- Post Queue -->
<div class="card">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;">
<div class="card-title" style="margin-bottom:0">📋 Publiceringsköen</div>
<button class="btn btn-secondary btn-sm" onclick="loadPostQueue()">🔄 Ladda om</button>
<div class="card-title" style="margin-bottom:0"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#666" stroke-width="1.5"/><path d="M6 5H10M6 8H10M6 11H8" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg> Publiceringsköen</div>
<button class="btn btn-secondary btn-sm" onclick="loadPostQueue()"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 8C2 11.3 4.7 14 8 14C10.5 14 12.7 12.5 13.7 10.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M14 8C14 4.7 11.3 2 8 2C5.5 2 3.3 3.5 2.3 5.7" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M13.5 7.5L14 10L11.5 9.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.5 8.5L2 6L4.5 6.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> Ladda om</button>
</div>
<div id="postQueueContent">
<div class="state-box"><div class="spinner"></div><br>Laddar...</div>
@@ -514,9 +514,9 @@
<script>
'use strict';
/* ═══════════════════════════════════════════════════════════
/*
CONSTANTS
═══════════════════════════════════════════════════════════ */
*/
const API = '';
const COMPANY_CREDS = {
@@ -541,10 +541,10 @@ const PLATFORM_CHAR_LIMITS = {
};
const PLATFORM_ICONS = {
twitter:'🐦', instagram:'📸', facebook:'👍', linkedin:'💼',
tiktok:'🎵', youtube:'▶️', reddit:'🤖', snapchat:'👻',
pinterest:'📌', spotify:'🎧', meta:'🌐',
jobteaser:'🎓', hired:'💼', outbrain:'📰'
twitter:'', instagram:'', facebook:'', linkedin:'',
tiktok:'', youtube:'', reddit:'', snapchat:'',
pinterest:'', spotify:'', meta:'',
jobteaser:'', hired:'', outbrain:''
};
const PLATFORM_LOGIN = {
@@ -568,13 +568,13 @@ const PLATFORM_LOGIN = {
ziprecruiter:'https://www.ziprecruiter.com/login',
};
/* ═══════════════════════════════════════════════════════════
/*
PLATFORM STEPS
Fix #5: replaced manual mail instructions with auto-fetch note
═══════════════════════════════════════════════════════════ */
*/
const PLATFORM_STEPS = {
tiktok: {
icon: '🎵',
icon: '',
title: 'TikTok for Business',
mailbox: true,
steps: [
@@ -587,7 +587,7 @@ const PLATFORM_STEPS = {
]
},
instagram: {
icon: '📸',
icon: '',
title: 'Instagram Business',
mailbox: true,
steps: [
@@ -601,7 +601,7 @@ const PLATFORM_STEPS = {
]
},
snapchat: {
icon: '👻',
icon: '',
title: 'Snapchat Business',
mailbox: true,
steps: [
@@ -614,7 +614,7 @@ const PLATFORM_STEPS = {
]
},
reddit: {
icon: '🤖',
icon: '',
title: 'Reddit Ads',
mailbox: false,
steps: [
@@ -627,7 +627,7 @@ const PLATFORM_STEPS = {
]
},
default: {
icon: '',
icon: '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
title: 'Granska &amp; godkänn',
mailbox: false,
steps: [
@@ -639,9 +639,9 @@ const PLATFORM_STEPS = {
}
};
/* ═══════════════════════════════════════════════════════════
/*
STATE
═══════════════════════════════════════════════════════════ */
*/
let currentCompany = 'landvex';
let currentMainTab = 'register';
let currentJobFilter = 'all';
@@ -651,9 +651,9 @@ let scheduleMode = 'now';
let countdownTimer = null;
let refreshCountdown = 15;
/* ═══════════════════════════════════════════════════════════
/*
AUTH
═══════════════════════════════════════════════════════════ */
*/
function jwtExpired(token) {
try {
const payload = JSON.parse(atob(token.split('.')[1].replace(/-/g,'+').replace(/_/g,'/')));
@@ -695,9 +695,9 @@ function getAuthToken() {
return getToken();
}
/* ═══════════════════════════════════════════════════════════
/*
TOAST
═══════════════════════════════════════════════════════════ */
*/
function toast(msg, type, durationMs) {
type = type || 'info';
durationMs = durationMs || 4000;
@@ -709,9 +709,9 @@ function toast(msg, type, durationMs) {
setTimeout(function() { t.remove(); }, durationMs);
}
/* ═══════════════════════════════════════════════════════════
/*
PLATFORMS
═══════════════════════════════════════════════════════════ */
*/
async function loadPlatforms() {
try {
await ensureAuth();
@@ -737,7 +737,7 @@ function renderPlatformDropdown() {
platformList.forEach(function(name) {
const opt = document.createElement('option');
opt.value = name;
opt.textContent = (PLATFORM_ICONS[name] || '🌐') + ' ' + name.charAt(0).toUpperCase() + name.slice(1);
opt.textContent = (PLATFORM_ICONS[name] || '') + ' ' + name.charAt(0).toUpperCase() + name.slice(1);
sel.appendChild(opt);
});
}
@@ -750,15 +750,15 @@ function renderPubPlatformChecks() {
const id = 'pubChk_' + name;
const wrap = document.createElement('label');
wrap.className = 'platform-checkbox';
wrap.style.cssText = 'display:flex;align-items:center;gap:6px;cursor:pointer;padding:5px 10px;border:1px solid #1e2737;border-radius:8px;';
wrap.innerHTML = '<input type="checkbox" id="' + id + '" value="' + name + '" onchange="updatePreview()" style="width:auto;cursor:pointer;"><span style="font-size:13px;color:#cbd5e1;">' + (PLATFORM_ICONS[name] || '🌐') + ' ' + name.charAt(0).toUpperCase() + name.slice(1) + '</span>';
wrap.style.cssText = 'display:flex;align-items:center;gap:6px;cursor:pointer;padding:5px 10px;border:1px solid #1e2737;border-radius: var(--radius-md);';
wrap.innerHTML = '<input type="checkbox" id="' + id + '" value="' + name + '" onchange="updatePreview()" style="width:auto;cursor:pointer;"><span style="font-size:13px;color:#cbd5e1;">' + (PLATFORM_ICONS[name] || '') + ' ' + name.charAt(0).toUpperCase() + name.slice(1) + '</span>';
container.appendChild(wrap);
});
}
/* ═══════════════════════════════════════════════════════════
/*
REGISTER FORM
═══════════════════════════════════════════════════════════ */
*/
async function fillForm(platform) {
if (!platform) return;
await ensureAuth();
@@ -839,19 +839,19 @@ async function startRegistration() {
});
const d = await r.json().catch(function() { return {}; });
if (r.ok) {
toast(' Automation startad för ' + platform + ' (' + company + ')', 'success');
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Automation startad för ' + platform + ' (' + company + ')', 'success');
switchTab('jobs');
} else {
toast(' Fel: ' + (d.error || d.message || r.status), 'error');
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Fel: ' + (d.error || d.message || r.status), 'error');
}
} catch(e) {
toast(' Nätverksfel: ' + e.message, 'error');
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Nätverksfel: ' + e.message, 'error');
}
}
/* ═══════════════════════════════════════════════════════════
/*
COMPANY SELECTION
═══════════════════════════════════════════════════════════ */
*/
function selectCompany(company) {
currentCompany = company;
syncCompanyTabs();
@@ -884,20 +884,20 @@ function syncCompanyTabs() {
btn.classList.toggle('active', isActive);
// Bygg badge-html
let badges = '';
if (c.done > 0) badges += '<span style="background:#16a34a;color:#fff;border-radius:10px;padding:1px 6px;font-size:10px;margin-left:4px">'+c.done+'</span>';
if (c.running > 0) badges += '<span style="background:#2563eb;color:#fff;border-radius:10px;padding:1px 6px;font-size:10px;margin-left:4px">'+c.running+'</span>';
if (c.awaiting > 0) badges += '<span style="background:#d97706;color:#fff;border-radius:10px;padding:1px 6px;font-size:10px;margin-left:4px">⏳'+c.awaiting+'</span>';
if (c.error > 0) badges += '<span style="background:#dc2626;color:#fff;border-radius:10px;padding:1px 6px;font-size:10px;margin-left:4px">'+c.error+'</span>';
if (c.manual > 0) badges += '<span style="background:#7c3aed;color:#fff;border-radius:10px;padding:1px 6px;font-size:10px;margin-left:4px">🔧'+c.manual+'</span>';
if (c.done > 0) badges += '<span style="background:#16a34a;color:#fff;border-radius: var(--radius-md);padding:1px 6px;font-size:10px;margin-left:4px"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>'+c.done+'</span>';
if (c.running > 0) badges += '<span style="background:#2563eb;color:#fff;border-radius: var(--radius-md);padding:1px 6px;font-size:10px;margin-left:4px"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg>'+c.running+'</span>';
if (c.awaiting > 0) badges += '<span style="background:#d97706;color:#fff;border-radius: var(--radius-md);padding:1px 6px;font-size:10px;margin-left:4px">⏳'+c.awaiting+'</span>';
if (c.error > 0) badges += '<span style="background:#dc2626;color:#fff;border-radius: var(--radius-md);padding:1px 6px;font-size:10px;margin-left:4px"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg>'+c.error+'</span>';
if (c.manual > 0) badges += '<span style="background:#7c3aed;color:#fff;border-radius: var(--radius-md);padding:1px 6px;font-size:10px;margin-left:4px">'+c.manual+'</span>';
// Uppdatera knapp-label (spara original-label om inte redan gjort)
if (!btn.dataset.label) btn.dataset.label = btn.innerHTML;
btn.innerHTML = btn.dataset.label + badges;
});
}
/* ═══════════════════════════════════════════════════════════
/*
MAIN TAB SWITCHING
═══════════════════════════════════════════════════════════ */
*/
function switchTab(tab) {
currentMainTab = tab;
document.querySelectorAll('.main-tab').forEach(function(btn) {
@@ -913,9 +913,9 @@ function switchTab(tab) {
if (tab === 'publishing') loadPostQueue();
}
/* ═══════════════════════════════════════════════════════════
/*
JOBS
═══════════════════════════════════════════════════════════ */
*/
function setJobFilter(filter) {
currentJobFilter = filter;
document.querySelectorAll('.filter-btn').forEach(function(btn) {
@@ -935,7 +935,7 @@ async function loadJobs() {
renderJobs();
updateApproveAllBanner();
} catch(e) {
container.innerHTML = '<div class="state-box"><div class="icon">⚠️</div><b>Kunde inte ladda jobb</b><p>' + esc(e.message) + '</p></div>';
container.innerHTML = '<div class="state-box"><div class="icon"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg></div><b>Kunde inte ladda jobb</b><p>' + esc(e.message) + '</p></div>';
}
}
@@ -989,14 +989,14 @@ async function approveAll() {
} catch(e) { fail++; }
}
if (fail === 0) {
toast(' Alla ' + ok + ' jobb godkända', 'success');
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Alla ' + ok + ' jobb godkända', 'success');
} else {
toast('⚠️ Godkände ' + ok + ', misslyckades ' + fail, 'warning');
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Godkände ' + ok + ', misslyckades ' + fail, 'warning');
}
loadJobs();
}
/* ─── Build steps HTML (Fix #1: "Gör så här", Fix #3: mail-knapp, Fix #4: credentials) ─── */
/* Build steps HTML (Fix #1: "Gör så här", Fix #3: mail-knapp, Fix #4: credentials) */
async function buildStepsHtml(job) {
const plat = (job.platform || '').toLowerCase();
const info = PLATFORM_STEPS[plat] || PLATFORM_STEPS.default;
@@ -1015,15 +1015,15 @@ async function buildStepsHtml(job) {
if (!isManual) {
return '<a href="' + autofillUrl + '" target="_blank" rel="noopener" ' +
'style="display:inline-flex;align-items:center;gap:5px;color:#a5b4fc;font-size:.75rem;font-weight:600;' +
'text-decoration:none;border:1px solid #4338ca;padding:4px 12px;border-radius:6px">' +
'text-decoration:none;border:1px solid #4338ca;padding:4px 12px;border-radius: 6px">' +
'Logga in på ' + esc(job.platform) + '</a>';
}
/* Build steps list */
const stepsHtml = info.steps.map(function(s) {
return '<li style="margin-bottom:8px;line-height:1.5">' +
s.replace(/{email}/g, '<code style="background:#1e293b;padding:2px 8px;border-radius:4px;color:#6ee7b7;font-size:.85em">' + email + '</code>')
.replace(/{username}/g, '<code style="background:#1e293b;padding:2px 8px;border-radius:4px;color:#a5b4fc;font-size:.85em">' + username + '</code>') +
s.replace(/{email}/g, '<code style="background:#1e293b;padding:2px 8px;border-radius: var(--radius-sm);color:#6ee7b7;font-size:.85em">' + email + '</code>')
.replace(/{username}/g, '<code style="background:#1e293b;padding:2px 8px;border-radius: var(--radius-sm);color:#a5b4fc;font-size:.85em">' + username + '</code>') +
'</li>';
}).join('');
@@ -1047,7 +1047,7 @@ async function buildStepsHtml(job) {
const safeId = String(id).replace(/[^a-z0-9\-]/gi, '');
credsBlock = '<div class="inline-creds">' +
'<div class="inline-creds-title">🔐 Inloggningsuppgifter</div>' +
'<div class="inline-creds-title"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="7" width="10" height="7" rx="1" stroke="#666" stroke-width="1.5"/><path d="M5 7V5C5 3.3 6.3 2 8 2C9.7 2 11 3.3 11 5V7" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="10.5" r="1" fill="#666"/></svg> Inloggningsuppgifter</div>' +
'<div class="cred-inline-row">' +
'<span class="cred-inline-label">E-post</span>' +
'<span class="cred-inline-val">' + email + '</span>' +
@@ -1057,9 +1057,9 @@ async function buildStepsHtml(job) {
'<span class="cred-inline-label">Lösenord</span>' +
(revealedPw
? '<input type="password" id="pw-' + safeId + '" value="' + esc(revealedPw) + '" readonly class="pw-field">' +
'<button class="reveal-btn" onclick="togglePw(\'pw-' + safeId + '\', this)">👁 Visa</button>' +
'<button class="reveal-btn" onclick="togglePw(\'pw-' + safeId + '\', this)"> Visa</button>' +
'<button class="copy-btn" onclick="copyText(\'' + revealedPw.replace(/'/g, "\\'") + '\', this)">Kopiera</button>'
: '<span style="color:#475569;font-size:12px;">🔒 Ej tillgängligt</span>'
: '<span style="color:#475569;font-size:12px;"> Ej tillgängligt</span>'
) +
'</div>' +
'</div>';
@@ -1067,9 +1067,9 @@ async function buildStepsHtml(job) {
/* Fix #3: Mail-code fetch button */
const mailCodeHtml = info.mailbox
? '<div class="mail-code-box">' +
'<div style="font-size:12px;color:#94a3b8;margin-bottom:8px;">📧 Väntar på verifieringskod via mail</div>' +
'<div style="font-size:12px;color:#94a3b8;margin-bottom:8px;"> Väntar på verifieringskod via mail</div>' +
'<button class="btn btn-teal btn-sm" onclick="fetchMailCode(\'' + safeId + '\', \'' + esc(plat) + '\', this)">' +
'📨 Hämta kod</button>' +
' Hämta kod</button>' +
'<div id="mail-code-result-' + safeId + '" class="mail-code-result hidden"></div>' +
'<div style="margin-top:6px;">' +
'<a href="https://mail.quixzoom.com/sso/login" target="_blank" rel="noopener" ' +
@@ -1080,10 +1080,10 @@ async function buildStepsHtml(job) {
const loginBtn = '<a href="' + autofillUrl + '" target="_blank" rel="noopener" ' +
'style="display:inline-flex;align-items:center;gap:6px;background:#6366f1;color:#fff;font-weight:700;' +
'font-size:.85rem;text-decoration:none;padding:10px 20px;border-radius:8px;margin-top:10px">' +
'font-size:.85rem;text-decoration:none;padding:10px 20px;border-radius: var(--radius-md);margin-top:10px">' +
info.icon + ' Logga in på ' + esc(job.platform) + ' &rarr;</a>';
return '<div style="background:#0a0e1a;border:1px solid #1e2a38;border-radius:10px;padding:16px;margin-top:10px">' +
return '<div style="background:#0a0e1a;border:1px solid #1e2a38;border-radius: var(--radius-md);padding:16px;margin-top:10px">' +
'<div style="font-weight:700;color:#e2e8f0;margin-bottom:10px;font-size:.9rem">' + info.icon + ' Gör så här:</div>' +
'<ol style="padding-left:0;list-style:none;margin:0;font-size:.82rem;color:#cbd5e1">' + stepsHtml + '</ol>' +
credsBlock +
@@ -1111,38 +1111,38 @@ async function fetchMailCode(jobId, platform, btn) {
resultEl.classList.remove('hidden');
resultEl.innerHTML = 'Kod: <strong style="font-size:1.4em;letter-spacing:4px;">' + esc(code) + '</strong>' +
'<button class="copy-btn" onclick="copyText(\'' + esc(code) + '\', this)">Kopiera</button>';
btn.textContent = ' Hämtad';
btn.textContent = '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Hämtad';
} else {
toast('⚠️ Ingen kod hittades ännu — försök om ett par sekunder', 'warning');
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Ingen kod hittades ännu — försök om ett par sekunder', 'warning');
btn.disabled = false;
btn.textContent = '📨 Hämta kod';
btn.textContent = ' Hämta kod';
}
} else if (r.status === 404) {
/* Endpoint doesn't exist — fallback to open mailbox */
toast(' Automatisk hämtning ej tillgänglig — öppna mailboxen manuellt', 'info');
toast(' Automatisk hämtning ej tillgänglig — öppna mailboxen manuellt', 'info');
window.open('https://mail.quixzoom.com/sso/login', '_blank');
btn.disabled = false;
btn.textContent = '📨 Hämta kod';
btn.textContent = ' Hämta kod';
} else {
const d = await r.json().catch(function() { return {}; });
toast(' Fel vid hämtning: ' + (d.error || r.status), 'error');
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Fel vid hämtning: ' + (d.error || r.status), 'error');
btn.disabled = false;
btn.textContent = '📨 Hämta kod';
btn.textContent = ' Hämta kod';
}
} catch(e) {
toast(' Nätverksfel: ' + e.message, 'error');
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Nätverksfel: ' + e.message, 'error');
btn.disabled = false;
btn.textContent = '📨 Hämta kod';
btn.textContent = ' Hämta kod';
}
}
/* ─── Render jobs table ─── */
/* Render jobs table */
async function renderJobs() {
const container = document.getElementById('jobsContent');
if (!container) return;
const jobs = filterJobs();
if (jobs.length === 0) {
container.innerHTML = '<div class="state-box"><div class="icon">📭</div><b>Inga jobb hittades</b><p>Byt filter eller starta ett nytt jobb.</p></div>';
container.innerHTML = '<div class="state-box"><div class="icon"></div><b>Inga jobb hittades</b><p>Byt filter eller starta ett nytt jobb.</p></div>';
return;
}
@@ -1177,18 +1177,18 @@ async function renderJobs() {
const badgeClass = badgeMap[status] || 'badge-pending';
const statusLabel = {
awaiting_approval: '⏳ Väntar godkännande',
pending_manual: '🖐 Manuell åtgärd',
manual_required: '🖐 Manuell krävs',
manual_handle_review:'🖐 Granska handle',
completed: ' Klar',
failed: ' Misslyckad',
pending_manual: ' Manuell åtgärd',
manual_required: ' Manuell krävs',
manual_handle_review:' Granska handle',
completed: '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Klar',
failed: '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Misslyckad',
timeout: '⌛ Timeout',
}[status] || status.replace(/_/g, ' ');
/* Action required column */
let actionRequired = '<span class="action-none">—</span>';
if (isAwait) actionRequired = '<span class="action-required-cell"> Godkänn / avvisa</span>';
if (isManual) actionRequired = '<span class="action-required-cell">🖐 Manuell inloggning krävs</span>';
if (isAwait) actionRequired = '<span class="action-required-cell"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Godkänn / avvisa</span>';
if (isManual) actionRequired = '<span class="action-required-cell"> Manuell inloggning krävs</span>';
/* Fix #2: Screenshot from /api/social-account/screenshot/:jobId */
const screenshotUrl = API + '/api/social-account/screenshot/' + encodeURIComponent(id);
@@ -1204,10 +1204,10 @@ async function renderJobs() {
/* Action buttons */
let actions = '';
if (isAwait) {
actions += '<button class="btn btn-success btn-sm" data-action="approve" data-jobid="' + esc(String(id)) + '"> Godkänn</button> ';
actions += '<button class="btn btn-danger btn-sm" data-action="reject" data-jobid="' + esc(String(id)) + '"> Avvisa</button>';
actions += '<button class="btn btn-success btn-sm" data-action="approve" data-jobid="' + esc(String(id)) + '"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Godkänn</button> ';
actions += '<button class="btn btn-danger btn-sm" data-action="reject" data-jobid="' + esc(String(id)) + '"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Avvisa</button>';
} else {
actions += '<button class="btn btn-ghost btn-sm" data-action="delete" data-jobid="' + esc(String(id)) + '">🗑</button>';
actions += '<button class="btn btn-ghost btn-sm" data-action="delete" data-jobid="' + esc(String(id)) + '"></button>';
}
/* Build the steps block (async) */
@@ -1215,7 +1215,7 @@ async function renderJobs() {
html += '<tr' + (isAwait ? ' class="row-awaiting"' : '') + '>';
html += '<td><code style="font-size:11px;color:#6366f1">' + esc(String(id).slice(0, 12)) + '</code></td>';
html += '<td>' + (PLATFORM_ICONS[job.platform] || '🌐') + ' ' + platform + '</td>';
html += '<td>' + (PLATFORM_ICONS[job.platform] || '') + ' ' + platform + '</td>';
html += '<td><div><span class="badge ' + badgeClass + '">' + esc(statusLabel) + '</span>' + stepsBlock + '</div></td>';
html += '<td>' + actionRequired + '</td>';
html += '<td style="white-space:nowrap;font-size:12px;">' + created + '</td>';
@@ -1234,13 +1234,13 @@ async function approveJob(jobId) {
method: 'POST', headers: authHeaders()
});
if (r.ok) {
toast(' Jobb godkänt', 'success');
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Jobb godkänt', 'success');
loadJobs();
} else {
const d = await r.json().catch(function() { return {}; });
toast(' Godkännande misslyckades: ' + (d.error || r.status), 'error');
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Godkännande misslyckades: ' + (d.error || r.status), 'error');
}
} catch(e) { toast(' ' + e.message, 'error'); }
} catch(e) { toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> ' + e.message, 'error'); }
}
async function rejectJob(jobId) {
@@ -1254,7 +1254,7 @@ async function deleteJob(jobId) {
method: 'DELETE', headers: authHeaders()
});
if (r.ok) {
toast('🗑 Jobb raderat', 'info');
toast(' Jobb raderat', 'info');
allJobs = allJobs.filter(function(j) {
const jid = j.jobId || j.id || j.job_id;
return String(jid) !== String(jobId);
@@ -1262,9 +1262,9 @@ async function deleteJob(jobId) {
renderJobs();
} else {
const d = await r.json().catch(function() { return {}; });
toast(' Radering misslyckades: ' + (d.error || r.status), 'error');
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Radering misslyckades: ' + (d.error || r.status), 'error');
}
} catch(e) { toast(' ' + e.message, 'error'); }
} catch(e) { toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> ' + e.message, 'error'); }
}
/* Fix #6: Auto-refresh every 15s */
@@ -1291,9 +1291,9 @@ function updateRefreshInfo() {
if (el) el.textContent = 'Auto-refresh om ' + refreshCountdown + 's';
}
/* ═══════════════════════════════════════════════════════════
/*
CREDENTIALS
═══════════════════════════════════════════════════════════ */
*/
async function loadCredentials() {
const container = document.getElementById('credsContent');
if (!container) return;
@@ -1320,7 +1320,7 @@ async function loadCredentials() {
}
renderCredentials(creds);
} catch(e) {
container.innerHTML = '<div class="state-box"><div class="icon">⚠️</div><b>Kunde inte ladda credentials</b><p>' + esc(e.message) + '</p></div>';
container.innerHTML = '<div class="state-box"><div class="icon"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg></div><b>Kunde inte ladda credentials</b><p>' + esc(e.message) + '</p></div>';
}
}
@@ -1328,7 +1328,7 @@ function renderCredentials(creds) {
const container = document.getElementById('credsContent');
if (!container) return;
if (!creds || creds.length === 0) {
container.innerHTML = '<div class="state-box"><div class="icon">🔐</div><b>Inga credentials för ' + esc(currentCompany) + '</b><p>Registrera konton för att se credentials här.</p></div>';
container.innerHTML = '<div class="state-box"><div class="icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="7" width="10" height="7" rx="1" stroke="#666" stroke-width="1.5"/><path d="M5 7V5C5 3.3 6.3 2 8 2C9.7 2 11 3.3 11 5V7" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="10.5" r="1" fill="#666"/></svg></div><b>Inga credentials för ' + esc(currentCompany) + '</b><p>Registrera konton för att se credentials här.</p></div>';
return;
}
@@ -1337,7 +1337,7 @@ function renderCredentials(creds) {
const platform = esc(c.platform || '?');
const email = esc(c.email || '—');
const username = esc(c.username || '—');
const icon = PLATFORM_ICONS[c.platform] || '🌐';
const icon = PLATFORM_ICONS[c.platform] || '';
const hasPw = c.password && c.password !== '' && c.password !== '•••••';
const pwId = 'cred-pw-' + idx;
@@ -1353,9 +1353,9 @@ function renderCredentials(creds) {
html += '<div class="cred-row"><span class="cred-label">Lösenord</span>' +
(hasPw
? '<input type="password" id="' + pwId + '" value="' + esc(c.password) + '" readonly class="pw-field">' +
'<button class="reveal-btn" onclick="togglePw(\'' + pwId + '\', this)">👁 Visa</button>' +
'<button class="reveal-btn" onclick="togglePw(\'' + pwId + '\', this)"> Visa</button>' +
'<button class="copy-btn" onclick="copyText(\'' + c.password.replace(/'/g, "\\'") + '\', this)">Kopiera</button>'
: '<span class="cred-value" style="color:#475569;">🔒 Krypterat (AES-256)</span>'
: '<span class="cred-value" style="color:#475569;"> Krypterat (AES-256)</span>'
) +
'</div>';
html += '</div></div>';
@@ -1385,7 +1385,7 @@ async function confirmRotate() {
let rotated = 0, failed = 0;
const logItems = [];
toast('🔄 Roterar lösenord...', 'info', 10000);
toast('<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 8C2 11.3 4.7 14 8 14C10.5 14 12.7 12.5 13.7 10.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M14 8C14 4.7 11.3 2 8 2C5.5 2 3.3 3.5 2.3 5.7" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M13.5 7.5L14 10L11.5 9.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.5 8.5L2 6L4.5 6.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> Roterar lösenord...', 'info', 10000);
for (const company of companies) {
try {
@@ -1412,9 +1412,9 @@ async function confirmRotate() {
}
if (failed === 0) {
toast(' Lösenord roterade för ' + rotated + ' bolag', 'success');
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Lösenord roterade för ' + rotated + ' bolag', 'success');
} else {
toast('⚠️ Roterade ' + rotated + ', misslyckades ' + failed, 'warning');
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Roterade ' + rotated + ', misslyckades ' + failed, 'warning');
}
const logDiv = document.getElementById('rotationLog');
@@ -1423,7 +1423,7 @@ async function confirmRotate() {
logItems.forEach(function(item) {
logHtml += '<div class="rotation-log-item">';
logHtml += '<span>' + esc(COMPANY_LABELS[item.company] || item.company) + '</span>';
logHtml += '<span>' + (item.status === 'ok' ? '✅ ' + (item.platforms || 0) + ' plattformar' : '❌ Fel') + '</span>';
logHtml += '<span>' + (item.status === 'ok' ? '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> ' + (item.platforms || 0) + ' plattformar' : '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Fel') + '</span>';
logHtml += '<span style="color:#374151;">' + esc(item.ts) + '</span>';
logHtml += '</div>';
});
@@ -1443,9 +1443,9 @@ async function confirmOffboard() {
await confirmRotate();
}
/* ═══════════════════════════════════════════════════════════
/*
PUBLISHING MODULE
═══════════════════════════════════════════════════════════ */
*/
function onPubCompanyChange() {
const sel = document.getElementById('pubCompany');
currentCompany = sel ? sel.value : 'landvex';
@@ -1486,19 +1486,19 @@ function updatePreview() {
const platforms = getSelectedPlatforms();
updateCharCounter();
if (platforms.length === 0 || !text.trim()) {
container.innerHTML = '<div style="color:#475569;font-size:13px;padding:16px;text-align:center;border:1px dashed #1e2737;border-radius:10px;">Välj plattformar och skriv innehåll för att se förhandsvisning</div>';
container.innerHTML = '<div style="color:#475569;font-size:13px;padding:16px;text-align:center;border:1px dashed #1e2737;border-radius: var(--radius-md);">Välj plattformar och skriv innehåll för att se förhandsvisning</div>';
return;
}
let html = '';
platforms.forEach(function(plat) {
const limit = PLATFORM_CHAR_LIMITS[plat] || 99999;
const preview = text.length > limit ? text.slice(0, limit - 3) + '...' : text;
const icon = PLATFORM_ICONS[plat] || '🌐';
const icon = PLATFORM_ICONS[plat] || '';
html += '<div class="post-preview" style="margin-bottom:12px;">';
html += '<div class="preview-platform">' + icon + ' ' + esc(plat) + '</div>';
html += '<div class="preview-content">' + esc(preview) + '</div>';
if (text.length > limit) {
html += '<div style="font-size:11px;color:#ef4444;margin-top:6px;">⚠️ ' + (text.length - limit) + ' tecken för långt</div>';
html += '<div style="font-size:11px;color:#ef4444;margin-top:6px;"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> ' + (text.length - limit) + ' tecken för långt</div>';
}
html += '</div>';
});
@@ -1533,14 +1533,14 @@ async function submitPost(asDraft) {
});
const d = await r.json().catch(function() { return {}; });
if (r.ok) {
toast(asDraft ? '💾 Utkast sparat' : '📤 Inlägg schemalagt/publicerat', 'success');
toast(asDraft ? ' Utkast sparat' : ' Inlägg schemalagt/publicerat', 'success');
clearPost();
loadPostQueue();
} else {
toast(' Fel: ' + (d.error || d.message || r.status), 'error');
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Fel: ' + (d.error || d.message || r.status), 'error');
}
} catch(e) {
toast(' Nätverksfel: ' + e.message, 'error');
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Nätverksfel: ' + e.message, 'error');
}
}
@@ -1565,7 +1565,7 @@ async function loadPostQueue() {
const posts = Array.isArray(data) ? data : (data.posts || data.queue || []);
renderPostQueue(posts);
} catch(e) {
container.innerHTML = '<div class="state-box"><div class="icon">⚠️</div><b>Kunde inte ladda kön</b><p>' + esc(e.message) + '</p></div>';
container.innerHTML = '<div class="state-box"><div class="icon"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg></div><b>Kunde inte ladda kön</b><p>' + esc(e.message) + '</p></div>';
}
}
@@ -1573,7 +1573,7 @@ function renderPostQueue(posts) {
const container = document.getElementById('postQueueContent');
if (!container) return;
if (posts.length === 0) {
container.innerHTML = '<div class="state-box"><div class="icon">📭</div><b>Inga inlägg i kön</b><p>Skapa ett inlägg ovan.</p></div>';
container.innerHTML = '<div class="state-box"><div class="icon"></div><b>Inga inlägg i kön</b><p>Skapa ett inlägg ovan.</p></div>';
return;
}
let html = '<table class="post-queue-table"><thead><tr><th>ID</th><th>Bolag</th><th>Plattformar</th><th>Status</th><th>Schemalagt</th><th>Innehåll</th><th>Åtgärder</th></tr></thead><tbody>';
@@ -1581,7 +1581,7 @@ function renderPostQueue(posts) {
const id = esc(String(p.id || p.post_id || '—').slice(0, 10));
const company = esc(p.company || '—');
const platforms = Array.isArray(p.platforms)
? p.platforms.map(function(x) { return (PLATFORM_ICONS[x] || '🌐') + ' ' + x; }).join(', ')
? p.platforms.map(function(x) { return (PLATFORM_ICONS[x] || '') + ' ' + x; }).join(', ')
: esc(String(p.platforms || '—'));
const status = (p.status || 'pending').toLowerCase();
const sched = p.scheduledAt ? new Date(p.scheduledAt).toLocaleString('sv-SE') : (status === 'pending' ? 'Nu' : '—');
@@ -1595,7 +1595,7 @@ function renderPostQueue(posts) {
html += '<td><span class="badge ' + badgeCls + '">' + esc(status) + '</span></td>';
html += '<td style="font-size:11px;">' + esc(sched) + '</td>';
html += '<td style="font-size:11px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + preview + '</td>';
html += '<td><button class="btn btn-ghost btn-sm" onclick="deletePost(' + JSON.stringify(p.id || p.post_id) + ')">🗑</button></td>';
html += '<td><button class="btn btn-ghost btn-sm" onclick="deletePost(' + JSON.stringify(p.id || p.post_id) + ')"></button></td>';
html += '</tr>';
});
html += '</tbody></table>';
@@ -1608,14 +1608,14 @@ async function deletePost(postId) {
const r = await fetch(API + '/api/social-account/publish/' + encodeURIComponent(postId), {
method: 'DELETE', headers: authHeaders()
});
if (r.ok) { toast('🗑 Inlägg borttaget', 'info'); loadPostQueue(); }
else { toast(' Radering misslyckades', 'error'); }
} catch(e) { toast(' ' + e.message, 'error'); }
if (r.ok) { toast(' Inlägg borttaget', 'info'); loadPostQueue(); }
else { toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Radering misslyckades', 'error'); }
} catch(e) { toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> ' + e.message, 'error'); }
}
/* ═══════════════════════════════════════════════════════════
/*
UTILITY
═══════════════════════════════════════════════════════════ */
*/
function esc(s) {
return String(s)
.replace(/&/g, '&amp;')
@@ -1628,10 +1628,10 @@ function esc(s) {
function copyText(text, btn) {
navigator.clipboard.writeText(text).then(function() {
const orig = btn.textContent;
btn.textContent = '';
btn.textContent = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8L6.5 11.5L13 4.5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
setTimeout(function() { btn.textContent = orig; }, 1500);
}).catch(function() {
toast(' Kopiering misslyckades', 'error');
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Kopiering misslyckades', 'error');
});
}
@@ -1640,10 +1640,10 @@ function togglePw(inputId, btn) {
if (!input) return;
if (input.type === 'password') {
input.type = 'text';
btn.textContent = '🙈 Dölj';
btn.textContent = ' Dölj';
} else {
input.type = 'password';
btn.textContent = '👁 Visa';
btn.textContent = ' Visa';
}
}
@@ -1654,14 +1654,14 @@ function setStatus(online) {
if (text) text.textContent = online ? 'Online' : 'Offline';
}
/* ═══════════════════════════════════════════════════════════
/*
INIT
═══════════════════════════════════════════════════════════ */
*/
async function init() {
try {
const tok = await ensureAuth();
setStatus(!!tok);
if (!tok) toast('⚠️ Autentisering misslyckades', 'error', 8000);
if (!tok) toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Autentisering misslyckades', 'error', 8000);
await loadPlatforms();
switchTab('register');
startAutoRefresh();
@@ -1669,15 +1669,15 @@ async function init() {
} catch(e) {
console.error('[ARGUS] Init error:', e);
setStatus(false);
toast(' Initieringsfel: ' + e.message, 'error', 8000);
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Initieringsfel: ' + e.message, 'error', 8000);
}
}
window.addEventListener('DOMContentLoaded', function() { init(); });
/* ═══════════════════════════════════════════════════════════
/*
EVENT DELEGATION for job buttons
═══════════════════════════════════════════════════════════ */
*/
document.addEventListener('click', function(e) {
const btn = e.target.closest('[data-action]');
if (!btn) return;
@@ -1689,9 +1689,9 @@ document.addEventListener('click', function(e) {
else if (action === 'delete') deleteJob(jobId);
});
/* ═══════════════════════════════════════════════════════
/*
ADD PLATFORM MODAL
═══════════════════════════════════════════════════════ */
*/
function openAddPlatformModal() {
var existing = document.getElementById('addPlatformModal');
@@ -1711,47 +1711,47 @@ function openAddPlatformModal() {
var companyLabels = {landvex:'LandveX',quixzoom:'QuiXzoom',apifly:'ApiFly',corpfitt:'CorpFitt',vyra:'VYRA',aamos:'AAMOS'};
var html = [
'<div style="background:#0d1117;border:1px solid #1e2737;border-radius:16px;padding:32px;max-width:560px;width:95%;max-height:90vh;overflow-y:auto;">',
'<div style="background:#0d1117;border:1px solid #1e2737;border-radius: var(--radius-lg);padding:32px;max-width:560px;width:95%;max-height:90vh;overflow-y:auto;">',
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:24px;">',
'<h3 style="font-size:18px;font-weight:700;color:#f1f5f9;margin:0;">+ Lägg till plattform</h3>',
'<button onclick="closeAddPlatformModal()" style="background:none;border:none;color:#64748b;font-size:20px;cursor:pointer;padding:4px 8px;border-radius:6px;" onmouseover="this.style.color=\'#e2e8f0\'" onmouseout="this.style.color=\'#64748b\'">&#x2715;</button>',
'<button onclick="closeAddPlatformModal()" style="background:none;border:none;color:#64748b;font-size:20px;cursor:pointer;padding:4px 8px;border-radius: var(--radius-sm);" onmouseover="this.style.color=\'#e2e8f0\'" onmouseout="this.style.color=\'#64748b\'">&#x2715;</button>',
'</div>',
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">',
'<div class="form-group">',
'<label>Plattformsnamn</label>',
'<input type="text" id="ap_name" placeholder="t.ex. Mastodon" style="background:#030712;border:1px solid #1e2737;border-radius:8px;color:#e2e8f0;padding:9px 12px;font-size:13px;width:100%;">',
'<input type="text" id="ap_name" placeholder="t.ex. Mastodon" style="background:#030712;border:1px solid #1e2737;border-radius: var(--radius-md);color:#e2e8f0;padding:9px 12px;font-size:13px;width:100%;">',
'</div>',
'<div class="form-group">',
'<label>Ikon/Emoji</label>',
'<input type="text" id="ap_icon" placeholder="🌐" value="🌐" style="background:#030712;border:1px solid #1e2737;border-radius:8px;color:#e2e8f0;padding:9px 12px;font-size:13px;width:100%;">',
'<input type="text" id="ap_icon" placeholder="" value="" style="background:#030712;border:1px solid #1e2737;border-radius: var(--radius-md);color:#e2e8f0;padding:9px 12px;font-size:13px;width:100%;">',
'</div>',
'<div class="form-group" style="grid-column:span 2;">',
'<label>Registrerings-URL</label>',
'<input type="url" id="ap_url" placeholder="https://platform.com/signup" style="background:#030712;border:1px solid #1e2737;border-radius:8px;color:#e2e8f0;padding:9px 12px;font-size:13px;width:100%;">',
'<input type="url" id="ap_url" placeholder="https://platform.com/signup" style="background:#030712;border:1px solid #1e2737;border-radius: var(--radius-md);color:#e2e8f0;padding:9px 12px;font-size:13px;width:100%;">',
'</div>',
'<div class="form-group">',
'<label>Login-URL</label>',
'<input type="url" id="ap_login" placeholder="https://platform.com/login" style="background:#030712;border:1px solid #1e2737;border-radius:8px;color:#e2e8f0;padding:9px 12px;font-size:13px;width:100%;">',
'<input type="url" id="ap_login" placeholder="https://platform.com/login" style="background:#030712;border:1px solid #1e2737;border-radius: var(--radius-md);color:#e2e8f0;padding:9px 12px;font-size:13px;width:100%;">',
'</div>',
'<div class="form-group">',
'<label>Teckenlängd max</label>',
'<input type="number" id="ap_charlimit" placeholder="500" style="background:#030712;border:1px solid #1e2737;border-radius:8px;color:#e2e8f0;padding:9px 12px;font-size:13px;width:100%;">',
'<input type="number" id="ap_charlimit" placeholder="500" style="background:#030712;border:1px solid #1e2737;border-radius: var(--radius-md);color:#e2e8f0;padding:9px 12px;font-size:13px;width:100%;">',
'</div>',
'</div>',
'<div class="form-group" style="margin-top:16px;">',
'<label>Bolag</label>',
'<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:6px;">']
.concat(companies.map(function(c) {
return '<label style="display:flex;align-items:center;gap:6px;cursor:pointer;padding:5px 10px;border:1px solid #1e2737;border-radius:8px;font-size:13px;color:#cbd5e1;">' +
return '<label style="display:flex;align-items:center;gap:6px;cursor:pointer;padding:5px 10px;border:1px solid #1e2737;border-radius: var(--radius-md);font-size:13px;color:#cbd5e1;">' +
'<input type="checkbox" id="ap_co_' + c + '" value="' + c + '" style="width:auto;cursor:pointer;"> ' + companyLabels[c] + '</label>';
}))
.concat(['</div></div>',
'<div class="form-group" style="margin-top:16px;">',
'<label>Anteckning / Särskilda krav</label>',
'<textarea id="ap_notes" rows="3" placeholder="t.ex. kräver företagsverifiering, manuell godkännande..." style="background:#030712;border:1px solid #1e2737;border-radius:8px;color:#e2e8f0;padding:9px 12px;font-size:13px;width:100%;resize:vertical;"></textarea>',
'<textarea id="ap_notes" rows="3" placeholder="t.ex. kräver företagsverifiering, manuell godkännande..." style="background:#030712;border:1px solid #1e2737;border-radius: var(--radius-md);color:#e2e8f0;padding:9px 12px;font-size:13px;width:100%;resize:vertical;"></textarea>',
'</div>',
'<div id="ap_confirm" style="display:none;background:#0f2818;border:1px solid #166534;border-radius:8px;padding:10px 14px;margin-top:12px;color:#86efac;font-size:13px;font-weight:600;">',
' Plattform tillagd!',
'<div id="ap_confirm" style="display:none;background:#0f2818;border:1px solid #166534;border-radius: var(--radius-md);padding:10px 14px;margin-top:12px;color:#86efac;font-size:13px;font-weight:600;">',
'<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Plattform tillagd!',
'</div>',
'<div style="display:flex;gap:12px;justify-content:flex-end;margin-top:24px;">',
'<button class="btn btn-secondary" onclick="closeAddPlatformModal()">Avbryt</button>',
@@ -1778,7 +1778,7 @@ function closeAddPlatformModal() {
async function saveAddPlatform() {
var name = (document.getElementById('ap_name') ? document.getElementById('ap_name').value : '').trim();
var icon = (document.getElementById('ap_icon') ? document.getElementById('ap_icon').value : '').trim() || '🌐';
var icon = (document.getElementById('ap_icon') ? document.getElementById('ap_icon').value : '').trim() || '';
var url = (document.getElementById('ap_url') ? document.getElementById('ap_url').value : '').trim();
var login = (document.getElementById('ap_login') ? document.getElementById('ap_login').value : '').trim();
var charLimit = parseInt(document.getElementById('ap_charlimit') ? document.getElementById('ap_charlimit').value : '0') || null;
@@ -1822,7 +1822,7 @@ async function saveAddPlatform() {
existing.push(platformData);
localStorage.setItem('argus_custom_platforms', JSON.stringify(existing));
saved = true;
} catch(e) { toast(' Kunde inte spara: ' + e.message, 'error'); return; }
} catch(e) { toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Kunde inte spara: ' + e.message, 'error'); return; }
}
// Update in-memory platform list
@@ -1839,7 +1839,7 @@ async function saveAddPlatform() {
// Show confirmation
var confirmEl = document.getElementById('ap_confirm');
if (confirmEl) confirmEl.style.display = '';
toast(' Plattform "' + name + '" tillagd!', 'success');
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Plattform "' + name + '" tillagd!', 'success');
setTimeout(function() { closeAddPlatformModal(); }, 1800);
}