feat(rcred): auto-seed demo scores on startup + open recompute for demo

Triggers recomputeSpace('demo') 10s after init if no scores exist.
Allows unauthenticated recompute on demo space so visitors can click
the Recompute button.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-04-15 16:31:25 -04:00
parent 030d69b29f
commit f2b125a72f
2 changed files with 22 additions and 9 deletions

View File

@ -13,7 +13,7 @@ import { renderShell } from '../../server/shell';
import { getModuleInfoList } from '../../shared/module'; import { getModuleInfoList } from '../../shared/module';
import type { RSpaceModule } from '../../shared/module'; import type { RSpaceModule } from '../../shared/module';
import type { SyncServer } from '../../server/local-first/sync-server'; import type { SyncServer } from '../../server/local-first/sync-server';
import { graphSchema, scoresSchema, configSchema, configDocId } from './schemas'; import { graphSchema, scoresSchema, configSchema, configDocId, scoresDocId } from './schemas';
import type { CredConfigDoc } from './schemas'; import type { CredConfigDoc } from './schemas';
import { createCredRoutes } from './routes'; import { createCredRoutes } from './routes';
import { recomputeSpace, ensureConfigDoc } from './grain-engine'; import { recomputeSpace, ensureConfigDoc } from './grain-engine';
@ -83,6 +83,16 @@ export const credModule: RSpaceModule = {
_syncServer = syncServer; _syncServer = syncServer;
startCredCron(); startCredCron();
console.log('[rCred] Module initialized, cron started (6h interval)'); console.log('[rCred] Module initialized, cron started (6h interval)');
// Auto-seed demo space on startup (delayed to let docs load)
setTimeout(() => {
if (!_syncServer) return;
const scores = _syncServer.getDoc(scoresDocId('demo'));
if (!scores) {
console.log('[rCred] Seeding demo space scores...');
recomputeSpace('demo', _syncServer);
}
}, 10_000);
}, },
feeds: [ feeds: [

View File

@ -136,19 +136,22 @@ export function createCredRoutes(getSyncServer: () => SyncServer | null) {
return c.json({ ok: true }); return c.json({ ok: true });
}); });
// ── POST /api/recompute — trigger immediate recompute (auth required) ── // ── POST /api/recompute — trigger immediate recompute ──
// Demo space: open access. Other spaces: require member+ auth.
routes.post('/api/recompute', async (c) => { routes.post('/api/recompute', async (c) => {
const space = c.req.param('space') || c.req.query('space') || ''; const space = c.req.param('space') || c.req.query('space') || '';
if (!space) return c.json({ error: 'space required' }, 400); if (!space) return c.json({ error: 'space required' }, 400);
const token = extractToken(c.req.raw.headers); if (space !== 'demo') {
if (!token) return c.json({ error: 'Auth required' }, 401); const token = extractToken(c.req.raw.headers);
const claims = await verifyToken(token); if (!token) return c.json({ error: 'Auth required' }, 401);
if (!claims) return c.json({ error: 'Invalid token' }, 401); const claims = await verifyToken(token);
if (!claims) return c.json({ error: 'Invalid token' }, 401);
const resolved = await resolveCallerRole(space, claims); const resolved = await resolveCallerRole(space, claims);
if (!resolved || resolved.role === 'viewer') { if (!resolved || resolved.role === 'viewer') {
return c.json({ error: 'Membership required' }, 403); return c.json({ error: 'Membership required' }, 403);
}
} }
const result = recomputeSpace(space, ss()); const result = recomputeSpace(space, ss());