From bdc0aabc07a092fc41509a0f52d9bd6a3784799e Mon Sep 17 00:00:00 2001 From: Florian Zumpe Date: Wed, 2 Sep 2026 01:12:50 +0200 Subject: [PATCH] minor optimization --- .../20260901_add_route_activated_at.sql | 2 + database/schema.sql | 11 ++++ docs/editor-merge-notes.md | 51 ++++++++++--------- public/css/editor.css | 6 +++ public/index.html | 2 +- public/js/editor-ui.js | 43 ++++++++++++---- test/editor-database-schema.test.js | 3 ++ test/editor-ui.test.js | 14 ++++- 8 files changed, 95 insertions(+), 37 deletions(-) diff --git a/database/migrations/20260901_add_route_activated_at.sql b/database/migrations/20260901_add_route_activated_at.sql index d0e8489..02cec13 100644 --- a/database/migrations/20260901_add_route_activated_at.sql +++ b/database/migrations/20260901_add_route_activated_at.sql @@ -2,3 +2,5 @@ -- Neue Strecken bleiben unveröffentlicht, solange activated_at is NULL. ALTER TABLE routes ADD COLUMN activated_at TEXT DEFAULT NULL; + +CREATE INDEX IF NOT EXISTS idx_routes_activation ON routes(status, activated_at); diff --git a/database/schema.sql b/database/schema.sql index 9f11ad2..3c959b3 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -68,7 +68,18 @@ CREATE TABLE IF NOT EXISTS poi_images ( created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ) STRICT; +CREATE TRIGGER IF NOT EXISTS pois_updated_at +AFTER UPDATE ON pois +FOR EACH ROW +WHEN NEW.updated_at IS OLD.updated_at +BEGIN + UPDATE pois + SET updated_at = CURRENT_TIMESTAMP + WHERE id = NEW.id; +END; + CREATE INDEX IF NOT EXISTS idx_routes_status ON routes(status); +CREATE INDEX IF NOT EXISTS idx_routes_activation ON routes(status, activated_at); CREATE INDEX IF NOT EXISTS idx_route_points_route ON route_points(route_id, sequence); CREATE INDEX IF NOT EXISTS idx_pois_route ON pois(route_id, sequence); CREATE INDEX IF NOT EXISTS idx_poi_images_poi ON poi_images(poi_id, sequence); diff --git a/docs/editor-merge-notes.md b/docs/editor-merge-notes.md index f74d552..4b42f58 100644 --- a/docs/editor-merge-notes.md +++ b/docs/editor-merge-notes.md @@ -1,28 +1,37 @@ -# Wegwichtel Editor v3 +# Wegwichtel Editor v5 -Dieses Paket ergänzt den einfachen Streckeneditor und die zugehörigen Tests. +Dieses Paket bereinigt den einfachen Streckeneditor nach dem grünen SonarQube-Lauf weiter. + +## Status + +Der zuletzt geprüfte SonarQube-Export meldet ein bestandenes Quality Gate, keine Bugs, keine Vulnerabilities, keine Code Smells und keine Security Hotspots. Die New-Coverage lag bei 85,1 %. + +## Fachliche Optimierungen gegenüber v4 + +- Der Editor sendet `activated_at` nicht mehr beim Speichern von Strecken. + - Neue Strecken bleiben durch den DB-Default unveröffentlicht. + - Bereits manuell aktivierte Strecken werden durch spätere Editor-Änderungen nicht versehentlich deaktiviert. +- Der Plus-Button besitzt `aria-haspopup="dialog"`, `aria-expanded` und `aria-controls`; beim Öffnen und Schließen wird `aria-expanded` synchronisiert. +- POI-Sortierpfeile sind am Listenanfang beziehungsweise Listenende deaktiviert. +- Das Initialschema enthält zusätzlich `idx_routes_activation` für spätere veröffentlichte Listenabfragen nach `status` und `activated_at`. +- POIs erhalten analog zu Strecken einen `pois_updated_at`-Trigger, damit `updated_at` bei Bearbeitungen automatisch aktualisiert wird. +- Die Dokumentation verwendet durchgehend `TEXT DEFAULT NULL` statt des für SQLite-STRICT falschen `DATETIME`. ## Datenbankschema +Für neu erzeugte Datenbanken steht die Spalte direkt in `database/schema.sql`: + +```sql +activated_at TEXT DEFAULT NULL +``` + Für bestehende SQLite-Datenbanken liegt weiterhin eine Migration vor: ```bash sqlite3 < database/migrations/20260901_add_route_activated_at.sql ``` -Für neu erzeugte Datenbanken muss `database/schema.sql` die Spalte direkt in der `routes`-Tabelle enthalten: - -```sql -activated_at DATETIME NULL -``` - -Falls das Paket ohne bereits angepasste `database/schema.sql` eingespielt wird, kann die Datei einmalig im Repository angepasst werden: - -```bash -node scripts/apply-editor-schema.mjs -``` - -Danach sollte `database/schema.sql` mit committed werden. +Die Migration darf nur auf Bestandsdatenbanken ohne `activated_at` ausgeführt werden. Auf einer frischen Datenbank, die schon mit dem aktuellen `database/schema.sql` erstellt wurde, ist sie nicht nötig. ## Tests @@ -33,14 +42,6 @@ npm test -- test/editor-database-schema.test.js npm run test:coverage ``` -## SonarQube +## Optionaler Cleanup -Der frühere HTML-Smell durch `role="status"` am Editorstatus wurde entfernt. Der Status ist jetzt ein semantisches `` mit `aria-live="polite"`. - - -## v4 SQLite-STRICT und Sonar-Hotspot - -- `database/schema.sql` enthält `routes.activated_at` direkt im Initialschema als `TEXT DEFAULT NULL`. -- Die Migration verwendet ebenfalls `TEXT DEFAULT NULL`, damit bestehende SQLite-STRICT-Datenbanken kompatibel bleiben. -- `scripts/apply-editor-schema.mjs` enthält keine Schema-Merge-Regex mehr; der frühere Hilfsmechanismus wurde durch eine einfache lineare Schema-Prüfung ersetzt. -- `test/editor-database-schema.test.js` prüft bewusst `TEXT DEFAULT NULL` und verbietet `DATETIME` in der STRICT-Route-Tabelle. +`scripts/apply-editor-schema.mjs` war nur als Übergangshilfe gedacht. Nach diesem Paket ist das Initialschema direkt enthalten. Die Datei wird nicht mehr benötigt und kann entfernt werden, falls sie aus v3/v4 noch im Repository vorhanden ist. diff --git a/public/css/editor.css b/public/css/editor.css index 9307cbc..4e29cbf 100644 --- a/public/css/editor.css +++ b/public/css/editor.css @@ -218,6 +218,12 @@ min-width: 2.4rem; } +.editor-icon-button:disabled, +.editor-icon-button[aria-disabled="true"] { + cursor: not-allowed; + opacity: .45; +} + .editor-poi-toolbar { margin-top: .5rem; } diff --git a/public/index.html b/public/index.html index fee1f2f..02b00d7 100644 --- a/public/index.html +++ b/public/index.html @@ -102,7 +102,7 @@

