From dcb303284c4139d29570699c397412d684a3b8d3 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Wed, 29 Jul 2026 11:52:26 +0200 Subject: [PATCH] Describe snapshot source revisions --- docs/current-architecture.md | 10 ++- docs/spec/08-current-product-backlog.md | 32 +++++----- .../project-history.repository.ts | 61 ++++++++++++++++++- src/domain/ports/project-history.store.ts | 4 ++ .../components/project-version-history.tsx | 14 ++++- src/frontend/types.ts | 1 + src/frontend/utils/project-version-history.ts | 29 ++++++++- .../project-snapshot.controller.ts | 38 ++++++++++-- tests/project-history.repository.test.ts | 27 ++++++++ tests/project-version-history.test.ts | 57 ++++++++++++++++- 10 files changed, 246 insertions(+), 27 deletions(-) diff --git a/docs/current-architecture.md b/docs/current-architecture.md index 56df731..60d9b3a 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -149,8 +149,14 @@ Upgrade-only-Verknüpfungen und Migrationsnachweise bleiben erhalten, obwohl sie nicht Bestandteil des logischen Snapshots sind. Die Projektseite bindet diese APIs in einem einklappbaren Bereich „Versionen und Sicherungspunkte“ ein. Dort können Benutzer Sicherungspunkte -benennen, nach expliziter Bestätigung wiederherstellen und die paginierte -Revisions-Timeline mit deutschen Quellen- und Änderungsbezeichnungen lesen. +benennen, jeden aufgeführten benannten oder automatischen Stand nach +expliziter Bestätigung wiederherstellen und die paginierte Revisions-Timeline +mit deutschen Quellen- und Änderungsbezeichnungen lesen. Die Snapshot-Liste +ordnet jede positive `sourceRevision` serverseitig ihren unveränderlichen +Revisionsmetadaten zu. Dadurch zeigt auch ein älterer automatischer Stand die +auslösende Änderung, ohne eine zweite frei formulierte Beschreibung zu +speichern oder von den zuletzt paginiert geladenen Timeline-Einträgen +abzuhängen. Revision null wird als Projektstart dargestellt. Projektweites Rückgängig/Wiederholen bleibt im Kopf dieses Bereichs auch im eingeklappten Zustand erreichbar. Die Verfügbarkeit stammt direkt aus den persistierten Server-Stacks; nach einer Historienaktion oder einem Restore lädt diff --git a/docs/spec/08-current-product-backlog.md b/docs/spec/08-current-product-backlog.md index e9a3e07..83b1658 100644 --- a/docs/spec/08-current-product-backlog.md +++ b/docs/spec/08-current-product-backlog.md @@ -10,53 +10,53 @@ requirements and intended sequencing, not proof of implementation. ## Project History -- Every listed logical snapshot, including automatic snapshots, must be +- [x] Every listed logical snapshot, including automatic snapshots, must be explicitly restorable from the project history UI. -- Show which project change produced a snapshot. Prefer immutable revision +- [x] Show which project change produced a snapshot. Prefer immutable revision metadata over storing a second free-form description of the same change. -- Keep restoration auditable as a new project revision and preserve persistent +- [x] Keep restoration auditable as a new project revision and preserve persistent undo/redo. ## Distribution Boards -- Assign an optional project floor to a distribution board. -- Store a supply classification for each distribution board: +- [x] Assign an optional project floor to a distribution board. +- [x] Store a supply classification for each distribution board: - `AV` (Allgemeine Stromversorgung) - `SV` (Sicherheitsstromversorgung) - `EV` (Ersatzstromversorgung) - `USV` (Unterbrechungsfreie Stromversorgung) - `MSR` (Mess-, Steuerungs- und Regelungstechnik) - `SiBe` (Sicherheitsbeleuchtung) -- Let project settings select which catalog supply types are used in a +- [x] Let project settings select which catalog supply types are used in a project. Distribution-board dialogs offer only that selection; a type in use cannot be disabled. -- Include both fields in project snapshots and portable project +- [x] Include both fields in project snapshots and portable project export/import. -- Persist changes through project commands so they remain undoable after a +- [x] Persist changes through project commands so they remain undoable after a restart. ## Device Voltage Derivation -- Derive voltage from the selected phase type and the project settings: +- [x] Derive voltage from the selected phase type and the project settings: - single-phase uses the project's single-phase voltage; - three-phase uses the project's three-phase voltage. -- Voltage is not editable on global devices, project devices or circuits. +- [x] Voltage is not editable on global devices, project devices or circuits. Global devices store only their phase; copying one into a project derives the target project's voltage. -- Project-device voltage follows its phase. Circuit voltage follows the +- [x] Project-device voltage follows its phase. Circuit voltage follows the section phase; an unassigned circuit is three-phase only when all assigned device rows with a valid phase are three-phase. -- Changing project voltage settings updates all project devices and circuits +- [x] Changing project voltage settings updates all project devices and circuits atomically in the same persistent Undo/Redo step. -- Database migration `0019` and snapshot schema version `5` normalize older +- [x] Database migration `0019` and snapshot schema version `5` normalize older stored values. Version-four and older imports remain supported and are normalized while being upgraded. ## Recommended Sequence -1. Distribution-board floor and supply fields. -2. Device voltage derivation without overrides. -3. Snapshot-to-revision descriptions and history presentation. +1. [x] Distribution-board floor and supply fields. +2. [x] Device voltage derivation without overrides. +3. [x] Snapshot-to-revision descriptions and history presentation. The Revit/CSV/IFCGUID round-trip remains a separate jointly planned phase and must not be inferred from these tasks. diff --git a/src/db/repositories/project-history.repository.ts b/src/db/repositories/project-history.repository.ts index daea7e3..2fe968c 100644 --- a/src/db/repositories/project-history.repository.ts +++ b/src/db/repositories/project-history.repository.ts @@ -1,10 +1,11 @@ -import { and, asc, desc, eq, lt } from "drizzle-orm"; +import { and, asc, desc, eq, inArray, lt } from "drizzle-orm"; import { deserializeProjectCommand } from "../../domain/models/project-command.model.js"; import type { ProjectHistoryCommand, ProjectHistoryDirection, ListProjectRevisionsInput, ProjectRevisionPage, + ProjectRevisionSummary, ProjectHistoryState, ProjectHistoryStore, } from "../../domain/ports/project-history.store.js"; @@ -118,6 +119,64 @@ export class ProjectHistoryRepository implements ProjectHistoryStore { }; } + listRevisionsByNumbers( + projectId: string, + revisionNumbers: number[] + ): ProjectRevisionSummary[] { + for (const revisionNumber of revisionNumbers) { + if ( + !Number.isSafeInteger(revisionNumber) || + revisionNumber < 0 + ) { + throw new Error( + "Project revision numbers must be non-negative integers." + ); + } + } + const normalizedRevisionNumbers = [ + ...new Set(revisionNumbers), + ].filter((revisionNumber) => revisionNumber > 0); + if (!normalizedRevisionNumbers.length) { + return []; + } + + return this.database + .select({ + revisionId: projectRevisions.id, + changeSetId: projectChangeSets.id, + revisionNumber: projectRevisions.revisionNumber, + createdAtIso: projectRevisions.createdAtIso, + actorId: projectRevisions.actorId, + source: projectRevisions.source, + description: projectRevisions.description, + commandType: projectChangeSets.commandType, + payloadSchemaVersion: projectChangeSets.payloadSchemaVersion, + }) + .from(projectRevisions) + .innerJoin( + projectChangeSets, + eq( + projectChangeSets.projectRevisionId, + projectRevisions.id + ) + ) + .where( + and( + eq(projectRevisions.projectId, projectId), + inArray( + projectRevisions.revisionNumber, + normalizedRevisionNumbers + ) + ) + ) + .orderBy(desc(projectRevisions.revisionNumber)) + .all() + .map((row) => ({ + ...row, + source: row.source as ProjectRevisionSource, + })); + } + getNextCommand( projectId: string, direction: ProjectHistoryDirection diff --git a/src/domain/ports/project-history.store.ts b/src/domain/ports/project-history.store.ts index fd59654..aa591e7 100644 --- a/src/domain/ports/project-history.store.ts +++ b/src/domain/ports/project-history.store.ts @@ -47,6 +47,10 @@ export interface ProjectHistoryStore { listRevisions( input: ListProjectRevisionsInput ): ProjectRevisionPage | null; + listRevisionsByNumbers( + projectId: string, + revisionNumbers: number[] + ): ProjectRevisionSummary[]; getNextCommand( projectId: string, direction: ProjectHistoryDirection diff --git a/src/frontend/components/project-version-history.tsx b/src/frontend/components/project-version-history.tsx index 826d9d5..3907640 100644 --- a/src/frontend/components/project-version-history.tsx +++ b/src/frontend/components/project-version-history.tsx @@ -25,6 +25,7 @@ import { getProjectRevisionDescription, getProjectRevisionSourceLabel, getProjectSnapshotKindLabel, + getProjectSnapshotRevisionDescription, mergeProjectRevisionPages, } from "../utils/project-version-history"; @@ -343,7 +344,7 @@ export function ProjectVersionHistory({ Name - Projektstand + Gesicherter Projektstand Erstellt Aktion @@ -368,7 +369,16 @@ export function ProjectVersionHistory({ ) : null} - Revision {snapshot.sourceRevision} + + + {snapshot.sourceRevision === 0 + ? "Ausgangsstand" + : `Revision ${snapshot.sourceRevision}`} + +
+ {getProjectSnapshotRevisionDescription(snapshot)} +
+ {formatDateTime(snapshot.createdAtIso)}