# AGENTS.md ## Overview Single-page app for tracking engineering projects across weekly sessions ("Sitzungen"). Vanilla JS/HTML/CSS frontend + Node.js/Express server. No build step, no bundler, no tests. Data is persisted in a SQLite database (`node:sqlite`, needs Node >= 22.13) at `sessions/projektuebersicht.db`. Two HTML entrypoints, both served statically by Express: - `Test.html` — the main app (tabs: Projektboard, Auslastung, Projekte, Projektbeteiligte). Served at `/`. - `project-plan.html` — per-project Gantt/TODO planning view, opened in a new tab via `openProjectPlan()` in `js/gantt.js`. Loads the shared helper modules (`globals`, `utils`, `ui`, `date-picker`, `server`, `gantt`) plus the plan modules under `js/plan/` — NOT the full main module set. All UI strings/comments are German. Preserve German in user-facing text. ## File structure ``` . ├── server.js # Express server + SQLite API ├── package.json # deps: express, cors only ├── docker-compose.yml # at repo ROOT; build context ., dockerfile Docker/Dockerfile ├── Docker/Dockerfile # node:22-alpine (nötig für node:sqlite) ├── .dockerignore ├── .vscode/extensions.json ├── public/ │ ├── Test.html # main app (served at /) │ ├── js/ # shared modules (classic scripts, global fns) │ │ ├── globals.js # Konstanten (LPHS, ROLES, LPH-Farben, DEPARTMENTS) + db/currentDepartment │ │ ├── utils.js # esc, uid, Datum-/Differenz-Helfer, personIdForName, peopleByRole, assignKey, weekSortKey │ │ ├── ui.js # LocalStorage-UI-State, Header-Collapse, Theme │ │ ├── date-picker.js # Datums-/Bereichspicker-Overlays │ │ ├── server.js # baseUrl, sendWeekToServer, sendPeopleToServer, blobToDataUrl │ │ ├── store.js # save/syncAllToServer, switchDepartment, load, seed │ │ ├── weeks.js # Wochen-Auswahl, -Erzeugung, -Klonen, Auto-Lock, -Löschung │ │ ├── gantt.js # GANTT-Diagramm (buildGanttHtml, openProjectPlan), geshared │ │ ├── showmode.js # Show-Mode (Präsentationsmodus) │ │ ├── tabs-auth.js # showTab, confirmPassword (SHA256-Passwortschutz) │ │ ├── pdf.js # PDF-Export (pdfmake, A3 quer) │ │ ├── csv.js # exportCSV (lädt ZIP von GET /export-csv) │ │ ├── backup.js # downloadBackup (lädt SQLite-Snapshot von GET /backup) │ │ ├── import.js # importDb (Backup-Wiederherstellung über POST /import, passwortgeschützt) │ │ └── init.js # renderAll/initUiState + Seiteneinstieg (nur Test.html) │ │ ├── board/ # tab-specific: Projektboard │ │ │ └── board.js # Projektboard-Rendering, Status-Editor, Anwesenheit, Zuordnung │ │ ├── people/ # tab-specific: Projektbeteiligte │ │ │ └── people.js # Personen-Tab-Rendering + Drag-and-Drop + Abwesenheiten │ │ ├── projects/ # tab-specific: Projekte │ │ │ └── projects.js # Projekte-Tab-Rendering + Drag-and-Drop │ │ ├── load/ # tab-specific: Auslastung │ │ │ └── load.js # Auslastungs-Tab, LP-Modal, Neues-Projekt-Modal │ ├── project-plan.html # Gantt/TODO planning view │ ├── js/plan/ # planning modules (nur von project-plan.html geladen) │ │ ├── plan-state.js # PlanApp-Zustand, Datenladepath, savePlan, Toasts │ │ ├── plan-build-gantt.js # buildGanttHtml-Erweiterung (TODO- und Meilensteinzeilen) │ │ ├── plan-gantt.js # renderPlanGantt, Abhängigkeitslinien, Zoom, Ressourcen-Load │ │ ├── plan-drag.js # GANTT-Balken-Drag/Resize + TODO-/Meilenstein-Zeilen-Drag │ │ ├── plan-modals.js # TODO-, Meilenstein- und Abwesenheits-Modals │ │ └── plan-schedule.js # Kritischer Pfad, Auto-Schedule, Mail-Link │ ├── styles.css │ └── dein_bild.ico └── sessions/ # SQLite-DB; bind-mounted into Docker container └── projektuebersicht.db # node:sqlite database (WAL mode) ``` ## Running ```bash npm install npm start # http://localhost:3003, or PORT=3004 if occupied docker compose up --build # Docker variant (compose file is at repo root) ``` - PORT env var overrides default 3003 in `server.js`. - Frontend must be served over HTTP: `baseUrl()` in `js/server.js` uses `window.location.origin` or falls back to `http://localhost:3003` when opened via `file://` (then only seed/demo data loads). - Check `/health` returns `{ok:true}` after starting. ## Server API (`server.js`) - `GET /` → `Test.html`; `GET /health`. - Deep links per department + tab: `GET //` → `Test.html`, e.g. `/HLS/Auslastung` or `/ELT/Projektübersicht`. Tab slugs (`TAB_SLUGS` in `js/globals.js`) are `Projektübersicht|Auslastung|Projekte|Projektbeteiligte`; anything else returns 404. Exception: `GET //Projektplan(/)?` (`PLAN_SLUG`) serves `project-plan.html`, the GANTT view, with `weekLabel|projNr` as query params (the project name is a readable path segment only; identification happens via weekLabel + projNr). The frontend reads/writes these routes via `applyUrlRoute()`/`updateRouteUrl()` in `js/ui.js` and all static assets in the HTML use **absolute** paths (`/js/...`, `/styles.css`) so they resolve on nested routes. - `GET /load-all?department=` → `{weeks:[weekObj], people:[personObj]}` with the global people list in `people`; weeks are full JSON week objects (id, label, date, locked, updatedAt, people, attendance, projects), sorted by label. - `POST /save-week {department, week, baseUpdatedAt}` → upserts one week (JSON object, keyed by department + label). Used by `sendWeekToServer()`. The client sends its local revision in `baseUpdatedAt` (the `updatedAt` value from `load-all`); the server compares it to `weeks.updated_at` before writing and answers `409 {ok:false, conflict:true, error, updatedAt}` if someone else saved the week in the meantime. A save with a **non-empty** base for a week the server no longer knows is also rejected with 409 ("wurde gelöscht") so a deleted week can't silently resurrect; only new weeks (empty/missing base) pass. On success it returns `{ok:true, updatedAt}` (the new revision), which the client stores as the new sync base. Clients WITHOUT `baseUpdatedAt` (old versions) always pass. The check runs synchronously in-process before the write transaction (no race between check and write). - `POST /save-people {department, people}` → replaces the department's global people list (JSON array). - `POST /delete-week {department, label}` → deletes the week; missing label yields 400, missing week tolerates with `deleted:false`. - `GET /export-csv?department=` → ZIP with one readable CSV per logical entity (wochen, personen, sitzungsteilnehmer, projekte, zuordnungen, leistungsphasen, aktuelle-leistungsphasen, aufwand-je-leistungsphase, projektplaene, todos, meilensteine) for that department. CSV: UTF-8 with BOM, CRLF, `;` separator by default (overridable via `?sep=`; German Excel opens it directly). ZIP is built in pure Node (`zlib.deflateRawSync`, manual CRC32, no external library). Used by `exportCSV()` in `js/csv.js`. - `GET /backup` → consistent SQLite snapshot (`VACUUM INTO`, WAL-safe) as a `.db` file download (temp file in `os.tmpdir()`, deleted after send). Used by `downloadBackup()` in `js/backup.js`. Additionally, on server startup the server writes a timestamped copy to `sessions/backups/` (also via `VACUUM INTO`) and keeps only the `MAX_BACKUPS` newest ones (default 10). - `POST /import` → restores the whole database from a `.db` backup file sent as a raw binary body (`express.raw`, limit 100 MB). Validates the schema (`pragma_table_info` on every table, exact column match vs. live DB), then swaps all data via `ATTACH DATABASE …` + DELETE/INSERT-SELECT inside one transaction (rollback on error). Immediately BEFORE the restore the server saves a fresh auto-backup of the current state via `runAutoBackup()` (so a restore can be undone), then performs the swap. Used by `importDb()` in `js/import.js`, which is password-gated (`requestedAction` type `'importDb'` in `js/tabs-auth.js`) like week deletion. - Valid departments hard-coded as `ELT|HLS|ARCH` in `server.js` (`VALID_DEPARTMENTS`); anything else silently falls back to `ELT`. Same set in `DEPARTMENTS` in `js/globals.js`. - Static assets get `Cache-Control: no-store`. - JSON body limit is 2 MB. ## Persistence (SQLite) - Data lives in `sessions/projektuebersicht.db` (`node:sqlite` `DatabaseSync`, WAL mode, `PRAGMA foreign_keys = ON`), created automatically. The schema is **fully relational** (18 tables) instead of one big JSON document per week: - `weeks` (department, label, week_id, date, locked, updated_at; PK `(department,label)`) — `updated_at` is the revision timestamp for the single-user conflict handling (see `/save-week` above) - `week_people` / `week_people_roles` — person copies per week (`array_order` keeps the list order, `role_order` the role array order) - `attendance` — présence per week/person - `projects` (`array_order` = position in the JSON array, `order_value` = the `p.order` field, which is only unique *within a group*, not globally!) - `project_lph`, `project_assign` (person_id|role keys split into columns), `project_aufwand` (LP effort per role), `project_aktuelle_lp` - `project_plan`, `plan_people`, `plan_todos` / `plan_todo_assignees` / `plan_todo_deps`, `plan_milestones` (`position` column!) / `plan_milestone_deps` - `people` / `person_roles` / `person_absences` — global people per department - The HTTP API stays JSON-shaped: `serializeWeek()` / `serializeGlobalPeople()` split incoming week/people objects across the tables inside a transaction, `loadWeeks()` / `loadGlobalPeople()` (re)assemble them on read. `saveWeekRelational()` deletes the week's rows first (`deleteWeekData()`, note `weeks` uses `label`, all child tables `week_label`) and returns the new `updated_at` revision written by `serializeWeek()`. - Migration on every start (idempotent): `migrateLegacySchema()` converts a DB from the old single-JSON-document format (detected via a `data` column in `weeks`): it reads the documents, drops all tables, recreates the relational schema and inserts everything in one transaction. Afterwards `migrateWeeksUpdatedAt()` adds the `updated_at` column to `weeks` (idempotent, `ALTER TABLE … ADD COLUMN`). Note: legacy rows keep `updated_at = ''`, so the first client save with an empty `baseUpdatedAt` matches and only then a real revision is written. - The DB file sits in `sessions/`, so the existing Docker bind-mount `./sessions:/app/sessions` keeps it persistent. The path is overridable via the `DB_PATH` environment variable (used for tests), production default is `sessions/projektuebersicht.db`. - Project/person IDs are stable across reloads; only week-level fields are queryable columns. ## Frontend architecture - The frontend is split into small classic non-module `