diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b44b71..8b84b59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,37 @@ not from the output of `docker push`. --- +## Unreleased + +**Changed** +- **A cover is now a colour and a pattern, chosen in turn**, rather than a list + with the combinations someone happened to write out. Every combination is + available, and each pattern is previewed over the colour in hand. Covers saved + during the day the patterns were one list are read back as the pair they were + drawn as, so nothing repaints itself. + +**Fixed** +- **The install button never appeared, even over HTTPS.** Two causes, both in + this app rather than in anyone's configuration: + - `beforeinstallprompt` fires once, early — usually before React has mounted — + and is never fired again, so a listener added from inside a component simply + missed it. It is caught at the top of `index.html` now. + - The manifest is fetched **without credentials** unless the link says + otherwise. Behind a reverse proxy that authenticates in front of the app, + that request comes back as the proxy's login page and the browser reads it + as an invalid manifest — while the app itself works perfectly, because its + own requests carry the cookie. The link now says + `crossorigin="use-credentials"`. +- **The service worker could cache a proxy's login page as the application.** + Nothing checked that a response was the thing that had been asked for, so a + lapsed session could be stored as the app shell: it then "worked offline" by + showing a login screen it could never get past, curable only by clearing the + site's data by hand. Redirected and unsuccessful responses are no longer + cached, and the cache name is bumped so anything already stored under the old + rules is dropped. + +--- + ## 2026-08-12 — Making the phone work **Images** — web `sha256:5ea846f3…`, server unchanged at `sha256:06f94f43…` diff --git a/CLAUDE.md b/CLAUDE.md index 8542396..349ea97 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -70,6 +70,19 @@ ist, was auf dem Server läuft. Der `readme.md` beschreibt den *jetzigen* Stand und wird mitgeändert, wenn sich Verhalten ändert; das Changelog beschreibt den Weg dahin. +## Betrieb bei Marc + +InkNote läuft hinter **Pangolin** (Reverse Proxy mit eigener Anmeldung), über +HTTPS, und wird am Handy als PWA auf **Android** genutzt. Daraus folgen drei +Dinge, die bei PWA- oder Sync-Fehlern zuerst zu prüfen sind: + +- `/manifest.json` und `/sw.js` müssen die Datei liefern, nicht die Login-Seite + des Proxys. Eine **Weiterleitung** auf `sw.js` lässt die Registrierung + scheitern — dann gibt es weder Install noch Offline. +- `/api/*` unverändert durchreichen, `/ws` mit WebSocket-Upgrade. +- Jede UI-Änderung auch am Handy denken: Touch löst kein HTML5-Drag aus, und + `onMouseEnter` gibt es dort nicht. + ## Stil Der Code erklärt in Kommentaren das *Warum*, nicht das *Was* – oft mit dem diff --git a/backend/server.js b/backend/server.js index cec4e79..b8cbe21 100644 --- a/backend/server.js +++ b/backend/server.js @@ -422,6 +422,14 @@ if (!tableColumns("users").includes("preferences")) { * * On `attachments` rather than in a table of its own: it is a property of the * file, it is deduplicated with the file, and it is freed with the file. */ +/* The pattern laid over a notebook's cover, chosen apart from its colour. + * Its own column rather than more values in `cover_index`: they are two + * decisions, and folding them into one number would mean renumbering every + * cover the day a pattern is added. */ +if (!tableColumns("notebooks").includes("cover_pattern")) { + db.exec("ALTER TABLE notebooks ADD COLUMN cover_pattern INTEGER NOT NULL DEFAULT 0"); +} + if (!tableColumns("attachments").includes("text_index")) { db.exec("ALTER TABLE attachments ADD COLUMN text_index BLOB"); } @@ -850,10 +858,10 @@ app.get("/api/notebooks", authMiddleware, (req, res) => { }); app.post("/api/notebooks", authMiddleware, (req, res) => { - const { name, coverIndex } = req.body; + const { name, coverIndex, coverPattern } = req.body; const id = uuid(); - db.prepare("INSERT INTO notebooks (id, user_id, name, cover_index, device_id) VALUES (?, ?, ?, ?, ?)") - .run(id, req.userId, name, coverIndex || 0, req.deviceId); + db.prepare("INSERT INTO notebooks (id, user_id, name, cover_index, cover_pattern, device_id) VALUES (?, ?, ?, ?, ?, ?)") + .run(id, req.userId, name, coverIndex || 0, coverPattern || 0, req.deviceId); db.prepare("INSERT INTO sync_log (user_id, entity_type, entity_id, action, device_id) VALUES (?, 'notebook', ?, 'create', ?)") .run(req.userId, id, req.deviceId); @@ -884,7 +892,7 @@ function cleanSeparators(input) { } app.put("/api/notebooks/:id", authMiddleware, (req, res) => { - const { name, coverIndex, sortOrder, maxVersions, sortMode, separators } = req.body; + const { name, coverIndex, coverPattern, sortOrder, maxVersions, sortMode, separators } = req.body; const nb = db.prepare("SELECT * FROM notebooks WHERE id = ? AND user_id = ?").get(req.params.id, req.userId); if (!nb) return res.status(404).json({ error: "Not found" }); @@ -902,8 +910,8 @@ app.put("/api/notebooks/:id", authMiddleware, (req, res) => { return res.status(400).json({ error: `sortMode must be one of ${SORT_MODES.join(", ")}` }); } - db.prepare("UPDATE notebooks SET name = COALESCE(?, name), cover_index = COALESCE(?, cover_index), sort_order = COALESCE(?, sort_order), updated_at = datetime('now'), version = version + 1 WHERE id = ?") - .run(name, coverIndex, sortOrder, req.params.id); + db.prepare("UPDATE notebooks SET name = COALESCE(?, name), cover_index = COALESCE(?, cover_index), cover_pattern = COALESCE(?, cover_pattern), sort_order = COALESCE(?, sort_order), updated_at = datetime('now'), version = version + 1 WHERE id = ?") + .run(name, coverIndex, coverPattern, sortOrder, req.params.id); if (maxVersions !== undefined) { db.prepare("UPDATE notebooks SET max_versions = ? WHERE id = ?").run(versions, req.params.id); } @@ -916,7 +924,11 @@ app.put("/api/notebooks/:id", authMiddleware, (req, res) => { db.prepare("UPDATE notebooks SET separators = ? WHERE id = ?").run(JSON.stringify(cleaned), req.params.id); } - broadcastToUser(req.userId, { type: "notebook_updated", data: { id: req.params.id, name, coverIndex } }, req.deviceId); + broadcastToUser( + req.userId, + { type: "notebook_updated", data: { id: req.params.id, name, coverIndex, coverPattern } }, + req.deviceId + ); res.json({ success: true, defaultMaxVersions: MAX_VERSIONS_PER_NOTE }); }); diff --git a/e2e/organise.mjs b/e2e/organise.mjs index 153ad9b..3de8a93 100644 --- a/e2e/organise.mjs +++ b/e2e/organise.mjs @@ -395,14 +395,28 @@ try { await p.waitForTimeout(600); await p.getByRole("button", { name: "Add Notebook" }).click(); await p.waitForTimeout(700); - const covers = p.locator('[data-cover-option]'); - check((await covers.count()) > 20, "there are patterned covers as well as plain ones", `${await covers.count()} covers`); + /* Colour and pattern are two choices, not one long list of combinations. */ + check((await p.locator("[data-cover-option]").count()) >= 15, "a colour to choose", `${await p.locator("[data-cover-option]").count()}`); + check((await p.locator("[data-pattern-option]").count()) > 5, "and a pattern to lay over it", `${await p.locator("[data-pattern-option]").count()}`); check( await p.getByRole("button", { name: "Create", exact: true }).isVisible(), "and the dialog still fits a laptop screen, Create and all" ); - await p.getByRole("button", { name: "Cancel" }).click(); - await p.waitForTimeout(400); + + // The two together make the cover, and both reach the server. + await p.getByPlaceholder("Notebook name...").fill("Kariert"); + await p.locator('[data-cover-option="6"]').click(); + await p.locator('[data-pattern-option="2"]').click(); + await p.waitForTimeout(300); + await p.getByRole("button", { name: "Create", exact: true }).click(); + await p.waitForTimeout(2000); + const books = (await api(p, "GET", "/notebooks")).body; + const made = books.find((b) => b.name === "Kariert"); + check( + made?.cover_index === 6 && made?.cover_pattern === 2, + "the colour and the pattern are both stored", + `cover ${made?.cover_index}, pattern ${made?.cover_pattern}` + ); /* ── moving a row with a finger ── * Last in the suite, because moving rows about would pull the ground from * under every check above that names an order. diff --git a/frontend/index.html b/frontend/index.html index 393c4fa..e7d8d1e 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -10,11 +10,32 @@ - + + InkNote +