Files
projektuebersicht/AGENTS.md
T

17 KiB
Raw Blame History

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

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 /<Dept>/<Tab> → 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 /<Dept>/Projektplan(/<ProjName>)? (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 <script> files under public/js/; all functions are globals on window. Two HTML files include them via <script src> tags; load order in the HTML defines dependencies, since there is no bundler:
    • Test.html loads globals → utils → ui → date-picker → server → store → weeks (shared modules), then the tab-specific folders board → people → projects → load, finally gantt → showmode → tabs-auth → pdf → init. See the exact list in Test.html.
    • project-plan.html loads only globals → utils → ui → date-picker → server → gantt plus the js/plan/* modules. It must NOT load modules that touch main-only page elements (store, weeks, board, …, init).
  • Shared state object window.PlanApp (js/plan/plan-state.js) holds all project-plan page state (week, project, zoom level, drag/modal-state), db/currentDepartment live in js/globals.js.
  • State: let db = { weeks, currentWeek, globalPeople } (in-memory only).
  • Project/person IDs are stable across reloads because the server splits and reassembles week objects deterministically (JSON IDs are kept in the DB columns); projects are addressed cross-page by department + week label + projNr (+ readable projName path segment) via URL params: openProjectPlan() builds /HLS/Projektplan/<ProjName>?weekLabel=…&projNr=…, findProjectForPlan resolves them. Person IDs follow personIdForName(name) → p_<lowercased alnum>.
  • save() debounces 500 ms then syncAllToServer() writes only dirty weeks (dirtyWeeks – track markWeekDirty()) via sendWeekToServer(), plus the people list markPeopleDirty() via sendPeopleToServer() if marked dirty. An in-flight guard (syncInFlight/syncQueued) prevents overlapping syncs from double-POSTing the same week with the same revision (which would look like a spurious 409). On success the server-provided updatedAt revision is adopted as the new sync base; weeks whose save failed (network errors) stay dirty and are retried. A 409 conflict response drops the local changes for that week and handleWeekConflict() reloads only that week from the server (reloadWeekFromServer()), leaving all other dirty weeks and the people list untouched. Exception: js/plan/plan-state.js savePlan() bypasses save() and writes only the current week directly via sendWeekToServer() (with the same in-flight/trailing-save guard), adopting the returned updatedAt and showing a toast on conflict. switchWeek() (in js/weeks.js) does NOT trigger a save – only actual edits do.
  • switchDepartment() cancels a pending save debounce before switching, and load() discards stale /load-all responses after a department switch (frozen requestedDepartment), so late responses can never render or persist data under the wrong department.
  • Person drag-and-drop in the people tab: drag handle (.drag-handle) binds ondragstart/ondragend, the .people-sortable container binds ondragover/ondrop; insertion is computed by getDragAfterElement(container, y, selector) (js/projects/projects.js, used with .person-card here and .person-list-item in the projects tab). reorderPeople() updates both db.globalPeople and the current week's person copy, then save(). No drag handles are rendered on locked weeks.
  • Weeks are sorted by calendar week extracted from the label (KW XX / YYYY) via weekSortKey(), not alphabetically/by filename.
  • applyAutoLock() locks every week except the chronologically last one after load/clone/new-week; localStorage UI state is not restored for locked tabs.
  • localStorage keys: projuebersicht_v1 (department only — tab is never restored from storage; at startup the tab comes from the deep-link URL if present, otherwise board), projuebersicht-theme, header-collapse key. Do not store data in localStorage; it is UI state only.
  • Deep-link URLs (/<Dept>/<Tab>) are shared via the address bar: updateRouteUrl() rewrites the path via history.replaceState on tab and department switches; protected tabs keep the URL but open the password modal.

Auth / security quirk

  • The projects and people tabs, week deletion, and week unlock are gated by a password modal comparing CryptoJS.SHA256(input) to PROTECTED_PASSWORD_HASH (js/globals.js, modal in js/tabs-auth.js). Client-side only — not real security. CryptoJS and pdfmake (PDF export) are loaded from cdnjs.cloudflare.com in Test.html, so those features need internet access and won't work offline. project-plan.html loads neither.
  • User-provided status HTML is sanitized on render via a small whitelist (sanitizeStatusHtml in js/board/board.js) to mitigate XSS.

Docker

  • docker compose up --build from the repo root. Compose maps 3003:3003, bind-mounts ./sessions:/app/sessions, restart: unless-stopped.
  • Dockerfile: node:22-alpine (nötig für node:sqlite), npm ci --production, runs as non-root node, HEALTHCHECK on /health. .dockerignore excludes Docker/* (except Docker/Dockerfile), node_modules/, .git, npm-debug.log.

Conventions & verification

  • Git repo; commits are German (e.g. "PDF Export von A4 auf A3 …"). Remote: http://192.168.3.187:3000/erik/projektuebersicht.git.
  • No linter, formatter, typecheck, or test suite. Manual verification only: start server, open http://localhost:3003, exercises the relevant tab, confirm SQLite changes (the DB file sessions/projektuebersicht.db grows).