diff --git a/frontend/src/api/moments.ts b/frontend/src/api/moments.ts new file mode 100644 index 00000000..9a8f8581 --- /dev/null +++ b/frontend/src/api/moments.ts @@ -0,0 +1,101 @@ +import { apiGet, apiPost, apiDelete } from "@/api/client"; + +/** + * The moments of work rules mount on (milestone 458), and the actions that + * reach each one on this install — `GET /api/retrieval/moments`, the payload + * the `list_moments` MCP tool returns, plus the counts the Settings view shows + * beside each moment. + */ +export interface Moment { + name: string; + /** What is happening at this moment. */ + means: string; + /** The kinds of action that typically reach it. */ + reached_by: string; +} + +/** A shape rather than an entry: `skill.`, one moment per procedure. */ +export interface MomentFamily { + name: string; + prefix: string; + means: string; + reached_by: string; +} + +/** `default` ships with the product; `install` is this install's own. */ +export type ActionVia = "default" | "install"; + +export interface MomentAction { + tool: string; + /** Empty = every call of the tool. A command prefix for a command tool, + * `field=value` pairs for any other. */ + match: string; + via: ActionVia; +} + +/** A shipped default this install switched off. */ +export interface RemovedDefault { + id: number; + tool: string; + match: string; + moment: string; + reason: string | null; + actor: string | null; + created_at: string | null; +} + +export interface MomentUsage { + /** How many times a mounted rule arrived at this moment. */ + delivered: number; + /** How many distinct rules did. */ + rules: number; + /** Of those, how many an agent then opened — an upper bound: a pull records + * the door, not the line that prompted it. */ + opened: number; + last_delivered_at: string | null; +} + +export interface MomentsPayload { + moments: Moment[]; + families: MomentFamily[]; + total: number; + actions: Record; + removed_defaults: RemovedDefault[]; + /** Rules mounted per moment; a moment carrying none is absent. */ + mounted: Record; + /** The count read failed — `mounted` is empty for that reason, not because + * nothing is mounted. */ + mounted_failed?: boolean; + usage: { + by_moment: Record; + days: number; + /** Same distinction as `mounted_failed`, for the usage read. */ + moment_usage_failed?: boolean; + }; +} + +export interface MappingChange { + tool: string; + match?: string; + moment: string; + reason?: string; +} + +export function getMoments(days = 30): Promise { + return apiGet(`/api/retrieval/moments?days=${days}`); +} + +export function mapAction(change: MappingChange): Promise { + return apiPost("/api/retrieval/moments/mappings", change); +} + +/** Query parameters, not a body: DELETE bodies are not reliably sent. */ +export function unmapAction(change: MappingChange): Promise { + const q = new URLSearchParams({ + tool: change.tool, + match: change.match ?? "", + moment: change.moment, + reason: change.reason ?? "", + }); + return apiDelete(`/api/retrieval/moments/mappings?${q.toString()}`); +} diff --git a/frontend/src/api/rulebooks.ts b/frontend/src/api/rulebooks.ts index 16c3c25b..e84f10c0 100644 --- a/frontend/src/api/rulebooks.ts +++ b/frontend/src/api/rulebooks.ts @@ -82,6 +82,9 @@ export interface Rule { updated_at: string | null; /** Present only when the rule has them (the server omits empty keys). */ systems?: { id: number; name: string }[]; + /** The moments this rule arrives at whenever they happen (milestone 458), + * in catalog order. Present only when the rule is mounted. */ + moments?: string[]; relations?: RuleRelation[]; /** The lessons that point at this rule (milestone 440) — the concrete * situations judged instances of it, plus any suggested and awaiting a @@ -210,6 +213,8 @@ export interface RuleWrite { how_to_apply: string; order_index: number; system_ids: number[]; + /** Replaces the set; [] unmounts the rule from every moment. */ + moments: string[]; arose_from_id: number | null; verify_with: string; expires_when: string; diff --git a/frontend/src/assets/moments-shared.css b/frontend/src/assets/moments-shared.css new file mode 100644 index 00000000..0bb072f0 --- /dev/null +++ b/frontend/src/assets/moments-shared.css @@ -0,0 +1,26 @@ +/* Moments (milestone 458): the pieces the rule editor's picker and the + Settings section share, so a moment's name and a chip's remove control look + the same wherever a person meets them. Loaded unscoped by both. */ + +/* The name as a session reads it in a delivered line — shown as written. */ +.moment-name { + font-family: var(--fs-font-mono); + color: var(--fs-text-primary); +} + +/* The × on a removable chip: quiet until hovered, and keyboard-reachable. */ +.chip-remove { + background: none; + border: none; + cursor: pointer; + padding: 0; + font: inherit; + color: var(--fs-text-tertiary); +} +.chip-remove:hover:not(:disabled) { color: var(--fs-text-primary); } +.chip-remove:disabled { opacity: var(--fs-disabled-opacity); cursor: default; } +.chip-remove:focus-visible { + outline: none; + box-shadow: var(--fs-focus-ring); + border-radius: var(--fs-radius-sm); +} diff --git a/frontend/src/components/MomentsSettings.vue b/frontend/src/components/MomentsSettings.vue new file mode 100644 index 00000000..80e89ce1 --- /dev/null +++ b/frontend/src/components/MomentsSettings.vue @@ -0,0 +1,294 @@ + + + + + + +