Merge branch 'dev'
This commit is contained in:
commit
ec67bf46a5
|
|
@ -17,7 +17,7 @@ import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
|
||||||
import { renderLanding } from "./landing";
|
import { renderLanding } from "./landing";
|
||||||
import type { SyncServer } from '../../server/local-first/sync-server';
|
import type { SyncServer } from '../../server/local-first/sync-server';
|
||||||
import { calendarSchema, calendarDocId } from './schemas';
|
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;
|
let _syncServer: SyncServer | null = null;
|
||||||
|
|
||||||
|
|
@ -313,7 +313,8 @@ routes.post("/api/events", async (c) => {
|
||||||
const space = c.req.param("space") || "demo";
|
const space = c.req.param("space") || "demo";
|
||||||
const body = await c.req.json();
|
const body = await c.req.json();
|
||||||
const { title, description, start_time, end_time, all_day, timezone, source_id, location_id, location_name,
|
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);
|
if (!title?.trim() || !start_time) return c.json({ error: "Title and start_time required" }, 400);
|
||||||
|
|
||||||
const docId = calendarDocId(space);
|
const docId = calendarDocId(space);
|
||||||
|
|
@ -321,6 +322,36 @@ routes.post("/api/events", async (c) => {
|
||||||
const eventId = crypto.randomUUID();
|
const eventId = crypto.randomUUID();
|
||||||
const now = Date.now();
|
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) => {
|
_syncServer!.changeDoc<CalendarDoc>(docId, `create event ${eventId}`, (d) => {
|
||||||
d.events[eventId] = {
|
d.events[eventId] = {
|
||||||
id: eventId,
|
id: eventId,
|
||||||
|
|
@ -346,11 +377,11 @@ routes.post("/api/events", async (c) => {
|
||||||
isVirtual: is_virtual || false,
|
isVirtual: is_virtual || false,
|
||||||
virtualUrl: virtual_url || null,
|
virtualUrl: virtual_url || null,
|
||||||
virtualPlatform: virtual_platform || null,
|
virtualPlatform: virtual_platform || null,
|
||||||
rToolSource: r_tool_source || null,
|
rToolSource: r_tool_source || 'rSchedule',
|
||||||
rToolEntityId: r_tool_entity_id || null,
|
rToolEntityId: r_tool_entity_id || null,
|
||||||
attendees: [],
|
attendees: [],
|
||||||
attendeeCount: 0,
|
attendeeCount: 0,
|
||||||
metadata: null,
|
metadata: metadata,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
};
|
};
|
||||||
|
|
@ -360,6 +391,41 @@ routes.post("/api/events", async (c) => {
|
||||||
return c.json(eventToRow(updated.events[eventId], updated.sources), 201);
|
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
|
// GET /api/events/:id
|
||||||
routes.get("/api/events/:id", async (c) => {
|
routes.get("/api/events/:id", async (c) => {
|
||||||
const space = c.req.param("space") || "demo";
|
const space = c.req.param("space") || "demo";
|
||||||
|
|
@ -399,6 +465,7 @@ routes.patch("/api/events/:id", async (c) => {
|
||||||
location_name: 'locationName',
|
location_name: 'locationName',
|
||||||
is_virtual: 'isVirtual',
|
is_virtual: 'isVirtual',
|
||||||
virtual_url: 'virtualUrl',
|
virtual_url: 'virtualUrl',
|
||||||
|
metadata: 'metadata',
|
||||||
};
|
};
|
||||||
|
|
||||||
const updates: Array<{ field: keyof CalendarEvent; value: any }> = [];
|
const updates: Array<{ field: keyof CalendarEvent; value: any }> = [];
|
||||||
|
|
|
||||||
|
|
@ -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 ──
|
// ── Helpers ──
|
||||||
|
|
||||||
export function calendarDocId(space: string) {
|
export function calendarDocId(space: string) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue