80 lines
2.8 KiB
TypeScript
80 lines
2.8 KiB
TypeScript
/**
|
|
* rPast — chronicle-of-self timeline.
|
|
*
|
|
* Projects every dated CRDT record across every rApp into a single
|
|
* timeline/calendar. Zero writes to source modules. The `.mw` text is
|
|
* regenerated on every request; saved "chronicles" are just filter configs.
|
|
*/
|
|
|
|
import { Hono } from 'hono';
|
|
import type { RSpaceModule } from '../../shared/module';
|
|
import {
|
|
enumerateCreations, listCreationEnumerators, renderMarkwhen,
|
|
} from '../../shared/markwhen';
|
|
import { renderMarkwhenHtml } from '../../shared/markwhen/html-render';
|
|
import { pastSchema } from './schemas';
|
|
import { renderLanding } from './landing';
|
|
|
|
const routes = new Hono();
|
|
|
|
routes.get('/api/modules', c => {
|
|
return c.json(listCreationEnumerators().map(e => ({
|
|
module: e.module, label: e.label, icon: e.icon, color: e.color,
|
|
})));
|
|
});
|
|
|
|
async function buildProjection(c: any) {
|
|
const space = c.req.param('space');
|
|
const modules = c.req.query('modules')?.split(',').filter(Boolean);
|
|
const from = c.req.query('from') ? Number(c.req.query('from')) : undefined;
|
|
const to = c.req.query('to') ? Number(c.req.query('to')) : undefined;
|
|
const view = (c.req.query('view') as 'timeline' | 'calendar') ?? 'timeline';
|
|
|
|
// Derive the public base URL from the request so in-timeline "Open in
|
|
// rApp" links resolve against the same origin the user arrived from.
|
|
const host = c.req.header('x-forwarded-host') ?? c.req.header('host');
|
|
const proto = c.req.header('x-forwarded-proto') ?? (host?.includes('localhost') ? 'http' : 'https');
|
|
const baseUrl = host ? `${proto}://${host}` : undefined;
|
|
|
|
const sources = await enumerateCreations(space, { modules, from, to });
|
|
const projection = renderMarkwhen(sources, {
|
|
view, title: `${space} — rPast`, baseUrl,
|
|
});
|
|
return { space, sources, projection, view };
|
|
}
|
|
|
|
routes.get('/api/chronicle', async c => {
|
|
const { sources, projection } = await buildProjection(c);
|
|
return c.json({
|
|
text: projection.text,
|
|
count: projection.count,
|
|
sections: sources.map(s => ({ id: s.id, label: s.label, count: s.events.length })),
|
|
});
|
|
});
|
|
|
|
routes.get('/api/chronicle.mw', async c => {
|
|
const { projection } = await buildProjection(c);
|
|
return c.text(projection.text, 200, { 'content-type': 'text/plain; charset=utf-8' });
|
|
});
|
|
|
|
routes.get('/render', async c => {
|
|
const { projection, view } = await buildProjection(c);
|
|
const html = renderMarkwhenHtml(projection.text, view);
|
|
return c.html(html);
|
|
});
|
|
|
|
export const rpastModule: RSpaceModule = {
|
|
id: 'rpast',
|
|
name: 'rPast',
|
|
icon: '🕰️',
|
|
description: 'Chronicle-of-self — every rApp creation, ever, on one timeline.',
|
|
routes,
|
|
landingPage: renderLanding,
|
|
scoping: { defaultScope: 'space', userConfigurable: false },
|
|
docSchemas: [{
|
|
pattern: '{space}:rpast:chronicles',
|
|
description: 'Saved chronicle configs (module selection + filters + mode)',
|
|
init: pastSchema.init,
|
|
}],
|
|
};
|