290 lines
47 KiB
HTML
290 lines
47 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<script src="./support.js"></script>
|
||
</head>
|
||
<body>
|
||
<x-dc>
|
||
<dc-import name="AppFrame" s="{{ shell }}" mobile="{{ mob }}" slot-body="{{ main }}" slot-overlays="{{ ov }}" hint-size="100%,100%"></dc-import>
|
||
</x-dc>
|
||
<script type="text/x-dc" data-dc-script data-props="{"$preview":{"width":1440,"height":900},"s":{"editor":null,"tsType":"object"},"mobile":{"editor":null,"tsType":"boolean"}}">
|
||
class Component extends DCLogic {
|
||
renderVals() {
|
||
const s = this.props.s || {}, m = !!this.props.mobile, h = React.createElement
|
||
const P = t => Object.fromEntries(t.split(';').filter(x => x.trim()).map(x => { const i = x.indexOf(':'); const k = x.slice(0, i).trim(); return [k.startsWith('--') ? k : k.replace(/-([a-z])/g, (_, c) => c.toUpperCase()), x.slice(i + 1).trim()] }))
|
||
const D = (t, ...c) => h('div', { style: P(t) }, ...c)
|
||
const S = (t, ...c) => h('span', { style: P(t) }, ...c)
|
||
const I = (n, z = 16, t = '') => h('span', { 'aria-hidden': 'true', style: { display: 'inline-block', flexShrink: 0, width: z, height: z, backgroundColor: 'currentColor', WebkitMask: 'url(https://api.iconify.design/lucide/' + n + '.svg) center / contain no-repeat', mask: 'url(https://api.iconify.design/lucide/' + n + '.svg) center / contain no-repeat', ...(t ? P(t) : {}) } })
|
||
const bh = m ? 44 : 32
|
||
const MONO = "font-family:'IBM Plex Mono',monospace"
|
||
const B = (label, o = {}) => {
|
||
const k = o.k || 'outline'
|
||
const bg = k === 'primary' ? 'var(--ui-primary)' : 'var(--ui-bg)'
|
||
const fg = k === 'primary' ? 'var(--ui-bg)' : 'var(--ui-text-highlighted)'
|
||
const bd = k === 'primary' ? bg : 'var(--ui-border-accented)'
|
||
return S('height:' + (o.h || bh) + 'px;' + (o.w ? 'width:' + o.w + 'px;' : '') + 'padding:0 ' + (o.px ?? 12) + 'px;display:flex;align-items:center;justify-content:center;gap:6px;background:' + bg + ';color:' + fg + ';border:1px solid ' + bd + ';font-weight:500;font-size:' + (o.fs || 13) + 'px;white-space:nowrap;flex-shrink:0;opacity:' + (o.dis ? 0.4 : 1) + ';' + (o.full ? 'flex:1;' : ''), o.icon && I(o.icon, o.z || 16), label)
|
||
}
|
||
const BG = { ok: ['border:1px solid var(--ui-border-accented);color:var(--ui-text-toned)', 'check'], fail: ['background:var(--rc-error-soft);color:var(--ui-error)', 'circle-x'], sec: ['background:var(--rc-warning-soft);color:var(--ui-warning)', 'shield-alert'], del: ['border:1px solid var(--ui-border-accented);color:var(--ui-text-toned)', 'trash-2'], neutral: ['border:1px solid var(--ui-border-accented);color:var(--ui-text-toned)'], info: ['background:var(--rc-info-soft);color:var(--ui-info)'] }
|
||
const Bd = (t, k, sz = 20) => S('height:' + sz + 'px;padding:0 6px;display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:500;white-space:nowrap;flex-shrink:0;' + BG[k][0], BG[k][1] && I(BG[k][1], 12), t)
|
||
const Out = e => e.ok ? Bd('Succeeded', 'ok') : Bd('Failed', 'fail')
|
||
const Alert = (tone, icon, title, text, action) => D('display:flex;gap:10px;padding:12px 14px;background:var(--rc-' + tone + '-soft);color:var(--ui-' + tone + ')', S('display:flex;padding-top:2px', I(icon)), D('flex:1;display:flex;flex-direction:column;gap:8px', D('display:flex;flex-direction:column;gap:2px', S('font-weight:600', title), text && S('color:var(--ui-text-toned)', text)), action && D('display:flex;gap:8px', ...[].concat(action).map(a => B(a, { h: m ? 44 : 28, fs: 12 })))))
|
||
const Empty = (icon, title, text, action, primary, pad = 56) => D('background:var(--ui-bg);border:1px solid var(--ui-border);padding:' + pad + 'px 24px;display:flex;flex-direction:column;align-items:center;gap:12px;text-align:center', S('width:44px;height:44px;display:flex;align-items:center;justify-content:center;background:var(--ui-bg-elevated);color:var(--ui-text-toned)', I(icon, 20)), S('font-weight:600;font-size:15px;color:var(--ui-text-highlighted)', title), S('color:var(--ui-text-muted);max-width:340px;text-wrap:pretty', text), action && B(action, { k: primary ? 'primary' : 'outline', px: 16 }))
|
||
const Note = t => S('align-self:flex-start;height:20px;padding:0 6px;display:inline-flex;align-items:center;border:1px dashed var(--ui-border-accented);color:var(--ui-text-toned);font:500 11px \'IBM Plex Mono\',monospace', t)
|
||
const sk = (w, hh = 12, extra = '') => S('display:block;width:' + w + ';height:' + hh + 'px;background:var(--ui-bg-accented);flex-shrink:0;' + extra)
|
||
|
||
// ---------- data
|
||
const U = n => ({ n, k: 'user' }), KEY = n => ({ n, k: 'key' }), SYS = { n: 'Scheduler', k: 'system' }, ANON = { n: 'Not signed in', k: 'anon' }
|
||
const p2 = n => String(n).padStart(2, '0')
|
||
const T = (d, hh, mm, ss) => ({ abs: d + ' 2026, ' + p2(hh) + ':' + p2(mm) + ':' + p2(ss), utc: d + ' 2026, ' + p2(hh - 1) + ':' + p2(mm) + ':' + p2(ss) + ' UTC' })
|
||
const WEB = { src: 'Web app', ip: '203.0.113.24', ua: 'Firefox 131 on macOS' }
|
||
const LONG = 'The complete guide to restoring a nineteenth-century timber-framed farmhouse: structural surveys, lime mortar, breathable insulation and what it all costs'
|
||
const E = [
|
||
{ id: 1, day: 'Today', rel: '2 min ago', ...T('6 Oct', 10, 42, 18), a: U('Marta Alves'), cat: 'articles', s: 'Published the article ', em: 'Spring release', tk: 'Article', ex: 1, ok: 1, tid: 'art_8f3k2', ...WEB, diff: [{ f: 'Publish state', b: 'Draft', a: 'Published' }] },
|
||
{ id: 2, day: 'Today', rel: '29 min ago', ...T('6 Oct', 10, 15, 4), a: U('Marta Alves'), cat: 'articles', s: 'Edited the article ', em: 'Why we reuse original floorboards', tk: 'Article', ex: 1, ok: 1, tid: 'art_2m9xq', ...WEB, diff: [{ f: 'Title', b: 'Why we reuse floorboards', a: 'Why we reuse original floorboards' }, { f: 'Body', note: 'Body edited (+312 characters)' }] },
|
||
{ id: 3, day: 'Today', rel: '1 h ago', ...T('6 Oct', 9, 31, 47), a: ANON, cat: 'auth', s: 'Failed login attempt for ', em: 'alex@example.com', ok: 0, sec: 1, why: 'The password did not match. 3 attempts in 10 minutes.', src: 'Web app', ip: '198.51.100.77', ua: 'Chrome 129 on Windows' },
|
||
{ id: 4, day: 'Today', rel: '2 h ago', ...T('6 Oct', 8, 50, 9), a: U('Jonas Weber'), cat: 'keys', s: 'Revoked API key ', em: 'Mobile app', tk: 'API key', ex: 1, ok: 1, sec: 1, tid: 'key_71ab0', src: 'Web app', ip: '203.0.113.90', ua: 'Safari 18 on iOS' },
|
||
{ id: 5, day: 'Today', rel: '7 h ago', ...T('6 Oct', 3, 0, 2), a: SYS, cat: 'system', s: 'Ran the nightly backup', em: '', ok: 1, src: 'System task', ip: null, ua: null },
|
||
{ id: 6, day: 'Yesterday', rel: '18:20', ...T('5 Oct', 18, 20, 36), a: U('Inès Duarte'), cat: 'users', s: 'Changed the role of ', em: 'Jonas Weber', tk: 'User', ex: 1, ok: 1, sec: 1, tid: 'usr_c40d1', tail: ' from Editor to Admin', ...WEB, ip: '203.0.113.12', diff: [{ f: 'Role', b: 'Editor', a: 'Admin' }] },
|
||
{ id: 7, day: 'Yesterday', rel: '16:05', ...T('5 Oct', 16, 5, 51), a: KEY('Website sync'), cat: 'media', s: 'Uploaded the image ', em: 'north-wall.jpg', tk: 'Image', ex: 1, ok: 1, tid: 'img_0pk4w', src: 'API key', ip: '192.0.2.18', ua: 'node-fetch 3.3' },
|
||
{ id: 8, day: 'Yesterday', rel: '14:40', ...T('5 Oct', 14, 40, 13), a: U('Marta Alves'), cat: 'media', s: 'Deleted the image ', em: 'old-banner.png', tk: 'Image', ex: 0, ok: 1, tid: 'img_9zq1c', ...WEB },
|
||
{ id: 9, day: 'Yesterday', rel: '11:12', ...T('5 Oct', 11, 12, 27), a: U('Inès Duarte'), cat: 'articles', s: 'Failed to publish the article ', em: LONG, tk: 'Article', ex: 1, ok: 0, tid: 'art_5t7yh', why: 'The article has no cover image. Add one, then publish again.', ...WEB, ip: '203.0.113.12' },
|
||
{ id: 10, day: 'Yesterday', rel: '03:00', ...T('5 Oct', 3, 0, 1), a: SYS, cat: 'users', s: 'Deleted 3 expired invites', em: '', ok: 1, src: 'System task', ip: null, ua: null },
|
||
{ id: 11, day: '3 Oct 2026', rel: '16:48', ...T('3 Oct', 16, 48, 22), a: U('Inès Duarte'), cat: 'system', s: 'Changed the setting ', em: 'Site description', tk: 'Setting', ex: 1, ok: 1, tid: 'set_site_desc', ...WEB, ip: '203.0.113.12', diff: [{ f: 'Site description', b: 'Renovation notes, project write-ups and practical guides from the Renew studio team in Lisbon, covering timber, lime plaster and stone restoration for older homes.', a: 'Renovation notes, project write-ups and practical guides from the Renew studio team in Lisbon, covering timber, lime plaster and stone restoration for older homes, plus a seasonal maintenance checklist.', long: 1 }, { f: 'Comments', b: 'Off', a: 'On' }] },
|
||
{ id: 12, day: '3 Oct 2026', rel: '15:02', ...T('3 Oct', 15, 2, 40), a: U('Jonas Weber'), cat: 'keys', s: 'Created API key ', em: 'Mobile app', tk: 'API key', ex: 0, ok: 1, sec: 1, tid: 'key_71ab0', ...WEB, ip: '203.0.113.90' },
|
||
{ id: 13, day: '3 Oct 2026', rel: '14:20', ...T('3 Oct', 14, 20, 5), a: U('Inès Duarte'), cat: 'users', s: 'Updated the user ', em: 'Sam Rivera', tk: 'User', ex: 1, ok: 1, tid: 'usr_77ea2', ...WEB, ip: '203.0.113.12', diff: [{ f: 'Name', b: 'Sam Rivera', a: 'Samuel Rivera' }, { f: 'Email', b: 'sam@example.com', a: 'samuel@example.com' }] },
|
||
{ id: 14, day: '3 Oct 2026', rel: '11:36', ...T('3 Oct', 11, 36, 58), a: U('Marta Alves'), cat: 'media', s: 'Uploaded the image ', em: 'floorboards-copy.jpg', tk: 'Image', ex: 1, ok: 1, tid: 'img_4ww8e', ...WEB },
|
||
{ id: 15, day: '3 Oct 2026', rel: '09:05', ...T('3 Oct', 9, 5, 12), a: U('Inès Duarte'), cat: 'auth', s: 'Reset the password for ', em: 'Sam Rivera', tk: 'User', ex: 1, ok: 1, sec: 1, tid: 'usr_77ea2', ...WEB, ip: '203.0.113.12' }
|
||
]
|
||
const CI = { articles: 'file-text', media: 'image', users: 'users', keys: 'key-round', auth: 'shield-check', system: 'server-cog' }
|
||
const CL = { articles: 'Articles', media: 'Media', users: 'Users & invites', keys: 'API keys', auth: 'Authentication', system: 'System' }
|
||
const user = s.role === 'user'
|
||
const ME = user ? 'Marta Alves' : 'Inès Duarte'
|
||
let list = s.ids ? s.ids.map(i => E.find(e => e.id === i)) : (user ? E.filter(e => e.a.n === 'Marta Alves') : E)
|
||
const total = s.total || (user ? '41' : '1,284')
|
||
const sentence = e => e.s + e.em + (e.tail || '')
|
||
const Av = (a, z = 24) => {
|
||
const base = 'width:' + z + 'px;height:' + z + 'px;flex-shrink:0;display:flex;align-items:center;justify-content:center;'
|
||
if (a.k === 'user') return S(base + 'background:var(--rc-sand);color:var(--rc-on-sand);font:600 ' + Math.round(z * 0.42) + "px 'IBM Plex Sans',sans-serif", a.n.split(' ').map(x => x[0]).join('').slice(0, 2).toUpperCase())
|
||
return S(base + 'border:1px solid var(--ui-border-accented);background:var(--ui-bg-elevated);color:var(--ui-text-toned)', I({ key: 'key-round', system: 'cog', anon: 'user-round-x' }[a.k], Math.round(z * 0.6)))
|
||
}
|
||
const ActorBd = a => a.k === 'key' ? Bd('API key', 'neutral') : a.k === 'system' ? Bd('System', 'neutral') : null
|
||
const ell = 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0'
|
||
const link = 'text-decoration:underline;text-underline-offset:3px;color:var(--ui-text-highlighted)'
|
||
const SecIcon = z => S('display:flex;color:var(--ui-warning)', I('shield-alert', z))
|
||
|
||
// ---------- controls
|
||
const Search = () => D('height:' + bh + 'px;' + (m ? 'flex:1;' : 'width:240px;') + 'padding:0 4px 0 10px;display:flex;align-items:center;gap:8px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:' + (F.q ? 'var(--ui-text-highlighted)' : 'var(--ui-text-muted)') + (m ? ';font-size:16px' : ''), I('search', 14), S('flex:1;' + ell, F.q || 'Search activity'), F.q && S('width:24px;height:24px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)', I('x', 14)))
|
||
const Sel = (val, ph, w, icon) => D('height:' + bh + 'px;padding:0 10px;display:flex;align-items:center;gap:8px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:' + (val ? 'var(--ui-text-highlighted)' : 'var(--ui-text-muted)') + ';' + (w ? 'width:' + w + 'px;flex-shrink:0' : 'width:100%'), icon && I(icon, 14, 'color:var(--ui-text-muted)'), S('flex:1;' + ell, val || ph), I('chevron-down', 14, 'color:var(--ui-text-muted)'))
|
||
const Seg = (opts, on, fw) => D('display:flex;border:1px solid var(--ui-border-accented);flex-shrink:0;' + (fw ? 'width:100%' : ''), ...opts.map(o => S('height:' + (bh - 2) + 'px;padding:0 ' + (m ? 12 : 10) + 'px;' + (fw ? 'flex:1;justify-content:center;' : '') + 'display:flex;align-items:center;gap:6px;white-space:nowrap;font-weight:' + (o === on ? 500 : 400) + ';background:' + (o === on ? 'var(--rc-sand)' : 'var(--ui-bg)') + ';color:' + (o === on ? 'var(--rc-on-sand)' : 'var(--ui-text-toned)'), o === 'Custom' && I('calendar', 14), o)))
|
||
const F = { q: '', actor: '', cat: '', out: 'All', range: '7 days', ...(s.f || {}) }
|
||
if (s.card === 2) F.out = 'Failed'
|
||
const RANGES = ['Today', '7 days', '30 days', 'Custom']
|
||
const chips = []
|
||
if (F.q) chips.push('Search: ' + F.q)
|
||
if (F.actor && !user) chips.push('Person: ' + F.actor)
|
||
if (F.cat) chips.push('Category: ' + F.cat)
|
||
if (s.card === 3) chips.push('Type: Security events')
|
||
if (F.out !== 'All') chips.push('Outcome: ' + F.out)
|
||
if (F.range !== '7 days') chips.push('Dates: ' + (F.range === 'Custom' ? (F.custom || '1 – 30 Sep 2026') : F.range))
|
||
const nF = chips.length
|
||
const Chips = () => nF > 0 && D('display:flex;flex-wrap:wrap;align-items:center;gap:8px', ...chips.map((c, ci) => S('height:' + (m ? 32 : 24) + 'px;padding:0 4px 0 8px;display:inline-flex;align-items:center;gap:4px;background:var(--ui-bg-elevated);border:1px solid var(--ui-border);color:var(--ui-text-highlighted);font-size:12px;font-weight:500', c, S('width:20px;height:20px;display:flex;align-items:center;justify-content:center;' + (s.chipHover === ci ? 'background:var(--ui-bg-accented);color:var(--ui-text-highlighted)' : 'color:var(--ui-text-muted)'), I('x', 12)))), S('height:' + (m ? 44 : 24) + 'px;display:flex;align-items:center;padding:0 6px;font-weight:500;font-size:12px;' + link + (s.clearHover ? ';background:var(--ui-bg-elevated)' : '') + (s.focus === 'clear' ? ';outline:2px solid var(--ui-primary);outline-offset:2px' : ''), 'Clear all'))
|
||
const FilterBar = () => m
|
||
? D('display:flex;gap:8px', Search(), h('span', { style: P('height:44px;padding:0 12px;display:flex;align-items:center;gap:6px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);font-weight:500;font-size:13px;flex-shrink:0') }, I('sliders-horizontal', 16), 'Filters', nF > 0 && S('min-width:18px;height:18px;padding:0 5px;display:inline-flex;align-items:center;justify-content:center;background:var(--ui-primary);color:var(--ui-bg);font:500 11px \'IBM Plex Mono\',monospace', String(nF))))
|
||
: D('display:flex;align-items:center;gap:8px;flex-wrap:wrap', Search(), !user && Sel(F.actor, 'All people', 168, 'user-round'), Sel(F.cat, 'All categories', 176, 'layout-list'), Seg(['All', 'Succeeded', 'Failed'], F.out), D('flex:1;min-width:0'), Seg(RANGES, F.range))
|
||
|
||
// ---------- summary
|
||
const mk = (l, v, sub, ic, tone, ser) => ({ l, v, sub, ic, tone, ser })
|
||
const STAT = s.sum === 'zero' ? [mk('Total actions', '214', '4% vs previous 7 days', 'trending-down', 'n', [3, 5, 2, 6, 4, 1, 2]), mk('Active people', '5', '4 people, 1 API key', '', 'n', [2, 3, 2, 3, 2, 1, 2]), mk('Failed actions', '0', 'Nothing failed', 'circle-check', 'ok0', [0, 0, 0, 0, 0, 0, 0]), mk('Security events', '0', 'Nothing to flag', 'circle-check', 'ok0', [0, 0, 0, 0, 0, 0, 0])]
|
||
: s.sum === 'none' ? [mk('Total actions', '0', 'No data yet', '', 'n', [0, 0, 0, 0, 0, 0, 0]), mk('Active people', '0', 'No data yet', '', 'n', [0, 0, 0, 0, 0, 0, 0]), mk('Failed actions', '0', 'Nothing failed', 'circle-check', 'ok0', [0, 0, 0, 0, 0, 0, 0]), mk('Security events', '0', 'Nothing to flag', 'circle-check', 'ok0', [0, 0, 0, 0, 0, 0, 0])]
|
||
: [mk('Total actions', '1,284', '12% vs previous 7 days', 'trending-up', 'n', [4, 6, 5, 8, 7, 9, 6]), mk('Active people', '9', '7 people, 2 API keys', '', 'n', [5, 6, 6, 7, 7, 8, 9]), mk('Failed actions', '6', '0.5% of all actions', 'circle-x', 'error', [0, 1, 0, 2, 1, 0, 2]), mk('Security events', '11', '5 failed logins', 'shield-alert', 'warning', [1, 0, 2, 1, 3, 2, 2])]
|
||
const Bars = (ser, tone, z) => {
|
||
const mx = Math.max(...ser, 1), col = tone === 'error' ? 'var(--ui-error)' : tone === 'warning' ? 'var(--ui-warning)' : tone === 'on' ? 'var(--rc-on-sand)' : tone === 'ok0' ? 'var(--ui-border-accented)' : 'var(--ui-text-muted)'
|
||
return D('display:flex;align-items:flex-end;gap:2px;height:' + z + 'px;flex-shrink:0', ...ser.map(v => S('display:block;width:4px;height:' + Math.max(2, Math.round(v / mx * z)) + 'px;background:' + col)))
|
||
}
|
||
const StatCard = (c, i) => {
|
||
const tog = i === 2 || i === 3, act = tog && s.card === i, hov = tog && s.hover === i, tone = c.tone
|
||
let bg = tone === 'error' ? 'var(--rc-error-soft)' : tone === 'warning' ? 'var(--rc-warning-soft)' : 'var(--ui-bg)'
|
||
let vc = tone === 'error' ? 'var(--ui-error)' : tone === 'warning' ? 'var(--ui-warning)' : 'var(--ui-text-highlighted)'
|
||
let sc = tone === 'ok0' ? 'var(--ui-success)' : 'var(--ui-text-muted)', lc = 'var(--ui-text-toned)', sub = c.sub, icn = c.ic, bt = tone
|
||
if (act) { bg = 'var(--rc-sand)'; vc = 'var(--rc-on-sand)'; sc = 'var(--rc-on-sand)'; lc = 'var(--rc-on-sand)'; bt = 'on'; sub = hov ? 'Click to clear this filter' : 'Filtering. Click to clear'; icn = hov ? 'x' : 'filter' }
|
||
else if (hov) { bg = 'var(--ui-bg-elevated)'; sub = 'Click to filter the list'; icn = 'filter' }
|
||
const bd = act ? 'var(--ui-primary)' : hov ? 'var(--ui-border-accented)' : 'var(--ui-border)'
|
||
const st = 'display:flex;flex-direction:column;gap:2px;padding:' + (m ? '10px 12px' : '12px 14px') + ';background:' + bg + ';border:1px solid ' + bd + ';' + (act ? 'box-shadow:inset 0 0 0 1px var(--ui-primary);' : '') + (s.focus === 'card' + i ? 'outline:2px solid var(--ui-primary);outline-offset:2px;' : '') + (m ? 'flex:0 0 156px;' : 'min-width:0;')
|
||
const kids = [D('display:flex;align-items:center;justify-content:space-between;gap:8px', S('font-weight:500;color:' + lc + ';' + ell, c.l), Bars(c.ser, bt, m ? 16 : 20)),
|
||
S('font:600 ' + (m ? 22 : 24) + "px/32px 'IBM Plex Sans',sans-serif;color:" + vc, c.v),
|
||
S('display:flex;align-items:center;gap:4px;font-size:12px;color:' + sc + ';' + ell, icn && I(icn, 12), sub)]
|
||
return tog ? h('div', { role: 'button', tabIndex: 0, 'aria-pressed': act, style: P(st) }, ...kids) : h('div', { style: P(st) }, ...kids)
|
||
}
|
||
const SkelCard = () => D('display:flex;flex-direction:column;gap:8px;padding:' + (m ? '10px 12px' : '14px') + ';background:var(--ui-bg);border:1px solid var(--ui-border);' + (m ? 'flex:0 0 156px' : 'min-width:0'), sk('55%', 12), sk('40%', 22), sk('70%', 10))
|
||
const Strip = () => {
|
||
if (user) return null
|
||
const kids = s.state === 'loading' ? [0, 1, 2, 3].map(SkelCard) : STAT.map(StatCard)
|
||
return m ? D('display:flex;gap:8px;overflow:hidden;margin:0 -16px;padding:0 16px', ...kids) : D('display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px', ...kids)
|
||
}
|
||
|
||
// ---------- table
|
||
const cols = user ? '96px minmax(0,1fr) 230px 120px' : '96px 210px minmax(0,1fr) 230px 120px'
|
||
const TableHead = () => D('display:grid;grid-template-columns:' + cols + ';height:36px;align-items:center;padding-right:16px;background:var(--ui-bg-elevated);color:var(--ui-text-muted);font-weight:500;flex-shrink:0', S('padding-left:16px;display:flex;align-items:center;gap:4px;color:var(--ui-text-highlighted)', 'When', I('arrow-down', 12)), !user && S('', 'Actor'), S('', 'Action'), S('', 'Target'), S('', 'Outcome'))
|
||
const Day = d => D('display:flex;align-items:center;height:32px;padding:0 ' + (m ? 16 : 16) + 'px;background:var(--ui-bg-muted);border-top:1px solid var(--ui-border);font-weight:600;font-size:12px;color:var(--ui-text-highlighted);flex-shrink:0', d)
|
||
const Tip = e => S('position:absolute;left:12px;top:38px;z-index:5;padding:6px 8px;background:var(--ui-bg-inverted);color:var(--ui-text-inverted);font-size:12px;white-space:nowrap;box-shadow:var(--rc-shadow-pop)', e.abs)
|
||
const TargetCell = e => !e.tk ? S('color:var(--ui-text-muted)', '—') : D('display:flex;flex-direction:column;min-width:0;padding-right:12px', e.ex ? S('display:flex;align-items:center;gap:4px;' + link, S(ell, e.em), I('arrow-up-right', 12, 'color:var(--ui-text-muted)')) : D('display:flex;align-items:center;gap:6px;min-width:0', S('flex:1;text-decoration:line-through;color:var(--ui-text-muted);' + ell, e.em), Bd('Deleted', 'del')), S('font-size:12px;color:var(--ui-text-muted)', e.tk))
|
||
const Row = e => {
|
||
const hov = s.tip === e.id, sel = s.panel === e.id
|
||
return D('position:relative;z-index:' + (hov ? 3 : 1) + ';display:grid;grid-template-columns:' + cols + ';align-items:center;min-height:52px;padding-right:16px;border-top:1px solid var(--ui-border);background:' + (sel || hov ? 'var(--ui-bg-elevated)' : 'var(--ui-bg)') + ';' + (e.sec ? 'box-shadow:inset 2px 0 0 var(--ui-warning);' : ''),
|
||
S('padding-left:16px;position:relative;color:var(--ui-text-toned);white-space:nowrap', e.rel, hov && Tip(e)),
|
||
!user && D('display:flex;align-items:center;gap:8px;min-width:0;padding-right:12px', Av(e.a), S('font-weight:500;color:var(--ui-text-highlighted);' + ell, e.a.n), ActorBd(e.a)),
|
||
D('display:flex;align-items:center;gap:10px;min-width:0;padding-right:16px', S('display:flex;color:var(--ui-text-muted)', I(CI[e.cat], 16)), S('color:var(--ui-text);' + ell, e.s, S('font-weight:600;color:var(--ui-text-highlighted)', e.em), e.tail || ''), e.sec && SecIcon(14)),
|
||
TargetCell(e), D('display:flex', Out(e)))
|
||
}
|
||
const Card = e => {
|
||
const sel = s.panel === e.id
|
||
return D('display:flex;flex-direction:column;gap:8px;padding:12px 16px;border-top:1px solid var(--ui-border);background:' + (sel ? 'var(--ui-bg-elevated)' : 'var(--ui-bg)') + ';' + (e.sec ? 'box-shadow:inset 2px 0 0 var(--ui-warning)' : ''),
|
||
D('display:flex;align-items:center;gap:8px;min-width:0', !user && Av(e.a, 24), !user && S('font-weight:500;color:var(--ui-text-highlighted);' + ell, e.a.n), !user && ActorBd(e.a), D('flex:1'), S('color:var(--ui-text-muted);font-size:12px;white-space:nowrap', e.rel)),
|
||
D('display:flex;gap:8px;align-items:flex-start', S('display:flex;padding-top:2px;color:var(--ui-text-muted)', I(CI[e.cat], 16)), S('flex:1;min-width:0;color:var(--ui-text);font-size:15px;line-height:22px;overflow-wrap:anywhere;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden', e.s, S('font-weight:600;color:var(--ui-text-highlighted)', e.em), e.tail || '')),
|
||
D('display:flex;align-items:center;gap:6px;flex-wrap:wrap', Out(e), e.sec && Bd('Security', 'sec'), e.tk && !e.ex && Bd('Target deleted', 'del')))
|
||
}
|
||
const rowsOf = fn => { const out = []; let day = null; list.forEach(e => { if (e.day !== day) { out.push(Day(e.day)); day = e.day } out.push(fn(e)) }); return out }
|
||
const SkelRows = () => Array.from({ length: m ? 5 : 9 }, (_, i) => m
|
||
? D('display:flex;flex-direction:column;gap:10px;padding:14px 16px;border-top:1px solid var(--ui-border)', D('display:flex;gap:8px;align-items:center', sk('24px', 24), sk('90px', 12), D('flex:1'), sk('40px', 10)), sk(['90%', '75%', '85%'][i % 3], 14), sk('70px', 20))
|
||
: D('display:grid;grid-template-columns:' + cols + ';align-items:center;height:52px;padding-right:16px;border-top:1px solid var(--ui-border)', S('padding-left:16px', sk('56px')), !user && D('display:flex;gap:8px;align-items:center', sk('24px', 24), sk('90px')), sk(['70%', '55%', '82%'][i % 3], 12), sk('60%'), sk('72px', 20)))
|
||
const Table = () => D('flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column;background:var(--ui-bg);border:1px solid var(--ui-border)', !m && TableHead(), ...(s.state === 'loading' ? (m ? [] : []).concat(SkelRows()) : rowsOf(m ? Card : Row)))
|
||
const PgBtn = (c, on, dis) => S('width:' + bh + 'px;height:' + bh + 'px;display:flex;align-items:center;justify-content:center;border:1px solid ' + (on ? 'var(--ui-primary)' : 'var(--ui-border-accented)') + ';background:' + (on ? 'var(--ui-primary)' : 'var(--ui-bg)') + ';color:' + (on ? 'var(--ui-bg)' : 'var(--ui-text-highlighted)') + ';font-weight:500;flex-shrink:0;opacity:' + (dis ? 0.4 : 1), c)
|
||
const Pager = () => s.state === 'loading' ? null : m
|
||
? D('display:flex;align-items:center;gap:8px;flex-shrink:0', PgBtn(I('chevron-left'), 0, 1), D('flex:1;display:flex;flex-direction:column;align-items:center;font-size:12px;color:var(--ui-text-muted)', S('color:var(--ui-text-highlighted);font-weight:500', 'Page 1 of ' + (user ? '2' : '52')), S('', '1–25 of ' + total)), D('width:96px;flex-shrink:0', Sel('25', '', 0)), PgBtn(I('chevron-right')))
|
||
: D('display:flex;align-items:center;gap:16px;flex-shrink:0', S('color:var(--ui-text-muted)', 'Showing 1–' + (user ? '25' : '25') + ' of ' + total), D('flex:1'), D('display:flex;align-items:center;gap:8px;color:var(--ui-text-muted)', 'Rows per page', D('width:80px', Sel('25', '', 0))), D('display:flex;gap:4px', PgBtn(I('chevron-left'), 0, 1), PgBtn('1', 1), PgBtn('2'), PgBtn('3'), PgBtn('…'), PgBtn(user ? '2' : '52'), PgBtn(I('chevron-right'))))
|
||
|
||
// ---------- header + page
|
||
const title = user ? 'My activity' : 'Activity'
|
||
const sub = user ? 'Everything you\'ve done on this site.' : 'Who did what, and when, across your site.'
|
||
const noData = s.state === 'none'
|
||
const ExportBtn = () => S('height:' + bh + 'px;padding:0 10px 0 12px;display:flex;align-items:center;gap:6px;border:1px solid var(--ui-border-accented);background:' + (s.menu ? 'var(--ui-bg-elevated)' : 'var(--ui-bg)') + ';color:var(--ui-text-highlighted);font-weight:500;white-space:nowrap;flex-shrink:0;opacity:' + (noData || s.state === 'error' ? 0.4 : 1) + ';' + (s.focus === 'export' ? 'outline:2px solid var(--ui-primary);outline-offset:2px;' : ''), I('download', 16), 'Export', I(s.menu ? 'chevron-up' : 'chevron-down', 14, 'color:var(--ui-text-muted)'))
|
||
const X = s.exp || {}
|
||
const CUR = s.menuCount || '214'
|
||
const MItem = (ic, t, d, dis, hov) => D('display:flex;gap:12px;align-items:flex-start;padding:' + (m ? '12px 16px' : '10px 12px') + ';min-height:' + (m ? 64 : 56) + 'px;background:' + (hov ? 'var(--ui-bg-elevated)' : 'transparent') + ';opacity:' + (dis ? 1 : 1) + ';color:' + (dis ? 'var(--ui-text-muted)' : 'var(--ui-text-highlighted)'), S('display:flex;padding-top:2px', I(ic, 16)), D('flex:1;min-width:0;display:flex;flex-direction:column;gap:2px', S('font-weight:500;color:' + (dis ? 'var(--ui-text-muted)' : 'var(--ui-text-highlighted)'), t), S('font-size:12px;line-height:18px;color:var(--ui-text-muted);text-wrap:pretty', d)))
|
||
const ExportMenu = () => {
|
||
const items = [MItem('download', 'Export current view', s.menuCount === '0' ? 'Nothing to export. No events match these filters.' : 'CSV · ' + CUR + ' events matching your filters and sort, across all pages.', s.menuCount === '0', s.mHover === 1), D('height:1px;background:var(--ui-border)'), MItem(s.noAll ? 'lock' : 'archive', 'Export all activity…', s.noAll ? 'Only owners and admins can export all activity.' : 'CSV · every event, ignoring filters. Owners and admins only.', !!s.noAll, s.mHover === 2)]
|
||
return m
|
||
? D('position:absolute;inset:0;z-index:40;display:flex;align-items:flex-end;background:var(--rc-overlay)', D('width:100%;background:var(--ui-bg);display:flex;flex-direction:column;box-shadow:var(--rc-shadow-modal)', D('display:flex;justify-content:center;padding-top:8px', S('display:block;width:36px;height:4px;background:var(--ui-border-accented)')), D('height:56px;display:flex;align-items:center;padding:0 8px 0 16px;border-bottom:1px solid var(--ui-border)', S('flex:1;font:600 16px/24px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted)', 'Export'), S('width:44px;height:44px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)', I('x'))), D('display:flex;flex-direction:column;padding-bottom:16px', ...items)))
|
||
: D('position:absolute;inset:0;z-index:40', D('position:absolute;right:24px;top:108px;width:360px;background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop);padding:4px;display:flex;flex-direction:column', ...items))
|
||
}
|
||
const Prog = () => D('height:4px;background:var(--ui-bg-accented);position:relative;overflow:hidden', S('display:block;position:absolute;top:0;bottom:0;background:var(--ui-primary);left:30%;width:35%'))
|
||
const Kv = (k, v) => D('display:flex;justify-content:space-between;gap:12px', S('color:var(--ui-text-muted)', k), S('color:var(--ui-text-highlighted);font-weight:500', v))
|
||
const ExportDlg = () => {
|
||
const st = X.stage || 'cfg', fmt = X.fmt || 'CSV', rng = X.range || 'All time'
|
||
const fld = (l, c) => D('display:flex;flex-direction:column;gap:6px', S('font-weight:500;color:var(--ui-text-highlighted)', l), c)
|
||
let body, foot
|
||
if (st === 'cfg') {
|
||
const fmtCtl = X.json ? Seg(['CSV', 'JSON'], fmt, 1) : D('height:32px;display:flex;align-items:center;padding:0 12px;border:1px solid var(--ui-border-accented);color:var(--ui-text-highlighted)', 'CSV')
|
||
body = [fld('Format', fmtCtl), fld('Date range', D('display:flex;flex-direction:column;gap:6px', Sel(rng, '', 0, 'calendar'), S('font-size:12px;color:var(--ui-text-muted)', 'Default is all time. Filters on the page are ignored.')))]
|
||
foot = [B('Cancel', { full: m }), B('Export ' + fmt, { k: 'primary', icon: 'download', full: m })]
|
||
} else if (st === 'prep') {
|
||
body = [D('display:flex;flex-direction:column;gap:12px', S('font-weight:500;color:var(--ui-text-highlighted);display:flex;align-items:center;gap:8px', I('loader-circle', 16), 'Preparing download…'), Prog(), S('color:var(--ui-text-muted);text-wrap:pretty', 'The file starts downloading as soon as it is ready. Keep this window open.'))]
|
||
foot = [B('Cancel', { full: m }), B('Preparing…', { k: 'primary', dis: 1, full: m })]
|
||
} else if (st === 'ready') {
|
||
body = [D('display:flex;gap:12px;align-items:flex-start', S('display:flex;color:var(--ui-success);padding-top:2px', I('circle-check', 20)), D('display:flex;flex-direction:column;gap:2px', S('font-weight:600;font-size:15px;color:var(--ui-text-highlighted)', 'Download started'), S('color:var(--ui-text-muted)', 'activity-all-2026-10-06.csv is downloading. Check your browser\'s downloads if nothing appears.')))]
|
||
foot = [B('Close', { full: m })]
|
||
} else if (st === 'fail') {
|
||
body = [Alert('error', 'circle-x', 'Couldn\'t prepare the download', 'Nothing was downloaded. Try again in a moment.')]
|
||
foot = [B('Cancel', { full: m }), B('Retry', { k: 'primary', icon: 'rotate-cw', full: m })]
|
||
} else if (st === 'denied') {
|
||
body = [Alert('error', 'lock', 'You can\'t export all activity', 'Only owners and admins can export all activity. Ask an owner if you need a copy.')]
|
||
foot = [B('Close', { full: m })]
|
||
} else {
|
||
body = [D('display:flex;flex-direction:column;align-items:center;gap:10px;text-align:center;padding:12px 0', S('width:44px;height:44px;display:flex;align-items:center;justify-content:center;background:var(--ui-bg-elevated);color:var(--ui-text-toned)', I('file-x', 20)), S('font-weight:600;font-size:15px;color:var(--ui-text-highlighted)', 'Nothing to export'), S('color:var(--ui-text-muted);max-width:300px;text-wrap:pretty', 'There is no activity in this date range. Choose a wider range.'))]
|
||
foot = [B('Close', { full: m })]
|
||
}
|
||
const sheet = D('display:flex;justify-content:center;padding-top:8px', S('display:block;width:36px;height:4px;background:var(--ui-border-accented)'))
|
||
const head = D('padding:20px 24px 0;display:flex;align-items:flex-start;gap:12px', D('flex:1;display:flex;flex-direction:column;gap:4px', S('font:600 16px/24px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted)', 'Export all activity'), S('color:var(--ui-text-muted);text-wrap:pretty', 'Downloads every event as a CSV file. Filters on the page are ignored.')), S('width:' + (m ? 44 : 28) + 'px;height:' + (m ? 44 : 28) + 'px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)', I('x')))
|
||
return D('position:absolute;inset:0;z-index:50;display:flex;align-items:' + (m ? 'flex-end' : 'flex-start') + ';justify-content:center;background:var(--rc-overlay);padding:' + (m ? '0' : '140px 16px 16px'), D('width:' + (m ? '100%' : '460px') + ';background:var(--ui-bg);display:flex;flex-direction:column;box-shadow:var(--rc-shadow-modal)', m && sheet, head, D('padding:20px 24px 0;display:flex;flex-direction:column;gap:16px', ...body), D('padding:20px 24px;display:flex;flex-direction:' + (m ? 'column-reverse' : 'row') + ';justify-content:flex-end;gap:8px', ...foot)))
|
||
}
|
||
const ToastX = () => D('position:absolute;z-index:60;' + (m ? 'left:16px;right:16px;bottom:16px' : 'right:24px;bottom:24px;width:356px') + ';display:flex;gap:10px;padding:12px 14px;background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop)', S('display:flex;padding-top:2px;color:var(--ui-success)', I('circle-check')), D('flex:1;display:flex;flex-direction:column;gap:2px', S('font-weight:600;color:var(--ui-text-highlighted)', s.toast.t), S('color:var(--ui-text-toned)', s.toast.d)))
|
||
const Header = () => D('display:flex;align-items:flex-end;gap:16px;flex-wrap:wrap;flex-shrink:0', D('flex:1;min-width:180px;display:flex;flex-direction:column;gap:2px', S('font:600 20px/28px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted)', title), S('color:var(--ui-text-muted)', sub)), !user && s.state !== 'forbidden' && ExportBtn())
|
||
const Page = () => {
|
||
const k = [Header()]
|
||
if (s.state === 'forbidden') { k.push(Empty('lock', 'You can\'t view this activity', 'Your role doesn\'t include this page. Ask an owner or admin if you need access.', 'Back to home', false)); return k }
|
||
const st = Strip(); if (st) k.push(st)
|
||
k.push(FilterBar()); const c = Chips(); if (c) k.push(c)
|
||
if (s.state === 'error') k.push(Alert('error', 'circle-x', 'Couldn\'t load activity', 'Check your connection and try again.', 'Try again'))
|
||
else if (s.state === 'none') k.push(Empty('history', 'No activity yet', user ? 'Your actions will appear here once you start working on the site.' : 'Actions on articles, media, users and API keys will appear here.'))
|
||
else if (s.state === 'nomatch') k.push(Empty('search-x', 'No activity matches these filters', 'Try a different search or a wider date range.', 'Clear filters', false))
|
||
else { k.push(Table()); const pg = Pager(); if (pg) k.push(pg) }
|
||
return k
|
||
}
|
||
|
||
// ---------- slide-over
|
||
const Av2 = Av
|
||
const Panel = () => {
|
||
const e = E.find(x => x.id === s.panel), i = list.indexOf(e)
|
||
const lab = t => S('color:var(--ui-text-muted)', t)
|
||
const val = (...c) => D('display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0;color:var(--ui-text-highlighted)', ...c)
|
||
const Diff = d => {
|
||
if (d.note) return D('display:flex;flex-direction:column;gap:6px;padding:12px;border-top:1px solid var(--ui-border-muted)', S('font-weight:500;color:var(--ui-text-highlighted)', d.f), S('display:flex;align-items:center;gap:6px;color:var(--ui-text-toned)', I('file-pen-line', 14), d.note))
|
||
const clamp = d.long && !s.expand
|
||
const box = (t, bg, bd, c) => S('display:block;padding:8px 10px;background:' + bg + ';border:1px solid ' + bd + ';color:' + c + ';overflow-wrap:anywhere;' + (clamp ? 'display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden' : ''), t)
|
||
return D('display:flex;flex-direction:column;gap:8px;padding:12px;border-top:1px solid var(--ui-border-muted)', S('font-weight:500;color:var(--ui-text-highlighted)', d.f),
|
||
D('display:grid;grid-template-columns:' + (m ? 'minmax(0,1fr)' : 'minmax(0,1fr) 16px minmax(0,1fr)') + ';gap:' + (m ? 6 : 8) + 'px;align-items:' + (m ? 'stretch' : 'start'), D('display:flex;flex-direction:column;gap:2px;min-width:0', S('font-size:11px;color:var(--ui-text-muted)', 'Before'), box(d.b, 'var(--ui-bg-elevated)', 'var(--ui-border)', 'var(--ui-text-toned)')), m ? S('display:flex;justify-content:center;color:var(--ui-text-muted)', I('arrow-down', 14)) : S('display:flex;padding-top:30px;color:var(--ui-text-muted)', I('arrow-right', 14)), D('display:flex;flex-direction:column;gap:2px;min-width:0', S('font-size:11px;color:var(--ui-text-muted)', 'After'), box(d.a, 'var(--ui-bg)', 'var(--ui-border-accented)', 'var(--ui-text-highlighted)'))),
|
||
d.long && S('align-self:flex-start;font-weight:500;font-size:12px;min-height:' + (m ? 32 : 16) + 'px;' + link, clamp ? 'Show more' : 'Show less'))
|
||
}
|
||
const body = D('flex:1;min-height:0;overflow:hidden;padding:' + (m ? 16 : 20) + 'px;display:flex;flex-direction:column;gap:20px',
|
||
D('display:flex;flex-direction:column;gap:10px', S('font:600 18px/26px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted);overflow-wrap:anywhere', sentence(e)), D('display:flex;gap:6px;flex-wrap:wrap;align-items:center', Out(e), e.sec && Bd('Security', 'sec'), S('display:inline-flex;align-items:center;gap:4px;font-size:12px;color:var(--ui-text-muted)', I(CI[e.cat], 14), CL[e.cat]))),
|
||
!e.ok && Alert('error', 'circle-x', 'Why it failed', e.why),
|
||
D('display:grid;grid-template-columns:96px minmax(0,1fr);gap:10px 12px;align-items:start', lab('When'), D('display:flex;flex-direction:column;gap:2px', S('color:var(--ui-text-highlighted)', e.abs + ' (your time)'), S('color:var(--ui-text-muted);font-size:12px', e.utc)), lab('Actor'), val(Av(e.a, 24), S('font-weight:500', e.a.n), ActorBd(e.a)), lab('Source'), val(e.src), lab('IP address'), e.ip ? S(MONO + ';font-size:12px;color:var(--ui-text-highlighted)', e.ip) : S('color:var(--ui-text-muted)', 'Not applicable'), lab('Browser'), e.ua ? S('color:var(--ui-text-highlighted)', e.ua) : S('color:var(--ui-text-muted)', 'Not applicable')),
|
||
e.tk && D('display:flex;flex-direction:column;gap:8px', S('font:600 14px/22px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted)', 'Target'), D('display:flex;flex-direction:column;gap:6px;border:1px solid var(--ui-border);padding:10px 12px', D('display:flex;align-items:center;gap:8px;min-width:0', e.ex ? S('flex:1;display:flex;align-items:center;gap:4px;overflow-wrap:anywhere;' + link, e.em, I('arrow-up-right', 14, 'color:var(--ui-text-muted)')) : S('flex:1;text-decoration:line-through;color:var(--ui-text-muted);overflow-wrap:anywhere', e.em), !e.ex && Bd('Deleted', 'del')), S('font-size:12px;color:var(--ui-text-muted)', e.tk + ' · ' + e.tid), !e.ex && S('font-size:12px;color:var(--ui-text-toned)', 'This item was deleted. Its name is kept for reference.'))),
|
||
e.diff && D('display:flex;flex-direction:column;gap:8px', S('font:600 14px/22px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted)', 'Changes'), D('border:1px solid var(--ui-border)', ...e.diff.map((d, j) => { const r = Diff(d); return r }))))
|
||
const foot = D('flex-shrink:0;display:flex;align-items:center;gap:8px;padding:12px ' + (m ? 16 : 20) + 'px;border-top:1px solid var(--ui-border)', B('Previous event', { icon: 'chevron-up', dis: i === 0, full: m }), B('Next event', { icon: 'chevron-down', dis: i === list.length - 1, full: m }), !m && D('flex:1'), !m && S('color:var(--ui-text-muted);font-size:12px', 'Event ' + (i + 1) + ' of ' + total))
|
||
const head = D('height:56px;flex-shrink:0;display:flex;align-items:center;gap:12px;padding:0 ' + (m ? 8 : 20) + 'px 0 ' + (m ? 16 : 20) + 'px;border-bottom:1px solid var(--ui-border)', S('flex:1;font:600 16px/24px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted)', 'Activity details'), m && S('color:var(--ui-text-muted);font-size:12px', (i + 1) + ' of ' + total), S('width:' + (m ? 44 : 28) + 'px;height:' + (m ? 44 : 28) + 'px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)', I('x')))
|
||
return D('position:absolute;inset:0;z-index:40;display:flex;justify-content:' + (m ? 'stretch' : 'flex-end') + ';align-items:' + (m ? 'flex-end' : 'stretch') + ';background:var(--rc-overlay)',
|
||
D((m ? 'width:100%;height:88%;' : 'width:480px;height:100%;') + 'background:var(--ui-bg);display:flex;flex-direction:column;box-shadow:var(--rc-shadow-modal)', m && D('flex-shrink:0;display:flex;justify-content:center;padding-top:8px', S('display:block;width:36px;height:4px;background:var(--ui-border-accented)')), head, body, foot))
|
||
}
|
||
const Drawer = () => {
|
||
const sec = (l, c) => D('display:flex;flex-direction:column;gap:6px', S('font-weight:500;color:var(--ui-text-highlighted)', l), c)
|
||
return D('position:absolute;inset:0;z-index:40;display:flex;align-items:flex-end;background:var(--rc-overlay)', D('width:100%;background:var(--ui-bg);display:flex;flex-direction:column;box-shadow:var(--rc-shadow-modal)',
|
||
D('display:flex;justify-content:center;padding-top:8px', S('display:block;width:36px;height:4px;background:var(--ui-border-accented)')),
|
||
D('height:56px;display:flex;align-items:center;padding:0 8px 0 16px;border-bottom:1px solid var(--ui-border)', S('flex:1;font:600 16px/24px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted)', 'Filters'), S('width:44px;height:44px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)', I('x'))),
|
||
D('padding:16px;display:flex;flex-direction:column;gap:16px', !user && sec('Person', Sel(F.actor, 'All people', 0, 'user-round')), sec('Category', Sel(F.cat, 'All categories', 0, 'layout-list')), sec('Outcome', Seg(['All', 'Succeeded', 'Failed'], F.out, 1)), sec('Dates', D('display:flex;flex-direction:column;gap:8px', Seg(['Today', '7 days', '30 days', 'Custom'], F.range, 1), F.range === 'Custom' && D('display:flex;gap:8px;align-items:center', D('flex:1', Sel('1 Sep 2026', '', 0, 'calendar')), D('flex:1', Sel('30 Sep 2026', '', 0, 'calendar')))))),
|
||
D('display:flex;gap:8px;padding:12px 16px 16px;border-top:1px solid var(--ui-border)', B('Clear all', { full: true, dis: nF === 0 }), B('Show ' + (s.count || '23') + ' results', { k: 'primary', full: true }))))
|
||
}
|
||
|
||
// ---------- home + settings
|
||
const Rows5 = (ls, own) => ls.map((e, i) => D('display:flex;gap:10px;align-items:flex-start;padding:10px 0;border-top:' + (i ? '1px solid var(--ui-border-muted)' : '0'), Av(e.a, 24), D('flex:1;min-width:0;display:flex;flex-direction:column;gap:2px', S('color:var(--ui-text);overflow-wrap:anywhere;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden', e.s, S('font-weight:600;color:var(--ui-text-highlighted)', e.em), e.tail || ''), S('font-size:12px;color:var(--ui-text-muted)', own ? e.rel : e.a.n + ' · ' + e.rel)), !e.ok && Bd('Failed', 'fail'), e.sec && e.ok && SecIcon(14)))
|
||
const ActCard = () => {
|
||
const own = user, ls = own ? E.filter(e => e.a.n === 'Marta Alves') : E.slice(0, 5)
|
||
const st = s.card || 'ok'
|
||
const inner = st === 'loading' ? Array.from({ length: 5 }, (_, i) => D('display:flex;gap:10px;padding:10px 0;border-top:' + (i ? '1px solid var(--ui-border-muted)' : '0'), sk('24px', 24), D('flex:1;display:flex;flex-direction:column;gap:6px', sk(['85%', '70%', '90%', '60%', '78%'][i], 12), sk('35%', 10))))
|
||
: st === 'error' ? [Alert('error', 'circle-x', 'Couldn\'t load recent activity', null, 'Try again')]
|
||
: st === 'empty' ? [D('padding:24px 0;display:flex;flex-direction:column;align-items:center;gap:6px;text-align:center', S('color:var(--ui-text-muted)', I('history', 20)), S('font-weight:600;color:var(--ui-text-highlighted)', 'No activity yet'), S('color:var(--ui-text-muted);max-width:240px', own ? 'Your actions will appear here.' : 'Actions on your site will appear here.'))]
|
||
: Rows5(ls, own)
|
||
return D('background:var(--ui-bg);border:1px solid var(--ui-border);display:flex;flex-direction:column;min-width:0', D('display:flex;align-items:center;gap:8px;padding:12px 16px;border-bottom:1px solid var(--ui-border)', S('flex:1;font:600 14px/22px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted)', own ? 'Your recent activity' : 'Recent activity'), S('display:flex;align-items:center;gap:4px;font-weight:500;min-height:' + (m ? 44 : 20) + 'px;' + link, 'View all', I('arrow-right', 14))), D('padding:4px 16px 6px;display:flex;flex-direction:column', ...inner))
|
||
}
|
||
const Ph = (t, hh) => D('height:' + hh + 'px;border:1px dashed var(--ui-border-accented);display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted);text-align:center;padding:16px', t)
|
||
const HomeBody = () => [D('display:flex;flex-direction:column;gap:2px', S('font:600 20px/28px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted)', 'Home'), S('color:var(--ui-text-muted)', 'Welcome back, ' + ME.split(' ')[0] + '.')), m ? D('display:flex;flex-direction:column;gap:16px', ActCard(), Ph('Existing Home cards', 160)) : D('display:grid;grid-template-columns:minmax(0,1fr) 420px;gap:16px;align-items:start', Ph('Existing Home cards', 420), ActCard())]
|
||
const RET = { '30': '30 days', '90': '90 days', '365': '365 days', '0': 'Forever' }
|
||
const SettingsBody = () => {
|
||
const r = s.ret || '90', fv = r === '0'
|
||
const dl = (k, v) => D('display:flex;flex-direction:' + (m ? 'column' : 'row') + ';gap:' + (m ? 0 : 12) + 'px', S('width:' + (m ? 'auto' : '120px') + ';color:var(--ui-text-muted);flex-shrink:0', k), S('color:var(--ui-text-highlighted)', v))
|
||
const card = D('background:var(--ui-bg);border:1px solid var(--ui-border);max-width:640px;display:flex;flex-direction:column',
|
||
D('display:flex;align-items:center;gap:8px;padding:12px 16px;border-bottom:1px solid var(--ui-border)', D('flex:1;display:flex;flex-direction:column', S('font:600 14px/22px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted)', 'Retention'), S('color:var(--ui-text-muted);font-size:12px', 'How long activity is kept.')), s.saved && S('display:flex;align-items:center;gap:4px;color:var(--ui-success);font-size:12px', I('check', 14), 'Saved')),
|
||
D('padding:16px;display:flex;flex-direction:column;gap:16px',
|
||
s.err && Alert('error', 'circle-x', 'Couldn\'t save the retention period', 'Your previous setting is still in place.', 'Try again'),
|
||
D('display:flex;flex-direction:column;gap:6px;max-width:260px', S('font-weight:500;color:var(--ui-text-highlighted)', 'Keep activity for'), Sel(RET[s.shorten ? '30' : r], '', 0), S('color:var(--ui-text-muted);font-size:12px', fv ? 'Nothing is removed automatically. The log grows over time.' : 'Older events are removed automatically.')),
|
||
s.shorten && Alert('warning', 'triangle-alert', 'Events older than 30 days will be removed', 'This happens at the next cleanup on 7 Oct 2026, 03:00. It can\'t be undone.', ['Apply 30 days', 'Cancel']),
|
||
Alert('info', 'lock', 'Activity logs are append-only', 'Events can\'t be edited or deleted from the app. Only the retention period removes them.'),
|
||
D('display:flex;flex-direction:column;gap:6px;padding-top:12px;border-top:1px solid var(--ui-border)', dl('Last cleanup', fv ? 'Never' : '6 Oct 2026, 03:00 · 142 events removed'), dl('Next cleanup', fv ? 'Not scheduled' : '7 Oct 2026, 03:00'))))
|
||
return [D('display:flex;flex-direction:column;gap:2px', S('font:600 20px/28px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted)', 'Settings'), S('color:var(--ui-text-muted)', 'Site-wide options. Only owners can change them.')), Ph('Existing Settings cards', m ? 100 : 120), card]
|
||
}
|
||
|
||
// ---------- shell
|
||
const kind = s.kind || 'page'
|
||
const kids = kind === 'home' ? HomeBody() : kind === 'settings' ? SettingsBody() : Page()
|
||
const content = D('flex:1;min-height:0;overflow:hidden;padding:' + (m ? 16 : 24) + 'px;display:flex;flex-direction:column;gap:16px', ...kids)
|
||
const ov = D('display:contents', s.panel && Panel(), m && s.drawer && Drawer(), s.menu && ExportMenu(), s.exp && ExportDlg(), s.toast && ToastX())
|
||
const shell = { kind: 'slot', role: user ? 'user' : undefined, display: ME, nav: kind === 'home' ? 'home' : kind === 'settings' ? 'settings' : 'activity', crumbs: [kind === 'home' ? 'Home' : kind === 'settings' ? 'Settings' : title], mTitle: kind === 'home' ? 'Home' : kind === 'settings' ? 'Settings' : title }
|
||
return { shell, mob: m, main: content, ov }
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|