DB Import und Export sowie Backup und kleinere Fehlerbeseitigung

This commit is contained in:
erik committed 2026-08-18 09:16:59 +02:00
1 parent 91cc898ec7
commit 1c176ea621
24 files changed
+1081 -96

No files matched your search

+61 -10
View File
@@ -42,6 +42,9 @@ user-facing text.
│ │ ├── 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
@@ -94,13 +97,44 @@ docker compose up --build # Docker variant (compose file is at repo root)
(`/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, people, attendance, projects), sorted by label.
- `POST /save-week {department, week}` → upserts one week (JSON object, keyed by
department + label). Used by `sendWeekToServer()`.
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`.
@@ -112,7 +146,9 @@ docker compose up --build # Docker variant (compose file is at repo root)
- 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; PK `(department,label)`)
- `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
@@ -129,12 +165,16 @@ docker compose up --build # Docker variant (compose file is at repo root)
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`).
`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.
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
@@ -166,10 +206,21 @@ docker compose up --build # Docker variant (compose file is at repo root)
`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 **all** weeks (via
`sendWeekToServer()`) plus the people list (via `sendPeopleToServer()`) — not
just the changed week. Exception: `js/plan/plan-state.js` `savePlan()` bypasses
`save()` and writes only the current week directly via `sendWeekToServer()`.
- `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