mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
c2950e709f
Fixes from the 0.27.9 pre-release review. Servers: an unreadable rendered-HTML root falls back to the startup snapshot on both runtimes with a once-per-process warning instead of hanging (Pi) or answering 500 (Bun); the version diff is recomputed against current bytes on reload and carried through the in-app Refresh instead of being dropped, with no history write on a GET. Client: a Refresh action on the compact touch shell; HtmlSurfaceControls renders Refresh independently of the eye; the dead HtmlSurfaceActions removed. Threading: one linear, cycle-safe reply resolution shared by the annotations panel, its sort, and the export (5,000-chain tests), PATCH ingest on both runtimes rejects self-references and cycles, nothing is ever dropped from feedback. WebMCP and viewer hygiene: bounded tombstone and request memories, per-instance minted ids, nudge id caps, waiter cleanup on unmount, a shared retry epoch for diagram blocks. Docs: HTML Refresh documented, the WebMCP design pointer fixed, marketing pages updated. AI-assisted (Claude) under maintainer direction.
109 lines
3.7 KiB
TypeScript
109 lines
3.7 KiB
TypeScript
import type { SourceSaveCapability } from '@plannotator/shared/source-save';
|
|
|
|
type EnabledSourceSaveCapability = Extract<SourceSaveCapability, { enabled: true }>;
|
|
|
|
export type SourceSaveProbeResult =
|
|
| { status: 'ok'; sourceSave: EnabledSourceSaveCapability }
|
|
| { status: 'missing' }
|
|
| { status: 'unavailable' };
|
|
|
|
export interface HtmlVersionDiffFields {
|
|
/** The previous-version page rendered with inline ins/del highlights. */
|
|
diffHtml?: string;
|
|
previousPlan?: string | null;
|
|
versionInfo?: { version: number; totalVersions: number; project: string };
|
|
}
|
|
|
|
interface SourceDocumentResponse extends HtmlVersionDiffFields {
|
|
markdown?: string;
|
|
rawHtml?: string;
|
|
filepath?: string;
|
|
sourceSave?: SourceSaveCapability;
|
|
renderAs?: 'markdown' | 'html';
|
|
}
|
|
|
|
type SourceDocumentFetchResult =
|
|
| { status: 'ok'; data: SourceDocumentResponse }
|
|
| { status: 'missing' }
|
|
| { status: 'unavailable' };
|
|
|
|
export interface SourceDocumentSnapshot {
|
|
markdown: string;
|
|
sourceSave: EnabledSourceSaveCapability;
|
|
}
|
|
|
|
export type SourceDocumentSnapshotResult =
|
|
| { status: 'ok'; snapshot: SourceDocumentSnapshot }
|
|
| { status: 'missing' }
|
|
| { status: 'unavailable' };
|
|
|
|
/**
|
|
* A rendered-HTML document read from /api/doc. The version-diff fields are
|
|
* present only when the server served the session's ROOT document (it
|
|
* recomputes them against the bytes just read); linked docs carry none.
|
|
*/
|
|
export interface HtmlDocumentSnapshot extends HtmlVersionDiffFields {
|
|
rawHtml: string;
|
|
filepath: string;
|
|
}
|
|
|
|
export type HtmlDocumentSnapshotResult =
|
|
| { status: 'ok'; snapshot: HtmlDocumentSnapshot }
|
|
| { status: 'missing' }
|
|
| { status: 'unavailable' };
|
|
|
|
async function fetchSourceDocument(path: string): Promise<SourceDocumentFetchResult> {
|
|
try {
|
|
const res = await fetch(`/api/doc?path=${encodeURIComponent(path)}`);
|
|
if (res.status === 404) return { status: 'missing' };
|
|
if (!res.ok) return { status: 'unavailable' };
|
|
return { status: 'ok', data: await res.json() as SourceDocumentResponse };
|
|
} catch {
|
|
return { status: 'unavailable' };
|
|
}
|
|
}
|
|
|
|
export async function probeSourceSave(path: string): Promise<SourceSaveProbeResult> {
|
|
const result = await fetchSourceDocument(path);
|
|
if (result.status !== 'ok') return { status: result.status };
|
|
|
|
const { sourceSave } = result.data;
|
|
if (sourceSave?.enabled) return { status: 'ok', sourceSave };
|
|
if (sourceSave?.enabled === false && sourceSave.reason === 'missing-file') {
|
|
return { status: 'missing' };
|
|
}
|
|
return { status: 'unavailable' };
|
|
}
|
|
|
|
export async function fetchSourceDocumentSnapshot(path: string): Promise<SourceDocumentSnapshotResult> {
|
|
const result = await fetchSourceDocument(path);
|
|
if (result.status !== 'ok') return { status: result.status };
|
|
|
|
const { markdown, renderAs, sourceSave } = result.data;
|
|
if (sourceSave?.enabled === false && sourceSave.reason === 'missing-file') {
|
|
return { status: 'missing' };
|
|
}
|
|
if (renderAs === 'html' || typeof markdown !== 'string' || !sourceSave?.enabled) return { status: 'unavailable' };
|
|
return { status: 'ok', snapshot: { markdown, sourceSave } };
|
|
}
|
|
|
|
export async function fetchHtmlDocumentSnapshot(path: string): Promise<HtmlDocumentSnapshotResult> {
|
|
const result = await fetchSourceDocument(path);
|
|
if (result.status !== 'ok') return { status: result.status };
|
|
|
|
const { filepath, rawHtml, renderAs, diffHtml, previousPlan, versionInfo } = result.data;
|
|
if (renderAs !== 'html' || typeof rawHtml !== 'string' || typeof filepath !== 'string') {
|
|
return { status: 'unavailable' };
|
|
}
|
|
return {
|
|
status: 'ok',
|
|
snapshot: {
|
|
rawHtml,
|
|
filepath,
|
|
...(typeof diffHtml === 'string' ? { diffHtml } : {}),
|
|
...(previousPlan !== undefined ? { previousPlan } : {}),
|
|
...(versionInfo ? { versionInfo } : {}),
|
|
},
|
|
};
|
|
}
|