11 KiB
11 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 as semicolon-separated CSVs
under sessions/.
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,csv,server,gantt) plus the plan modules underjs/plan/— NOT the full main module set.
All UI strings/comments/CSV content are German. Preserve German in user-facing text.
File structure
.
├── server.js # Express server + CSV API
├── package.json # deps: express, cors only
├── docker-compose.yml # at repo ROOT; build context ., dockerfile Docker/Dockerfile
├── Docker/Dockerfile # node:20-alpine
├── .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-Quelle
│ │ ├── ui.js # LocalStorage-UI-State, Header-Collapse, Theme
│ │ ├── date-picker.js # Datums-/Bereichspicker-Overlays
│ │ ├── csv.js # CSV-Parser/Serializer + weekSortKey, filenameForWeek
│ │ ├── server.js # baseUrl, sendCsvToServer, deleteCsvFromServer, 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/ # data; bind-mounted into Docker container
├── ELT/ HLS/ ARCH/ # one folder per department
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=→{sessions:[{filename,content}], people}with_people.csvcontent inpeople; sessions sorted by filename.POST /save-csv {filename, csv, department}→ writes UTF-8 BOM CSV.POST /delete-csv {filename, department}.- Valid departments hard-coded as
ELT|HLS|ARCHinserver.js(VALID_DEPARTMENTS); anything else silently falls back toELT. Same set inDEPARTMENTSinjs/globals.js. safeName()strictly validates filenames (path traversal protection); static assets getCache-Control: no-store.- JSON body limit is 2 MB.
CSV format
- Delimiter
;, UTF-8 with BOM. Hand-editing is possible but risky; the parser reconstructs objects from the fixed column layout. - Week files
<Label>.csv(e.g.KW_30___2026.csv—filenameForWeek()replaces non-word chars with_). Row layout is positional:WOCHE(label, date, locked flag) →PERSONEN(name|role|fullNameper column) → header row →ANWESENHEIT→ one row per project. _people.csvper department:NAME;Rollen;Voller Name;E-Mail;Abwesenheiten(absences as JSON). People are global per department; new people are copied into the current week. Historical weeks keep their own person copies, so deleting a global person leaves old sessions unaffected no matter what.- Don't change the column layout without updating both
weekToCsv()andparseSessionCsv()(js/csv.js), which must stay inverse of each other.
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 → csv → 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 → csv → 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 IDs are regenerated on every CSV load, so 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 are stable across reloads via
personIdForName(name)→p_<lowercased alnum>. save()debounces 500 ms thensyncAllToServer()writes all weeks plus_people.csv— not just the changed week. Exception:js/plan/plan-state.jssavePlan()bypassessave()and writes only the current week's CSV directly viasendCsvToServer().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:20-alpine,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 CSV changes insessions/.