Strecken verwalten

Strecken, Stationen, Bilder und Audiodateien bearbeiten.

- diff --git a/public/js/editor-ui.js b/public/js/editor-ui.js index 27cc0ab..412b782 100644 --- a/public/js/editor-ui.js +++ b/public/js/editor-ui.js @@ -124,6 +124,20 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {}; if (element) element.hidden = hidden; } + function setExpanded(element, expanded) { + if (element) element.setAttribute('aria-expanded', String(expanded)); + } + + function setDialogExpanded(expanded) { + setExpanded(byId(ids.button), expanded); + } + + function setDisabled(element, disabled) { + if (!element) return; + element.disabled = disabled; + element.setAttribute('aria-disabled', String(disabled)); + } + function setStatus(message, isError = false) { const status = byId(ids.status); if (!status) return; @@ -247,7 +261,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {}; return child; } - function iconButton(label, symbol, action) { + function iconButton(label, symbol, action, disabled = false) { const button = document.createElement('button'); button.className = classes.iconButton; button.dataset.editorAction = action; @@ -255,6 +269,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {}; button.title = label; button.type = 'button'; button.setAttribute('aria-label', label); + setDisabled(button, disabled); return button; } @@ -280,15 +295,16 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {}; item.className = classes.poiItem; item.dataset.poiId = poiId(poi); + const title = poiTitle(poi); const label = document.createElement('div'); - appendText(label, 'strong', poiTitle(poi)); + appendText(label, 'strong', title); appendText(label, 'small', `Position ${index + 1}`); const actions = document.createElement('div'); actions.className = classes.iconRow; - actions.appendChild(iconButton('Station nach oben schieben', '↑', 'move-poi-up')); - actions.appendChild(iconButton('Station nach unten schieben', '↓', 'move-poi-down')); - actions.appendChild(iconButton('Station bearbeiten', '✎', 'edit-poi')); + actions.appendChild(iconButton(`${title} nach oben schieben`, '↑', 'move-poi-up', index === 0)); + actions.appendChild(iconButton(`${title} nach unten schieben`, '↓', 'move-poi-down', index === state.pois.length - 1)); + actions.appendChild(iconButton(`${title} bearbeiten`, '✎', 'edit-poi')); item.append(label, actions); return item; @@ -337,6 +353,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {}; if (typeof dialog.showModal === 'function') dialog.showModal(); else dialog.setAttribute('open', ''); } + setDialogExpanded(true); showListView(); await guarded(loadRoutes); } @@ -345,13 +362,12 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {}; const dialog = byId(ids.dialog); if (dialog?.open && typeof dialog.close === 'function') dialog.close(); else if (dialog?.open) dialog.removeAttribute('open'); + setDialogExpanded(false); setStatus(''); } function routeFromForm() { return { - activatedAt: null, - activated_at: null, description: byId(ids.routeDescription)?.value.trim() || '', name: byId(ids.routeName)?.value.trim() || '' }; @@ -419,7 +435,6 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {}; const file = byId(ids.routeGpx)?.files?.[0]; data.append('name', values.name); data.append('description', values.description); - data.append('activated_at', ''); if (file) data.append('file', file); return data; } @@ -533,8 +548,14 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {}; function poiListAction(event) { const action = event.target.closest('[data-editor-action]')?.dataset.editorAction; - if (action === 'edit-poi') editPoi(poiForItem(event.target)); - if (action === 'move-poi-up') swapPois(poiIndexForItem(event.target), -1); + if (action === 'edit-poi') { + editPoi(poiForItem(event.target)); + return; + } + if (action === 'move-poi-up') { + swapPois(poiIndexForItem(event.target), -1); + return; + } if (action === 'move-poi-down') swapPois(poiIndexForItem(event.target), 1); } @@ -590,6 +611,8 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {}; savePoi, sendForm, sendJson, + setDialogExpanded, + setDisabled, setStatus, savePoiOrder, saveRoute, diff --git a/test/editor-database-schema.test.js b/test/editor-database-schema.test.js index 45726d6..af2ef06 100644 --- a/test/editor-database-schema.test.js +++ b/test/editor-database-schema.test.js @@ -57,7 +57,10 @@ test('initial database schema creates routes with a nullable activation timestam assert.equal(hasActivatedAtTextDefaultNull(routes), true); assert.equal(containsTokenSequence(routes.join('\n'), ['ACTIVATED_AT', 'DATETIME']), false); assert.equal(containsTokenSequence(routes.join('\n'), ['ON', 'UPDATE', 'CURRENT_TIMESTAMP']), false); + assert.equal(containsTokenSequence(schema, ['CREATE', 'INDEX', 'IF', 'NOT', 'EXISTS', 'IDX_ROUTES_ACTIVATION', 'ON', 'ROUTES']), true); + assert.equal(containsTokenSequence(schema, ['CREATE', 'TRIGGER', 'IF', 'NOT', 'EXISTS', 'POIS_UPDATED_AT']), true); assert.equal(containsTokenSequence(migration, ['ALTER', 'TABLE', 'ROUTES', 'ADD', 'COLUMN', 'ACTIVATED_AT', 'TEXT']), true); + assert.equal(containsTokenSequence(migration, ['CREATE', 'INDEX', 'IF', 'NOT', 'EXISTS', 'IDX_ROUTES_ACTIVATION', 'ON', 'ROUTES']), true); assert.equal(containsTokenSequence(migration, ['ACTIVATED_AT', 'DATETIME']), false); assert.equal(migration.includes('activated_at is NULL'), true); }); diff --git a/test/editor-ui.test.js b/test/editor-ui.test.js index 96726bd..8cd8ff1 100644 --- a/test/editor-ui.test.js +++ b/test/editor-ui.test.js @@ -17,6 +17,8 @@ test('route editor entry point, dialog and forms are present', async () => { assert.match(html, /id="editor-button"/); assert.match(html, /aria-label="Streckeneditor öffnen"/); + assert.match(html, /aria-haspopup="dialog"/); + assert.match(html, /aria-controls="editor-dialog"/); assert.match(html, /