// Activity Log — audit trail of actions across the app. (() => { const { Card, Badge } = window.TechyFuelOSDesignSystem_be0222; const ENTITY_ICON = { task: 'circle-check-big', project: 'folder-kanban', client: 'contact', document: 'file-text', file: 'paperclip', team_member: 'user-plus', call: 'phone', message: 'message-square', expense: 'wallet', auth: 'log-in', invoice: 'receipt', employee: 'id-card', deal: 'trending-up', announcement: 'megaphone', }; const ENTITY_LABEL = { task: 'Task', project: 'Project', client: 'Client', document: 'Document', file: 'File', team_member: 'Team member', call: 'Call', message: 'Message', expense: 'Expense', auth: 'Login', invoice: 'Invoice', employee: 'Employee', deal: 'Deal', announcement: 'Announcement', }; const ENTITY_TONE = { task: 'brand', project: 'info', client: 'success', document: 'violet', file: 'teal', team_member: 'warning', call: 'danger', message: 'neutral', expense: 'warning', auth: 'success', invoice: 'success', employee: 'warning', deal: 'info', announcement: 'brand', }; // Verb map. Actions come as either a bare verb ("login") or "_" // ("client_created", "invoice_updated", "announcement_posted", "task_assigned"). // actionVerb() strips the entity prefix and maps the verb to friendly text. const ACTION_LABEL = { created: 'created', updated: 'updated', deleted: 'deleted', uploaded: 'uploaded', invited: 'invited', posted: 'posted', assigned: 'assigned', started: 'started', ended: 'ended', sent: 'sent', suspended: 'suspended', reactivated: 'reactivated', login: 'signed in', }; function actionVerb(action) { if (!action) return ''; if (ACTION_LABEL[action]) return ACTION_LABEL[action]; // bare verb (login) const parts = String(action).split('_'); const verb = parts[parts.length - 1]; // client_created -> created return ACTION_LABEL[verb] || action.replace(/_/g, ' '); } // The API returns UTC timestamps as "YYYY-MM-DD HH:MM:SS" (no zone). A bare // "new Date()" reads that as LOCAL time, so every row looked hours off (the // "5h ago on everything" bug). Normalise to real UTC before comparing. function parseUtc(ts) { if (!ts) return null; let s = String(ts).trim().replace(' ', 'T'); if (!/[zZ]|[+-]\d\d:?\d\d$/.test(s)) s += 'Z'; const d = new Date(s); return isNaN(d) ? null : d; } function fmtWhen(ts) { const d = parseUtc(ts); if (!d) return ''; const diff = Math.round((Date.now() - d) / 60000); if (diff < 1) return 'just now'; if (diff < 60) return diff + 'm ago'; if (diff < 1440) return Math.round(diff / 60) + 'h ago'; return Math.round(diff / 1440) + 'd ago'; } function initials(name) { if (!name) return '?'; return name.split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase(); } function ActivityRow({ item }) { const icon = ENTITY_ICON[item.entity_type] || 'activity'; const tone = ENTITY_TONE[item.entity_type] || 'neutral'; const actorName = item.team_members?.name || item.actor_name || 'Someone'; const entityLabel = ENTITY_LABEL[item.entity_type] || item.entity_type; const actionLabel = actionVerb(item.action); const isLogin = item.action === 'login'; const meta = item.meta || {}; const device = [meta.browser, meta.os].filter(Boolean).join(' · '); return (
{initials(actorName)}
{actorName} {' '}{actionLabel}{' '} {isLogin ? 'Login' : entityLabel} {!isLogin && item.entity_name && "{item.entity_name}"} {isLogin && device &&
{device}{meta.ip ? ' · ' + meta.ip : ''}
}
{fmtWhen(item.created_at)}
); } function ActivityLog() { useLucide(); const [items, setItems] = React.useState([]); const [loading, setLoading] = React.useState(true); const [filter, setFilter] = React.useState(''); const load = React.useCallback(() => { if (!window.API) { setLoading(false); return; } setLoading(true); window.API.getActivityLog({ entityType: filter || undefined, limit: 150 }) .then(r => { if (r.data) setItems(r.data); }) .catch(() => {}) .finally(() => setLoading(false)); }, [filter]); React.useEffect(() => { load(); }, [load]); const entityTypes = Object.keys(ENTITY_LABEL); return (

Activity Log

Recent actions across your workspace

{loading &&
Loading activity…
} {!loading && items.length === 0 && (
No activity yet
Actions like creating tasks, projects, or docs will show up here.
)} {!loading && items.map(item => )}
); } Object.assign(window, { ActivityLog }); })();