/** * 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 { renderShell } from '../../server/shell'; import { getModuleInfoList } from '../../shared/module'; import { pastSchema } from './schemas'; import { renderLanding } from './landing'; const routes = new Hono(); // GET / — marketing landing on bare domain, interactive viewer in a space. routes.get('/', c => { const space = c.req.param('space') || 'demo'; if (!space || space === 'rpast.online') { return c.html(renderLanding()); } return c.html(renderShell({ title: `${space} — rPast | rSpace`, moduleId: 'rpast', spaceSlug: space, modules: getModuleInfoList(), theme: 'dark', body: ``, scripts: ``, })); }); 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. // Default to https — rSpace always runs behind Cloudflare/Traefik TLS in // production. Only fall back to http for loopback dev hosts. const host = c.req.header('x-forwarded-host') ?? c.req.header('host'); const isLocal = !host || /^(localhost|127\.|\[::1\])/i.test(host); const forwardedProto = c.req.header('x-forwarded-proto'); const proto = forwardedProto === 'http' || forwardedProto === 'https' ? forwardedProto : (isLocal ? '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, }], };