Spaces:
Sleeping
Sleeping
| /** Typed API client with credentials and envelope handling. */ | |
| export type ApiErrorBody = { | |
| code: string; | |
| message: string; | |
| }; | |
| export class ApiError extends Error { | |
| code: string; | |
| constructor(error: ApiErrorBody) { | |
| super(error.message); | |
| this.code = error.code; | |
| this.name = "ApiError"; | |
| } | |
| } | |
| type Envelope<T> = { | |
| ok: boolean; | |
| data: T | null; | |
| error: ApiErrorBody | null; | |
| }; | |
| export type MeData = { | |
| authenticated: boolean; | |
| app_name: string; | |
| }; | |
| export type WalkType = "interrupt" | "fantasy" | "mixed"; | |
| export type Entry = { | |
| id: string; | |
| ts: string; | |
| created_at: string; | |
| updated_at: string; | |
| activity: string; | |
| happened: string; | |
| emotions: string[]; | |
| intensity: number; | |
| remedy: string; | |
| result: "worked" | "partial" | "failed" | "pending"; | |
| tags: string[]; | |
| notes: string; | |
| fse_spike?: boolean | null; | |
| avoidance_types?: string[]; | |
| proof_brick?: string | null; | |
| walk_type?: WalkType | null; | |
| coach: { | |
| text: string | null; | |
| source: string | null; | |
| model: string | null; | |
| ts: string | null; | |
| trace_id: string | null; | |
| }; | |
| }; | |
| export type EntryListData = { | |
| items: Entry[]; | |
| total: number; | |
| limit: number; | |
| offset: number; | |
| }; | |
| export type PrimaryBrick = "A" | "B" | "C" | "D" | "E" | "S" | "none"; | |
| export type ProofBrickKind = | |
| | "interrupt_walk" | |
| | "body_or_room" | |
| | "trip_admin" | |
| | "earn" | |
| | "boundary" | |
| | "food" | |
| | "survive" | |
| | "other" | |
| | "leave_room" | |
| | "admin_line" | |
| | "send_message" | |
| | "body_care"; | |
| export type DailyRow = { | |
| date: string; | |
| primary_brick: PrimaryBrick; | |
| brick_done: boolean; | |
| corn_sessions: number; | |
| delay_ok: boolean; | |
| daydream: "none" | "done" | "fc"; | |
| rerun: "clean" | "R"; | |
| court: "closed" | "court"; | |
| stayed_indoors_all_day: boolean; | |
| left_room: boolean; | |
| left_home: boolean; | |
| movement_minutes: number; | |
| interrupt_walk_minutes: number; | |
| fantasy_walk_minutes: number; | |
| headphones_on_walk: boolean; | |
| music_cinematic_on_walk: boolean; | |
| proof_brick_done: boolean; | |
| proof_brick_kind: ProofBrickKind | null; | |
| fantasy_minutes_scheduled: number; | |
| fantasy_minutes_unplanned: number; | |
| points: number; | |
| note: string; | |
| win?: string; | |
| updated_at: string; | |
| }; | |
| export type DailyRangeData = { | |
| items: DailyRow[]; | |
| week_points: number; | |
| band: "incomplete" | "strong" | "mixed" | "escape_heavy"; | |
| days_present: number; | |
| }; | |
| export type EntryCreate = { | |
| activity: string; | |
| happened: string; | |
| emotions: string[]; | |
| intensity: number; | |
| remedy: string; | |
| result: Entry["result"]; | |
| tags?: string[]; | |
| notes?: string; | |
| fse_spike?: boolean | null; | |
| avoidance_types?: string[]; | |
| proof_brick?: string | null; | |
| walk_type?: WalkType | null; | |
| }; | |
| export type EntryUpdate = Partial<EntryCreate & { ts: string }>; | |
| export type DailyUpsert = Omit<DailyRow, "date" | "points" | "updated_at">; | |
| export type ServerPick = { | |
| remedy_key: string; | |
| n: number; | |
| p_helped: number; | |
| p_worked?: number; | |
| rank?: number; | |
| pick?: number; | |
| match?: number; | |
| }; | |
| export type CoachResponse = { | |
| text: string; | |
| source: "model" | "backup" | "model_unparsed_fallback"; | |
| model: string | null; | |
| trace_id: string; | |
| flags: string[]; | |
| server_picks: ServerPick[]; | |
| parsed: Record<string, string> | null; | |
| }; | |
| export type StatRow = { | |
| key: string; | |
| n: number; | |
| p_helped?: number; | |
| p_worked?: number; | |
| p_failed?: number; | |
| rank?: number; | |
| }; | |
| export type StatsData = { | |
| n_entries_total: number; | |
| n_entries_scored: number; | |
| outcomes: Record<Entry["result"], number>; | |
| by_remedy: StatRow[]; | |
| by_tag: StatRow[]; | |
| by_emotion: StatRow[]; | |
| by_intensity_bucket: StatRow[]; | |
| daily: Record<string, number>; | |
| generated_at: string; | |
| DATA_THIN: boolean; | |
| }; | |
| export type SettingsStatus = { | |
| app_name: string; | |
| app_version: string; | |
| coach_configured: boolean; | |
| model: string; | |
| data_ok: boolean; | |
| env: string; | |
| }; | |
| function redirectLogin(): void { | |
| if (typeof window === "undefined") return; | |
| const hash = window.location.hash.replace(/^#/, "") || "/"; | |
| if (hash.startsWith("/login")) return; | |
| window.location.hash = "#/login"; | |
| } | |
| export async function api<T>( | |
| path: string, | |
| options: RequestInit = {}, | |
| ): Promise<T> { | |
| const headers = new Headers(options.headers || {}); | |
| if (!headers.has("Accept")) headers.set("Accept", "application/json"); | |
| if (options.body && !headers.has("Content-Type")) { | |
| headers.set("Content-Type", "application/json"); | |
| } | |
| const res = await fetch(path, { | |
| ...options, | |
| credentials: "include", | |
| headers, | |
| }); | |
| if (res.status === 401) { | |
| redirectLogin(); | |
| throw new ApiError({ code: "unauthorized", message: "Login required" }); | |
| } | |
| const body = (await res.json()) as Envelope<T>; | |
| if (!body.ok || body.error) { | |
| throw new ApiError( | |
| body.error || { code: "internal", message: "Request failed" }, | |
| ); | |
| } | |
| return body.data as T; | |
| } | |
| export function getMe(): Promise<MeData> { | |
| return api<MeData>("/api/auth/me"); | |
| } | |
| export function login(password: string): Promise<{ authenticated: boolean }> { | |
| return api("/api/auth/login", { | |
| method: "POST", | |
| body: JSON.stringify({ password }), | |
| }); | |
| } | |
| export function createEntry(payload: EntryCreate): Promise<Entry> { | |
| return api<Entry>("/api/entries", { | |
| method: "POST", | |
| body: JSON.stringify(payload), | |
| }); | |
| } | |
| export function listEntries(params: { | |
| result?: Entry["result"]; | |
| tag?: string; | |
| start?: string; | |
| end?: string; | |
| limit?: number; | |
| offset?: number; | |
| }): Promise<EntryListData> { | |
| const query = new URLSearchParams(); | |
| if (params.result) query.set("result", params.result); | |
| if (params.tag) query.set("tag", params.tag); | |
| if (params.start) query.set("start", params.start); | |
| if (params.end) query.set("end", params.end); | |
| if (params.limit) query.set("limit", String(params.limit)); | |
| if (params.offset) query.set("offset", String(params.offset)); | |
| const qs = query.toString(); | |
| return api<EntryListData>(`/api/entries${qs ? `?${qs}` : ""}`); | |
| } | |
| export function getEntry(id: string): Promise<Entry> { | |
| return api<Entry>(`/api/entries/${encodeURIComponent(id)}`); | |
| } | |
| export function updateEntry(id: string, payload: EntryUpdate): Promise<Entry> { | |
| return api<Entry>(`/api/entries/${encodeURIComponent(id)}`, { | |
| method: "PATCH", | |
| body: JSON.stringify(payload), | |
| }); | |
| } | |
| /** PATCH helper for resolve flows (result + optional remedy). */ | |
| export function patchEntry( | |
| id: string, | |
| payload: { result: Exclude<Entry["result"], "pending">; remedy?: string }, | |
| ): Promise<Entry> { | |
| return updateEntry(id, payload); | |
| } | |
| export function listPending(limit = 50): Promise<EntryListData> { | |
| return listEntries({ result: "pending", limit, offset: 0 }); | |
| } | |
| export function deleteEntry(id: string): Promise<{ deleted: boolean; id: string }> { | |
| return api(`/api/entries/${encodeURIComponent(id)}`, { method: "DELETE" }); | |
| } | |
| export function getDaily(day: string): Promise<DailyRow> { | |
| return api<DailyRow>(`/api/daily/${day}`); | |
| } | |
| export function listDaily(start: string, end: string): Promise<DailyRangeData> { | |
| return api<DailyRangeData>( | |
| `/api/daily?start=${encodeURIComponent(start)}&end=${encodeURIComponent(end)}`, | |
| ); | |
| } | |
| export function putDaily(day: string, payload: DailyUpsert): Promise<DailyRow> { | |
| return api<DailyRow>(`/api/daily/${day}`, { | |
| method: "PUT", | |
| body: JSON.stringify(payload), | |
| }); | |
| } | |
| export function runCoach(payload: { | |
| text?: string; | |
| entry_id?: string; | |
| persist?: boolean; | |
| }): Promise<CoachResponse> { | |
| return api<CoachResponse>("/api/coach", { | |
| method: "POST", | |
| body: JSON.stringify(payload), | |
| }); | |
| } | |
| export function coachEntry(id: string): Promise<CoachResponse> { | |
| return api<CoachResponse>(`/api/entries/${encodeURIComponent(id)}/coach`, { | |
| method: "POST", | |
| body: JSON.stringify({}), | |
| }); | |
| } | |
| export function getStats(): Promise<StatsData> { | |
| return api<StatsData>("/api/stats"); | |
| } | |
| export function getRemedies(): Promise<{ items: StatRow[]; min_n: number }> { | |
| return api("/api/stats/remedies"); | |
| } | |
| export function getSettingsStatus(): Promise<SettingsStatus> { | |
| return api("/api/settings/status"); | |
| } | |
| export function logout(): Promise<{ authenticated: boolean }> { | |
| return api("/api/auth/logout", { method: "POST" }); | |
| } | |
| export async function getDebugPaste(): Promise<string> { | |
| const res = await fetch("/api/debug/last/paste", { credentials: "include" }); | |
| if (res.status === 401) { | |
| redirectLogin(); | |
| throw new ApiError({ code: "unauthorized", message: "Login required" }); | |
| } | |
| if (!res.ok) { | |
| let message = "Debug paste unavailable"; | |
| try { | |
| const body = (await res.json()) as Envelope<never>; | |
| message = body.error?.message || message; | |
| } catch { | |
| // Plain response: keep the generic message. | |
| } | |
| throw new ApiError({ code: "request_failed", message }); | |
| } | |
| return res.text(); | |
| } | |
| export async function downloadExport(path: string): Promise<Blob> { | |
| const res = await fetch(path, { credentials: "include" }); | |
| if (res.status === 401) { | |
| redirectLogin(); | |
| throw new ApiError({ code: "unauthorized", message: "Login required" }); | |
| } | |
| if (!res.ok) throw new ApiError({ code: "request_failed", message: "Export failed" }); | |
| return res.blob(); | |
| } | |
| /* —— Schedule / Plan —— */ | |
| export type TaskKind = | |
| | "earn_ship" | |
| | "admin_spain" | |
| | "body_care" | |
| | "move_out" | |
| | "boundary" | |
| | "food_out" | |
| | "stabilize" | |
| | "explore" | |
| | "restore_fun" | |
| | "sleep_window" | |
| | "other"; | |
| export type ScheduledBlock = { | |
| id: string; | |
| date: string; | |
| start: string; | |
| end: string; | |
| title: string; | |
| kind: TaskKind; | |
| intent: "duty" | "explore" | "restore_fun" | "measure"; | |
| priority: "P0" | "P1" | "P2"; | |
| planned_min: number; | |
| status: "planned" | "done" | "partial" | "skipped" | "moved" | "cancelled"; | |
| source: string; | |
| locked: boolean; | |
| notes: string; | |
| version_added: number; | |
| label?: string; | |
| feedback?: BlockFeedback | null; | |
| }; | |
| export type BlockFeedback = { | |
| block_id: string; | |
| date: string; | |
| did: "done" | "partial" | "skipped"; | |
| actual_min: number | null; | |
| quality: number | null; | |
| fun: number | null; | |
| energy_after: number | null; | |
| money_amount: number | null; | |
| money_currency: string; | |
| would_repeat: "yes" | "no" | "maybe" | null; | |
| skip_reason: string | null; | |
| note: string; | |
| emotions?: string[]; | |
| fse_event?: string; | |
| intensity?: number | null; | |
| strong: boolean; | |
| ts: string; | |
| }; | |
| export type DayReview = { | |
| comment: string; | |
| emotions: string[]; | |
| fse_events: string; | |
| what_moved: string; | |
| what_avoided: string; | |
| tomorrow_change: string; | |
| }; | |
| export type DayPlanView = { | |
| date: string; | |
| version: number; | |
| source: string; | |
| blocks: ScheduledBlock[]; | |
| capacity_hint: number; | |
| notes: string; | |
| title?: string; | |
| intention?: string; | |
| constraints?: string[]; | |
| day_review?: DayReview; | |
| warnings?: string[]; | |
| updated_at: string; | |
| parent_version: number | null; | |
| health?: { | |
| score: number; | |
| strong_feedback_count: number; | |
| planned_count: number; | |
| p0_done_rate: number; | |
| explore_or_restore_done: boolean; | |
| }; | |
| import_warnings?: string[]; | |
| }; | |
| export type ChatPlan = { | |
| schema_version: number; | |
| date: string; | |
| title: string; | |
| intention: string; | |
| constraints: string[]; | |
| blocks: Array<Record<string, unknown>>; | |
| day_review: DayReview; | |
| summary?: { | |
| blocks_total: number; | |
| blocks_done: number; | |
| blocks_skipped: number; | |
| p0_done: number; | |
| p0_total: number; | |
| planned_min_sum: number; | |
| actual_min_sum: number; | |
| }; | |
| }; | |
| export type ScheduleTemplate = { | |
| id: string; | |
| title: string; | |
| kind: TaskKind; | |
| intent: string; | |
| priority: string; | |
| default_min: number; | |
| label: string; | |
| }; | |
| export function getPlan(day: string): Promise<DayPlanView> { | |
| return api(`/api/plan/${day}`); | |
| } | |
| export function putPlan( | |
| day: string, | |
| payload: { | |
| blocks: Partial<ScheduledBlock>[]; | |
| source?: string; | |
| notes?: string; | |
| capacity_hint?: number; | |
| force_p0_move?: boolean; | |
| }, | |
| ): Promise<DayPlanView> { | |
| return api(`/api/plan/${day}`, { | |
| method: "PUT", | |
| body: JSON.stringify(payload), | |
| }); | |
| } | |
| export function addPlanBlock( | |
| day: string, | |
| payload: { | |
| start: string; | |
| end: string; | |
| title: string; | |
| kind: TaskKind; | |
| intent: ScheduledBlock["intent"]; | |
| priority: ScheduledBlock["priority"]; | |
| planned_min?: number; | |
| locked?: boolean; | |
| notes?: string; | |
| }, | |
| ): Promise<DayPlanView> { | |
| return api(`/api/plan/${day}/blocks`, { | |
| method: "POST", | |
| body: JSON.stringify(payload), | |
| }); | |
| } | |
| export function patchPlanBlock( | |
| day: string, | |
| id: string, | |
| payload: Partial<ScheduledBlock>, | |
| ): Promise<DayPlanView> { | |
| return api(`/api/plan/${day}/blocks/${encodeURIComponent(id)}`, { | |
| method: "PATCH", | |
| body: JSON.stringify(payload), | |
| }); | |
| } | |
| export function deletePlanBlock(day: string, id: string): Promise<DayPlanView> { | |
| return api(`/api/plan/${day}/blocks/${encodeURIComponent(id)}`, { | |
| method: "DELETE", | |
| }); | |
| } | |
| export function postBlockFeedback( | |
| day: string, | |
| id: string, | |
| payload: Partial<BlockFeedback> & { did: BlockFeedback["did"] }, | |
| ): Promise<{ feedback: BlockFeedback; plan: DayPlanView }> { | |
| return api(`/api/plan/${day}/blocks/${encodeURIComponent(id)}/feedback`, { | |
| method: "POST", | |
| body: JSON.stringify(payload), | |
| }); | |
| } | |
| export function getScheduleTemplates(): Promise<{ items: ScheduleTemplate[] }> { | |
| return api("/api/schedule/templates"); | |
| } | |
| export function getSchedulePriors(): Promise<{ kinds: Array<Record<string, unknown>>; markdown: string }> { | |
| return api("/api/schedule/priors"); | |
| } | |
| export function reschedulePlan( | |
| day: string, | |
| payload: { reason?: string; force?: boolean } = {}, | |
| ): Promise<{ plan: DayPlanView; source: string; version: number }> { | |
| return api(`/api/plan/${day}/reschedule`, { | |
| method: "POST", | |
| body: JSON.stringify(payload), | |
| }); | |
| } | |
| export function seedPlan(day: string): Promise<DayPlanView> { | |
| return api(`/api/plan/${day}/seed`, { method: "POST", body: "{}" }); | |
| } | |
| export function importChatPlan( | |
| day: string, | |
| plan: unknown, | |
| mode: "replace" | "merge" = "replace", | |
| ): Promise<DayPlanView> { | |
| return api(`/api/plan/${day}/import`, { | |
| method: "POST", | |
| body: JSON.stringify({ plan, mode }), | |
| }); | |
| } | |
| export function exportChatPlan(day: string): Promise<ChatPlan> { | |
| return api(`/api/plan/${day}/export`); | |
| } | |
| export function checkPlanBlock( | |
| day: string, | |
| id: string, | |
| payload: { status: "done" | "partial" | "skipped" | "planned"; skip_reason?: string }, | |
| ): Promise<DayPlanView> { | |
| return api(`/api/plan/${day}/blocks/${encodeURIComponent(id)}/check`, { | |
| method: "POST", | |
| body: JSON.stringify(payload), | |
| }); | |
| } | |
| export function patchPlanMeta( | |
| day: string, | |
| payload: { | |
| title?: string; | |
| intention?: string; | |
| constraints?: string[]; | |
| day_review?: Partial<DayReview>; | |
| }, | |
| ): Promise<DayPlanView> { | |
| return api(`/api/plan/${day}/meta`, { | |
| method: "PATCH", | |
| body: JSON.stringify(payload), | |
| }); | |
| } | |
| export type LoopSummary = { | |
| days: number; | |
| as_of: string; | |
| indoors_streak: number; | |
| proof_rate: number; | |
| proof_days: number; | |
| days_with_daily: number; | |
| label_only_fail_rate: number; | |
| label_only_fail_n: number; | |
| high_intensity_n: number; | |
| movement_minutes_sum: number; | |
| pending_count: number; | |
| }; | |
| export function getLoopSummary(days = 7): Promise<LoopSummary> { | |
| return api(`/api/loop/summary?days=${days}`); | |
| } | |