Home: recent activity feed from /api/audit/actor?limit=20 with relative timestamps and entity-typed links into detail views. Space: header + three-column row of Projects / Open tasks (status=todo) / Recent pages + refs cards. Status badges on projects and tasks use the shared .status palette. Project: header (status + start/complete dates), Tasks card with inline status badges that cycle todo->doing->blocked->done on click (PATCH /api/tasks/:id), Pages in space card, Add-task inline form bound to project_id. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
74 lines
2.4 KiB
JavaScript
74 lines
2.4 KiB
JavaScript
// Home — recent activity feed pulled from the audit log.
|
|
import { api } from '../api.js';
|
|
import { el, mount } from '../dom.js';
|
|
|
|
const ROUTE_BY_TYPE = {
|
|
space: id => '#/space/' + id,
|
|
project: id => '#/project/' + id,
|
|
task: () => '#/', // tasks list inside their project — no detail view yet
|
|
page: id => '#/page/' + id,
|
|
ref: id => '#/ref/' + id,
|
|
resource: id => '#/resource/' + id,
|
|
source_doc: () => '#/',
|
|
conversation: () => '#/'
|
|
};
|
|
|
|
const ACTION_VERB = {
|
|
create: 'created', update: 'updated', delete: 'deleted',
|
|
suggest: 'suggested', approve: 'approved', reject: 'rejected'
|
|
};
|
|
|
|
function actorLabel(row) {
|
|
if (row.actor_kind === 'user') return 'You';
|
|
if (row.actor_kind === 'agent') return 'Agent ' + (row.actor_id || '').slice(0, 8);
|
|
return row.actor_kind;
|
|
}
|
|
|
|
function fmtWhen(ts) {
|
|
const d = new Date(ts);
|
|
const diff = (Date.now() - d.getTime()) / 1000;
|
|
if (diff < 60) return Math.round(diff) + 's ago';
|
|
if (diff < 3600) return Math.round(diff / 60) + 'm ago';
|
|
if (diff < 86400) return Math.round(diff / 3600) + 'h ago';
|
|
return d.toISOString().slice(0, 10);
|
|
}
|
|
|
|
function activityRow(row) {
|
|
const route = ROUTE_BY_TYPE[row.entity_type];
|
|
const link = route && row.entity_id ? route(row.entity_id) : null;
|
|
return el('li', {},
|
|
el('span', { class: 'muted' }, fmtWhen(row.occurred_at) + ' — '),
|
|
actorLabel(row),
|
|
' ',
|
|
ACTION_VERB[row.action] || row.action,
|
|
' ',
|
|
link
|
|
? el('a', { href: link }, row.entity_type)
|
|
: el('span', {}, row.entity_type)
|
|
);
|
|
}
|
|
|
|
export async function render(main) {
|
|
mount(main,
|
|
el('h1', { class: 'view-h1' }, 'The Void'),
|
|
el('p', { class: 'view-sub' }, 'Recent activity across all spaces.'),
|
|
el('div', { class: 'card' },
|
|
el('h3', {}, 'Recent activity'),
|
|
el('div', { id: 'home-activity' }, el('span', { class: 'muted' }, 'Loading …'))
|
|
)
|
|
);
|
|
try {
|
|
const rows = await api.get('/api/audit/actor?limit=20');
|
|
const wrap = document.getElementById('home-activity');
|
|
if (!wrap) return;
|
|
if (!rows.length) {
|
|
mount(wrap, el('span', { class: 'muted' }, 'Nothing yet — create a space to begin.'));
|
|
return;
|
|
}
|
|
mount(wrap, el('ul', { class: 'plain' }, rows.map(activityRow)));
|
|
} catch (e) {
|
|
const wrap = document.getElementById('home-activity');
|
|
if (wrap) mount(wrap, el('span', { class: 'muted' }, 'Could not load activity: ' + e.message));
|
|
}
|
|
}
|