/* Aktivitet (KUN superadmin): full oversikt over hva hver bruker GJØR og SER PÅ. Bygger på audit-loggen: alle skrive-handlinger + GET-visninger (dedupliseret til én pr. minutt pr. side). Filtrer på bruker, se tidslinje med menneskelesbare beskrivelser. */ const ACT_LABELS = [ [/\/route-plan/, 'Kjørerute-planlegger'], [/\/refill\/plans\/[^/]+\/manifest/, 'Digitalt fraktbrev'], [/\/refill\/plans\/[^/]+\/stops\/[^/]+\/fill-status/, 'Fylle-status (stopp)'], [/\/refill\/plans\/[^/]+\/stops\/[^/]+\/arrive/, 'Ankom stopp (åpnet dører)'], [/\/refill\/plans\/[^/]+\/stops\/[^/]+\/rescan/, 'Re-skann av kammer'], [/\/refill\/plans\/[^/]+\/stops\/[^/]+\/complete/, 'Signerte fylle-tall'], [/\/refill\/plans/, 'Fylleplaner'], [/\/refill\/.+\/overview/, 'Utkjøringslista'], [/\/service\/(.+)/, (m) => `Servicelogg: ${m[1]}`], [/\/automats\/bulk-edit/, 'Bulk-redigering av automater'], [/\/automats\/([^/]+)\/shelves/, (m) => `Hyller på ${m[1]}`], [/\/shelves\/([^/]+)\/shelves\/\d+\/override/, (m) => `Overstyrte hylle på ${m[1]}`], [/\/shelves\/([^/]+)\/shelves/, (m) => `Lager på ${m[1]}`], [/\/shelves\/([^/]+)\/fill/, (m) => `Påfyll på ${m[1]}`], [/\/automats\/([^/]+)\/settings/, (m) => `Innstillinger for ${m[1]}`], [/\/automats\/([^/]+)\/location/, (m) => `Posisjon for ${m[1]}`], [/\/automats\/([^/]+)\/telemetry/, (m) => `Telemetri for ${m[1]}`], [/\/automats\/([^/]+)$/, (m) => `Automat ${m[1]}`], [/\/automats$/, 'Automat-oversikten'], [/\/sales\/orders/, 'Salgshistorikk'], [/\/sales\/summary/, 'Salgssammendrag'], [/\/sales/, 'Salg'], [/\/orders\/alarms/, 'Alarmer'], [/\/orders\/([^/]+)/, (m) => `Ordre ${m[1]}`], [/\/areas/, 'Områder'], [/\/bottles\/price-overrides/, 'Prisoverstyringer'], [/\/bottles/, 'Flasketyper'], [/\/analytics/, 'Statistikk'], [/\/tenants/, 'Tenants'], [/\/users/, 'Brukere'], [/\/audit/, 'Aktivitetsloggen'], [/\/exports/, 'Eksport'], [/\/cameras/, 'Kameraer'], [/\/production/, 'Produksjonskonsoll'], ]; function actLabel(path) { const p = path.replace('/api/admin', ''); for (const [re, label] of ACT_LABELS) { const m = p.match(re); if (m) return typeof label === 'function' ? label(m) : label; } return p; } const ActivityPage = ({ lang, dataVersion }) => { const no = lang === 'no'; const [tab, setTab] = React.useState('activity'); // activity | logins const [rows, setRows] = React.useState(null); const [logins, setLogins] = React.useState(null); const [userFilter, setUserFilter] = React.useState(''); const [live, setLive] = React.useState(true); const refresh = React.useCallback(async () => { try { if (tab === 'logins') { setLogins(await window.api.logins(userFilter)); } else { const q = userFilter ? `&user_email=${encodeURIComponent(userFilter)}` : ''; setRows(await window.api.req('GET', `/api/admin/audit?limit=300${q}`)); } } catch (e) { if (tab === 'logins') setLogins({ error: e.message }); else setRows({ error: e.message }); } }, [userFilter, tab]); React.useEffect(() => { refresh(); if (!live) return; const iv = setInterval(refresh, 10000); return () => clearInterval(iv); }, [refresh, live, dataVersion]); const users = React.useMemo(() => { if (!Array.isArray(rows)) return []; const m = new Map(); rows.forEach(r => { if (!r.user_email) return; const u = m.get(r.user_email) || { email: r.user_email, n: 0, last: r.created_at }; u.n += 1; m.set(r.user_email, u); }); return [...m.values()].sort((a, b) => b.n - a.n); }, [rows]); const fmtTime = (iso) => (iso || '').replace('T', ' ').slice(5, 16); return (

{no ? 'Aktivitet' : 'Activity'}

{no ? 'Hva brukerne gjør og ser på — alle handlinger + sidevisninger' : 'What users do and view'}
{tab === 'logins' && ( )} {tab === 'activity' && (

{no ? 'Tidslinje' : 'Timeline'}

{!rows &&
⏳
} {rows && rows.error &&
❌ {rows.error}
} {Array.isArray(rows) && rows.length === 0 && (
{no ? 'Ingen aktivitet ennå.' : 'No activity yet.'}
)} {Array.isArray(rows) && rows.map(r => (
{r.method === 'GET' ? (no ? 'SÅ PÅ' : 'VIEWED') : r.method === 'DELETE' ? (no ? 'SLETTET' : 'DELETED') : (no ? 'ENDRET' : 'CHANGED')}
{actLabel(r.path)}
{r.user_email || (no ? 'ukjent' : 'unknown')} {r.is_superadmin && super} {' · '}{fmtTime(r.created_at)} · {r.ip_address || ''} {r.status_code >= 400 && · HTTP {r.status_code}}
{r.payload && r.method !== 'GET' && (
{JSON.stringify(r.payload).slice(0, 140)}
)}
))}

{no ? 'Brukere (siste 300 hendelser)' : 'Users'}

{users.map(u => (
setUserFilter(userFilter === u.email ? '' : u.email)} style={{ padding: '10px 16px', cursor: 'pointer', borderBottom: '1px solid var(--border)', background: userFilter === u.email ? 'var(--accent-soft)' : 'transparent' }}>
{u.email}
{no ? 'sist aktiv' : 'last'}: {fmtTime(u.last)}
{u.n}
))} {users.length === 0 &&
—
}
)}
); }; /* Innloggingshistorikk: hvem logget inn, med hvilken enhet, hvor fra. Posisjon er GPS (presis, brukeren delte) eller IP-basert (grov fallback). */ const LoginsPanel = ({ no, logins, userFilter, setUserFilter }) => { const fmtTime = (iso) => (iso || '').replace('T', ' ').slice(5, 16); const flag = (cc) => (cc && cc.length === 2) ? String.fromCodePoint(...[...cc.toUpperCase()].map(c => 127397 + c.charCodeAt(0))) : ''; const list = Array.isArray(logins) ? logins : []; const emails = React.useMemo( () => [...new Set(list.map(l => l.user_email).filter(Boolean))], [list]); return (

{no ? 'Innlogginger — enhet og posisjon' : 'Logins — device & location'}

{!logins &&
⏳
} {logins && logins.error && (
❌ {logins.error}
)} {Array.isArray(logins) && logins.length === 0 && (
{no ? 'Ingen innlogginger ennå.' : 'No logins yet.'}
)} {list.map(l => (
{l.success ? (no ? 'INNLOGGET' : 'LOGIN') : (no ? 'MISLYKKET' : 'FAILED')}
{l.user_email || (no ? 'ukjent' : 'unknown')} {l.is_superadmin && super}
{fmtTime(l.created_at)} · {l.device || (no ? 'ukjent enhet' : 'unknown device')} {l.ip_address && <> · {l.ip_address}}
{l.place ? <>📍 {flag(l.country_code)} {l.place} : 📍 {no ? 'ukjent posisjon' : 'unknown location'}} {l.geo_source && ( {l.geo_source === 'gps' ? `GPS${l.accuracy_m ? ` ±${Math.round(l.accuracy_m)} m` : ''}` : 'IP'} )} {l.latitude != null && l.longitude != null && ( {no ? 'kart' : 'map'} ↗ )}
))}
); }; Object.assign(window, { ActivityPage });