Merge branch 'dev'

This commit is contained in:
Jeff Emmett 2026-03-03 17:43:17 -08:00
commit ec67bf46a5
2 changed files with 119 additions and 4 deletions

View File

@ -17,7 +17,7 @@ import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import { renderLanding } from "./landing";
import type { SyncServer } from '../../server/local-first/sync-server';
import { calendarSchema, calendarDocId } from './schemas';
import type { CalendarDoc, CalendarEvent, CalendarSource } from './schemas';
import type { CalendarDoc, CalendarEvent, CalendarSource, ScheduledItemMetadata } from './schemas';
let _syncServer: SyncServer | null = null;
@ -313,7 +313,8 @@ routes.post("/api/events", async (c) => {
const space = c.req.param("space") || "demo";
const body = await c.req.json();
const { title, description, start_time, end_time, all_day, timezone, source_id, location_id, location_name,
is_virtual, virtual_url, virtual_platform, r_tool_source, r_tool_entity_id } = body;
is_virtual, virtual_url, virtual_platform, r_tool_source, r_tool_entity_id,
is_scheduled_item, provenance, item_preview } = body;
if (!title?.trim() || !start_time) return c.json({ error: "Title and start_time required" }, 400);
const docId = calendarDocId(space);
@ -321,6 +322,36 @@ routes.post("/api/events", async (c) => {
const eventId = crypto.randomUUID();
const now = Date.now();
// Build metadata for scheduled knowledge items
let metadata: ScheduledItemMetadata | null = null;
if (is_scheduled_item && provenance) {
metadata = {
isScheduledItem: true,
provenance: {
rid: provenance.rid || '',
contentHash: provenance.content_hash ?? provenance.contentHash ?? null,
sourceType: provenance.source_type ?? provenance.sourceType ?? '',
sourceSpace: provenance.source_space ?? provenance.sourceSpace ?? space,
lifecycleEvent: provenance.lifecycle_event ?? provenance.lifecycleEvent ?? 'NEW',
originalCreatedAt: provenance.original_created_at ?? provenance.originalCreatedAt ?? now,
scheduledBy: provenance.scheduled_by ?? provenance.scheduledBy ?? null,
scheduledAt: now,
},
itemPreview: item_preview ? {
textPreview: item_preview.text_preview ?? item_preview.textPreview ?? '',
mimeType: item_preview.mime_type ?? item_preview.mimeType ?? 'text/plain',
thumbnailUrl: item_preview.thumbnail_url ?? item_preview.thumbnailUrl,
canvasUrl: item_preview.canvas_url ?? item_preview.canvasUrl,
shapeSnapshot: item_preview.shape_snapshot ?? item_preview.shapeSnapshot,
} : {
textPreview: description || title,
mimeType: 'text/plain',
},
reminderSent: false,
reminderSentAt: null,
};
}
_syncServer!.changeDoc<CalendarDoc>(docId, `create event ${eventId}`, (d) => {
d.events[eventId] = {
id: eventId,
@ -346,11 +377,11 @@ routes.post("/api/events", async (c) => {
isVirtual: is_virtual || false,
virtualUrl: virtual_url || null,
virtualPlatform: virtual_platform || null,
rToolSource: r_tool_source || null,
rToolSource: r_tool_source || 'rSchedule',
rToolEntityId: r_tool_entity_id || null,
attendees: [],
attendeeCount: 0,
metadata: null,
metadata: metadata,
createdAt: now,
updatedAt: now,
};
@ -360,6 +391,41 @@ routes.post("/api/events", async (c) => {
return c.json(eventToRow(updated.events[eventId], updated.sources), 201);
});
// GET /api/events/scheduled — query only scheduled knowledge items
routes.get("/api/events/scheduled", async (c) => {
const space = c.req.param("space") || "demo";
const { date, upcoming, pending_only } = c.req.query();
const doc = ensureDoc(space);
let events = Object.values(doc.events).filter((e) => {
const meta = e.metadata as ScheduledItemMetadata | null;
return meta?.isScheduledItem === true;
});
if (date) {
const dayStart = new Date(date).setHours(0, 0, 0, 0);
const dayEnd = dayStart + 86400000;
events = events.filter((e) => e.startTime >= dayStart && e.startTime < dayEnd);
}
if (upcoming) {
const nowMs = Date.now();
const futureMs = nowMs + parseInt(upcoming) * 86400000;
events = events.filter((e) => e.startTime >= nowMs && e.startTime <= futureMs);
}
if (pending_only === "true") {
events = events.filter((e) => {
const meta = e.metadata as ScheduledItemMetadata;
return !meta.reminderSent;
});
}
events.sort((a, b) => a.startTime - b.startTime);
const rows = events.map((e) => eventToRow(e, doc.sources));
return c.json({ count: rows.length, results: rows });
});
// GET /api/events/:id
routes.get("/api/events/:id", async (c) => {
const space = c.req.param("space") || "demo";
@ -399,6 +465,7 @@ routes.patch("/api/events/:id", async (c) => {
location_name: 'locationName',
is_virtual: 'isVirtual',
virtual_url: 'virtualUrl',
metadata: 'metadata',
};
const updates: Array<{ field: keyof CalendarEvent; value: any }> = [];

View File

@ -87,6 +87,54 @@ export const calendarSchema: DocSchema<CalendarDoc> = {
}),
};
// ── KOI-Inspired Provenance (stored in CalendarEvent.metadata) ──
/** Lifecycle events following BlockScience KOI's RID pattern */
export type ProvenanceLifecycle = 'NEW' | 'UPDATE' | 'FORGET';
/** KOI-inspired provenance — stored in CalendarEvent.metadata.provenance */
export interface KnowledgeProvenance {
/** Reference identifier in "type:uuid" format, e.g. "folk-note:abc-123" */
rid: string;
/** SHA-256 hex digest of content at scheduling time */
contentHash: string | null;
/** Shape type that originated this item (e.g. "folk-note", "folk-photo") */
sourceType: string;
/** Space slug where the item lives */
sourceSpace: string;
/** KOI lifecycle event */
lifecycleEvent: ProvenanceLifecycle;
/** Original creation timestamp of the source item (epoch ms) */
originalCreatedAt: number;
/** DID or user identifier who scheduled this */
scheduledBy: string | null;
/** When the scheduling action occurred (epoch ms) */
scheduledAt: number;
}
/** Preview snapshot for email rendering — stored in metadata.itemPreview */
export interface ItemPreview {
/** First ~200 chars of content */
textPreview: string;
/** MIME type: text/markdown, image/jpeg, etc. */
mimeType: string;
/** Thumbnail URL for photos/media */
thumbnailUrl?: string;
/** Deep link back to canvas shape */
canvasUrl?: string;
/** Full serialized shape data for reconstruction */
shapeSnapshot?: Record<string, unknown>;
}
/** Typed metadata shape when CalendarEvent represents a scheduled knowledge item */
export interface ScheduledItemMetadata {
isScheduledItem: true;
provenance: KnowledgeProvenance;
itemPreview: ItemPreview;
reminderSent: boolean;
reminderSentAt: number | null;
}
// ── Helpers ──
export function calendarDocId(space: string) {