Commit Graph
189 Commits
Author SHA1 Message Date
bvandeusenandClaude Opus 5.5 0f787c6363 The board places each card in the shortest column, instead of leaving it to CSS columns
Android / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 8s
CI & Build / integration (push) Successful in 1m37s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 1m58s
CI & Build / Python tests (push) Successful in 13s
Android / Core and FFI clippy and tests (push) Successful in 40s
CI & Build / Build & push image (push) Skipped
Android / Kotlin + Rust (APK) (push) Successful in 7m38s
Android / Build the server image (push) Successful in 1s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m47s
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 3m43s
Desktop (Tauri) / Update manifest (push) Skipped
CSS multi-column decides placement by balancing column heights, so the same
notes could land differently at the same width: an edit left two cards alone in
the fourth column with the middle two empty, and a refresh left the fourth empty
beside a stack of three in the first.

Cards now go, in board order, into the shortest column, leftmost on a tie, so the
same notes at the same width always land the same way and every column fills
before any stacks. This changes the reading order to match Android: the second
note sits top-right rather than under the first.

useMasonry measures the cards and watches them for height changes. Placement is a
pure function, tested in notes/masonry.test.ts. Cards are absolutely placed, so
TransitionGroup's move animation carries on working unchanged.

Issue #6709.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 09:56:58 -04:00
bvandeusenandClaude Opus 5.5 c6f10f157d Timeline shows archived notes too, each one labelled Archived
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Android / Core and FFI clippy and tests (push) Skipped
Android / Kotlin + Rust (APK) (push) Skipped
Android / Build the server image (push) Skipped
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Web typecheck and unit tests (push) Successful in 9s
CI & Build / Python tests (push) Successful in 13s
CI & Build / integration (push) Successful in 1m34s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 1m48s
CI & Build / Build & push image (push) Successful in 30s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m2s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m10s
Desktop (Tauri) / Update manifest (push) Successful in 3s
Timeline lists everything not in Trash. The card's Archived label is now
hidden only on the Archive page, read off the route: Timeline keeps its
own list and never sets the board's view, so arriving from the Archive
used to leave the store saying "archived".

Task #6318.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 17:29:40 -04:00
bvandeusenandClaude Opus 5.5 b98a0c315f Search and tags look through the Archive, and archived cards say so
A search or a tag filter on the main board, and a tag's own view, now
list every note not in Trash (a new `all` board view on the server and in
the core), so finished work can still be looked up. Archive and Trash
keep a search inside themselves.

An archived note met anywhere but the Archive carries an "Archived" chip
at the top of its card, on the web and on Android.

