13 KiB
13 KiB
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 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
├── .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)
│ │ └── 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, people, attendance, projects), sorted by label.POST /save-week {department, week}→ upserts one week (JSON object, keyed by department + label). Used bysendWeekToServer().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.- 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), created automatically. The schema is fully relational (18 tables) instead of one big JSON document per week:weeks(department, label, week_id, date, locked; PK(department,label))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). - 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. - 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 all weeks (viasendWeekToServer()) plus the people list (viasendPeopleToServer()) — not just the changed week. Exception:js/plan/plan-state.jssavePlan()bypassessave()and writes only the current week directly viasendWeekToServer().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).