Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
950b844
Centralize all backend environment access into a single typed, valida…
aaronstevenwhite Jun 17, 2026
3cc1193
Merge pull request #145 from parafovea/refactor/phase-1a-backend-config
aaronstevenwhite Jun 17, 2026
360f1e8
Add a create-release job to release.yml that extracts the matching CH…
aaronstevenwhite Jun 17, 2026
fac87bb
Merge pull request #146 from parafovea/fix/release-workflow-auto-gith…
aaronstevenwhite Jun 17, 2026
d0c633b
Centralize all frontend environment access into a single typed, deep-…
aaronstevenwhite Jun 17, 2026
3bc662d
Merge pull request #147 from parafovea/refactor/phase-1b-frontend-config
aaronstevenwhite Jun 17, 2026
10038a1
Promote the model-service to a single typed pydantic-settings Setting…
aaronstevenwhite Jun 17, 2026
df2c545
Merge pull request #148 from parafovea/refactor/phase-1c-model-servic…
aaronstevenwhite Jun 17, 2026
075576c
Add a CI configuration-drift guard (scripts/check-env-example.mjs, wi…
aaronstevenwhite Jun 17, 2026
fbb6cd3
Merge pull request #149 from parafovea/refactor/phase-1d-config-drift…
aaronstevenwhite Jun 17, 2026
96a7454
Pin Node and pnpm in one place by adding a root .nvmrc (Node 22) and …
aaronstevenwhite Jun 17, 2026
7b27938
Merge pull request #150 from parafovea/refactor/phase-2a-node-pnpm-pin
aaronstevenwhite Jun 17, 2026
206314a
Add a verify-compose CI job that validates all ten docker compose con…
aaronstevenwhite Jun 17, 2026
909a288
Merge pull request #151 from parafovea/refactor/phase-2b-compose-vali…
aaronstevenwhite Jun 17, 2026
eb03c3a
Delete the duplicate server/docker-compose.dev.yml that redefined Pos…
aaronstevenwhite Jun 17, 2026
e6b19c8
Merge pull request #152 from parafovea/refactor/phase-2c-dev-infra-co…
aaronstevenwhite Jun 17, 2026
9b2541c
Consolidate the model-service's orphaned tests directory into the con…
aaronstevenwhite Jun 17, 2026
252963a
Merge pull request #153 from parafovea/refactor/phase-3a-consolidate-…
aaronstevenwhite Jun 17, 2026
7453b6e
Add a root Makefile as the single install/lint/typecheck/test/build e…
aaronstevenwhite Jun 17, 2026
1a11923
Merge pull request #154 from parafovea/refactor/phase-3b-unified-test…
aaronstevenwhite Jun 17, 2026
2994852
Reconcile CI documentation with reality by rewriting the stale workfl…
aaronstevenwhite Jun 17, 2026
dcf10da
Merge pull request #155 from parafovea/refactor/phase-3c-ci-reconcili…
aaronstevenwhite Jun 17, 2026
c7cfc3b
Stop the startup config validation from requiring API_KEY_ENCRYPTION_…
aaronstevenwhite Jun 18, 2026
3f194e0
Merge pull request #156 from parafovea/fix/api-key-encryption-startup…
aaronstevenwhite Jun 18, 2026
b09a655
Extract the personas domain into a PersonaRepository (pure data acces…
aaronstevenwhite Jun 18, 2026
25dddcc
Make the persistent test persona name unique per test (workerIndex pl…
aaronstevenwhite Jun 18, 2026
9f14854
Merge pull request #157 from parafovea/refactor/phase-4-1-personas-se…
aaronstevenwhite Jun 18, 2026
ca12444
Extract the projects domain into a ProjectRepository and ProjectServi…
aaronstevenwhite Jun 18, 2026
bb90695
Merge pull request #158 from parafovea/refactor/phase-4-2-projects-se…
aaronstevenwhite Jun 18, 2026
6950e35
Extract the claims domain into a ClaimRepository and ClaimService, re…
aaronstevenwhite Jun 18, 2026
19caa51
Merge pull request #159 from parafovea/refactor/phase-4-3-claims-serv…
aaronstevenwhite Jun 18, 2026
e77e2f9
Extract the world-state domain into a WorldStateRepository and WorldS…
aaronstevenwhite Jun 18, 2026
646e6d8
Merge pull request #160 from parafovea/refactor/phase-4-4-world-servi…
aaronstevenwhite Jun 18, 2026
2e1bee8
Split the 2232-line import-handler.ts import state machine into focus…
aaronstevenwhite Jun 18, 2026
aaf2a87
Merge pull request #161 from parafovea/refactor/phase-4-5-import-hand…
aaronstevenwhite Jun 18, 2026
293795c
Stand up the OpenAPI to frontend-types generation pipeline by emittin…
aaronstevenwhite Jun 18, 2026
ccac9ec
Merge pull request #162 from parafovea/refactor/phase-4-6a-openapi-fr…
aaronstevenwhite Jun 18, 2026
5e3750b
Decouple the model-service contract layer from the ML stack by removi…
aaronstevenwhite Jun 19, 2026
1d9832d
Merge pull request #163 from parafovea/refactor/phase-4-6b-model-serv…
aaronstevenwhite Jun 19, 2026
4ae665e
Add the server↔model-service contract pipeline by emitting a committe…
aaronstevenwhite Jun 19, 2026
bb3d97d
Merge pull request #164 from parafovea/refactor/phase-4-6b2-model-ser…
aaronstevenwhite Jun 19, 2026
8c76b87
Resolve cross-user object names server-side by adding an optional lin…
aaronstevenwhite Jun 19, 2026
e060f7b
Merge pull request #165 from parafovea/refactor/phase-4-6c-linked-obj…
aaronstevenwhite Jun 19, 2026
2ac946d
Make POST /api/annotations idempotent on an optional client-supplied …
aaronstevenwhite Jun 19, 2026
28cdd11
Merge pull request #166 from parafovea/refactor/phase-4-7-annotation-…
aaronstevenwhite Jun 19, 2026
7db24c2
Fix the annotation autosave idle loop and box duplication by strippin…
aaronstevenwhite Jun 19, 2026
aba44e8
Merge pull request #167 from parafovea/refactor/phase-4-8-annotation-…
aaronstevenwhite Jun 19, 2026
cc4c627
Split the model-service vlm and detection loader modules into one-cla…
aaronstevenwhite Jun 19, 2026
7b82384
Merge pull request #168 from parafovea/refactor/phase-4-9-model-servi…
aaronstevenwhite Jun 19, 2026
7b88130
Consolidate the scattered demo-mode RBAC read-widening into a single …
aaronstevenwhite Jun 19, 2026
39fbf23
Merge pull request #169 from parafovea/refactor/phase-4-10-demo-gatin…
aaronstevenwhite Jun 19, 2026
62c0315
Decompose the 1403-line AnnotationWorkspace component into three cohe…
aaronstevenwhite Jun 20, 2026
abb35c2
Merge pull request #170 from parafovea/refactor/phase-4-8b-annotation…
aaronstevenwhite Jun 20, 2026
34e1f4b
Finalize the 0.5.0 changelog date to the release date.
aaronstevenwhite Jun 20, 2026
b627ef2
Wrap the literal video tag in the useAnnotationState return-type JSDo…
aaronstevenwhite Jun 20, 2026
8f4c60d
Reword the import-module JSDoc and the ImportResultDialog summary so …
aaronstevenwhite Jun 20, 2026
2a3749d
fix: resolve a batch of field-reported bugs (0.5.1) (#173)
aaronstevenwhite Jun 22, 2026
212884f
Finalize the 0.5.1 changelog date to the 2026-06-22 release date in b…
aaronstevenwhite Jun 22, 2026
07ada4c
Merge main into release/0.5.x to reconcile the squash-flattened linea…
aaronstevenwhite Jun 22, 2026
bf61da5
Parse suffix and edge HTTP byte ranges per RFC 7233 in the video stre…
aaronstevenwhite Jun 22, 2026
75f95d0
Make clicking a claim card select it in place on the Claims tab
aaronstevenwhite Jun 24, 2026
ff1ac1b
Bump the version to 0.5.3 and record the claim-card selection fix in …
aaronstevenwhite Jun 24, 2026
2278ad3
fix: stamp project scope on summary and claim writes so collaborators…
aaronstevenwhite Jun 25, 2026
5ecf50a
Merge main into release/0.5.x to reconcile the squash-flattened linea…
aaronstevenwhite Jun 25, 2026
4b0e204
Merge main into release/0.5.x to reconcile the v0.5.4 squash-promote …
aaronstevenwhite Jun 27, 2026
51febf3
Fix ten audit-reported defects spanning group-delete ability-cache in…
aaronstevenwhite Jun 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,62 @@ All notable changes to the Fovea project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.5.5] - 2026-06-29

The 0.5.5 patch is an audit-driven hardening release ([#186](https://github.com/parafovea/fovea/pull/186)). It closes two higher-severity defects — a former group member kept the group's permissions until the server restarted, and concurrent edits to the world graph silently overwrote one another — together with a set of data-scope, idempotency, and cache-staleness gaps. Nothing is breaking; the API additively gains explicit DELETE endpoints for world collections and relations, an optional client-supplied `id` on claim creation, and a `409` on a duplicate video assignment.

### Fixed

#### Deleting a Group Clears Its Former Members' Cached Abilities

- Both group-delete handlers (`server/src/routes/groups.ts`) removed the group without invalidating its former members' in-memory ability cache. A group-scope, non-own-only permission compiles into a globally unconditioned CASL rule, so a former member kept the group's system-wide access until the process restarted or they logged in again. Each handler now snapshots the membership before the cascade and calls `invalidateUserAbilities` for every former member, mirroring `ProjectService.delete`.

#### Concurrent World-Graph Edits No Longer Overwrite One Another

- Every world add and update went through a whole-blob read-modify-write `PUT /api/world` with no serialization, so two edits in quick succession — or a Wikidata import racing a manual edit — read the same stale base and the last writer silently dropped the other's entities, events, or relations. Writes are now safe on two fronts: the client (`annotation-tool/src/store/queries/useWorld.ts`) funnels mutations through a single-flight chain that threads the latest server state into the next write, and the server (`server/src/services/world-state-service.ts`, `server/src/repositories/WorldStateRepository.ts`) merges each of the seven object arrays by `id` under an optimistic-concurrency guard rather than replacing them wholesale. Because merge-by-id makes removal-by-omission a no-op, every object removal — entities, events, times, the three collection kinds, and relations — now goes through an explicit DELETE endpoint; the client delete hooks call the graceful `DELETE /api/world/...` routes, which also clean up dependent relations, collection memberships, and gloss references.

#### Personaless Object Annotations Inherit the Video's Project Scope

- Object annotations carry no persona, so the create path (`server/src/routes/annotations.ts`) had no project to stamp and persisted them with `projectId = NULL`, invisible to every project reviewer but the creator. When the caller is a member of exactly one project the video is assigned to, the annotation now inherits that project; an ambiguous (multiple) or absent assignment stays personal, and the existing CASL pre-authorization still validates the resolved scope.

#### Claim Creation Is Idempotent on a Client-Supplied ID

- Neither claim-create route accepted a client `id`, so a network retry or programmatic resend minted a duplicate claim. Both routes (`server/src/routes/claims.ts`, `server/src/services/claim-service.ts`) now accept an optional `id`; a resend carrying an existing id re-authorizes against the stored row and returns it instead of creating a duplicate, with a `P2002` race fallback. This mirrors the annotation idempotent-create hardening from 0.5.4.

#### Duplicate Video Assignment Returns 409 Instead of 500

- Re-assigning a video already assigned to a project violated the `@@unique([projectId, videoId])` constraint and surfaced as an unhandled `500`. The route (`server/src/routes/video-assignments.ts`) now catches the constraint violation and returns a `409 Conflict`.

#### The Summary Editor Autosaves Comment-Only Edits

- The video summary editor's autosave watched only the summary body, so editing the comment without touching the summary never triggered a save and the edit was lost on navigation. Autosave now compares a snapshot that includes the comment (`annotation-tool/src/components/video/VideoSummaryEditor.tsx`).

#### Relating Claims Refreshes Both Endpoints' Relation Panels

- Creating or deleting a claim relation invalidated only the source claim's relation query, leaving the target claim's relation panel stale until a manual refetch. Both mutations (`annotation-tool/src/store/queries/useClaims.ts`) now invalidate the target as well.

#### Membership and Role Changes Refresh the Client Ability Mirror

- The client ability cache carried a five-minute stale time and no mutation invalidated it, so a user's own role or membership change took up to five minutes to reflect in the UI even though the server always enforced it correctly. The six self-affecting project- and group-membership mutations (`annotation-tool/src/store/queries/useProjects.ts`, `useGroups.ts`) now invalidate the ability mirror on success.

#### Admin isAdmin Changes Stay in Sync With systemRole

- The admin user-update endpoint (`server/src/routes/users.ts`) wrote `isAdmin` but never `systemRole`, which is what CASL's `manage all` keys on, so a promotion or demotion left the two fields divergent and the cached abilities stale. The endpoint now sets `systemRole` to match and invalidates the affected user's abilities.

#### Removed a Dead Auto-Save Hook

- `useAutoSaveAnnotations` had no callers and re-armed the exact save-loop footgun the live autosave was hardened against; it has been deleted.

### Added

#### Explicit DELETE Endpoints for World Collections and Relations

- `server/src/routes/world.ts` gains `DELETE` routes for entity collections, event collections, time collections, and relations, so these objects are removed explicitly rather than by omission from a whole-blob `PUT`. This is what lets the non-clobbering merge-by-id persistence be safe (see Fixed).

#### Optional Client-Supplied ID on Claim Creation

- Both claim-create endpoints accept an optional `id`, enabling idempotent retries; the `Claim` response already echoes it.

## [0.5.4] - 2026-06-25

The 0.5.4 patch fixes project-scope and ownership stamping on video summaries and claims ([#181](https://github.com/parafovea/fovea/pull/181)). Project collaborators could not see a teammate's summary or add claims under it because summaries were persisted without their persona's project, and model-generated summaries and extracted claims were left unowned. Nothing is breaking; the API additively gains a `projectId` field on summary and claim responses.
Expand Down
2 changes: 1 addition & 1 deletion annotation-tool/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@fovea/annotation-tool",
"private": true,
"version": "0.5.4",
"version": "0.5.5",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
268 changes: 268 additions & 0 deletions annotation-tool/src/api/generated/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3774,6 +3774,254 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/world/entity-collections/{objectId}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
/** @description Delete a world entity collection from the caller's personal world */
delete: {
parameters: {
query?: never;
header?: never;
path: {
objectId: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Default Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
success: boolean;
};
};
};
/** @description Default Response */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
};
};
/** @description Default Response */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
};
};
};
};
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/world/event-collections/{objectId}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
/** @description Delete a world event collection from the caller's personal world */
delete: {
parameters: {
query?: never;
header?: never;
path: {
objectId: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Default Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
success: boolean;
};
};
};
/** @description Default Response */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
};
};
/** @description Default Response */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
};
};
};
};
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/world/time-collections/{objectId}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
/** @description Delete a world time collection from the caller's personal world */
delete: {
parameters: {
query?: never;
header?: never;
path: {
objectId: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Default Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
success: boolean;
};
};
};
/** @description Default Response */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
};
};
/** @description Default Response */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
};
};
};
};
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/world/relations/{objectId}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
/** @description Delete a world relation from the caller's personal world */
delete: {
parameters: {
query?: never;
header?: never;
path: {
objectId: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Default Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
success: boolean;
};
};
};
/** @description Default Response */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
};
};
/** @description Default Response */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
error: string;
};
};
};
};
};
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/world/events/{eventId}/deletion-preview": {
parameters: {
query?: never;
Expand Down Expand Up @@ -4901,6 +5149,8 @@ export interface paths {
requestBody: {
content: {
"application/json": {
/** Format: uuid */
id?: string;
summaryType: "video" | "collection";
text: string;
gloss?: {
Expand Down Expand Up @@ -5937,6 +6187,8 @@ export interface paths {
requestBody: {
content: {
"application/json": {
/** Format: uuid */
id?: string;
text: string;
gloss?: {
type: "text" | "typeRef" | "objectRef" | "annotationRef" | "claimRef";
Expand Down Expand Up @@ -10996,6 +11248,22 @@ export interface paths {
};
};
};
/** @description Default Response */
409: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
/** @description Machine-readable error code */
error: string;
/** @description Human-readable error message */
message: string;
/** @description Additional error context */
details?: unknown;
};
};
};
};
};
delete?: never;
Expand Down
Loading
Loading