Choose a cover in two parts, and let the app be installed
A cover is a colour *and* a pattern now, chosen in turn. Offering every combination as its own entry meant fifteen colours times a dozen patterns in a grid nobody could take in — and in practice only the handful someone had thought to write out. Each pattern is previewed over the colour in hand, so what is on screen is the cover as it will be. `cover_pattern` is its own column: they are two decisions, and folding them into one number would mean renumbering every cover the day a pattern is added. For one day the patterns *were* twelve appended entries. Those indices are read back as the colour-and-pattern pair they were drawn as, so nothing chosen then looks different now. And three separate reasons the install button never appeared, none of them in anyone's configuration: - `beforeinstallprompt` fires once, early — usually before React has mounted — and is never fired again. A listener added from inside a component simply missed it. It is caught at the top of index.html now. - A `<link rel="manifest">` is fetched with credentials omitted unless it says otherwise. Behind a reverse proxy that authenticates in front of the app, that one 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 service worker cached whatever came back, so a lapsed session could be stored as the application itself. The app would then "work offline" by showing a login screen it could never get past, curable only by clearing the site's data. Only successful, unredirected responses are cached now, and the cache name is bumped to drop anything already stored under the old rules. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0d8d1180de
commit
8ca1ed6e06
17 files changed
+455
-221
No files matched your search
+19
-7
@@ -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 });
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user