105 lines
3.7 KiB
TypeScript
105 lines
3.7 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 { 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: `<rpast-viewer space="${space}" style="height:calc(100vh - 52px);display:block"></rpast-viewer>`,
|
|
scripts: `<script type="module" src="/modules/rpast/rpast-viewer.js?v=1"></script>`,
|
|
}));
|
|
});
|
|
|
|
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,
|
|
}],
|
|
};
|