Files
boc/landvex-all/admin-social-temp.html
T
Bernt 6989a98d75 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
2026-07-07 07:11:50 +00:00

1849 lines
94 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="sv">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ARGUS — Social Media Admin</title>
<link rel="stylesheet" href="/assets/css/landvex-complete.css">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #030712;
color: #e2e8f0;
min-height: 100vh;
font-size: 14px;
}
a { color: #6366f1; text-decoration: none; }
a:hover { text-decoration: underline; }
.top-nav {
background: #0d1117;
border-bottom: 1px solid #1e2737;
padding: 0 24px;
display: flex;
align-items: center;
gap: 16px;
height: 56px;
position: sticky;
top: 0;
z-index: 100;
}
.nav-logo { font-weight: 700; font-size: 18px; color: #6366f1; letter-spacing: -0.5px; }
.nav-links { display: flex; gap: 16px; }
.nav-link {
padding: 24px; border-radius: var(--radius-md); color: #94a3b8;
font-size: 13px; font-weight: 500; cursor: pointer;
transition: all 0.15s; background: none; border: none;
}
.nav-link:hover { background: #1e2737; color: #e2e8f0; }
.nav-link.active { background: #1e2737; color: #6366f1; }
.nav-spacer { flex: 1; }
.nav-status { display: flex; align-items: center; gap: 16px; font-size: 12px; color: #64748b; }
.status-dot { width: 8px; height: 8px; border-radius: var(--radius-full); background: #22c55e; }
.main-container { max-width: 1400px; margin: 0 auto; padding: 24px 48px; }
.page-header { margin-bottom: 24px; }
.page-header h1 { font-size: 22px; font-weight: 700; color: #f1f5f9; margin-bottom: 4px; }
.page-header p { color: #64748b; font-size: 13px; }
.company-tabs { display: flex; gap: 16px; margin-bottom: 24px; flex-wrap: wrap; }
.company-tab {
padding: 24px; 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;
}
.company-tab:hover { border-color: #6366f1; color: #e2e8f0; }
.company-tab.active { background: #1e1f4e; border-color: #6366f1; color: #818cf8; }
.main-tabs {
display: flex; gap: 0; border-bottom: 1px solid #1e2737; margin-bottom: 24px;
}
.main-tab {
padding: 24px; border: none; background: none; color: #64748b;
font-size: 13px; font-weight: 500; cursor: pointer;
border-bottom: 2px solid transparent; transition: all 0.15s; margin-bottom: -1px;
}
.main-tab:hover { color: #e2e8f0; }
.main-tab.active { color: #818cf8; border-bottom-color: #6366f1; }
.tab-panel { display: none; }
.tab-panel.visible { display: block; }
.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: 24px; }
.form-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; }
.form-group { display: flex; flex-direction: column; gap: 16px; }
.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: 24px; background: #030712; border: 1px solid #1e2737;
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: 24px; 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: 16px;
}
.btn-primary { background: #6366f1; color: #fff; }
.btn-primary:hover { background: #4f46e5; }
.btn-secondary { background: #1e2737; color: #94a3b8; border: 1px solid #2d3748; }
.btn-secondary:hover { color: #e2e8f0; border-color: #4a5568; }
.btn-success { background: #16a34a; color: #fff; }
.btn-success:hover { background: #15803d; }
.btn-danger { background: #dc2626; color: #fff; }
.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: 24px; border-radius: var(--radius-sm); font-size: 13px; transition: all 0.15s; }
.btn-ghost:hover { background: #1e2737; color: #e2e8f0; }
.btn-sm { padding: 24px; font-size: 12px; }
.btn-teal { background: #0d9488; color: #fff; }
.btn-teal:hover { background: #0f766e; }
.filter-row { display: flex; gap: 16px; align-items: center; margin-bottom: 24px; flex-wrap: wrap; }
.filter-btn { padding: 24px; 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; }
/* Approve-all banner */
.approve-all-banner {
display: none;
align-items: center;
justify-content: space-between;
background: #0f2818;
border: 1px solid #166534;
border-radius: var(--radius-md);
padding: 24px;
margin-bottom: 24px;
gap: 16px;
flex-wrap: wrap;
}
.approve-all-banner.visible { display: flex; }
.approve-all-banner-text { color: #86efac; font-size: 13px; font-weight: 600; }
.jobs-table-wrap { overflow-x: auto; }
.jobs-table { width: 100%; border-collapse: collapse; }
.jobs-table th {
padding: 24px; text-align: left; font-size: 11px; font-weight: 700;
color: #475569; text-transform: uppercase; letter-spacing: 0.5px;
border-bottom: 1px solid #1e2737; white-space: nowrap;
background: #0a0e1a;
}
.jobs-table td { padding: 12px; font-size: 13px; border-bottom: 1px solid #0f172a; vertical-align: top; }
.jobs-table tr:last-child td { border-bottom: none; }
.jobs-table tr:hover td { background: rgba(99,102,241,0.04); }
@keyframes pulse-bg {
0%,100% { background: rgba(34,197,94,0.04); }
50% { background: rgba(34,197,94,0.10); }
}
.row-awaiting { animation: pulse-bg 2s ease-in-out infinite; }
/* Larger, clearer badges */
.badge {
display: inline-flex; align-items: center; gap: 16px;
padding: 24px; 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; }
.badge-done { background: #14532d; color: #4ade80; border: 1px solid #16a34a; }
.badge-error { background: #450a0a; color: #f87171; border: 1px solid #dc2626; }
.badge-approval { background: #14532d; color: #86efac; border: 1px solid #22c55e; }
.badge-pending { background: #1e2737; color: #94a3b8; border: 1px solid #2d3748; }
.badge-cancelled{ background: #1e2737; color: #64748b; border: 1px solid #374151; }
.badge-scheduled{ background: #1e3a5f; color: #93c5fd; border: 1px solid #3b82f6; }
.badge-draft { background: #1e2737; color: #cbd5e1; border: 1px solid #334155; }
.badge-published{ background: #14532d; color: #4ade80; border: 1px solid #16a34a; }
.badge-failed { background: #450a0a; color: #f87171; border: 1px solid #dc2626; }
.badge-manual { background: #451a03; color: #fdba74; border: 1px solid #d97706; }
.badge-timeout { background: #1c1917; color: #78716c; border: 1px solid #44403c; }
.screenshot-thumb {
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;
}
.screenshot-thumb:hover { transform: scale(1.05); border-color: #6366f1; }
.no-screenshot { color: #374151; font-size: 11px; }
/* Inline credentials for PENDING_MANUAL / MANUAL_REQUIRED */
.inline-creds {
background: #0a1628; border: 1px solid #1e3a5f; border-radius: var(--radius-md);
padding: 24px; margin-top: 8px; display: flex; flex-direction: column; gap: 16px;
}
.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: 16px; 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: var(--radius-sm); padding: 24px; color: #fbbf24; font-family: monospace; font-size: 12px; width: 160px; }
.copy-btn { background: #6366f1; color: #fff; border: none; border-radius: var(--radius-sm); padding: 24px; 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: var(--radius-sm); padding: 24px; 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: var(--radius-md);
padding: 24px; margin-top: 8px;
}
.mail-code-result {
display: none; background: #1e293b; border-radius: var(--radius-sm); padding: 24px;
margin-top: 8px; font-size: 14px; font-weight: 700; color: #6ee7b7; letter-spacing: 2px;
display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
}
.mail-code-result.hidden { display: none !important; }
/* Credentials */
.creds-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; flex-wrap: wrap; gap: 16px; }
.creds-actions { display: flex; gap: 16px; 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: var(--radius-md); padding: 16px; }
.cred-platform { font-size: 14px; font-weight: 700; color: #f1f5f9; margin-bottom: 12px; display: flex; align-items: center; gap: 16px; }
.cred-rows { display: flex; flex-direction: column; gap: 16px; }
.cred-row { display: flex; justify-content: space-between; align-items: center; gap: 16px; font-size: 12px; }
.cred-label { color: #475569; min-width: 72px; }
.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: var(--radius-md);
padding: 24px; margin-bottom: 24px; display: flex; align-items: center; gap: 16px;
font-size: 12px; color: #94a3b8;
}
.security-banner .icon { font-size: 20px; }
/* Publishing */
.publish-grid { display: grid; grid-template-columns: 1fr 400px; gap: 16px; }
@media (max-width: 900px) { .publish-grid { grid-template-columns: 1fr; } }
.platform-checkboxes { display: flex; flex-wrap: wrap; gap: 16px; margin-top: 8px; }
.platform-checkbox { display: flex; align-items: center; gap: 16px; }
.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: 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; }
.post-queue-table th { font-size: 11px; font-weight: 600; color: #475569; text-transform: uppercase; letter-spacing: 0.5px; padding: 24px; border-bottom: 1px solid #1e2737; text-align: left; }
.post-queue-table td { padding: 24px; font-size: 12px; border-bottom: 1px solid #0f172a; vertical-align: middle; }
.post-queue-table tr:hover td { background: rgba(99,102,241,0.04); }
.char-counter { font-size: 11px; color: #475569; text-align: right; margin-top: 4px; }
.char-counter.warn { color: #f59e0b; }
.char-counter.over { color: #ef4444; }
.schedule-row { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; margin-top: 8px; }
.schedule-toggle { display: flex; gap: 16px; }
.sched-btn { padding: 24px; 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 */
.action-required-cell { font-size: 12px; font-weight: 700; color: #f97316; }
.action-none { color: #374151; font-size: 12px; }
/* State */
.state-box { text-align: center; padding: 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: 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: 16px; z-index: 999; }
.toast { padding: 24px; 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; }
.toast-info { background: #1e2737; color: #94a3b8; border: 1px solid #2d3748; }
.toast-warning { background: #451a03; color: #fdba74; border: 1px solid #d97706; }
/* 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: 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: 16px; justify-content: flex-end; }
.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: 16px; }
.rotation-log-item:last-child { border-bottom: none; }
</style>
</head>
<body>
<nav class="top-nav" aria-label="Main navigation">
<div class="nav-logo"> ARGUS Social</div>
<div class="nav-links">
<button class="nav-link active">Admin</button>
</div>
<div class="nav-spacer"></div>
<div class="nav-status">
<div class="status-dot" id="statusDot"></div>
<span id="statusText">Ansluter...</span>
</div>
</nav>
<div class="toast-container" id="toastContainer"></div>
<!-- Rotate Confirm Modal -->
<div class="modal-overlay" id="rotateModal">
<div class="modal">
<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()"><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>
<!-- Employee Offboard Modal -->
<div class="modal-overlay" id="offboardModal">
<div class="modal">
<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()"><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>
<div class="main-container">
<div class="page-header">
<div>
<h1>ARGUS Social Media Admin</h1>
<p>Hantera registrering, automation och publicering av sociala mediekonton</p>
</div>
<button class="lv-btn lv-btn--primary" onclick="openAddPlatformModal()">+ Lägg till plattform</button>
</div>
<!-- Company Tabs -->
<div class="company-tabs">
<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')"><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 -->
<div id="panel-register" class="tab-panel visible">
<div class="card">
<div class="card-title">Registrera nytt konto</div>
<div class="form-grid">
<div class="form-group">
<label>Bolag</label>
<select id="regCompany" onchange="onRegCompanyChange()">
<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">
<label>Plattform</label>
<select id="regPlatform" onchange="fillForm(this.value)">
<option value="">— Välj plattform —</option>
</select>
</div>
<div class="form-group">
<label>E-post</label>
<input type="email" id="regEmail" placeholder="social@bolag.com">
</div>
<div class="form-group">
<label>Recovery-mail</label>
<input type="email" id="regRecovery" placeholder="recovery@bolag.com">
</div>
<div class="form-group">
<label>Namn</label>
<input type="text" id="regName" placeholder="Johan Berglund">
</div>
<div class="form-group">
<label>Telefon</label>
<input type="text" id="regPhone" placeholder="+46709123223">
</div>
<div class="form-group">
<label>Webbplats</label>
<input type="text" id="regWebsite" placeholder="https://bolag.com">
</div>
<div class="form-group">
<label>Användarnamn / Handle</label>
<input type="text" id="regUsername" placeholder="@bolag">
</div>
</div>
<div>
<span></span>
<span>Lösenord genereras automatiskt av servern (32+ tecken). Aldrig synligt i UI.</span>
</div>
<div>
<button class="lv-btn lv-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 -->
<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()"><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">
<button class="filter-btn active" data-filter="all" onclick="setJobFilter('all')">Alla</button>
<button class="filter-btn" data-filter="running" onclick="setJobFilter('running')">Aktiva</button>
<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()"><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">
<div class="state-box"><div class="spinner"></div><br>Laddar jobb...</div>
</div>
</div>
</div>
<!-- PANEL: Credentials -->
<div id="panel-credentials" class="tab-panel">
<div class="card">
<div class="creds-header">
<div class="card-title">Credentials — <span id="credsCompanyLabel">LandveX</span></div>
<div class="creds-actions">
<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>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">
<div class="state-box"><div class="spinner"></div><br>Laddar credentials...</div>
</div>
<div id="rotationLog"></div>
</div>
</div>
<!-- PANEL: Publishing -->
<div id="panel-publishing" class="tab-panel">
<!-- Compose -->
<div class="card">
<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">
<label>Bolag</label>
<select id="pubCompany" onchange="onPubCompanyChange()">
<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">
<label>Plattformar</label>
<div class="platform-checkboxes" id="pubPlatformChecks">
<!-- Populated by JS -->
</div>
</div>
<div class="form-group">
<label>Innehåll</label>
<textarea id="pubContent" rows="6" placeholder="Skriv ditt inlägg här..." oninput="updateCharCounter(); updatePreview()"></textarea>
</div>
<div class="char-counter" id="charCounter">0 tecken</div>
<div class="form-group">
<label>Schemaläggning</label>
<div class="schedule-row">
<div class="schedule-toggle">
<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" onchange="updatePreview()">
</div>
</div>
<div>
<button class="lv-btn lv-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>Förhandsvisning</div>
<div id="pubPreviews">
<div>Välj plattformar och skriv innehåll för att se förhandsvisning</div>
</div>
</div>
</div>
</div>
<!-- Post Queue -->
<div class="card">
<div>
<div class="card-title"><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>
</div>
</div>
</div>
</div>
<script>
'use strict';
/*
CONSTANTS
*/
const API = '';
const COMPANY_CREDS = {
landvex: { email:'social@landvex.com', recovery:'recovery@landvex.com', name:'Johan Berglund', phone:'+46709123223', website:'https://landvex.se', company:'LandveX' },
quixzoom: { email:'social@quixzoom.com', recovery:'recovery@quixzoom.com', name:'Johan Berglund', phone:'+46709123223', website:'https://quixzoom.com', company:'QuiXzoom' },
apifly: { email:'social@apifly.com', recovery:'recovery@apifly.com', name:'Johan Berglund', phone:'+46709123223', website:'https://apifly.com', company:'ApiFly' },
corpfitt: { email:'social@corpfitt.com', recovery:'recovery@corpfitt.com', name:'Johan Berglund', phone:'+46709123223', website:'https://corpfitt.com', company:'CorpFitt' },
vyra: { email:'social@vyra.gg', recovery:'recovery@vyra.gg', name:'Johan Berglund', phone:'+46709123223', website:'https://vyra.gg', company:'VYRA' },
aamos: { email:'social@aamos.ai', recovery:'recovery@aamos.ai', name:'Johan Berglund', phone:'+46709123223', website:'https://aamos.ai', company:'AAMOS' },
};
const COMPANY_LABELS = {
landvex:'LandveX', quixzoom:'QuiXzoom', apifly:'ApiFly',
corpfitt:'CorpFitt', vyra:'VYRA', aamos:'AAMOS'
};
const PLATFORM_CHAR_LIMITS = {
twitter: 280, linkedin: 3000, reddit: 40000,
instagram: 2200, facebook: 63206, tiktok: 2200,
pinterest: 500, spotify: 200, youtube: 5000,
snapchat: 250
};
const PLATFORM_ICONS = {
twitter:'', instagram:'', facebook:'', linkedin:'',
tiktok:'', youtube:'', reddit:'', snapchat:'',
pinterest:'', spotify:'', meta:'',
jobteaser:'', hired:'', outbrain:''
};
const PLATFORM_LOGIN = {
pinterest:'https://www.pinterest.com/login/',
tiktok:'https://www.tiktok.com/login/',
instagram:'https://www.instagram.com/accounts/login/',
snapchat:'https://accounts.snapchat.com/',
reddit:'https://www.reddit.com/login/',
linkedin:'https://www.linkedin.com/login/',
twitter:'https://twitter.com/login',
meta:'https://business.facebook.com/',
google:'https://ads.google.com/',
spotify:'https://accounts.spotify.com/login',
indeed:'https://employers.indeed.com/',
monster:'https://hiring.monster.com/',
crunchbase:'https://www.crunchbase.com/login',
producthunt:'https://www.producthunt.com/login',
thehub:'https://thehub.io/login',
stepstone:'https://www.stepstone.com/',
reed:'https://www.reed.co.uk/login',
ziprecruiter:'https://www.ziprecruiter.com/login',
};
/*
PLATFORM STEPS
Fix #5: replaced manual mail instructions with auto-fetch note
*/
const PLATFORM_STEPS = {
tiktok: {
icon: '',
title: 'TikTok for Business',
mailbox: true,
steps: [
'1. Klicka på knappen <b>Öppna TikTok</b> nedan',
'2. Ange e-post: <code>{email}</code>',
'3. TikTok skickar en 6-siffrig verifieringskod till den e-posten',
'4. Vi hämtar koden automatiskt från mailboxen &mdash; klicka <b>Hämta kod</b> nedan',
'5. Klistra in koden på TikTok och slutför inloggningen',
'6. Klart! Tryck <b>Godkänn</b> här när du är klar'
]
},
instagram: {
icon: '',
title: 'Instagram Business',
mailbox: true,
steps: [
'1. Klicka på knappen <b>Öppna Instagram</b> nedan',
'2. Välj "Registrera med e-post"',
'3. Ange e-post: <code>{email}</code>',
'4. Välj användarnamn: <code>{username}</code>',
'5. Vi hämtar koden automatiskt från mailboxen &mdash; klicka <b>Hämta kod</b> nedan',
'6. Ange koden på Instagram och slutför registreringen',
'7. Tryck <b>Godkänn</b> här när kontot är skapat'
]
},
snapchat: {
icon: '',
title: 'Snapchat Business',
mailbox: true,
steps: [
'1. Klicka på knappen <b>Öppna Snapchat</b> nedan',
'2. Klicka "Create Business Account"',
'3. Ange e-post: <code>{email}</code>',
'4. Vi hämtar koden automatiskt från mailboxen &mdash; klicka <b>Hämta kod</b> nedan',
'5. Ange koden och slutför registreringen',
'6. Tryck <b>Godkänn</b> här när klart'
]
},
reddit: {
icon: '',
title: 'Reddit Ads',
mailbox: false,
steps: [
'1. Klicka på knappen <b>Öppna Reddit</b> nedan',
'2. Ange e-post: <code>{email}</code>',
'3. OBS: Användarnamnet <b>"{username}"</b> kan vara taget',
'4. Välj ett alternativt namn om det krävs, t.ex. <b>LandveX_AB</b>',
'5. Slutför registreringen',
'6. Tryck <b>Godkänn</b> här när kontot är skapat'
]
},
default: {
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: [
'1. Botten har fyllt i formuläret automatiskt',
'2. Granska skärmdumpen ovan &mdash; ser den korrekt ut?',
'3. Om allt ser bra ut &mdash; klicka <b>Godkänn</b> för att slutföra',
'4. Verifieringsmail kan ha skickats till <code>{email}</code> om kontot kräver det'
]
}
};
/*
STATE
*/
let currentCompany = 'landvex';
let currentMainTab = 'register';
let currentJobFilter = 'all';
let allJobs = [];
let platformList = [];
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,'/')));
return payload.exp ? (payload.exp * 1000 < Date.now() + 60000) : false;
} catch(e) { return true; }
}
async function ensureAuth() {
let tok = localStorage.getItem('aamos_token');
if (tok && !jwtExpired(tok)) return tok;
try {
const r = await fetch(API + '/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'erik@hypbit.com', password: 'WavultCEO2026!' })
});
if (!r.ok) throw new Error('Login HTTP ' + r.status);
const d = await r.json();
tok = d.token || d.access_token || '';
if (tok) localStorage.setItem('aamos_token', tok);
return tok;
} catch(e) {
console.error('ensureAuth failed:', e);
return '';
}
}
function getToken() { return localStorage.getItem('aamos_token') || ''; }
function authHeaders() {
return { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + getToken() };
}
function getAuthToken() {
try {
const keys = ['qz_auth_token','aamos_token','qz_token'];
for (const k of keys) { const t = localStorage.getItem(k); if (t) return t; }
} catch(e) {}
return getToken();
}
/*
TOAST
*/
function toast(msg, type, durationMs) {
type = type || 'info';
durationMs = durationMs || 4000;
const c = document.getElementById('toastContainer');
const t = document.createElement('div');
t.className = 'toast toast-' + type;
t.textContent = msg;
c.appendChild(t);
setTimeout(function() { t.remove(); }, durationMs);
}
/*
PLATFORMS
*/
async function loadPlatforms() {
try {
await ensureAuth();
const r = await fetch(API + '/api/social-account/platforms', { headers: authHeaders() });
if (!r.ok) throw new Error('HTTP ' + r.status);
const data = await r.json();
const raw = Array.isArray(data) ? data : (data.platforms || []);
platformList = raw.map(function(p) {
return typeof p === 'object' ? (p.platform || p.name || p.id || String(p)) : String(p);
});
} catch(e) {
console.warn('loadPlatforms failed, using fallback:', e);
platformList = ['twitter','instagram','facebook','linkedin','tiktok','youtube','reddit','pinterest'];
}
renderPlatformDropdown();
renderPubPlatformChecks();
}
function renderPlatformDropdown() {
const sel = document.getElementById('regPlatform');
if (!sel) return;
sel.innerHTML = '<option value="">— Välj plattform —</option>';
platformList.forEach(function(name) {
const opt = document.createElement('option');
opt.value = name;
opt.textContent = (PLATFORM_ICONS[name] || '') + ' ' + name.charAt(0).toUpperCase() + name.slice(1);
sel.appendChild(opt);
});
}
function renderPubPlatformChecks() {
const container = document.getElementById('pubPlatformChecks');
if (!container) return;
container.innerHTML = '';
platformList.forEach(function(name) {
const id = 'pubChk_' + name;
const wrap = document.createElement('label');
wrap.className = 'platform-checkbox';
wrap.style.cssText = 'display:flex;align-items:center;gap: 16px;cursor:pointer;padding: 24px;border:1px solid #1e2737;border-radius: var(--radius-md);';
wrap.innerHTML = '<input type="checkbox" id="' + id + '" value="' + name + '" onchange="updatePreview()"><span>' + (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();
const creds = COMPANY_CREDS[currentCompany] || {};
setVal('regEmail', creds.email || '');
setVal('regRecovery', creds.recovery || '');
setVal('regName', creds.name || '');
setVal('regPhone', creds.phone || '');
setVal('regWebsite', creds.website || '');
try {
const r = await fetch(API + '/api/social-account/credentials/' + encodeURIComponent(currentCompany), {
headers: authHeaders()
});
if (r.ok) {
const data = await r.json();
const acc = data && data.accounts ? data.accounts[platform] : null;
if (acc && acc.username) {
setVal('regUsername', '@' + acc.username);
return;
}
}
} catch(e) { /* fallback */ }
const handleBase = (creds.company || currentCompany).toLowerCase().replace(/[^a-z0-9]/g, '');
setVal('regUsername', '@' + handleBase);
}
function setVal(id, val) {
const el = document.getElementById(id);
if (el) el.value = val;
}
function onRegCompanyChange() {
const sel = document.getElementById('regCompany');
currentCompany = sel ? sel.value : 'landvex';
syncCompanyTabs();
const plat = document.getElementById('regPlatform');
if (plat && plat.value) fillForm(plat.value);
}
function clearRegForm() {
['regEmail','regRecovery','regName','regPhone','regWebsite','regUsername'].forEach(function(id) { setVal(id, ''); });
const ps = document.getElementById('regPlatform');
if (ps) ps.value = '';
}
async function startRegistration() {
const company = document.getElementById('regCompany') ? document.getElementById('regCompany').value : currentCompany;
const platform = document.getElementById('regPlatform') ? document.getElementById('regPlatform').value : '';
const email = document.getElementById('regEmail') ? document.getElementById('regEmail').value : '';
const username = document.getElementById('regUsername') ? document.getElementById('regUsername').value : '';
const recovery = document.getElementById('regRecovery') ? document.getElementById('regRecovery').value : '';
const name = document.getElementById('regName') ? document.getElementById('regName').value : '';
const phone = document.getElementById('regPhone') ? document.getElementById('regPhone').value : '';
const website = document.getElementById('regWebsite') ? document.getElementById('regWebsite').value : '';
if (!platform) { toast('Välj en plattform först', 'error'); return; }
if (!email) { toast('E-post krävs', 'error'); return; }
const payload = {
company: company,
platform: platform,
email: email,
username: username.replace(/^@/, ''),
recovery_email: recovery,
name: name,
phone: phone,
website: website
};
try {
const r = await fetch(API + '/api/social-account/register', {
method: 'POST',
headers: authHeaders(),
body: JSON.stringify(payload)
});
const d = await r.json().catch(function() { return {}; });
if (r.ok) {
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('<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('<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();
const sel = document.getElementById('regCompany');
if (sel) sel.value = company;
document.getElementById('credsCompanyLabel').textContent = COMPANY_LABELS[company] || company;
if (currentMainTab === 'credentials') loadCredentials();
const pubSel = document.getElementById('pubCompany');
if (pubSel) pubSel.value = company;
}
function syncCompanyTabs() {
// Räkna jobb per bolag
const counts = {};
allJobs.forEach(function(j) {
const co = (j.company || '').toLowerCase();
const s = (j.status || '').toLowerCase();
if (!counts[co]) counts[co] = {total:0, done:0, running:0, error:0, manual:0, awaiting:0};
counts[co].total++;
if (s === 'completed' || s === 'done' || s === 'success') counts[co].done++;
else if (s === 'launching' || s === 'navigating' || s === 'filling_form' || s === 'solving_captcha' || s === 'submitting' || s === 'queued') counts[co].running++;
else if (s === 'failed' || s === 'timeout' || s === 'error') counts[co].error++;
else if (s === 'awaiting_approval') counts[co].awaiting++;
else if (s === 'pending_manual' || s === 'manual_required' || s === 'manual_handle_review') counts[co].manual++;
});
document.querySelectorAll('.company-tab').forEach(function(btn) {
const co = btn.dataset.company;
const c = counts[co] || {total:0, done:0, running:0, error:0, awaiting:0, manual:0};
const isActive = co === currentCompany;
btn.classList.toggle('active', isActive);
// Bygg badge-html
let badges = '';
if (c.done > 0) badges += '<span><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><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>⏳'+c.awaiting+'</span>';
if (c.error > 0) badges += '<span><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>'+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) {
btn.classList.toggle('active', btn.dataset.tab === tab);
});
document.querySelectorAll('.tab-panel').forEach(function(p) {
const show = p.id === 'panel-' + tab;
p.classList.toggle('visible', show);
p.style.display = show ? '' : 'none';
});
if (tab === 'jobs') loadJobs();
if (tab === 'credentials') loadCredentials();
if (tab === 'publishing') loadPostQueue();
}
/*
JOBS
*/
function setJobFilter(filter) {
currentJobFilter = filter;
document.querySelectorAll('.filter-btn').forEach(function(btn) {
btn.classList.toggle('active', btn.dataset.filter === filter);
});
renderJobs();
}
async function loadJobs() {
const container = document.getElementById('jobsContent');
if (!container) return;
try {
const r = await fetch(API + '/api/social-account/jobs', { headers: authHeaders() });
if (!r.ok) throw new Error('HTTP ' + r.status);
const data = await r.json();
allJobs = Array.isArray(data) ? data : (data.jobs || data.data || []);
renderJobs();
updateApproveAllBanner();
} catch(e) {
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>';
}
}
function filterJobs() {
// Filtrera alltid på valt bolag FÖRST
let jobs = allJobs.filter(function(j) {
return (j.company || '').toLowerCase() === (currentCompany || '').toLowerCase();
});
// Sedan filtrera på status
if (currentJobFilter === 'all') return jobs;
return jobs.filter(function(j) {
const s = (j.status || '').toLowerCase();
if (currentJobFilter === 'running') return s === 'running' || s === 'launching' || s === 'navigating' || s === 'filling_form' || s === 'solving_captcha' || s === 'submitting' || s === 'pending' || s === 'queued';
if (currentJobFilter === 'done') return s === 'done' || s === 'completed' || s === 'success';
if (currentJobFilter === 'error') return s === 'error' || s === 'failed' || s === 'timeout';
if (currentJobFilter === 'awaiting') return s === 'awaiting_approval';
if (currentJobFilter === 'manual') return s === 'pending_manual' || s === 'manual_required' || s === 'manual_handle_review';
return true;
});
}
function updateApproveAllBanner() {
const banner = document.getElementById('approveAllBanner');
const text = document.getElementById('approveAllText');
if (!banner) return;
const awaitingJobs = allJobs.filter(function(j) {
return (j.status || '').toLowerCase() === 'awaiting_approval';
});
if (awaitingJobs.length > 0) {
text.textContent = '⏳ ' + awaitingJobs.length + ' jobb väntar på godkännande';
banner.classList.add('visible');
} else {
banner.classList.remove('visible');
}
}
async function approveAll() {
const awaitingJobs = allJobs.filter(function(j) {
return (j.status || '').toLowerCase() === 'awaiting_approval';
});
if (awaitingJobs.length === 0) { toast('Inga jobb att godkänna', 'info'); return; }
toast('Godkänner ' + awaitingJobs.length + ' jobb...', 'info', 3000);
let ok = 0, fail = 0;
for (const job of awaitingJobs) {
const id = job.jobId || job.id || job.job_id;
try {
const r = await fetch(API + '/api/social-account/approve/' + encodeURIComponent(id), {
method: 'POST', headers: authHeaders()
});
if (r.ok) { ok++; } else { fail++; }
} catch(e) { fail++; }
}
if (fail === 0) {
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('<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) */
async function buildStepsHtml(job) {
const plat = (job.platform || '').toLowerCase();
const info = PLATFORM_STEPS[plat] || PLATFORM_STEPS.default;
const prof = job.profile || {};
const platAcct = (prof.accounts || {})[plat] || {};
const email = esc(prof.email || job.profileEmail || '?');
const username = esc(platAcct.username || prof.username || job.profileUsername || (job.extraData || {}).username || '?');
const status = (job.status || '').toLowerCase();
const isManual = ['awaiting_approval','pending_manual','manual_required','manual_handle_review'].includes(status);
const id = job.jobId || job.id || job.job_id || 'x';
const autofillUrl = '/autofill-login.html?company=' + encodeURIComponent(job.company || '') +
'&platform=' + encodeURIComponent(plat) +
'&token=' + encodeURIComponent(getAuthToken());
if (!isManual) {
return '<a href="' + autofillUrl + '" target="_blank" rel="noopener" ' +
'>' +
'Logga in på ' + esc(job.platform) + '</a>';
}
/* Build steps list */
const stepsHtml = info.steps.map(function(s) {
return '<li>' +
s.replace(/{email}/g, '<code>' + email + '</code>')
.replace(/{username}/g, '<code>' + username + '</code>') +
'</li>';
}).join('');
/* Fix #4: Credentials block for manual-status jobs */
let credsBlock = '';
let revealedPw = platAcct.password || prof.password || '';
if (job.company) {
try {
const credRes = await fetch(API + '/api/social-account/credentials?company=' +
encodeURIComponent(job.company) + '&reveal=1', { headers: authHeaders() });
if (credRes.ok) {
const creds = await credRes.json();
const arr = Array.isArray(creds) ? creds : (creds.credentials || []);
const match = arr.find(function(c) { return c.platform === plat; });
if (match) {
if (match.password && match.password !== '' && match.password !== '•••••') revealedPw = match.password;
}
}
} catch(e) { /* ignore */ }
}
const safeId = String(id).replace(/[^a-z0-9\-]/gi, '');
credsBlock = '<div class="inline-creds">' +
'<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>' +
'<button class="copy-btn" onclick="copyText(\'' + email.replace(/'/g, "\\'") + '\', this)">Kopiera</button>' +
'</div>' +
'<div class="cred-inline-row">' +
'<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="copy-btn" onclick="copyText(\'' + revealedPw.replace(/'/g, "\\'") + '\', this)">Kopiera</button>'
: '<span> Ej tillgängligt</span>'
) +
'</div>' +
'</div>';
/* Fix #3: Mail-code fetch button */
const mailCodeHtml = info.mailbox
? '<div class="mail-code-box">' +
'<div> Väntar på verifieringskod via mail</div>' +
'<button class="btn btn-teal btn-sm" onclick="fetchMailCode(\'' + safeId + '\', \'' + esc(plat) + '\', this)">' +
' Hämta kod</button>' +
'<div id="mail-code-result-' + safeId + '" class="mail-code-result hidden"></div>' +
'<div>' +
'<a href="https://mail.quixzoom.com/sso/login" target="_blank" rel="noopener" ' +
'>Eller öppna mailboxen manuellt →</a>' +
'</div>' +
'</div>'
: '';
const loginBtn = '<a href="' + autofillUrl + '" target="_blank" rel="noopener" ' +
'>' +
info.icon + ' Logga in på ' + esc(job.platform) + ' &rarr;</a>';
return '<div>' +
'<div>' + info.icon + ' Gör så här:</div>' +
'<ol>' + stepsHtml + '</ol>' +
credsBlock +
mailCodeHtml +
'<div>' + loginBtn + '</div>' +
'</div>';
}
/* Fix #3: Fetch mail code */
async function fetchMailCode(jobId, platform, btn) {
const resultEl = document.getElementById('mail-code-result-' + jobId);
btn.disabled = true;
btn.textContent = '⏳ Hämtar...';
try {
const r = await fetch(API + '/api/social-account/fetch-mail-code?jobId=' +
encodeURIComponent(jobId) + '&platform=' + encodeURIComponent(platform), {
headers: authHeaders()
});
if (r.ok) {
const d = await r.json();
const code = d.code || d.verification_code || d.otp || '';
if (code && resultEl) {
resultEl.classList.remove('hidden');
resultEl.innerHTML = 'Kod: <strong>' + esc(code) + '</strong>' +
'<button class="copy-btn" onclick="copyText(\'' + esc(code) + '\', this)">Kopiera</button>';
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('<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';
}
} else if (r.status === 404) {
/* Endpoint doesn't exist — fallback to open mailbox */
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';
} else {
const d = await r.json().catch(function() { return {}; });
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';
}
} 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> Nätverksfel: ' + e.message, 'error');
btn.disabled = false;
btn.textContent = ' Hämta kod';
}
}
/* 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>';
return;
}
let html = '<div class="jobs-table-wrap"><table class="jobs-table">';
html += '<thead><tr>' +
'<th>ID</th><th>Bolag</th><th>Plattform</th>' +
'<th>Status</th><th>Åtgärd krävs</th>' +
'<th>Skapad</th><th>Screenshot</th><th>Åtgärder</th>' +
'</tr></thead><tbody>';
for (const job of jobs) {
const id = job.jobId || job.id || job.job_id || '—';
const company = esc(job.company || job.company_id || '—');
const platform = esc(job.platform || job.platform_id || '—');
const status = (job.status || 'unknown').toLowerCase().replace(/ /g, '_');
const created = (job.createdAt || job.created_at)
? new Date(job.createdAt || job.created_at).toLocaleString('sv-SE')
: '—';
const isAwait = status === 'awaiting_approval';
const isManual = status === 'pending_manual' || status === 'manual_required' || status === 'manual_handle_review';
const badgeMap = {
running: 'badge-running', in_progress: 'badge-running', pending: 'badge-running', submitting: 'badge-running',
done: 'badge-done', completed: 'badge-done', success: 'badge-done',
error: 'badge-error', failed: 'badge-error',
timeout: 'badge-timeout',
awaiting_approval: 'badge-approval',
pending_manual: 'badge-manual', manual_required: 'badge-manual', manual_handle_review: 'badge-manual',
cancelled: 'badge-cancelled'
};
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: '<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"><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);
const showScreenshot = ['awaiting_approval','completed','failed','pending_manual','manual_required','manual_handle_review'].includes(status);
let screenshotHtml = '<span class="no-screenshot">—</span>';
if (showScreenshot) {
screenshotHtml = '<img class="screenshot-thumb" src="' + screenshotUrl + '" ' +
'onerror="this.style.display=\'none\'" ' +
'onclick="window.open(\'' + screenshotUrl + '\',\'_blank\')" ' +
'alt="screenshot" loading="lazy">';
}
/* Action buttons */
let actions = '';
if (isAwait) {
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>';
}
/* Build the steps block (async) */
const stepsBlock = await buildStepsHtml(job);
html += '<tr' + (isAwait ? ' class="row-awaiting"' : '') + '>';
html += '<td><code>' + esc(String(id).slice(0, 12)) + '</code></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>' + created + '</td>';
html += '<td>' + screenshotHtml + '</td>';
html += '<td>' + actions + '</td>';
html += '</tr>';
}
html += '</tbody></table></div>';
container.innerHTML = html;
}
async function approveJob(jobId) {
try {
const r = await fetch(API + '/api/social-account/approve/' + encodeURIComponent(jobId), {
method: 'POST', headers: authHeaders()
});
if (r.ok) {
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('<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('<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) {
if (!confirm('Avvisa jobb ' + jobId + '?')) return;
await deleteJob(jobId);
}
async function deleteJob(jobId) {
try {
const r = await fetch(API + '/api/social-account/jobs/' + encodeURIComponent(jobId), {
method: 'DELETE', headers: authHeaders()
});
if (r.ok) {
toast(' Jobb raderat', 'info');
allJobs = allJobs.filter(function(j) {
const jid = j.jobId || j.id || j.job_id;
return String(jid) !== String(jobId);
});
renderJobs();
} else {
const d = await r.json().catch(function() { return {}; });
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('<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 */
function startAutoRefresh() {
stopAutoRefresh();
refreshCountdown = 15;
updateRefreshInfo();
countdownTimer = setInterval(function() {
refreshCountdown--;
updateRefreshInfo();
if (refreshCountdown <= 0) {
refreshCountdown = 15;
if (currentMainTab === 'jobs') loadJobs();
}
}, 1000);
}
function stopAutoRefresh() {
if (countdownTimer) { clearInterval(countdownTimer); countdownTimer = null; }
}
function updateRefreshInfo() {
const el = document.getElementById('refreshInfo');
if (el) el.textContent = 'Auto-refresh om ' + refreshCountdown + 's';
}
/*
CREDENTIALS
*/
async function loadCredentials() {
const container = document.getElementById('credsContent');
if (!container) return;
container.innerHTML = '<div class="state-box"><div class="spinner"></div><br>Laddar credentials för ' + esc(currentCompany) + '...</div>';
document.getElementById('credsCompanyLabel').textContent = COMPANY_LABELS[currentCompany] || currentCompany;
try {
await ensureAuth();
const r = await fetch(API + '/api/social-account/credentials?company=' +
encodeURIComponent(currentCompany) + '&reveal=1', { headers: authHeaders() });
if (!r.ok) throw new Error('HTTP ' + r.status);
const data = await r.json();
let creds = [];
if (Array.isArray(data)) {
creds = data;
} else if (data.credentials) {
creds = data.credentials;
} else if (data.accounts) {
const email = data.email || '';
creds = Object.entries(data.accounts).map(function(entry) {
return { platform: entry[0], email: email, username: entry[1].username || '', password: entry[1].password || '' };
});
}
renderCredentials(creds);
} catch(e) {
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>';
}
}
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"><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;
}
let html = '<div class="creds-grid">';
creds.forEach(function(c, idx) {
const platform = esc(c.platform || '?');
const email = esc(c.email || '—');
const username = esc(c.username || '—');
const icon = PLATFORM_ICONS[c.platform] || '';
const hasPw = c.password && c.password !== '' && c.password !== '•••••';
const pwId = 'cred-pw-' + idx;
html += '<div class="cred-card">';
html += '<div class="cred-platform">' + icon + ' ' + platform + '</div>';
html += '<div class="cred-rows">';
html += '<div class="cred-row"><span class="cred-label">E-post</span>' +
'<span class="cred-value">' + email + '</span>' +
'<button class="copy-btn" onclick="copyText(\'' + email.replace(/'/g, "\\'") + '\', this)">Kopiera</button>' +
'</div>';
html += '<div class="cred-row"><span class="cred-label">Handle</span>' +
'<span class="cred-value">@' + username + '</span></div>';
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="copy-btn" onclick="copyText(\'' + c.password.replace(/'/g, "\\'") + '\', this)">Kopiera</button>'
: '<span class="cred-value"> Krypterat (AES-256)</span>'
) +
'</div>';
html += '</div></div>';
});
html += '</div>';
container.innerHTML = html;
}
/* Password Rotation */
let rotateTarget = null;
function openRotateModal() {
rotateTarget = currentCompany;
document.getElementById('rotateModalText').textContent =
'Generera nya starka lösenord (32+ tecken) för alla plattformar på ' +
(COMPANY_LABELS[currentCompany] || currentCompany) + '? Detta kan inte ångras.';
document.getElementById('rotateModal').classList.add('visible');
}
function closeRotateModal() {
document.getElementById('rotateModal').classList.remove('visible');
}
async function confirmRotate() {
closeRotateModal();
const companies = rotateTarget === 'ALL' ? Object.keys(COMPANY_LABELS) : [rotateTarget];
let rotated = 0, failed = 0;
const logItems = [];
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 {
const url = company === 'ALL'
? API + '/api/social-account/credentials-all/rotate'
: API + '/api/social-account/credentials/' + company + '/rotate';
const r = await fetch(url, {
method: 'POST',
headers: authHeaders(),
body: JSON.stringify({ performedBy: 'admin' })
});
if (r.ok) {
const d = await r.json();
rotated++;
logItems.push({ company: company, status: 'ok', platforms: (d.rotated || []).length, ts: new Date().toLocaleString('sv-SE') });
} else {
failed++;
logItems.push({ company: company, status: 'error', ts: new Date().toLocaleString('sv-SE') });
}
} catch(e) {
failed++;
logItems.push({ company: company, status: 'error', error: e.message, ts: new Date().toLocaleString('sv-SE') });
}
}
if (failed === 0) {
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('<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');
if (logDiv) {
let logHtml = '<div><div class="card-title">Rotationslogg</div><div class="rotation-log">';
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' ? '<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>' + esc(item.ts) + '</span>';
logHtml += '</div>';
});
logHtml += '</div></div>';
logDiv.innerHTML = logHtml;
logDiv.style.display = '';
}
if (currentMainTab === 'credentials') loadCredentials();
}
async function confirmOffboard() {
document.getElementById('offboardModal').classList.remove('visible');
rotateTarget = 'ALL';
document.getElementById('rotateModalText').textContent =
'VARNING: Roterar ALLA lösenord för ALLA 6 bolag och alla plattformar. Använd vid anställds avslut.';
await confirmRotate();
}
/*
PUBLISHING MODULE
*/
function onPubCompanyChange() {
const sel = document.getElementById('pubCompany');
currentCompany = sel ? sel.value : 'landvex';
syncCompanyTabs();
}
function setScheduleMode(mode) {
scheduleMode = mode;
document.getElementById('schedNowBtn').classList.toggle('active', mode === 'now');
document.getElementById('schedLaterBtn').classList.toggle('active', mode === 'later');
const timeInput = document.getElementById('pubScheduleTime');
if (timeInput) timeInput.style.display = mode === 'later' ? '' : 'none';
updatePreview();
}
function getSelectedPlatforms() {
const checks = document.querySelectorAll('#pubPlatformChecks input[type=checkbox]:checked');
return Array.from(checks).map(function(c) { return c.value; });
}
function updateCharCounter() {
const text = document.getElementById('pubContent') ? document.getElementById('pubContent').value : '';
const len = text.length;
const el = document.getElementById('charCounter');
if (!el) return;
const platforms = getSelectedPlatforms();
const minLimit = platforms.length > 0
? Math.min.apply(null, platforms.map(function(p) { return PLATFORM_CHAR_LIMITS[p] || 99999; }))
: 280;
el.textContent = len + ' / ' + minLimit + ' tecken';
el.className = 'char-counter' + (len > minLimit ? ' over' : len > minLimit * 0.9 ? ' warn' : '');
}
function updatePreview() {
const container = document.getElementById('pubPreviews');
if (!container) return;
const text = document.getElementById('pubContent') ? document.getElementById('pubContent').value : '';
const platforms = getSelectedPlatforms();
updateCharCounter();
if (platforms.length === 0 || !text.trim()) {
container.innerHTML = '<div>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] || '';
html += '<div class="post-preview">';
html += '<div class="preview-platform">' + icon + ' ' + esc(plat) + '</div>';
html += '<div class="preview-content">' + esc(preview) + '</div>';
if (text.length > limit) {
html += '<div><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>';
});
container.innerHTML = html;
}
async function submitPost(asDraft) {
asDraft = asDraft || false;
const company = document.getElementById('pubCompany') ? document.getElementById('pubCompany').value : currentCompany;
const platforms = getSelectedPlatforms();
const text = document.getElementById('pubContent') ? document.getElementById('pubContent').value.trim() : '';
const timeInput = document.getElementById('pubScheduleTime') ? document.getElementById('pubScheduleTime').value : '';
const scheduledAt = (scheduleMode === 'later' && timeInput) ? new Date(timeInput).toISOString() : null;
if (platforms.length === 0) { toast('Välj minst en plattform', 'error'); return; }
if (!text) { toast('Innehåll saknas', 'error'); return; }
const status = asDraft ? 'DRAFT' : (scheduledAt ? 'SCHEDULED' : 'PENDING');
try {
await ensureAuth();
const r = await fetch(API + '/api/social-account/publish', {
method: 'POST',
headers: authHeaders(),
body: JSON.stringify({
company: company,
platforms: platforms,
content: { text: text },
scheduledAt: scheduledAt,
status: status
})
});
const d = await r.json().catch(function() { return {}; });
if (r.ok) {
toast(asDraft ? ' Utkast sparat' : ' Inlägg schemalagt/publicerat', 'success');
clearPost();
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> Fel: ' + (d.error || d.message || r.status), '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> Nätverksfel: ' + e.message, 'error');
}
}
function saveDraft() { submitPost(true); }
function clearPost() {
setVal('pubContent', '');
document.querySelectorAll('#pubPlatformChecks input[type=checkbox]').forEach(function(c) { c.checked = false; });
updatePreview();
updateCharCounter();
}
async function loadPostQueue() {
const container = document.getElementById('postQueueContent');
if (!container) return;
container.innerHTML = '<div class="state-box"><div class="spinner"></div><br>Laddar...</div>';
try {
await ensureAuth();
const r = await fetch(API + '/api/social-account/publish', { headers: authHeaders() });
if (!r.ok) throw new Error('HTTP ' + r.status);
const data = await r.json();
const posts = Array.isArray(data) ? data : (data.posts || data.queue || []);
renderPostQueue(posts);
} catch(e) {
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>';
}
}
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>';
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>';
posts.forEach(function(p) {
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(', ')
: esc(String(p.platforms || '—'));
const status = (p.status || 'pending').toLowerCase();
const sched = p.scheduledAt ? new Date(p.scheduledAt).toLocaleString('sv-SE') : (status === 'pending' ? 'Nu' : '—');
const contentText = p.content && p.content.text ? p.content.text : (p.text || '');
const preview = esc(contentText.slice(0, 60)) + (contentText.length > 60 ? '…' : '');
const badgeMap = { scheduled:'badge-scheduled', draft:'badge-draft', published:'badge-published', failed:'badge-failed', pending:'badge-running' };
const badgeCls = badgeMap[status] || 'badge-pending';
html += '<tr>';
html += '<td><code>' + id + '</code></td>';
html += '<td>' + platforms + '</td>';
html += '<td><span class="badge ' + badgeCls + '">' + esc(status) + '</span></td>';
html += '<td>' + esc(sched) + '</td>';
html += '<td>' + preview + '</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>';
container.innerHTML = html;
}
async function deletePost(postId) {
if (!confirm('Ta bort inlägg?')) return;
try {
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('<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;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function copyText(text, btn) {
navigator.clipboard.writeText(text).then(function() {
const orig = 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('<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');
});
}
function togglePw(inputId, btn) {
const input = document.getElementById(inputId);
if (!input) return;
if (input.type === 'password') {
input.type = 'text';
btn.textContent = ' Dölj';
} else {
input.type = 'password';
btn.textContent = ' Visa';
}
}
function setStatus(online) {
const dot = document.getElementById('statusDot');
const text = document.getElementById('statusText');
if (dot) dot.style.background = online ? '#22c55e' : '#ef4444';
if (text) text.textContent = online ? 'Online' : 'Offline';
}
/*
INIT
*/
async function init() {
try {
const tok = await ensureAuth();
setStatus(!!tok);
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();
console.log('[ARGUS] Init complete — token:', tok ? tok.slice(0, 20) + '...' : 'EMPTY');
} catch(e) {
console.error('[ARGUS] Init error:', e);
setStatus(false);
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;
const action = btn.getAttribute('data-action');
const jobId = btn.getAttribute('data-jobid');
if (!jobId || jobId === '—') return;
if (action === 'approve') approveJob(jobId);
else if (action === 'reject') rejectJob(jobId);
else if (action === 'delete') deleteJob(jobId);
});
/*
ADD PLATFORM MODAL
*/
function openAddPlatformModal() {
var existing = document.getElementById('addPlatformModal');
if (existing) {
existing.classList.add('visible');
return;
}
// Create overlay
var overlay = document.createElement('div');
overlay.id = 'addPlatformModal';
overlay.className = 'modal-overlay';
overlay.style.cssText = 'z-index:1000;';
overlay.addEventListener('click', function(e) { if (e.target === overlay) closeAddPlatformModal(); });
var companies = ['landvex','quixzoom','apifly','corpfitt','vyra','aamos'];
var companyLabels = {landvex:'LandveX',quixzoom:'QuiXzoom',apifly:'ApiFly',corpfitt:'CorpFitt',vyra:'VYRA',aamos:'AAMOS'};
var html = [
'<div>',
'<div>',
'<h3>+ Lägg till plattform</h3>',
'<button onclick="closeAddPlatformModal()" onmouseover="this.style.color=\'#e2e8f0\'" onmouseout="this.style.color=\'#64748b\'">&#x2715;</button>',
'</div>',
'<div>',
'<div class="form-group">',
'<label>Plattformsnamn</label>',
'<input type="text" id="ap_name" placeholder="t.ex. Mastodon">',
'</div>',
'<div class="form-group">',
'<label>Ikon/Emoji</label>',
'<input type="text" id="ap_icon" placeholder="" value="">',
'</div>',
'<div class="form-group">',
'<label>Registrerings-URL</label>',
'<input type="url" id="ap_url" placeholder="https://platform.com/signup">',
'</div>',
'<div class="form-group">',
'<label>Login-URL</label>',
'<input type="url" id="ap_login" placeholder="https://platform.com/login">',
'</div>',
'<div class="form-group">',
'<label>Teckenlängd max</label>',
'<input type="number" id="ap_charlimit" placeholder="500">',
'</div>',
'</div>',
'<div class="form-group">',
'<label>Bolag</label>',
'<div>']
.concat(companies.map(function(c) {
return '<label>' +
'<input type="checkbox" id="ap_co_' + c + '" value="' + c + '"> ' + companyLabels[c] + '</label>';
}))
.concat(['</div></div>',
'<div class="form-group">',
'<label>Anteckning / Särskilda krav</label>',
'<textarea id="ap_notes" rows="3" placeholder="t.ex. kräver företagsverifiering, manuell godkännande..."></textarea>',
'</div>',
'<div id="ap_confirm">',
'<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>',
'<button class="btn btn-secondary" onclick="closeAddPlatformModal()">Avbryt</button>',
'<button class="lv-btn lv-btn--primary" onclick="saveAddPlatform()">+ Spara plattform</button>',
'</div>',
'</div>'
]);
overlay.innerHTML = html.join('');
document.body.appendChild(overlay);
requestAnimationFrame(function() { overlay.classList.add('visible'); });
// Focus name field
setTimeout(function() {
var nameEl = document.getElementById('ap_name');
if (nameEl) nameEl.focus();
}, 150);
}
function closeAddPlatformModal() {
var overlay = document.getElementById('addPlatformModal');
if (overlay) overlay.classList.remove('visible');
}
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 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;
var notes = (document.getElementById('ap_notes') ? document.getElementById('ap_notes').value : '').trim();
var selectedCompanies = Array.from(document.querySelectorAll('#addPlatformModal input[type=checkbox]:checked'))
.map(function(c) { return c.value; });
if (!name) { toast('Plattformsnamn krävs', 'error'); return; }
var platformData = {
name: name.toLowerCase().replace(/[^a-z0-9]/g, ''),
displayName: name,
icon: icon,
signupUrl: url,
loginUrl: login,
charLimit: charLimit,
companies: selectedCompanies,
notes: notes,
addedAt: new Date().toISOString()
};
// Try API endpoint first
var saved = false;
try {
await ensureAuth();
var r = await fetch(API + '/api/social-account/custom-platform', {
method: 'POST',
headers: authHeaders(),
body: JSON.stringify(platformData)
});
if (r.ok) {
saved = true;
}
} catch(e) { /* fallback to localStorage */ }
// Fallback: localStorage
if (!saved) {
try {
var existing = JSON.parse(localStorage.getItem('argus_custom_platforms') || '[]');
existing.push(platformData);
localStorage.setItem('argus_custom_platforms', JSON.stringify(existing));
saved = true;
} 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
var key = platformData.name;
if (key && !platformList.includes(key)) {
platformList.push(key);
PLATFORM_ICONS[key] = icon;
if (charLimit) PLATFORM_CHAR_LIMITS[key] = charLimit;
if (login) PLATFORM_LOGIN[key] = login;
renderPlatformDropdown();
renderPubPlatformChecks();
}
// Show confirmation
var confirmEl = document.getElementById('ap_confirm');
if (confirmEl) confirmEl.style.display = '';
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);
}
</script>
</body>
</html>