17 KiB
17 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
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 viaopenProjectPlan()injs/gantt.js. Loads the shared helper modules (globals,utils,ui,date-picker,server,gantt) plus the plan modules underjs/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
├── 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()injs/server.jsuseswindow.location.originor falls back tohttp://localhost:3003when opened viafile://(then only seed/demo data loads). - Check
/healthreturns{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/Auslastungor/ELT/Projektübersicht. Tab slugs (TAB_SLUGSinjs/globals.js) areProjektübersicht|Auslastung|Projekte|Projektbeteiligte; anything else returns 404. Exception:GET /<Dept>/Projektplan(/<ProjName>)?(PLAN_SLUG) servesproject-plan.html, the GANTT view, withweekLabel|projNras query params (the project name is a readable path segment only; identification happens via weekLabel + projNr). The frontend reads/writes these routes viaapplyUrlRoute()/updateRouteUrl()injs/ui.jsand 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 inpeople; 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 bysendWeekToServer(). The client sends its local revision inbaseUpdatedAt(theupdatedAtvalue fromload-all); the server compares it toweeks.updated_atbefore writing and answers409 {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 WITHOUTbaseUpdatedAt(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 withdeleted: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 byexportCSV()injs/csv.js.GET /backup→ consistent SQLite snapshot (VACUUM INTO, WAL-safe) as a.dbfile download (temp file inos.tmpdir(), deleted after send). Used bydownloadBackup()injs/backup.js. Additionally, on server startup the server writes a timestamped copy tosessions/backups/(also viaVACUUM INTO) and keeps only theMAX_BACKUPSnewest ones (default 10).POST /import→ restores the whole database from a.dbbackup file sent as a raw binary body (express.raw, limit 100 MB). Validates the schema (pragma_table_infoon every table, exact column match vs. live DB), then swaps all data viaATTACH 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 viarunAutoBackup()(so a restore can be undone), then performs the swap. Used byimportDb()injs/import.js, which is password-gated (requestedActiontype'importDb'injs/tabs-auth.js) like week deletion.- Valid departments hard-coded as
ELT|HLS|ARCHinserver.js(VALID_DEPARTMENTS); anything else silently falls back toELT. Same set inDEPARTMENTSinjs/globals.js. - Static assets get
Cache-Control: no-store. - JSON body limit is 2 MB.
Persistence (SQLite)
- Data lives in
sessions/projektuebersicht.db(node:sqliteDatabaseSync, WAL mode,PRAGMA foreign_keys = ON,PRAGMA busy_timeout = 5000to avoidSQLITE_BUSYunder concurrent writers), created automatically. The schema is fully relational (19 tables) instead of one big JSON document per week:weeks(department, label, week_id, date, locked, updated_at; PK(department,label)) —updated_atis the revision timestamp for the single-user conflict handling (see/save-weekabove)week_people/week_people_roles— person copies per week (array_orderkeeps the list order,role_orderthe role array order)attendance— présence per week/personprojects(array_order= position in the JSON array,order_value= thep.orderfield, 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_lpproject_plan,plan_people,plan_todos/plan_todo_assignees/plan_todo_deps,plan_milestones(positioncolumn!) /plan_milestone_depspeople/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(), noteweeksuseslabel, all child tablesweek_label) and returns the newupdated_atrevision written byserializeWeek(). - Migration on every start (idempotent):
migrateLegacySchema()converts a DB from the old single-JSON-document format (detected via adatacolumn inweeks): it reads the documents, drops all tables, recreates the relational schema and inserts everything in one transaction. AfterwardsmigrateWeeksUpdatedAt()adds theupdated_atcolumn toweeks(idempotent,ALTER TABLE … ADD COLUMN). Note: legacy rows keepupdated_at = '', so the first client save with an emptybaseUpdatedAtmatches and only then a real revision is written. - The DB file sits in
sessions/, so the existing Docker bind-mount./sessions:/app/sessionskeeps it persistent. The path is overridable via theDB_PATHenvironment variable (used for tests), production default issessions/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 underpublic/js/; all functions are globals onwindow. Two HTML files include them via<script src>tags; load order in the HTML defines dependencies, since there is no bundler:Test.htmlloadsglobals → utils → ui → date-picker → server → store → weeks(shared modules), then the tab-specific foldersboard → people → projects → load, finallygantt → showmode → tabs-auth → pdf → init. See the exact list inTest.html.project-plan.htmlloads onlyglobals → utils → ui → date-picker → server → ganttplus thejs/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/currentDepartmentlive injs/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+ weeklabel+projNr(+ readable projName path segment) via URL params:openProjectPlan()builds/HLS/Projektplan/<ProjName>?weekLabel=…&projNr=…,findProjectForPlanresolves them. Person IDs followpersonIdForName(name)→p_<lowercased alnum>. save()debounces 500 ms thensyncAllToServer()writes only dirty weeks (dirtyWeeks– trackmarkWeekDirty()) viasendWeekToServer(), plus the people listmarkPeopleDirty()viasendPeopleToServer()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-providedupdatedAtrevision is adopted as the new sync base; weeks whose save failed (network errors) stay dirty and are retried. A409conflict response drops the local changes for that week andhandleWeekConflict()reloads only that week from the server (reloadWeekFromServer()), leaving all other dirty weeks and the people list untouched. Exception:js/plan/plan-state.jssavePlan()bypassessave()and writes only the current week directly viasendWeekToServer()(with the same in-flight/trailing-save guard), adopting the returnedupdatedAtand showing a toast on conflict.switchWeek()(injs/weeks.js) does NOT trigger a save – only actual edits do.switchDepartment()cancels a pending save debounce before switching, andload()discards stale/load-allresponses after a department switch (frozenrequestedDepartment), 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) bindsondragstart/ondragend, the.people-sortablecontainer bindsondragover/ondrop; insertion is computed bygetDragAfterElement(container, y, selector)(js/projects/projects.js, used with.person-cardhere and.person-list-itemin the projects tab).reorderPeople()updates bothdb.globalPeopleand the current week's person copy, thensave(). No drag handles are rendered on locked weeks. - Weeks are sorted by calendar week extracted from the label (
KW XX / YYYY) viaweekSortKey(), not alphabetically/by filename. applyAutoLock()locks every week except the chronologically last one after load/clone/new-week;localStorageUI state is not restored for locked tabs.localStoragekeys:projuebersicht_v1(department only — tab is never restored from storage; at startup the tab comes from the deep-link URL if present, otherwiseboard),projuebersicht-theme, header-collapse key. Do not store data inlocalStorage; it is UI state only.- Deep-link URLs (
/<Dept>/<Tab>) are shared via the address bar:updateRouteUrl()rewrites the path viahistory.replaceStateon tab and department switches; protected tabs keep the URL but open the password modal.
Auth / security quirk
- The
projectsandpeopletabs, week deletion, and week unlock are gated by a password modal comparingCryptoJS.SHA256(input)toPROTECTED_PASSWORD_HASH(js/globals.js, modal injs/tabs-auth.js). Client-side only — not real security. CryptoJS and pdfmake (PDF export) are loaded fromcdnjs.cloudflare.cominTest.html, so those features need internet access and won't work offline.project-plan.htmlloads neither. - User-provided status HTML is sanitized on render via a small whitelist
(
sanitizeStatusHtmlinjs/board/board.js) to mitigate XSS.
Docker
docker compose up --buildfrom the repo root. Compose maps3003: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-rootnode, HEALTHCHECK on/health..dockerignoreexcludesDocker/*(exceptDocker/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 filesessions/projektuebersicht.dbgrows).