Task #6315.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 16:23:08 -04:00
bvandeusenandClaude Opus 5.5 d1f8d1f561 The app links to its source, from an admin setting (#6228)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Android / Core and FFI clippy and tests (push) Skipped
Android / Kotlin + Rust (APK) (push) Skipped
Android / Build the server image (push) Skipped
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 9s
CI & Build / Python tests (push) Successful in 14s
CI & Build / integration (push) Successful in 1m45s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m17s
CI & Build / Build & push image (push) Successful in 42s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m25s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m10s
Desktop (Tauri) / Update manifest (push) Successful in 3s
#6228 "The app offers its source: a Source link from an admin setting
(AGPL §13)". The AGPL requires a modified copy run for others over a
network to offer them its source; Forge's #2282 is the shape followed.

- Settings → General gains "Source code" (`source_url`), defaulting to
  the public repo so a stock install makes the offer. Empty hides it.
- It is an http(s)-only URL setting, refused otherwise on save, and
  `safe_link` refuses anything else again when it is published, because
  the web renders it as an href.
- /api/config carries it; the web shows "Source" beside the build number
  at the foot of the sidebar. The offline desktop's config has no such
  field, so the link stays hidden there.
- ci.yml's client-bake comments no longer call the forge private; the
  repo has been public since #2091.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:20:07 -04:00
bvandeusenandClaude Opus 5.5 06ed76814f Inkwell is licensed AGPL-3.0-only (#6227)
Android / Build, or is the channel already serving this? (push) Successful in 2s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
Android / Core and FFI clippy and tests (push) Successful in 55s
CI & Build / Build now, or wait for Android? (push) Successful in 4s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m25s
CI & Build / Web typecheck and unit tests (push) Successful in 12s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Python tests (push) Successful in 14s
CI & Build / integration (push) Successful in 2m20s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m29s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m27s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Successful in 9m17s
Android / Build the server image (push) Successful in 1s
#6227 "License Inkwell under AGPL-3.0-only: LICENSE file and SPDX
identifier". The operator chose AGPL-3.0-only, as for FabledStash (#6212).

- LICENSE is the canonical GNU AGPL v3 text from gnu.org: 661 lines, md5
  eb1e647870add0502f8f010b19de32af, the same file as FabledStash's.
- AGPL-3.0-only is declared in pyproject.toml, frontend/package.json (and
  its lockfile root entry), the four crates, and tauri.conf.json's bundle.
- The pacman package declared MIT; it now declares AGPL-3.0-only.
- pyproject's build floor moves to setuptools>=77, the first release that
  reads `license` as an SPDX expression.
- The README's Licence section names the licence, links LICENSE, and
  states the network clause in one sentence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:12:37 -04:00
bvandeusenandClaude Opus 5.5 f16224b07c Root, CI, docs and packaging describe the project to a stranger (#6225)
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 12s
CI & Build / Python tests (push) Successful in 23s
Android / Core and FFI clippy and tests (push) Successful in 51s
CI & Build / integration (push) Successful in 1m49s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m20s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m6s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m46s
Desktop (Tauri) / Update manifest (push) Successful in 3s
Android / Kotlin + Rust (APK) (push) Successful in 8m31s
Android / Build the server image (push) Successful in 1s
#6225 "Comment standard sweep: root, config, CI workflows, docs and
packaging", milestone 546 "Bring the repo to the comment standard (rule 32)
and compose defaults (rule 198)".

- README restructured in newcomer order: what it is, quick start,
  configuration, setup pitfalls, updating and backups, features,
  troubleshooting, development, licence. Its quick-start compose carries
  no restart policy.
- Dockerfile and .env.example trimmed to one or two lines per setting.
- Workflows, packaging scripts and docs lose issue, run, note, rule and
  milestone references and their history narration. Comments that still
  described path filters and "step 6" as future work now state what the
  workflows do.
- ci-requirements.md rewritten against the current workflows. It said the
  build gated on typecheck and lint only, the integration lane did not
  gate, a v* tag published :latest, and ci-tauri lacked bsdtar and zstd;
  none of those is true now (the last checked against run 9630's log).
- Echo strings in guard-forward.sh and version.sh drop their note
  citations; every string a test asserts on is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:44:02 -04:00
bvandeusenandClaude Opus 5.5 715b648b95 Web client comments describe the code to a stranger (#6222)
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Android / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Python lint (push) Successful in 2s
Android / Core and FFI clippy and tests (push) Skipped
Android / Kotlin + Rust (APK) (push) Skipped
Android / Build the server image (push) Skipped
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 10s
CI & Build / Python tests (push) Successful in 13s
CI & Build / integration (push) Successful in 1m40s
CI & Build / Build & push image (push) Successful in 35s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m20s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m36s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m21s
Desktop (Tauri) / Update manifest (push) Successful in 3s
Milestone 546 "Bring the repo to the comment standard (rule 32) and
compose defaults (rule 198)", step #6222 "Comment standard sweep: web
client (frontend/)".

Comments, docstrings and template notes across frontend/ now say what
the code does and the non-obvious why as present-tense facts. Issue,
task, note, milestone and family-idea references are gone, along with
narration of what used to happen: the titles index's wiki-link origin
and its once-per-session load (#5165), the card-controls overlap a
phone showed (task 2697), the M7 and M10.9 labels, and the card
unification behind the reminders lens (task 1913). Hazards stay as
facts, among them LEAVE_MS matching the editor's leave duration, the
Android client twin that must not be widened, and keeping leavers in
flow in the multi-column masonry.

62 files, comments only: a comment-stripping diff of every changed file
against HEAD is identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:17:13 -04:00
bvandeusenandClaude Opus 5.5 19487b079a Attached images open full size and copy back out (#6198)
Scribe #6198 "An attached image can't be viewed large or copied back out".

Web and desktop: clicking an image in the editor opens it full size, with
Copy image (re-encoded to PNG, the one type every clipboard accepts),
Download and Close. Copy hides where the browser has no async clipboard,
such as a server reached over plain http; right-clicking the full-size
image still copies there.

Android: long-pressing an image in the editor copies it to the clipboard;
a tap still opens it full size in the phone's viewer. Opening and copying
share one hand-out that copies the file out of the blob store behind a
FileProvider URI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 17:14:32 -04:00
bvandeusenandClaude Opus 5.5 ea831d1857 Rail and drawer: only the 5 tags most recently used or viewed (#5931)
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Android / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Web typecheck and unit tests (push) Successful in 12s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Python tests (push) Successful in 14s
Android / Core and FFI clippy and tests (push) Successful in 1m9s
CI & Build / integration (push) Successful in 2m10s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 3m25s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m20s
Android / Kotlin + Rust (APK) (push) Failing after 6m3s
Android / Build the server image (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m5s
Desktop (Tauri) / Update manifest (push) Successful in 4s
A long tag list pushed the drawer's Archive, Trash and Sync off screen on
Android, where Tags still sat above them (#5915 only reordered the web rail).
Tags now come last on Android too, and both clients list the five tags most
recently used or viewed, by name, with "All tags (N)" opening the rest in
place.

Used: the newest untrashed note carrying the tag. GET /api/labels and the
core's list_labels return it as last_used, and each client also folds in the
notes it holds, so a tag applied a moment ago counts before the next listing.
Viewed: opening the tag's lens, remembered on the device (per user on the web).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 14:41:33 -04:00
bvandeusenandClaude Opus 5.5 c04e949c40 Stored credentials are encrypted at rest (#5927)
CI & Build / Build now, or wait for Android? (push) Successful in 2s
CI & Build / Python lint (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Android / Core and FFI clippy and tests (push) Skipped
Android / Kotlin + Rust (APK) (push) Skipped
Android / Build the server image (push) Skipped
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 10s
CI & Build / Python tests (push) Successful in 12s
CI & Build / integration (push) Successful in 1m40s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 1m44s
CI & Build / Build & push image (push) Successful in 38s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m18s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m9s
Desktop (Tauri) / Update manifest (push) Successful in 4s
#5927 "Adopt the family idea secret.Box": the SMTP password and the backup
bucket's secret key were kept as plain text in the settings table, so a copy
of the database alone could read them.

- New sealing.py: AES-GCM under an HKDF-derived key, each value bound to its
  setting's name. The key is a random file, /var/inkwell/sealing.key (0600),
  made on first use. It lives on the data volume, not in the database, which
  is where the signing secret sits by default.
- set_settings seals a secret before storing it; get_setting opens it. A value
  that no longer opens (key file lost) reads as not set, so Settings asks
  for it again.
- At startup, credentials stored before this are sealed in place, so nothing
  has to be re-entered. A data folder that can't be written never stops the
  server; saving a password then answers with the reason.
- New dependency: cryptography (approved by the operator).

Also #1919 "Deprecate / remove the PWA": kept. Its service worker caches
only the offline page, so an installed copy always runs the live build. One
stale comment in sw.js is corrected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 14:18:41 -04:00
bvandeusenandClaude Opus 5.5 edcb5c4c11 Backups: a daily dedupe-friendly tar to a folder or S3, with restore (#5918-#5921)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 4s
Android / Core and FFI clippy and tests (push) Skipped
Android / Kotlin + Rust (APK) (push) Skipped
Android / Build the server image (push) Skipped
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Web typecheck and unit tests (push) Successful in 10s
CI & Build / Python tests (push) Successful in 14s
CI & Build / integration (push) Successful in 1m38s
CI & Build / Build & push image (push) Successful in 41s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 1m41s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m18s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m5s
Desktop (Tauri) / Update manifest (push) Successful in 3s
#5918 "Backup file": an uncompressed tar of per-table JSON Lines (primary-key
order, sorted keys) plus every attachment file, with fixed tar headers, a
dated name, and an atomic write through a dot-named .part file. Unchanged
rows are the same bytes every day, so restic off-site stores only the edits.
No credential ever enters a backup.

#5919 "Backup destinations": a folder, or an S3-compatible bucket through a
stdlib SigV4 put/list client (checked against AWS's published signatures),
with an upload limit. Test destination only lists, so an append-only key works.

#5920 "Backup job": daily at a chosen hour, Back up now, a 1/2/4 h retry
ladder, a one-hour deadline, an advisory lock, the last 30 runs, and an
overdue verdict on Settings -> Backups.

#5921 "Restore": python -m inkwell.restore FILE into an empty instance, an
integration test that backs up and restores, and docs/backups.md with
Fabled Archives as the worked example.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:54:51 -04:00
bvandeusenandClaude Opus 5.5 c0de50d85d Email: one branded HTML layout with a text part, and invites can be emailed (#5917)
Each email was a subject and a plain-text f-string at its call site.
mail_templates.py now holds one layout: the site name, a heading, the
message, a button for the link and a footer saying who sent it and why.
Every email (password reset, test, invite) is a function returning its
subject, text and HTML. mailer.send sends them as multipart/alternative,
text first.

The HTML is email-shaped: tables, inline styles, no images or fonts, and
every value from settings or a person escaped.

POST /api/invites takes send_email. It is refused before anything is made
when email is off or the invite names no address. The link is built from
the public address. A failed send leaves the invite standing with
email_error, so the admin still gets the link once. The invite form offers
"Email the link to …" when email is on and an address is filled in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:43:13 -04:00
bvandeusenandClaude Opus 5.5 5cc71c3050 Settings: tabs instead of one long page, and a review pass (#5916)
Settings was every registry group, then invites, accounts, groups and
activity, in one scroll. It is now six tabs held in ?tab=:
- General: General, Notes, Attachments, Links.
- People: Access, Invites, Accounts, Groups.
- Email, with a line saying whether email is on and what it still needs.
- Security.
- Activity.
A registry group that no tab names lands on General, so it can't go missing.

Save saves only the current tab. Edits waiting on other tabs keep their
values and mark their tab with a dot. Leaving the page with unsaved edits
asks first.

Review fixes:
- On a phone, a field stacks under its label instead of overflowing beside
  it.
- Descriptions are easier to read.
- The SMTP encryption choice shows readable names (choice_labels).
- The test-email button uses .btn-sm and is disabled while email is off.
- The accounts list is headed Accounts, not People, now that People is a
  tab.
- The docs point at the new paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:41:03 -04:00
bvandeusenandClaude Opus 5.5 c63375b092 Rail: Tags sit below the other nav items (#5915)
CI & Build / Python lint (push) Successful in 2s
Android / Build the server image (push) Skipped
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Android / Core and FFI clippy and tests (push) Skipped
Android / Kotlin + Rust (APK) (push) Skipped
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Web typecheck and unit tests (push) Successful in 9s
CI & Build / Python tests (push) Successful in 11s
CI & Build / integration (push) Successful in 1m35s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m9s
CI & Build / Build & push image (push) Successful in 49s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m23s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m6s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Tags is the one list in the rail that grows without limit, so everything
with a fixed place goes above it and stays put.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:38:49 -04:00
bvandeusenandClaude Opus 5.5 8d67fc0da5 Web editor: a typed #tag shows as a chip, and # suggests existing tags (#5913, #5914)
The tag row under the text was a copy of the note's labels taken when the
editor opened. A #tag in the body becomes a tag on the server's side of a
save, so the copy never learned of it until the note was reopened. Every
save now hands its note back, and the row is refreshed from it.

Typing # in a prose block or a checklist item opens the tags that already
exist: prefix matches first, busiest first. Arrows move, Enter or Tab puts
the tag in, Esc closes only the menu. The grammar and ranking live in
notes/tags.ts with tests, so the menu never offers a tag the server would
not derive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 13:38:49 -04:00
bvandeusenandClaude Opus 5.5 d6204d320a One tag-name fold on the web and on Android, beside the core's (#5373, #5385)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Web typecheck and unit tests (push) Successful in 9s
CI & Build / Python tests (push) Successful in 11s
Android / Core and FFI clippy and tests (push) Successful in 1m0s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m16s
CI & Build / integration (push) Successful in 2m41s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m8s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m45s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Successful in 9m26s
Android / Build the server image (push) Successful in 1s
The web compared or keyed tag names with name.toLowerCase() at five sites:
the labels store's merge check, LabelPicker's create check, NoteCard's
colour map, and colors.ts twice. Android had three, plus TagsScreen's rename
dialog using equals(ignoreCase = true). Each now calls foldTag(name), which
says what the core's derive::fold and the server's labeling.named say.

Android's rename dialog now predicts a merge the same way the core decides
one. Per-character ignoreCase and full lowercasing disagree on letters like
İ, whose lowercase is two characters. LabelPicker's search filter keeps
its own lowercasing: that is search, not identity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 17:05:30 -04:00
bvandeusenandClaude Opus 5.5 921c0a47bd Real plurals: the web's sync messages and Android's tag counts
Web: one counted(n, one, other) helper. The sync screen's three "(s)"
messages (attachments that didn't download, changes refused, files that
didn't upload) now say "1 file" / "2 files". The six hand-written ternaries
(SyncView x2, ImportNotes, GroupList, ShareDialog, LabelsModal) use it too.

Android: tags_count was two strings chosen in code, and is now <plurals>.
The delete confirmation read "It is on 1 notes … removed from all of them"
and now has a one form of its own.

Operator decision on #5371, 2026-10-08.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:57:09 -04:00
bvandeusenandClaude Opus 5.5 eb6a1ff3ef Web: Activity loads older events as you scroll, no Show older button
Preference 172. ScrollSentinel watches the end of a cursor-paged list and
asks for the next page a screen early. It asks once per cursor, keeps going
when a page lands without pushing the end off screen, and stops on a failure
until Try again (an observer re-firing on a failure is a request loop). A
failed older page now gets its own error beside the retry, instead of
replacing the list's load error.

Adapted from fabledstash's ScrollSentinel (snippet 3760).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 16:54:19 -04:00
bvandeusenandClaude Opus 5.5 ef22910154 DRY pass #3: the labels store swaps a returned label in one way (#5373)
rename and setColor each found the label and replaced it, keeping the count
the single-label PATCH doesn't recompute; keepingCount(updated) is that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 15:39:25 -04:00
bvandeusenandClaude Opus 5.5 cd8df384a4 DRY pass #3: one compact dialog button (#5373)
ShareDialog's Share and NoteEditor's Add note / Close each spelled out the
same compact button, smaller than .btn. .btn-compact is its shape and
.btn-compact-primary its brand fill; Close keeps its neutral colours beside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 15:38:36 -04:00
bvandeusenandClaude Opus 5.5 1b128a9374 DRY pass #3: a secret setting is the text input with a password type (#5373)
SettingsView drew secret and plain text settings as two inputs differing only
in type, autocomplete and the saved-value placeholder; one input binds those.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 15:16:16 -04:00
bvandeusenandClaude Opus 5.5 0e21b8e7f1 DRY pass #3: SyncView's update choices are one fieldset, twice (#5373)
The source and channel fieldsets were the same markup over different options.
updateChoices holds each set with its legend, radio name and when it is
offered, and one v-for draws them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 15:14:10 -04:00
bvandeusenandClaude Opus 5.5 498b23789c DRY pass #3: one one-time secret reveal (#5373)
AccountView's device-token reveal was OneTimeLink's markup and copy handler
again. OneTimeLink becomes OneTimeSecret (value, note, noun = link | token),
and the account page shows a new token through it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 15:13:18 -04:00
bvandeusenandClaude Opus 5.5 a0249564e2 DRY pass #3: the web asks one question about a password's length (#5373)
RegisterView and ResetPasswordView each wrote the min-length check and its
message; config.passwordRefusal(password) is that check, the web's side of
auth._password_refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 15:12:32 -04:00
bvandeusenandClaude Opus 5.5 8c45725a2a DRY pass #2, batch 8, F17: a person and an account have one shape each (#5372)
Web: GroupList's Person was adapters/repo's Member field for field, and
AccountList's Account was the session store's User; both now import them.
The bridge's Identity stays, as the Tauri mirror of the core's struct.

Server: auth._serialize_user and accounts' _serialize_account wrote the same
four fields. serialize.serialize_user is serialize_person plus is_admin, and
the account list adds created_at to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 15:11:29 -04:00
bvandeusenandClaude Opus 5.5 cba428461c DRY pass #2, batch 8: the Shared-with-me facet is tested once (#5372)
sharing.test.ts round-tripped shared=with_me, which facets.test.ts already
does with every facet; its unknown-value case moves there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 15:08:38 -04:00
bvandeusenandClaude Opus 5.5 2ce68f578a DRY pass #2, batch 8: stale comments (#5372)
useNoteList no longer names a Search view; the desktop adapter's M10.7 plan
gave way to sync under the local core; local.ts's sharing comment sat above
settings; AccountList and password_resets still said there was no mail path;
NoteEditor kept an orphan checklist-flag comment, a textarea comment from
before blocks, and the link-preview comment above the file picker; the
serialize docstring's growth plan is now what it holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 15:03:50 -04:00
bvandeusenandClaude Opus 5.5 e911f1d757 DRY pass #2, batch 8: the web's leftovers (#5372)
- notes/datetime.ts formatLocalDay had no caller; removed.
- style.css set body in two consecutive blocks; they are one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 15:01:13 -04:00
bvandeusenandClaude Opus 5.5 3dd47fa178 DRY pass #2, batch 5, F17: the web's smaller pairs (#5372)
- desktop/bridge.ts listen<T>(): the no-op-off-desktop event listener
  that onSynced, onCaptured and onReminderDue each wrote.
- size.ts roughSize(): one decimal below 10, none above, for the client
  download sizes (MB) and the storage line (GB).
- BaseModal's title prop draws the heading row and close button that
  LabelsModal and ShareDialog each built.
- SyncView: switchChannel and switchSource share recheck(); the four
  update-card radios render from two option lists.

Kept: the board's and the shell's is-typing guards (they ignore different
elements), the drawer's nav beside the palette's commands (one is a laid-
out list with tags between its entries, the other a command list), the
status pills (each colours differently), and plurals (the copy is the
operator's call, raised in #5371).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:48:31 -04:00
bvandeusenandClaude Opus 5.5 6e4c4930cb DRY pass #2, batch 5, F16: the admin lists load through useLoad (#5372)
Accounts, groups, invites and activity each wrote the same first load:
clear the error, try, put the server's reason or a fallback in error,
and stop loading. useLoad (composables/useAction.ts) is that, and its
load() is also the retry.

Kept: ShareDialog and Settings. Their load sets loading back to true on a
reload, which the lists never did, so moving them would change what a
retry shows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:46:14 -04:00
bvandeusenandClaude Opus 5.5 52d53fb78a InviteList: load() keeps its own error line; revoke toasts
ebbe4a6 converted the wrong try block in InviteList: the edit matched from
load()'s try down to revoke()'s catch, which left load() half-converted.
load() goes back to its own try/catch/finally. revoke() uses
toastOnFailure, as intended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:45:19 -04:00
bvandeusenandClaude Opus 5.5 ebbe4a678a DRY pass #2, batch 5, F16: one way an action reports its failure (#5372)
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 4s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 4s
Android / Build, or is the channel already serving this? (push) Successful in 8s
CI & Build / Web typecheck and unit tests (push) Failing after 11s
CI & Build / Python tests (push) Successful in 19s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Failing after 29s
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
Android / Core and FFI clippy and tests (push) Successful in 44s
CI & Build / integration (push) Successful in 1m32s
CI & Build / Build & push image (push) Skipped
Android / Kotlin + Rust (APK) (push) Failing after 4m11s
Android / Build the server image (push) Successful in 1s
composables/useAction.ts: toastOnFailure runs an action and toasts the
server's reason or a fallback; useAction adds the busy flag a button
waits on; useRowAction keeps the id of the row whose action is running.
Import, export, the menu entry, the integration prompt, sign out
elsewhere, the account reset link, the group actions (whose local act()
it replaces), invite revoke and sync disconnect each wrote that
try/catch/finally out.

Kept: AccountView's device revoke. It shows a fixed message rather than
the server's reason, and moving it would change the text. ShareDialog
shows its errors inline, not in a toast.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:44:43 -04:00
bvandeusenandClaude Opus 5.5 8899bd205f DRY pass #2, batch 5, F15: the four signed-out pages share AuthLayout (#5372)
Sign in, register, forgot and reset each wrote the same page: a centred
column, the app icon, a title, a subtitle, the form and a footer link. That
is now components/AuthLayout.vue, with the title as a prop and the
subtitle, the form, the footer and anything after it as slots. The footer
links wear the new .text-link class. Markup and classes are unchanged, so
the pages render as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:42:31 -04:00
bvandeusenandClaude Opus 5.5 b481e6857c DRY pass #2, batch 5, F14: the web's repeated class lists become component classes (#5372)
style.css gains the classes the views spelled out in full: .section-label
(17 sites), .hint (20), .form-error (13), .alert-error (5), .row-card (5),
.list-empty (5), .field (5), .page-shell (3), and the small row action
.btn-sm (4) / .btn-sm-danger (3). Only exact runs moved, so nothing renders
differently; spacing a site adds beyond a run stays a utility beside it.

Kept: the Reminders and Timeline small buttons. They carry no text colour
and inherit it, so putting them on .btn-sm would recolour them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:41:34 -04:00
bvandeusenandClaude Opus 5.5 bfe2ed783f The web's password forms read the server's minimum length
The server's MIN_PASSWORD_LEN was 8, and the web wrote 8 out five times: two
checks and three placeholders. The constant moves beside the other policy numbers
in settings.py (auth.py imports it), /api/config serves it as
min_password_length, and the config store hands it to Register, Reset and
Account. 8 stays only as the fallback until the config answers.

DRY pass #2, batch 3 (#5372).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:31:22 -04:00
bvandeusenandClaude Opus 5.5 1c4bf56058 A lone link's preview is looked up where the server filed it
The server unfurls what detect_urls finds, trailing .,;:!? trimmed, and files the
preview under that. The web and Android cards looked a lone link's preview up
under body.trim(), punctuation included, so a note reading
"https://example.com/a." never showed its card.

- grammar.json gains a urls section: what the server finds in a body, and the
  link a lone-link note is filed under.
- The web's rule moves out of NoteCard into notes/links.ts loneUrl(); Android's
  LinkPreviewRow gets the same loneUrl(); both trim like the server.
- The server and web suites run the cases; Android's JVM test pins them by hand,
  as it does the tint.

Fixes #5399. DRY pass #2, batch 3 (#5372).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:30:32 -04:00
bvandeusenandClaude Opus 5.5 4285026cfe The repeat rules are pinned by the shared fixture
daily/weekly/monthly/yearly was written out in the server (REMINDER_RECURRENCES),
the core (recur::RECURRENCES), the web editor's <option>s and Android's picker,
with nothing holding them together. grammar.json now has a recurrences list; the
server, core and web suites each check theirs against it, and the web's options
come from notes/recurrence.ts rather than the template. Android's picker pins the
list by hand with its localised labels, as it does the tint: its JVM tests do not
read the fixture.

DRY pass #2, batch 3 (#5372).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:28:22 -04:00
bvandeusenandClaude Opus 5.5 17ae8c0863 A failed update install says the install failed, not the check
installUpdate, and a failed channel or source switch, all fell back to "The
update check failed." Each now names what failed. A check that runs after a
successful switch keeps its own message.

Fixes #5387.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:12:13 -04:00
bvandeusenandClaude Opus 5.5 39b1ebae96 Settings → Activity: an audit log of what happened to accounts
CI & Build / Python lint (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Android / Core and FFI clippy and tests (push) Skipped
Android / Kotlin + Rust (APK) (push) Skipped
Android / Build the server image (push) Skipped
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 10s
CI & Build / Python tests (push) Successful in 14s
CI & Build / integration (push) Successful in 1m27s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 1m47s
CI & Build / Build & push image (push) Successful in 45s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m24s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m4s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Sign-ins and failed sign-ins, accounts created and sign-ups refused,
password changes, resets and reset links, devices linked and unlinked,
invites made and revoked. Each is kept in `audit_events` with the address
it came from, for `audit_retention_days` (Settings → Security, 90 by
default, 0 keeps them forever), and listed newest first for admins under
Settings → Activity. The retention loop deletes older events.

`audit.record` writes in its own session, so a refusal is kept even when
the request's transaction rolls back. A failure to record is logged and
swallowed, never the reason a sign-in fails. A throttled attempt (429) is
not recorded: a row per refused request would make each request in a
flood cost a database write. Throttle trips stay in the app log.

Also: the storage-limit test puts `storage_quota_gb` back afterwards,
since settings outlive the per-test truncate.

#2939 §5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 11:19:41 -04:00
bvandeusenandClaude Opus 5.5 043c87a8dc Each account may store 5 GB of attachments; admins have no limit
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 4s
Android / Core and FFI clippy and tests (push) Skipped
Android / Kotlin + Rust (APK) (push) Skipped
Android / Build the server image (push) Skipped
CI & Build / Python lint (push) Successful in 2s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Web typecheck and unit tests (push) Successful in 10s
CI & Build / Python tests (push) Successful in 12s
CI & Build / integration (push) Successful in 1m33s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m12s
CI & Build / Build & push image (push) Successful in 55s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m43s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m32s
Desktop (Tauri) / Update manifest (push) Successful in 5s
#2939 §4. max_attachment_mb capped one file, so any account could fill the
volume. The new Settings → Attachments → storage_quota_gb (default 5, 0 for
no limit) caps an account's total. That total is every attachment on every
note the account owns, trash included, since trashed files stay on disk until
emptied. Admins are exempt.

storage.upload_refusal is now the one check every upload makes: per file, then
per account. It is used by:
- the web upload route;
- the sync PUT, which judges the declared Content-Length before reading the
  bytes;
- the importer, which learns the room left up front and refuses the whole
  archive if its attachments don't fit (nothing is committed).

Over the limit is answered 507 Insufficient Storage, not 413. The core treats
a 4xx as a permanent refusal it never retries, and a 5xx as worth another try.
So a file refused for want of room syncs by itself once space is freed. The
cost is that an over-limit device re-sends that file each cycle until then.

GET /api/auth/storage returns used and limit, and the Account page shows it as
a Storage row ("1.2 GB of 5 GB used").

docs/public-hosting.md drops the quota gap and gains a section on the limit.
Its Android paragraph still said a public http:// address was only warned
about; since 1dd6fc1 it is refused, and the paragraph now says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 11:12:26 -04:00
bvandeusenandClaude Opus 5.5 bb591871a4 Account page: change your password, or sign out everywhere else
Family idea #5105, practice 4. Either action signs the account out of every
other browser and unlinks every device. The browser that made the change stays
signed in.

- POST /api/auth/password needs the current password. A wrong one returns 403,
  not 401, so this browser doesn't read as signed out, and it counts against the
  sign-in throttle. A short new password returns 400.
- POST /api/auth/sign-out-elsewhere does the same sign-out without a password
  change. Called from a device, it keeps that device linked.
- _sign_out_elsewhere moves session_epoch on and deletes device tokens. The
  reset route now uses it too, keeping no device.
- The page is renamed from "Linked devices" to "Account", in the router title
  and both nav entries. Its sections are Linked devices, Password (one short
  line, then the form) and Sessions (a single "Sign out everywhere else" row in
  the device rows' style), per preference 188: one line each, no paragraphs.
- docs/public-hosting.md says how sessions end, and why a browser session isn't
  listed the way a device is: it is a signed cookie, ended by moving the epoch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:22:16 -04:00
bvandeusenandClaude Opus 5.5 8cdb36dc2a web: two board columns on a phone, as the Android app has
CI & Build / Python lint (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Android / Core and FFI clippy and tests (push) Skipped
Android / Kotlin + Rust (APK) (push) Skipped
Android / Build the server image (push) Skipped
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 5s
CI & Build / Web typecheck and unit tests (push) Successful in 11s
CI & Build / Python tests (push) Successful in 18s
CI & Build / integration (push) Successful in 1m22s
CI & Build / Build & push image (push) Successful in 47s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m11s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m26s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m8s
Desktop (Tauri) / Update manifest (push) Successful in 3s
The board was one column below 640px, so on a phone it read as a list
while the app showed two. It is now two columns from the smallest width,
with an 8px gap there (16px from sm up). Two columns at 16px on a 390px
screen would leave ~170px cards. NoteCard's bottom margin, the vertical
half of the gap, tightens with it.

NoteGrid is the only place the board's columns are defined, so board,
search, timeline and reminders all change together.

Fill order is untouched. CSS columns fill top to bottom, so note #2 sits
under #1 rather than beside it, unlike Android. That is left until it has
been looked at on a phone, as #2950 recommends.

Scribe #2950.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 08:32:16 -04:00
bvandeusenandClaude Opus 5.5 b03c9cf81a desktop: in-app updates follow the server you installed from
CI & Build / Python lint (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 4s
Android / Core and FFI clippy and tests (push) Skipped
Android / Kotlin + Rust (APK) (push) Skipped
Android / Build the server image (push) Skipped
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 10s
CI & Build / Python tests (push) Successful in 11s
CI & Build / integration (push) Successful in 1m17s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Failing after 1m45s
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / Build & push image (push) Successful in 40s
Milestone 325 step 6 (Scribe #3254).

The server publishes its AppImage in the updater's own format at
/api/client/linux-appimage/update.json: the ordering key as `version`, the
signature, and an absolute download URL built on the host that was asked,
so the token the updater attaches goes nowhere else. Unsigned platforms and
a server with no AppImage 404.

The desktop's update source is now Fabled-Git (and its channel) or one
server:
- `read_source` is the one reader. The installer's `install-server` marker
  feeds the `update_server` pref once per new value, exactly as the channel
  marker feeds its pref; tauri.conf.json's endpoint is never consulted.
- From a server, the check and the download carry the sync link's token
  when the app is linked to that same server. Without one the update shows
  and says to link rather than offering a button that 401s.
- A server with no build says so. A 404 is "up to date" only on the forge,
  where it means an unpublished channel.
- Sync → App updates offers the source once there is a server to offer (the
  chosen one, or the linked one), and only shows the channel for the forge.

The trust anchor does not move: whatever the source, the updater verifies
the AppImage against the public key built into the app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 06:56:36 -04:00
bvandeusenandClaude Opus 5.5 871878de41 install.sh installs from your own server, not just from the forge
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
Android / Core and FFI clippy and tests (push) Skipped
Android / Kotlin + Rust (APK) (push) Skipped
Android / Build the server image (push) Skipped
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 10s
CI & Build / Python tests (push) Successful in 10s
CI & Build / integration (push) Successful in 1m15s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m5s
CI & Build / Build & push image (push) Successful in 55s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m15s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 2m57s
Desktop (Tauri) / Update manifest (push) Successful in 6s
Milestone 325 step 5 (Scribe #3253).

    curl -fsSL https://notes.example.com/install.sh | sh

The server serves the installer at /install.sh with its own address written
into it (installer.py). Settings → Public address when set, the request's own
address otherwise. The substitution is one variable, given a value that has
passed a strict shape check, and the script checks it again; an address that
cannot pass makes the route refuse rather than serve a script pointed
elsewhere. `public_url` joins the live settings cache so the route needs no
database.

From a server, the script:
- resolves each Linux bundle from the public /api/client/<platform>, and
  builds the download URL from the platform id rather than reading it from
  the reply;
- asks for a device token (from the terminal, since stdin is the script),
  or takes TS_TOKEN, and sends it from a file rather than the command line;
- checks the sha256 the server published before anything installs;
- revokes a prompted token once the download is done;
- records `install-server` for the updater (step 6) instead of the channel.

The forge path is unchanged, and stays the default for the copy the forge
serves. The Account page's downloads card shows the one-line command
whenever the server holds a Linux client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 06:44:39 -04:00
bvandeusenandClaude Opus 5.5 4149229988 web: component tests for the Share dialog
@vue/test-utils and jsdom as dev dependencies, jsdom chosen per file with the
vitest environment comment so the existing unit tests stay on node. The dialog's
repo.shares is faked; the tests cover offering everyone, sharing with a person
and a group, changing and removing a share (and the board's shared flag that
follows), a refused share keeping the choice, the load retry, and the
single-person instance. #5313.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:28:22 -04:00
bvandeusenandClaude Opus 5.5 b019172d47 all: remove saved views, the Has-reminder filter and the Created range
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 13s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Python tests (push) Successful in 15s
CI & Build / Build & push image (push) Skipped
CI & Build / integration (push) Successful in 1m29s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 3m34s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m5s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m49s
Desktop (Tauri) / Update manifest (push) Successful in 3s
Android / Kotlin + Rust (APK) (push) Successful in 9m29s
Step 18 of the audit follow-through (#5180), on the operator's decisions.
Inkwell is for capture and recall (note 2897), and these three duplicated a
surface that does the job already:

- Saved views. They lived only on the web; the desktop kept its own set that
  never synced, and Android had none. Tags in the drawer already give
  one-click recall. Gone from the server (routes, model, migration 0038 drops
  the table), the core (store functions, schema v13 drops its table), the
  desktop commands, the web adapters, the drawer's Views list and the
  "Save view" link.
- The "Has reminder" facet. The Reminders page lists them, sorted by due.
- The FilterBar's "Created" range. Timeline is the date lens and keeps the
  created_after/created_before query it builds from local days, which also
  retires the UTC/local-day disagreement between the two (B3).

An old link that still carries the removed keys opens the plain board; a
web test pins that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 21:29:28 -04:00
bvandeusenandClaude Opus 5.5 888c6410f0 all: trim the history essays out of the longest comments
CI & Build / Build now, or wait for Android? (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 3s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 9s
CI & Build / Python tests (push) Successful in 12s
CI & Build / integration (push) Successful in 2m0s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 3m11s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m51s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m39s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Successful in 9m4s
From the audit (#5179). Ten comment blocks narrated how the code got here:
milestone numbers, earlier values, the operator's verdict on an old design.
Each now says what the code does and why, and the history stays in git,
Scribe and docs/sync.md. The protocol-version comment in sync.py points at
docs/sync.md's policy section, which already lists every bump.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:03:44 -04:00
bvandeusenandClaude Opus 5.5 e75e3d37d0 web: shared note actions, reminder chips, page header, formatters and settings seam
From the audit (#5179, web half).

- NoteActions: share, pin, archive and trash (restore and delete forever
  when trashed), as both the card and the editor offer them. The editor's
  history toggle goes in its slot, and it closes on `acted`.
- ReminderActions: the Done / 1h / 1d chips on the card and in the editor,
  which are now the same chips.
- PageHeader: the back-to-board header that Settings, Sync and Linked
  devices each wrote out, now with a `back` icon from the shared set.
- notes/datetime: formatShortDateTime (was formatReminder and the editor's
  revLabel) and formatDateTime (the two `fmt` copies).
- notes/colors: labelDotClasses (the sidebar's and the tag manager's
  labelDot).
- Drawer links use exact-active-class instead of route.name ternaries.
- BoardView binds its three grids from one gridBinds object.
- Settings goes through repo.settings (rest, plus a local adapter that
  answers "needs a server") and shows load failures through AsyncState.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:02:18 -04:00
bvandeusenandClaude Opus 5.5 7eacd0569c all: delete the code nothing calls
CI & Build / Python lint (push) Successful in 2s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 8s
CI & Build / Python tests (push) Successful in 10s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / integration (push) Failing after 1m21s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m47s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m48s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m39s
Desktop (Tauri) / Update manifest (push) Successful in 3s
Android / Kotlin + Rust (APK) (push) Successful in 8m37s
From the audit (#5178). Each was unreachable from every client:

- Checklist add-item and delete-item: REST POST /items and DELETE /items/<id>,
  the Tauri commands, the store, rest and local adapters, the core's
  add_item/delete_item, set_item_text and remove_item, and the FFI exports.
  Adding, rewording and removing an item are body edits in every editor. The
  checked toggle stays, and its rewriter is simpler without the drop branch.
- Manual unfurl: POST /unfurl and its adapters. Previews arrive in the
  background after a save (unfurl_queue).
- The /api/config `android_client` key, android_release() and the
  APK_NAME/MANIFEST_NAME aliases. Phones poll /api/client/android.
- users.email_verified and users.avatar_path (migration 0037). Nothing set
  the first or read the second; the SMTP reset never checked verification.
- derive::extract_tags (only tests used it; the shared fixture now runs
  through extract_tag_spans), the unused check and link icons, and the
  unused editor_add_item string.
- The blob scheme is renamed tsblob -> inkblob. URLs are built as notes are
  read, so nothing stored carries the old one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 19:00:44 -04:00
bvandeusenandClaude Opus 5.5 442cca4398 web, core, desktop: share a note with a group, and Settings → Groups
The Share dialog lists people and groups in one picker and shows a group share
as its name and member count. Settings gains a Groups section for the admin:
create, rename, delete, and add or remove people.

The core client reads the directory's groups and group shares (ShareTarget:
a member or a group); the desktop command takes user_id or group_id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 18:25:16 -04:00