Projects with several buildings could not be structured: floors and rooms
hung directly off the project. Floors can now belong to a building
(migration 0009: table buildings, nullable floors.building_id). Rooms and
distribution boards keep their floor; their building follows from it.
Existing floors stay without a building.
Buildings use the location-structure command store with
project-building.insert/.update/.delete, inverses and persistent Undo/Redo;
deleting a building with assigned floors is rejected. Floor commands carry
buildingId since command version 2. Version 1 floor commands from existing
history predate buildings and are upgraded with buildingId = null before
they are executed and persisted, so forward and inverse keep one version.
Project snapshot schema 7 stores buildings and floor.buildingId; versions
1-6 are upgraded without buildings. Restore and portable transfer include
buildings, and the transfer remaps building ids.
Buildings, floors and rooms move from the project overview to their own
page /projects/:id/struktur with three columns (Gebäude -> Etagen -> Räume)
that filter each other, and a sidebar entry. Floor selects in the board and
room dialogs are grouped by building. Unchanged dialogs close without a
command. Details in docs/building-structure.md; see issue #6.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Project settings gain siteAddress, workStage, projectLead and
projectEditor (project.update-settings schema version 3, migration 0009);
versions 2 and 1 stay executable and keep the current cover values.
Snapshot schema 6 carries the fields; older versions upgrade with null.
- Global office profile (office name/address) next to the document logo:
table document_branding_settings, PUT /api/document-branding/office,
editable from the print view toolbar.
- Cover page reads all fields; empty values show the "[ergänzen]" marker.
The Planstände editor column shows the project editor.
- Shared logo constants moved to a dependency-free module so the Next.js
bundler can import them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Seitenansicht (/projects/:projectId/print): tabular cover page, board
overview with supply structure and power sums, one plan per board with a
repeating title block; PDF via the browser print dialog with named @page
rules (A4 portrait cover, A4/A3 landscape plans, page x of y).
- Distribution-board info card (board number, location, fed-from, remark)
in the board edit dialog, persisted through the additively extended
distribution-board.update command (migration 0008, snapshot schema 6,
legacy subtree commands normalized).
- Global document logo upload (PNG/JPEG/SVG, max 2 MB) outside project
revisions and snapshots.
- fed-from stays free text; the overview hierarchy and cumulated sums are
preliminary until the feed link is modeled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Running the stack on built output stopped the idle CPU load, but the
remaining findings from the deployment report were still open.
The Dockerfile is now multi-stage. The prod target runs on node:24-slim as
the "node" user instead of root and carries only its runtime dependencies;
"npm prune --omit=dev" drops tsx, drizzle-kit and typescript. The migration
scripts need drizzle-orm and better-sqlite3, both regular dependencies, so
they keep working. The dev target keeps devDependencies for compose.dev.yaml.
Both compose files get a mem_limit. The server stack also sets memswap_limit
to the same value, so a leak lets the OOM killer replace the container
instead of pushing the host into swap. The Express API is no longer
published on the host: it has no authentication and is reached through the
Next.js rewrite. Development ports are bound to the loopback interface.
Services drop all capabilities and run with no-new-privileges.
SIGTERM and SIGINT now close the HTTP server and the SQLite handle and exit,
instead of only logging the signal and waiting for the container's SIGKILL
ten seconds later. server.closeIdleConnections() releases the keep-alive
sockets the Next.js server holds, which would otherwise keep server.close()
pending for the whole grace period. The compose start commands exec into
node, because otherwise sh stays the foreground process and the signal never
reaches it.
Delete scripts/docker-start.sh rather than fixing its unsupervised
background API: both compose stacks set their own command, so the
single-container entrypoint has no caller left. Also remove drizzle.config.js
(a stray tsc artifact drizzle-kit never reads) and the pre-Next index.html
and styles.css.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
compose.yaml overrode the image's own entrypoint to start "tsx watch" and
"next dev" with CHOKIDAR_USEPOLLING and WATCHPACK_POLLING enabled. The
built dist/ and .next from the image were never used, and the polling
watchers kept both containers busy around the clock whether or not
anyone used the application.
Split the two use cases: compose.yaml now runs node dist/server/index.js
and next start with no source mounts and no watchers, and the watching
setup moves to compose.dev.yaml.
next build writes the rewrite destinations from next.config.mjs into
.next/routes-manifest.json, so next start cannot pick up API_INTERNAL_URL
at runtime the way next dev does. Pass it as a build argument.
Give the web container its own probe. The healthcheck hit "/", which
redirects to "/projects", and fetch follows redirects, so every probe
rendered two pages every five seconds. "/health" could not serve as the
probe because next.config.mjs rewrites it to the API. Add "/web-health"
as a route handler, exclude it from the proxy matcher, and relax both
intervals to 30s.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- circuit-list-editor-api.md: next-identifier moved under
/projects/:projectId and now documents its 404 scoping behavior.
- current-architecture.md: clarify that the BMK trigger register's
DB-level normalization is ASCII-only and describe the added
application-level Unicode-aware uniqueness check that closes the
umlaut gap; note the additive migration 0006 indexes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Review of the logging work from the previous commit turned up a real
correctness issue: the uncaughtException/unhandledRejection handlers
logged the error but let the process keep running, which silently
disabled Node's default crash-on-fatal-error behavior and could leave
a zombie process serving broken requests instead of restarting. Both
processes now log and then exit(1); restart: unless-stopped is added
to both services so Docker actually brings them back up.
Also: harden the logger against JSON.stringify throwing on
non-serializable meta, log aborted (closed-before-finished) requests
in the API access log, and stop the Next.js proxy from logging the
Docker healthcheck's request to "/" every few seconds so real
navigation events aren't drowned out.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a leveled JSON logger (error/warn/info/verbose/debug, controlled via
LOG_LEVEL) wired into the Express API (access log, error middleware,
crash handlers, memory heartbeat) and the Next.js server (page-request
proxy, instrumentation crash handlers, heartbeat). LOG_LEVEL is exposed
through compose.yaml, and both services now rotate their Docker logs
(json-file, 20m x 10 files) instead of growing unbounded. Intended to
capture long-running diagnostic data for the intermittent 502s seen on
the Docker host.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>