2026-07-05 06:41:32 +00:00
<!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 >
< 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 : 100 vh ;
font-size : 14 px ;
}
a { color : #6366f1 ; text-decoration : none ; }
a : hover { text-decoration : underline ; }
. top-nav {
background : #0d1117 ;
border-bottom : 1 px solid #1e2737 ;
padding : 0 24 px ;
display : flex ;
align-items : center ;
gap : 32 px ;
height : 56 px ;
position : sticky ;
top : 0 ;
z-index : 100 ;
}
. nav-logo { font-weight : 700 ; font-size : 18 px ; color : #6366f1 ; letter-spacing : -0.5 px ; }
. nav-links { display : flex ; gap : 4 px ; }
. nav-link {
2026-07-07 07:11:50 +00:00
padding : 6 px 14 px ; border-radius : var ( - - radius - md ) ; color : #94a3b8 ;
2026-07-05 06:41:32 +00:00
font-size : 13 px ; font-weight : 500 ; cursor : pointer ;
transition : all 0.15 s ; 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 : 8 px ; font-size : 12 px ; color : #64748b ; }
2026-07-07 07:11:50 +00:00
. status-dot { width : 8 px ; height : 8 px ; border-radius : var ( - - radius - full ) ; background : #22c55e ; }
2026-07-05 06:41:32 +00:00
. main-container { max-width : 1400 px ; margin : 0 auto ; padding : 24 px 24 px 48 px ; }
. page-header { margin-bottom : 24 px ; }
. page-header h1 { font-size : 22 px ; font-weight : 700 ; color : #f1f5f9 ; margin-bottom : 4 px ; }
. page-header p { color : #64748b ; font-size : 13 px ; }
. company-tabs { display : flex ; gap : 4 px ; margin-bottom : 24 px ; flex-wrap : wrap ; }
. company-tab {
2026-07-07 07:11:50 +00:00
padding : 8 px 16 px ; border-radius : var ( - - radius - md ) ; border : 1 px solid #1e2737 ;
2026-07-05 06:41:32 +00:00
background : #0d1117 ; color : #94a3b8 ; font-size : 13 px ; font-weight : 500 ;
cursor : pointer ; transition : all 0.15 s ;
}
. 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 : 1 px solid #1e2737 ; margin-bottom : 24 px ;
}
. main-tab {
padding : 10 px 20 px ; border : none ; background : none ; color : #64748b ;
font-size : 13 px ; font-weight : 500 ; cursor : pointer ;
border-bottom : 2 px solid transparent ; transition : all 0.15 s ; margin-bottom : -1 px ;
}
. main-tab : hover { color : #e2e8f0 ; }
. main-tab . active { color : #818cf8 ; border-bottom-color : #6366f1 ; }
. tab-panel { display : none ; }
. tab-panel . visible { display : block ; }
2026-07-07 07:11:50 +00:00
. card { background : #0d1117 ; border : 1 px solid #1e2737 ; border-radius : var ( - - radius - md ) ; padding : 24 px ; }
2026-07-05 06:41:32 +00:00
. card + . card { margin-top : 16 px ; }
. card-title { font-size : 15 px ; font-weight : 600 ; color : #f1f5f9 ; margin-bottom : 16 px ; }
. form-grid { display : grid ; grid-template-columns : repeat ( auto - fill , minmax ( 260 px , 1 fr ) ) ; gap : 16 px ; }
. form-group { display : flex ; flex-direction : column ; gap : 6 px ; }
. form-group label { font-size : 12 px ; font-weight : 500 ; color : #94a3b8 ; text-transform : uppercase ; letter-spacing : 0.5 px ; }
. form-group input , . form-group select , . form-group textarea {
padding : 9 px 12 px ; background : #030712 ; border : 1 px solid #1e2737 ;
2026-07-07 07:11:50 +00:00
border-radius : var ( - - radius - md ) ; color : #e2e8f0 ; font-size : 13 px ; width : 100 % ;
2026-07-05 06:41:32 +00:00
transition : border-color 0.15 s ; outline : none ;
}
. form-group input : focus , . form-group select : focus , . form-group textarea : focus { border-color : #6366f1 ; }
. form-group input :: placeholder { color : #374151 ; }
. btn {
2026-07-07 07:11:50 +00:00
padding : 9 px 18 px ; border-radius : var ( - - radius - md ) ; border : none ; font-size : 13 px ;
2026-07-05 06:41:32 +00:00
font-weight : 600 ; cursor : pointer ; transition : all 0.15 s ;
display : inline-flex ; align-items : center ; gap : 6 px ;
}
. btn-primary { background : #6366f1 ; color : #fff ; }
. btn-primary : hover { background : #4f46e5 ; }
. btn-secondary { background : #1e2737 ; color : #94a3b8 ; border : 1 px 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 ; }
2026-07-07 07:11:50 +00:00
. btn-ghost { background : none ; border : none ; color : #64748b ; cursor : pointer ; padding : 4 px 8 px ; border-radius : var ( - - radius - sm ) ; font-size : 13 px ; transition : all 0.15 s ; }
2026-07-05 06:41:32 +00:00
. btn-ghost : hover { background : #1e2737 ; color : #e2e8f0 ; }
. btn-sm { padding : 5 px 12 px ; font-size : 12 px ; }
. btn-teal { background : #0d9488 ; color : #fff ; }
. btn-teal : hover { background : #0f766e ; }
. filter-row { display : flex ; gap : 8 px ; align-items : center ; margin-bottom : 16 px ; flex-wrap : wrap ; }
2026-07-07 07:11:50 +00:00
. filter-btn { padding : 6 px 14 px ; border-radius : var ( - - radius - xl ) ; border : 1 px solid #1e2737 ; background : none ; color : #64748b ; font-size : 12 px ; cursor : pointer ; transition : all 0.15 s ; }
2026-07-05 06:41:32 +00:00
. 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 : 12 px ; color : #475569 ; }
/* Approve-all banner */
. approve-all-banner {
display : none ;
align-items : center ;
justify-content : space-between ;
background : #0f2818 ;
border : 1 px solid #166534 ;
2026-07-07 07:11:50 +00:00
border-radius : var ( - - radius - md ) ;
2026-07-05 06:41:32 +00:00
padding : 12 px 18 px ;
margin-bottom : 16 px ;
gap : 12 px ;
flex-wrap : wrap ;
}
. approve-all-banner . visible { display : flex ; }
. approve-all-banner-text { color : #86efac ; font-size : 13 px ; font-weight : 600 ; }
. jobs-table-wrap { overflow-x : auto ; }
. jobs-table { width : 100 % ; border-collapse : collapse ; }
. jobs-table th {
padding : 10 px 12 px ; text-align : left ; font-size : 11 px ; font-weight : 700 ;
color : #475569 ; text-transform : uppercase ; letter-spacing : 0.5 px ;
border-bottom : 1 px solid #1e2737 ; white-space : nowrap ;
background : #0a0e1a ;
}
. jobs-table td { padding : 12 px ; font-size : 13 px ; border-bottom : 1 px 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 2 s ease-in-out infinite ; }
/* Larger, clearer badges */
. badge {
display : inline-flex ; align-items : center ; gap : 4 px ;
2026-07-07 07:11:50 +00:00
padding : 4 px 12 px ; border-radius : var ( - - radius - xl ) ; font-size : 12 px ; font-weight : 700 ;
2026-07-05 06:41:32 +00:00
white-space : nowrap ; letter-spacing : 0.2 px ;
}
. badge-running { background : #1e3a5f ; color : #60a5fa ; border : 1 px solid #1d4ed8 ; }
. badge-done { background : #14532d ; color : #4ade80 ; border : 1 px solid #16a34a ; }
. badge-error { background : #450a0a ; color : #f87171 ; border : 1 px solid #dc2626 ; }
. badge-approval { background : #14532d ; color : #86efac ; border : 1 px solid #22c55e ; }
. badge-pending { background : #1e2737 ; color : #94a3b8 ; border : 1 px solid #2d3748 ; }
. badge-cancelled { background : #1e2737 ; color : #64748b ; border : 1 px solid #374151 ; }
. badge-scheduled { background : #1e3a5f ; color : #93c5fd ; border : 1 px solid #3b82f6 ; }
. badge-draft { background : #1e2737 ; color : #cbd5e1 ; border : 1 px solid #334155 ; }
. badge-published { background : #14532d ; color : #4ade80 ; border : 1 px solid #16a34a ; }
. badge-failed { background : #450a0a ; color : #f87171 ; border : 1 px solid #dc2626 ; }
. badge-manual { background : #451a03 ; color : #fdba74 ; border : 1 px solid #d97706 ; }
. badge-timeout { background : #1c1917 ; color : #78716c ; border : 1 px solid #44403c ; }
. screenshot-thumb {
2026-07-07 07:11:50 +00:00
max-width : 80 px ; max-height : 60 px ; border-radius : var ( - - radius - sm ) ; object-fit : cover ;
2026-07-05 06:41:32 +00:00
border : 1 px solid #1e2737 ; cursor : pointer ;
transition : transform 0.15 s , border-color 0.15 s ;
}
. screenshot-thumb : hover { transform : scale ( 1.05 ) ; border-color : #6366f1 ; }
. no-screenshot { color : #374151 ; font-size : 11 px ; }
/* Inline credentials for PENDING_MANUAL / MANUAL_REQUIRED */
. inline-creds {
2026-07-07 07:11:50 +00:00
background : #0a1628 ; border : 1 px solid #1e3a5f ; border-radius : var ( - - radius - md ) ;
2026-07-05 06:41:32 +00:00
padding : 10 px 12 px ; margin-top : 8 px ; display : flex ; flex-direction : column ; gap : 6 px ;
}
. inline-creds-title { font-size : 11 px ; font-weight : 700 ; color : #60a5fa ; text-transform : uppercase ; letter-spacing : 0.5 px ; margin-bottom : 2 px ; }
. cred-inline-row { display : flex ; align-items : center ; gap : 6 px ; font-size : 12 px ; flex-wrap : wrap ; }
. cred-inline-label { color : #475569 ; min-width : 70 px ; }
. cred-inline-val { font-family : 'Monaco' , 'Menlo' , monospace ; font-size : 12 px ; color : #cbd5e1 ; }
2026-07-07 07:11:50 +00:00
. pw-field { background : #030712 ; border : 1 px solid #1e2737 ; border-radius : var ( - - radius - sm ) ; padding : 3 px 8 px ; color : #fbbf24 ; font-family : monospace ; font-size : 12 px ; width : 160 px ; }
. copy-btn { background : #6366f1 ; color : #fff ; border : none ; border-radius : var ( - - radius - sm ) ; padding : 3 px 10 px ; cursor : pointer ; font-size : 11 px ; font-weight : 600 ; transition : background 0.15 s ; }
2026-07-05 06:41:32 +00:00
. copy-btn : hover { background : #4f46e5 ; }
2026-07-07 07:11:50 +00:00
. reveal-btn { background : #1e2737 ; color : #94a3b8 ; border : 1 px solid #2d3748 ; border-radius : var ( - - radius - sm ) ; padding : 3 px 8 px ; cursor : pointer ; font-size : 11 px ; transition : all 0.15 s ; }
2026-07-05 06:41:32 +00:00
. reveal-btn : hover { background : #2d3748 ; color : #e2e8f0 ; }
/* Fetch-mail-code */
. mail-code-box {
2026-07-07 07:11:50 +00:00
background : #0a1628 ; border : 1 px solid #1e3a5f ; border-radius : var ( - - radius - md ) ;
2026-07-05 06:41:32 +00:00
padding : 10 px 12 px ; margin-top : 8 px ;
}
. mail-code-result {
2026-07-07 07:11:50 +00:00
display : none ; background : #1e293b ; border-radius : var ( - - radius - sm ) ; padding : 8 px 12 px ;
2026-07-05 06:41:32 +00:00
margin-top : 8 px ; font-size : 14 px ; font-weight : 700 ; color : #6ee7b7 ; letter-spacing : 2 px ;
display : flex ; align-items : center ; gap : 10 px ; flex-wrap : wrap ;
}
. mail-code-result . hidden { display : none !important ; }
/* Credentials */
. creds-header { display : flex ; justify-content : space-between ; align-items : center ; margin-bottom : 16 px ; flex-wrap : wrap ; gap : 8 px ; }
. creds-actions { display : flex ; gap : 8 px ; flex-wrap : wrap ; }
. creds-grid { display : grid ; grid-template-columns : repeat ( auto - fill , minmax ( 340 px , 1 fr ) ) ; gap : 16 px ; }
2026-07-07 07:11:50 +00:00
. cred-card { background : #030712 ; border : 1 px solid #1e2737 ; border-radius : var ( - - radius - md ) ; padding : 16 px ; }
2026-07-05 06:41:32 +00:00
. cred-platform { font-size : 14 px ; font-weight : 700 ; color : #f1f5f9 ; margin-bottom : 12 px ; display : flex ; align-items : center ; gap : 8 px ; }
. cred-rows { display : flex ; flex-direction : column ; gap : 8 px ; }
. cred-row { display : flex ; justify-content : space-between ; align-items : center ; gap : 8 px ; font-size : 12 px ; }
. cred-label { color : #475569 ; min-width : 72 px ; }
. cred-value { color : #cbd5e1 ; font-family : 'Monaco' , 'Menlo' , monospace ; font-size : 12 px ; word-break : break-all ; flex : 1 ; text-align : right ; }
. pw-hidden { color : #e2e8f0 ; }
. security-banner {
2026-07-07 07:11:50 +00:00
background : #1a1f2e ; border : 1 px solid #2d3748 ; border-radius : var ( - - radius - md ) ;
2026-07-05 06:41:32 +00:00
padding : 12 px 16 px ; margin-bottom : 16 px ; display : flex ; align-items : center ; gap : 10 px ;
font-size : 12 px ; color : #94a3b8 ;
}
. security-banner . icon { font-size : 20 px ; }
/* Publishing */
. publish-grid { display : grid ; grid-template-columns : 1 fr 400 px ; gap : 24 px ; }
@ media ( max-width : 900px ) { . publish-grid { grid-template-columns : 1 fr ; } }
. platform-checkboxes { display : flex ; flex-wrap : wrap ; gap : 8 px ; margin-top : 8 px ; }
. platform-checkbox { display : flex ; align-items : center ; gap : 6 px ; }
. platform-checkbox input { width : auto ; }
. platform-checkbox label { font-size : 13 px ; color : #cbd5e1 ; text-transform : none ; font-weight : 400 ; letter-spacing : 0 ; }
2026-07-07 07:11:50 +00:00
. post-preview { background : #030712 ; border : 1 px solid #1e2737 ; border-radius : var ( - - radius - md ) ; padding : 16 px ; }
2026-07-05 06:41:32 +00:00
. preview-platform { font-size : 12 px ; font-weight : 600 ; color : #6366f1 ; text-transform : uppercase ; letter-spacing : 1 px ; margin-bottom : 8 px ; }
. preview-content { font-size : 13 px ; color : #cbd5e1 ; white-space : pre-wrap ; word-break : break-word ; }
. post-queue-table { width : 100 % ; border-collapse : collapse ; margin-top : 16 px ; }
. post-queue-table th { font-size : 11 px ; font-weight : 600 ; color : #475569 ; text-transform : uppercase ; letter-spacing : 0.5 px ; padding : 8 px 12 px ; border-bottom : 1 px solid #1e2737 ; text-align : left ; }
. post-queue-table td { padding : 10 px 12 px ; font-size : 12 px ; border-bottom : 1 px solid #0f172a ; vertical-align : middle ; }
. post-queue-table tr : hover td { background : rgba ( 99 , 102 , 241 , 0.04 ) ; }
. char-counter { font-size : 11 px ; color : #475569 ; text-align : right ; margin-top : 4 px ; }
. char-counter . warn { color : #f59e0b ; }
. char-counter . over { color : #ef4444 ; }
. schedule-row { display : flex ; gap : 12 px ; align-items : center ; flex-wrap : wrap ; margin-top : 8 px ; }
. schedule-toggle { display : flex ; gap : 8 px ; }
2026-07-07 07:11:50 +00:00
. sched-btn { padding : 6 px 14 px ; border-radius : var ( - - radius - md ) ; border : 1 px solid #1e2737 ; background : none ; color : #64748b ; font-size : 12 px ; cursor : pointer ; transition : all 0.15 s ; }
2026-07-05 06:41:32 +00:00
. sched-btn . active { background : #1e1f4e ; border-color : #6366f1 ; color : #818cf8 ; }
/* Action required column */
. action-required-cell { font-size : 12 px ; font-weight : 700 ; color : #f97316 ; }
. action-none { color : #374151 ; font-size : 12 px ; }
/* State */
. state-box { text-align : center ; padding : 48 px 24 px ; color : #475569 ; font-size : 14 px ; }
. state-box . icon { font-size : 36 px ; margin-bottom : 12 px ; }
. state-box p { margin-top : 4 px ; font-size : 13 px ; }
2026-07-07 07:11:50 +00:00
. spinner { display : inline-block ; width : 24 px ; height : 24 px ; border : 3 px solid #1e2737 ; border-top-color : #6366f1 ; border-radius : var ( - - radius - full ) ; animation : spin 0.8 s linear infinite ; margin-bottom : 12 px ; }
2026-07-05 06:41:32 +00:00
@ keyframes spin { to { transform : rotate ( 360 deg ) ; } }
. toast-container { position : fixed ; bottom : 24 px ; right : 24 px ; display : flex ; flex-direction : column ; gap : 8 px ; z-index : 999 ; }
2026-07-07 07:11:50 +00:00
. toast { padding : 10 px 16 px ; border-radius : var ( - - radius - md ) ; font-size : 13 px ; font-weight : 500 ; max-width : 320 px ; animation : slideIn 0.2 s ease ; }
2026-07-05 06:41:32 +00:00
@ keyframes slideIn { from { transform : translateX ( 100 % ) ; opacity : 0 ; } to { transform : none ; opacity : 1 ; } }
. toast-success { background : #14532d ; color : #4ade80 ; border : 1 px solid #16a34a ; }
. toast-error { background : #450a0a ; color : #f87171 ; border : 1 px solid #dc2626 ; }
. toast-info { background : #1e2737 ; color : #94a3b8 ; border : 1 px solid #2d3748 ; }
. toast-warning { background : #451a03 ; color : #fdba74 ; border : 1 px 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 ; }
2026-07-07 07:11:50 +00:00
. modal { background : #0d1117 ; border : 1 px solid #1e2737 ; border-radius : var ( - - radius - lg ) ; padding : 32 px ; max-width : 480 px ; width : 90 % ; }
2026-07-05 06:41:32 +00:00
. modal h3 { font-size : 18 px ; font-weight : 700 ; color : #f1f5f9 ; margin-bottom : 12 px ; }
. modal p { color : #94a3b8 ; font-size : 14 px ; margin-bottom : 24 px ; }
. modal-actions { display : flex ; gap : 12 px ; justify-content : flex-end ; }
2026-07-07 07:11:50 +00:00
. rotation-log { background : #030712 ; border : 1 px solid #1e2737 ; border-radius : var ( - - radius - md ) ; padding : 16 px ; margin-top : 16 px ; max-height : 300 px ; overflow-y : auto ; }
2026-07-05 06:41:32 +00:00
. rotation-log-item { font-size : 12 px ; color : #64748b ; padding : 6 px 0 ; border-bottom : 1 px solid #0f172a ; display : flex ; justify-content : space-between ; gap : 8 px ; }
. rotation-log-item : last-child { border-bottom : none ; }
< / style >
< / head >
< body >
< nav class = "top-nav" >
2026-07-07 07:11:50 +00:00
< div class = "nav-logo" > ARGUS Social< / div >
2026-07-05 06:41:32 +00:00
< 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" >
2026-07-07 07:11:50 +00:00
< 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 >
2026-07-05 06:41:32 +00:00
< 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 >
2026-07-07 07:11:50 +00:00
< 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 >
2026-07-05 06:41:32 +00:00
< / div >
< / div >
< / div >
<!-- Employee Offboard Modal -->
< div class = "modal-overlay" id = "offboardModal" >
< div class = "modal" >
2026-07-07 07:11:50 +00:00
< 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 >
2026-07-05 06:41:32 +00:00
< 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 >
2026-07-07 07:11:50 +00:00
< 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 >
2026-07-05 06:41:32 +00:00
< / div >
< / div >
< / div >
< div class = "main-container" >
< div class = "page-header" style = "display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:12px;" >
< div >
< h1 > ARGUS Social Media Admin< / h1 >
< p > Hantera registrering, automation och publicering av sociala mediekonton< / p >
< / div >
< button class = "btn btn-primary" onclick = "openAddPlatformModal()" style = "white-space:nowrap;" > + Lägg till plattform< / button >
< / div >
<!-- Company Tabs -->
< div class = "company-tabs" >
2026-07-07 07:11:50 +00:00
< 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 >
2026-07-05 06:41:32 +00:00
< / div >
<!-- Main Tabs -->
< div class = "main-tabs" >
2026-07-07 07:11:50 +00:00
< 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 >
2026-07-05 06:41:32 +00:00
< / div >
2026-07-07 07:11:50 +00:00
<!-- PANEL: Register -->
2026-07-05 06:41:32 +00:00
< 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()" >
2026-07-07 07:11:50 +00:00
< 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 >
2026-07-05 06:41:32 +00:00
< / 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 >
2026-07-07 07:11:50 +00:00
< 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 >
2026-07-05 06:41:32 +00:00
< 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;" >
2026-07-07 07:11:50 +00:00
< 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 >
2026-07-05 06:41:32 +00:00
< / div >
< / div >
< / div >
2026-07-07 07:11:50 +00:00
<!-- PANEL: Jobs -->
2026-07-05 06:41:32 +00:00
< 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 >
2026-07-07 07:11:50 +00:00
< 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 >
2026-07-05 06:41:32 +00:00
< / 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 >
2026-07-07 07:11:50 +00:00
< 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 >
2026-07-05 06:41:32 +00:00
< 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 >
2026-07-07 07:11:50 +00:00
<!-- PANEL: Credentials -->
2026-07-05 06:41:32 +00:00
< 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" >
2026-07-07 07:11:50 +00:00
< 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 >
2026-07-05 06:41:32 +00:00
< / div >
< / div >
< div class = "security-banner" >
2026-07-07 07:11:50 +00:00
< span class = "icon" > < / span >
2026-07-05 06:41:32 +00:00
< 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" style = "display:none" > < / div >
< / div >
< / div >
2026-07-07 07:11:50 +00:00
<!-- PANEL: Publishing -->
2026-07-05 06:41:32 +00:00
< div id = "panel-publishing" class = "tab-panel" >
<!-- Compose -->
< div class = "card" >
2026-07-07 07:11:50 +00:00
< 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 >
2026-07-05 06:41:32 +00:00
< div class = "publish-grid" >
< div >
< div class = "form-group" style = "margin-bottom:16px;" >
< label > Bolag< / label >
< select id = "pubCompany" onchange = "onPubCompanyChange()" >
2026-07-07 07:11:50 +00:00
< 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 >
2026-07-05 06:41:32 +00:00
< / select >
< / div >
< div class = "form-group" style = "margin-bottom:16px;" >
< label > Plattformar< / label >
< div class = "platform-checkboxes" id = "pubPlatformChecks" >
<!-- Populated by JS -->
< / div >
< / div >
< div class = "form-group" style = "margin-bottom:4px;" >
< label > Innehåll< / label >
< textarea id = "pubContent" rows = "6" placeholder = "Skriv ditt inlägg här..." oninput = "updateCharCounter(); updatePreview()" style = "resize:vertical; font-family:inherit;" > < / textarea >
< / div >
< div class = "char-counter" id = "charCounter" > 0 tecken< / div >
< div class = "form-group" style = "margin-top:16px;" >
< label > Schemaläggning< / label >
< div class = "schedule-row" >
< div class = "schedule-toggle" >
2026-07-07 07:11:50 +00:00
< 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 >
2026-07-05 06:41:32 +00:00
< / div >
2026-07-07 07:11:50 +00:00
< 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()" >
2026-07-05 06:41:32 +00:00
< / div >
< / div >
< div style = "margin-top:20px; display:flex; gap:12px; flex-wrap:wrap;" >
2026-07-07 07:11:50 +00:00
< 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 >
2026-07-05 06:41:32 +00:00
< / 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" >
2026-07-07 07:11:50 +00:00
< 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 >
2026-07-05 06:41:32 +00:00
< / div >
< / div >
< / div >
< / div >
<!-- Post Queue -->
< div class = "card" >
< div style = "display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;" >
2026-07-07 07:11:50 +00:00
< 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 >
2026-07-05 06:41:32 +00:00
< / div >
< div id = "postQueueContent" >
< div class = "state-box" > < div class = "spinner" > < / div > < br > Laddar...< / div >
< / div >
< / div >
< / div >
< / div >
< script >
'use strict' ;
2026-07-07 07:11:50 +00:00
/*
2026-07-05 06:41:32 +00:00
CONSTANTS
2026-07-07 07:11:50 +00:00
*/
2026-07-05 06:41:32 +00:00
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 = {
2026-07-07 07:11:50 +00:00
twitter : '' , instagram : '' , facebook : '' , linkedin : '' ,
tiktok : '' , youtube : '' , reddit : '' , snapchat : '' ,
pinterest : '' , spotify : '' , meta : '' ,
jobteaser : '' , hired : '' , outbrain : ''
2026-07-05 06:41:32 +00:00
} ;
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' ,
} ;
2026-07-07 07:11:50 +00:00
/*
2026-07-05 06:41:32 +00:00
PLATFORM STEPS
Fix #5: replaced manual mail instructions with auto-fetch note
2026-07-07 07:11:50 +00:00
*/
2026-07-05 06:41:32 +00:00
const PLATFORM _STEPS = {
tiktok : {
2026-07-07 07:11:50 +00:00
icon : '' ,
2026-07-05 06:41:32 +00:00
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 — 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 : {
2026-07-07 07:11:50 +00:00
icon : '' ,
2026-07-05 06:41:32 +00:00
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 — 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 : {
2026-07-07 07:11:50 +00:00
icon : '' ,
2026-07-05 06:41:32 +00:00
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 — 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 : {
2026-07-07 07:11:50 +00:00
icon : '' ,
2026-07-05 06:41:32 +00:00
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 : {
2026-07-07 07:11:50 +00:00
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>' ,
2026-07-05 06:41:32 +00:00
title : 'Granska & godkänn' ,
mailbox : false ,
steps : [
'1. Botten har fyllt i formuläret automatiskt' ,
'2. Granska skärmdumpen ovan — ser den korrekt ut?' ,
'3. Om allt ser bra ut — klicka <b>Godkänn</b> för att slutföra' ,
'4. Verifieringsmail kan ha skickats till <code>{email}</code> om kontot kräver det'
]
}
} ;
2026-07-07 07:11:50 +00:00
/*
2026-07-05 06:41:32 +00:00
STATE
2026-07-07 07:11:50 +00:00
*/
2026-07-05 06:41:32 +00:00
let currentCompany = 'landvex' ;
let currentMainTab = 'register' ;
let currentJobFilter = 'all' ;
let allJobs = [ ] ;
let platformList = [ ] ;
let scheduleMode = 'now' ;
let countdownTimer = null ;
let refreshCountdown = 15 ;
2026-07-07 07:11:50 +00:00
/*
2026-07-05 06:41:32 +00:00
AUTH
2026-07-07 07:11:50 +00:00
*/
2026-07-05 06:41:32 +00:00
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 ( ) ;
}
2026-07-07 07:11:50 +00:00
/*
2026-07-05 06:41:32 +00:00
TOAST
2026-07-07 07:11:50 +00:00
*/
2026-07-05 06:41:32 +00:00
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 ) ;
}
2026-07-07 07:11:50 +00:00
/*
2026-07-05 06:41:32 +00:00
PLATFORMS
2026-07-07 07:11:50 +00:00
*/
2026-07-05 06:41:32 +00:00
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 ;
2026-07-07 07:11:50 +00:00
opt . textContent = ( PLATFORM _ICONS [ name ] || '' ) + ' ' + name . charAt ( 0 ) . toUpperCase ( ) + name . slice ( 1 ) ;
2026-07-05 06:41:32 +00:00
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' ;
2026-07-07 07:11:50 +00:00
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>' ;
2026-07-05 06:41:32 +00:00
container . appendChild ( wrap ) ;
} ) ;
}
2026-07-07 07:11:50 +00:00
/*
2026-07-05 06:41:32 +00:00
REGISTER FORM
2026-07-07 07:11:50 +00:00
*/
2026-07-05 06:41:32 +00:00
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 ) {
2026-07-07 07:11:50 +00:00
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' ) ;
2026-07-05 06:41:32 +00:00
switchTab ( 'jobs' ) ;
} else {
2026-07-07 07:11:50 +00:00
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' ) ;
2026-07-05 06:41:32 +00:00
}
} catch ( e ) {
2026-07-07 07:11:50 +00:00
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' ) ;
2026-07-05 06:41:32 +00:00
}
}
2026-07-07 07:11:50 +00:00
/*
2026-07-05 06:41:32 +00:00
COMPANY SELECTION
2026-07-07 07:11:50 +00:00
*/
2026-07-05 06:41:32 +00:00
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 = '' ;
2026-07-07 07:11:50 +00:00
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>' ;
2026-07-05 06:41:32 +00:00
// 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 ;
} ) ;
}
2026-07-07 07:11:50 +00:00
/*
2026-07-05 06:41:32 +00:00
MAIN TAB SWITCHING
2026-07-07 07:11:50 +00:00
*/
2026-07-05 06:41:32 +00:00
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 ( ) ;
}
2026-07-07 07:11:50 +00:00
/*
2026-07-05 06:41:32 +00:00
JOBS
2026-07-07 07:11:50 +00:00
*/
2026-07-05 06:41:32 +00:00
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 ) {
2026-07-07 07:11:50 +00:00
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>' ;
2026-07-05 06:41:32 +00:00
}
}
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 ) {
2026-07-07 07:11:50 +00:00
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' ) ;
2026-07-05 06:41:32 +00:00
} else {
2026-07-07 07:11:50 +00:00
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' ) ;
2026-07-05 06:41:32 +00:00
}
loadJobs ( ) ;
}
2026-07-07 07:11:50 +00:00
/* Build steps HTML (Fix #1: "Gör så här", Fix #3: mail-knapp, Fix #4: credentials) */
2026-07-05 06:41:32 +00:00
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" ' +
'style="display:inline-flex;align-items:center;gap:5px;color:#a5b4fc;font-size:.75rem;font-weight:600;' +
2026-07-07 07:11:50 +00:00
'text-decoration:none;border:1px solid #4338ca;padding:4px 12px;border-radius: 6px">' +
2026-07-05 06:41:32 +00:00
'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">' +
2026-07-07 07:11:50 +00:00
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>' ) +
2026-07-05 06:41:32 +00:00
'</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">' +
2026-07-07 07:11:50 +00:00
'<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>' +
2026-07-05 06:41:32 +00:00
'<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">' +
2026-07-07 07:11:50 +00:00
'<button class="reveal-btn" onclick="togglePw(\'pw-' + safeId + '\', this)"> Visa</button>' +
2026-07-05 06:41:32 +00:00
'<button class="copy-btn" onclick="copyText(\'' + revealedPw . replace ( /'/g , "\\'" ) + '\', this)">Kopiera</button>'
2026-07-07 07:11:50 +00:00
: '<span style="color:#475569;font-size:12px;"> Ej tillgängligt</span>'
2026-07-05 06:41:32 +00:00
) +
'</div>' +
'</div>' ;
/* Fix #3: Mail-code fetch button */
const mailCodeHtml = info . mailbox
? '<div class="mail-code-box">' +
2026-07-07 07:11:50 +00:00
'<div style="font-size:12px;color:#94a3b8;margin-bottom:8px;"> Väntar på verifieringskod via mail</div>' +
2026-07-05 06:41:32 +00:00
'<button class="btn btn-teal btn-sm" onclick="fetchMailCode(\'' + safeId + '\', \'' + esc ( plat ) + '\', this)">' +
2026-07-07 07:11:50 +00:00
' Hämta kod</button>' +
2026-07-05 06:41:32 +00:00
'<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" ' +
'style="font-size:11px;color:#475569;">Eller öppna mailboxen manuellt →</a>' +
'</div>' +
'</div>'
: '' ;
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;' +
2026-07-07 07:11:50 +00:00
'font-size:.85rem;text-decoration:none;padding:10px 20px;border-radius: var(--radius-md);margin-top:10px">' +
2026-07-05 06:41:32 +00:00
info . icon + ' Logga in på ' + esc ( job . platform ) + ' →</a>' ;
2026-07-07 07:11:50 +00:00
return '<div style="background:#0a0e1a;border:1px solid #1e2a38;border-radius: var(--radius-md);padding:16px;margin-top:10px">' +
2026-07-05 06:41:32 +00:00
'<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 +
mailCodeHtml +
'<div style="display:flex;gap:10px;flex-wrap:wrap">' + 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 style="font-size:1.4em;letter-spacing:4px;">' + esc ( code ) + '</strong>' +
'<button class="copy-btn" onclick="copyText(\'' + esc ( code ) + '\', this)">Kopiera</button>' ;
2026-07-07 07:11:50 +00:00
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' ;
2026-07-05 06:41:32 +00:00
} else {
2026-07-07 07:11:50 +00:00
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' ) ;
2026-07-05 06:41:32 +00:00
btn . disabled = false ;
2026-07-07 07:11:50 +00:00
btn . textContent = ' Hämta kod' ;
2026-07-05 06:41:32 +00:00
}
} else if ( r . status === 404 ) {
/* Endpoint doesn't exist — fallback to open mailbox */
2026-07-07 07:11:50 +00:00
toast ( 'ℹ Automatisk hämtning ej tillgänglig — öppna mailboxen manuellt' , 'info' ) ;
2026-07-05 06:41:32 +00:00
window . open ( 'https://mail.quixzoom.com/sso/login' , '_blank' ) ;
btn . disabled = false ;
2026-07-07 07:11:50 +00:00
btn . textContent = ' Hämta kod' ;
2026-07-05 06:41:32 +00:00
} else {
const d = await r . json ( ) . catch ( function ( ) { return { } ; } ) ;
2026-07-07 07:11:50 +00:00
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' ) ;
2026-07-05 06:41:32 +00:00
btn . disabled = false ;
2026-07-07 07:11:50 +00:00
btn . textContent = ' Hämta kod' ;
2026-07-05 06:41:32 +00:00
}
} catch ( e ) {
2026-07-07 07:11:50 +00:00
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' ) ;
2026-07-05 06:41:32 +00:00
btn . disabled = false ;
2026-07-07 07:11:50 +00:00
btn . textContent = ' Hämta kod' ;
2026-07-05 06:41:32 +00:00
}
}
2026-07-07 07:11:50 +00:00
/* Render jobs table */
2026-07-05 06:41:32 +00:00
async function renderJobs ( ) {
const container = document . getElementById ( 'jobsContent' ) ;
if ( ! container ) return ;
const jobs = filterJobs ( ) ;
if ( jobs . length === 0 ) {
2026-07-07 07:11:50 +00:00
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>' ;
2026-07-05 06:41:32 +00:00
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' ,
2026-07-07 07:11:50 +00:00
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' ,
2026-07-05 06:41:32 +00:00
timeout : '⌛ Timeout' ,
} [ status ] || status . replace ( /_/g , ' ' ) ;
/* Action required column */
let actionRequired = '<span class="action-none">—</span>' ;
2026-07-07 07:11:50 +00:00
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>' ;
2026-07-05 06:41:32 +00:00
/* 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 ) {
2026-07-07 07:11:50 +00:00
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>' ;
2026-07-05 06:41:32 +00:00
} else {
2026-07-07 07:11:50 +00:00
actions += '<button class="btn btn-ghost btn-sm" data-action="delete" data-jobid="' + esc ( String ( id ) ) + '"></button>' ;
2026-07-05 06:41:32 +00:00
}
/* Build the steps block (async) */
const stepsBlock = await buildStepsHtml ( job ) ;
html += '<tr' + ( isAwait ? ' class="row-awaiting"' : '' ) + '>' ;
html += '<td><code style="font-size:11px;color:#6366f1">' + esc ( String ( id ) . slice ( 0 , 12 ) ) + '</code></td>' ;
2026-07-07 07:11:50 +00:00
html += '<td>' + ( PLATFORM _ICONS [ job . platform ] || '' ) + ' ' + platform + '</td>' ;
2026-07-05 06:41:32 +00:00
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>' ;
html += '<td>' + screenshotHtml + '</td>' ;
html += '<td style="white-space:nowrap">' + 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 ) {
2026-07-07 07:11:50 +00:00
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' ) ;
2026-07-05 06:41:32 +00:00
loadJobs ( ) ;
} else {
const d = await r . json ( ) . catch ( function ( ) { return { } ; } ) ;
2026-07-07 07:11:50 +00:00
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' ) ;
2026-07-05 06:41:32 +00:00
}
2026-07-07 07:11:50 +00:00
} 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' ) ; }
2026-07-05 06:41:32 +00:00
}
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 ) {
2026-07-07 07:11:50 +00:00
toast ( ' Jobb raderat' , 'info' ) ;
2026-07-05 06:41:32 +00:00
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 { } ; } ) ;
2026-07-07 07:11:50 +00:00
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' ) ;
2026-07-05 06:41:32 +00:00
}
2026-07-07 07:11:50 +00:00
} 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' ) ; }
2026-07-05 06:41:32 +00:00
}
/* 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' ;
}
2026-07-07 07:11:50 +00:00
/*
2026-07-05 06:41:32 +00:00
CREDENTIALS
2026-07-07 07:11:50 +00:00
*/
2026-07-05 06:41:32 +00:00
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 ) {
2026-07-07 07:11:50 +00:00
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>' ;
2026-07-05 06:41:32 +00:00
}
}
function renderCredentials ( creds ) {
const container = document . getElementById ( 'credsContent' ) ;
if ( ! container ) return ;
if ( ! creds || creds . length === 0 ) {
2026-07-07 07:11:50 +00:00
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>' ;
2026-07-05 06:41:32 +00:00
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 || '—' ) ;
2026-07-07 07:11:50 +00:00
const icon = PLATFORM _ICONS [ c . platform ] || '' ;
2026-07-05 06:41:32 +00:00
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">' +
2026-07-07 07:11:50 +00:00
'<button class="reveal-btn" onclick="togglePw(\'' + pwId + '\', this)"> Visa</button>' +
2026-07-05 06:41:32 +00:00
'<button class="copy-btn" onclick="copyText(\'' + c . password . replace ( /'/g , "\\'" ) + '\', this)">Kopiera</button>'
2026-07-07 07:11:50 +00:00
: '<span class="cred-value" style="color:#475569;"> Krypterat (AES-256)</span>'
2026-07-05 06:41:32 +00:00
) +
'</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 = [ ] ;
2026-07-07 07:11:50 +00:00
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 ) ;
2026-07-05 06:41:32 +00:00
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 ) {
2026-07-07 07:11:50 +00:00
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' ) ;
2026-07-05 06:41:32 +00:00
} else {
2026-07-07 07:11:50 +00:00
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' ) ;
2026-07-05 06:41:32 +00:00
}
const logDiv = document . getElementById ( 'rotationLog' ) ;
if ( logDiv ) {
let logHtml = '<div style="margin-top:16px;"><div class="card-title" style="font-size:13px;">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>' ;
2026-07-07 07:11:50 +00:00
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>' ;
2026-07-05 06:41:32 +00:00
logHtml += '<span style="color:#374151;">' + 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 ( ) ;
}
2026-07-07 07:11:50 +00:00
/*
2026-07-05 06:41:32 +00:00
PUBLISHING MODULE
2026-07-07 07:11:50 +00:00
*/
2026-07-05 06:41:32 +00:00
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 ( ) ) {
2026-07-07 07:11:50 +00:00
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>' ;
2026-07-05 06:41:32 +00:00
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 ;
2026-07-07 07:11:50 +00:00
const icon = PLATFORM _ICONS [ plat ] || '' ;
2026-07-05 06:41:32 +00:00
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 ) {
2026-07-07 07:11:50 +00:00
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>' ;
2026-07-05 06:41:32 +00:00
}
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 ) {
2026-07-07 07:11:50 +00:00
toast ( asDraft ? ' Utkast sparat' : ' Inlägg schemalagt/publicerat' , 'success' ) ;
2026-07-05 06:41:32 +00:00
clearPost ( ) ;
loadPostQueue ( ) ;
} else {
2026-07-07 07:11:50 +00:00
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' ) ;
2026-07-05 06:41:32 +00:00
}
} catch ( e ) {
2026-07-07 07:11:50 +00:00
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' ) ;
2026-07-05 06:41:32 +00:00
}
}
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 ) {
2026-07-07 07:11:50 +00:00
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>' ;
2026-07-05 06:41:32 +00:00
}
}
function renderPostQueue ( posts ) {
const container = document . getElementById ( 'postQueueContent' ) ;
if ( ! container ) return ;
if ( posts . length === 0 ) {
2026-07-07 07:11:50 +00:00
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>' ;
2026-07-05 06:41:32 +00:00
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 )
2026-07-07 07:11:50 +00:00
? p . platforms . map ( function ( x ) { return ( PLATFORM _ICONS [ x ] || '' ) + ' ' + x ; } ) . join ( ', ' )
2026-07-05 06:41:32 +00:00
: 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 style="font-size:11px;color:#6366f1">' + id + '</code></td>' ;
html += '<td style="font-size:11px;">' + platforms + '</td>' ;
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>' ;
2026-07-07 07:11:50 +00:00
html += '<td><button class="btn btn-ghost btn-sm" onclick="deletePost(' + JSON . stringify ( p . id || p . post _id ) + ')"></button></td>' ;
2026-07-05 06:41:32 +00:00
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 ( )
} ) ;
2026-07-07 07:11:50 +00:00
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' ) ; }
2026-07-05 06:41:32 +00:00
}
2026-07-07 07:11:50 +00:00
/*
2026-07-05 06:41:32 +00:00
UTILITY
2026-07-07 07:11:50 +00:00
*/
2026-07-05 06:41:32 +00:00
function esc ( s ) {
return String ( s )
. replace ( /&/g , '&' )
. replace ( /</g , '<' )
. replace ( />/g , '>' )
. replace ( /"/g , '"' )
. replace ( /'/g , ''' ) ;
}
function copyText ( text , btn ) {
navigator . clipboard . writeText ( text ) . then ( function ( ) {
const orig = btn . textContent ;
2026-07-07 07:11:50 +00:00
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>' ;
2026-07-05 06:41:32 +00:00
setTimeout ( function ( ) { btn . textContent = orig ; } , 1500 ) ;
} ) . catch ( function ( ) {
2026-07-07 07:11:50 +00:00
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' ) ;
2026-07-05 06:41:32 +00:00
} ) ;
}
function togglePw ( inputId , btn ) {
const input = document . getElementById ( inputId ) ;
if ( ! input ) return ;
if ( input . type === 'password' ) {
input . type = 'text' ;
2026-07-07 07:11:50 +00:00
btn . textContent = ' Dölj' ;
2026-07-05 06:41:32 +00:00
} else {
input . type = 'password' ;
2026-07-07 07:11:50 +00:00
btn . textContent = ' Visa' ;
2026-07-05 06:41:32 +00:00
}
}
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' ;
}
2026-07-07 07:11:50 +00:00
/*
2026-07-05 06:41:32 +00:00
INIT
2026-07-07 07:11:50 +00:00
*/
2026-07-05 06:41:32 +00:00
async function init ( ) {
try {
const tok = await ensureAuth ( ) ;
setStatus ( ! ! tok ) ;
2026-07-07 07:11:50 +00:00
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 ) ;
2026-07-05 06:41:32 +00:00
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 ) ;
2026-07-07 07:11:50 +00:00
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 ) ;
2026-07-05 06:41:32 +00:00
}
}
window . addEventListener ( 'DOMContentLoaded' , function ( ) { init ( ) ; } ) ;
2026-07-07 07:11:50 +00:00
/*
2026-07-05 06:41:32 +00:00
EVENT DELEGATION for job buttons
2026-07-07 07:11:50 +00:00
*/
2026-07-05 06:41:32 +00:00
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 ) ;
} ) ;
2026-07-07 07:11:50 +00:00
/*
2026-07-05 06:41:32 +00:00
ADD PLATFORM MODAL
2026-07-07 07:11:50 +00:00
*/
2026-07-05 06:41:32 +00:00
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 = [
2026-07-07 07:11:50 +00:00
'<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;">' ,
2026-07-05 06:41:32 +00:00
'<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>' ,
2026-07-07 07:11:50 +00:00
'<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\'">✕</button>' ,
2026-07-05 06:41:32 +00:00
'</div>' ,
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">' ,
'<div class="form-group">' ,
'<label>Plattformsnamn</label>' ,
2026-07-07 07:11:50 +00:00
'<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%;">' ,
2026-07-05 06:41:32 +00:00
'</div>' ,
'<div class="form-group">' ,
'<label>Ikon/Emoji</label>' ,
2026-07-07 07:11:50 +00:00
'<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%;">' ,
2026-07-05 06:41:32 +00:00
'</div>' ,
'<div class="form-group" style="grid-column:span 2;">' ,
'<label>Registrerings-URL</label>' ,
2026-07-07 07:11:50 +00:00
'<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%;">' ,
2026-07-05 06:41:32 +00:00
'</div>' ,
'<div class="form-group">' ,
'<label>Login-URL</label>' ,
2026-07-07 07:11:50 +00:00
'<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%;">' ,
2026-07-05 06:41:32 +00:00
'</div>' ,
'<div class="form-group">' ,
'<label>Teckenlängd max</label>' ,
2026-07-07 07:11:50 +00:00
'<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%;">' ,
2026-07-05 06:41:32 +00:00
'</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 ) {
2026-07-07 07:11:50 +00:00
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;">' +
2026-07-05 06:41:32 +00:00
'<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>' ,
2026-07-07 07:11:50 +00:00
'<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>' ,
2026-07-05 06:41:32 +00:00
'</div>' ,
2026-07-07 07:11:50 +00:00
'<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!' ,
2026-07-05 06:41:32 +00:00
'</div>' ,
'<div style="display:flex;gap:12px;justify-content:flex-end;margin-top:24px;">' ,
'<button class="btn btn-secondary" onclick="closeAddPlatformModal()">Avbryt</button>' ,
'<button class="btn 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 ( ) ;
2026-07-07 07:11:50 +00:00
var icon = ( document . getElementById ( 'ap_icon' ) ? document . getElementById ( 'ap_icon' ) . value : '' ) . trim ( ) || '' ;
2026-07-05 06:41:32 +00:00
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 ;
2026-07-07 07:11:50 +00:00
} 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 ; }
2026-07-05 06:41:32 +00:00
}
// 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 = '' ;
2026-07-07 07:11:50 +00:00
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' ) ;
2026-07-05 06:41:32 +00:00
setTimeout ( function ( ) { closeAddPlatformModal ( ) ; } , 1800 ) ;
}
< / script >
< / body >
< / html >