7fca1f6c7f531ed59f88842ce303056e7aa580c6
120
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7fca1f6c7f |
A forwarding header shorter than the hop count is read from its leftmost entry
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 2s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / Web typecheck and unit tests (push) Successful in 9s
CI & Build / Python tests (push) Successful in 17s
CI & Build / integration (push) Successful in 1m30s
CI & Build / Build & push image (push) Successful in 45s
At Trusted proxy hops 2 (Cloudflare → Traefik → app), a LAN request reaching Traefik directly carries one X-Forwarded-For entry. trusted_entry returned None for it, so every LAN visitor was filed under Traefik's overlay address in the throttles and the audit log. Cloudflare's single X-Forwarded-Proto: https read as plain HTTP, so the public road lost the Secure cookie and HSTS. A short header now clamps to its leftmost entry, the one the first proxy wrote, as family canon #5004 does in Minstrel, Librarian and Forge. A caller's own entries can only make the header longer, so this trusts nothing more than before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b74dc7c1fe |
One app fixture for the tests, and ViewModels built where they are used
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 2s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / Web typecheck and unit tests (push) Successful in 8s
CI & Build / Python tests (push) Successful in 13s
Android / Core and FFI clippy and tests (push) Successful in 25s
CI & Build / integration (push) Successful in 1m31s
CI & Build / Build & push image (push) Skipped
Android / Kotlin + Rust (APK) (push) Successful in 6m41s
Android / Build the server image (push) Successful in 1s
The tests' `app` fixture, the same two lines in eleven files, now lives once in
tests/conftest.py.
On Android each ViewModel carried a companion `factory` that only wrapped its
constructor in viewModelFactory { initializer { … } }. MainActivity now builds
each one with Compose's viewModel { … } builder, so the five factories and their
imports are gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
463b22bbe4 |
A push that loses a create race applies its change instead of failing
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
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
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 1m31s
CI & Build / Build & push image (push) Successful in 34s
Two devices pushing the same new note or tag at the same moment both found no row and both inserted it; the slower insert hit the primary key and the whole push answered 500. Each pushed note or label change now runs in a savepoint. If its insert loses the race, only that change is rolled back, and it is applied again against the row the other push committed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7e96683f01 |
The archive-search tests sign in and keep the listed notes alive
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 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 14s
Android / Core and FFI clippy and tests (push) Successful in 31s
CI & Build / integration (push) Successful in 1m38s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 1m52s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m5s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m44s
Android / Kotlin + Rust (APK) (push) Failing after 5m10s
Android / Build the server image (push) Skipped
Desktop (Tauri) / Update manifest (push) Successful in 4s
The integration test registers the owner before writing notes, and the core test binds the "all" listing before borrowing ids from it. Task #6315. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
a51a563fe7 |
Server comments describe the code to a stranger (#6221)
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
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 13s
Android / Build the server image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / integration (push) Successful in 1m30s
CI & Build / Build & push image (push) Successful in 32s
Rule 32 "Comments describe the product to a stranger" sweep over src/, alembic/ and tests/. Comments and docstrings now say what the code does and the reason as a present-tense constraint, briefly. Removed: - issue, task, note, rule, milestone and family-idea references - "used to" / before-and-after narration - quotes of decisions and incident stories from the deploying instance - paragraph-length arguments, where a line or two carries the fact Kept every hazard as a present-tense fact, e.g. timing oracles, the proxy hop direction, and migrations that must stay frozen. Also: - Three test names lose "still"/"used to"/"pre_m304" phrasing. - pyproject's description drops "FabledSword family". The history these comments carried stays in Scribe and in git log. A comment-stripping diff (Scribe snippet #6226 "codeonly.py — prove a diff changed only comments") shows no code changed apart from the renamed tests and the description string. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
74d0d3d88e |
Email: the Inkwell logo in the header
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 2s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / Python tests (push) Successful in 19s
CI & Build / Web typecheck and unit tests (push) Successful in 10s
CI & Build / integration (push) Successful in 1m35s
CI & Build / Build & push image (push) Successful in 48s
The header showed only a yellow dot before the site name. It now shows the app icon, embedded in the message as an inline part (cid:inkwell-logo, the HTML part becoming multipart/related) rather than linked: most clients block remote images, the app fetches nothing (rule 164), and Gmail drops SVG and data: images. The PNG is a copy of frontend/public/icon-192.png, shipped as package data; a test keeps the two identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
91ec677025 |
DRY pass #3: checklist near-misses live in the grammar fixture (#5373)
test_notes.py and derive.rs each kept the same six lines of prose that look like a checklist item. Four were already task_lines cases in grammar.json; the other two (empty brackets, no bullet) join them, so the web suite now tests them too, and both hand lists go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
45c286a71f |
DRY pass #2, batch 8: inkwell.notes stops re-exporting its siblings (#5372)
notes/__init__ re-exported 17 names so it could stand in for the old single
module. Only sync.py and test_notes.py used that, and each now imports from
the module that defines the name; __all__ and the nine imports __init__ held
only for it go. sync.py also drops sa_delete, func, live and NoteLabel,
unused since
|
||
|
|
fae03c7eca |
install.sh reads INKWELL_SERVER, INKWELL_CHANNEL and INKWELL_TOKEN (#5372)
The installer's environment variables kept the ThoughtSync-era TS_ prefix after the rename to Inkwell. They are now INKWELL_*, as is the INKWELL_SERVER_DEFAULT line the server fills in when it serves the script. The old names are not read any more; the operator approved the clean cut. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
38b94bd57e |
DRY pass #2, batch 4, F13: the server's remaining pairs (#5372)
- common.detach: the hold-a-reference-until-done task start that mailer and unfurl_queue each wrote. - common.expired_before: the retention window, now shared by trash and the audit log (it moves out of retention.py, which imports audit). - responses.too_many: the 429 with Retry-After from the credential throttle and the client-download throttle. - share_sync.revoke_lost: revoke whoever could see a note before and no longer can, after a share or a group goes. - serialize.serialize_person: a member as the directory, a share and a group listing show them. - groups_api._get_group: the path-id lookup four group routes wrote. - settings.apply_session_ttl: the session lifetime set at boot and on save. Kept: the attachment-id claim check (one query; each caller answers an id it already holds differently), the strict UUID-list parses in reorder and set_note_labels (distinct error messages), and the checklist-items loops (one line each). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
3766d59b2e |
The server's palette is checked against the shared fixture's hues
The web already checked its tint keys against grammar.json; the server's NOTE_COLORS, which normalize_color accepts, had no such guard. It is now the fixture's hues plus "default". The core has no palette to check: it stores whatever the UI (which only offers palette keys) or the server sends. DRY pass #2, batch 3 (#5372). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
db9e9a2dfb |
A note's name is one rule, pinned by the shared fixture
The server and the core each derived display_title and disagreed twice: the server cut it at 200 characters and the core didn't, and the server split lines with splitlines(), which also breaks on a lone \r or a U+2028, where the core and every other reading of the grammar split on \n alone. - grammar.json gains a display_titles section: blank lines, markers, an empty item, \r\n, a lone \r, U+2028, and a 201-character line of 'é' (the cut is characters, not bytes). - derive::display_title and DISPLAY_TITLE_CAP are the core's half, moved next to strip_marker. The server splits on "\n". Both suites run the cases. Behaviour: a device now names a note with a first line over 200 characters the way the web always has, and the server names a note containing a lone \r or a U+2028 the way devices always have. Fixes #5398. DRY pass #2, batch 3 (#5372). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7259708f28 |
Session length, trash retention and attachment size have bounds
Each accepted any integer. A session length of 0 expired every session at once, the admin's own included; an attachment limit above the 64 MB body ceiling allowed files no request could carry, and a negative one refused every upload. - session_ttl_days 1..3650, trash_retention_days 0..3650 (0 = keep), and max_attachment_mb 1..MAX_BODY_MB-1, leaving room for the multipart envelope. - MAX_BODY_MB is the one number app.py's MAX_CONTENT_LENGTH and that maximum both read. - A value stored before its bounds existed reads as the nearest bound. Fixes #5384. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
bfab8746ad |
Opening a purged note, or one of its files, is a 404
get_note and get_attachment selected with the ACL inline and skipped the purged filter, so a tombstone came back 200 (#2128 says a purged note reads as absent). Both now go through _get_visible, which cannot skip it. Reorder's batch lookup gains the same filter, so a stale id cannot write a place onto a tombstone. Fixes #5383. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8eff5f60a6 |
A deleted tag is a tombstone on every path, and the web skips tombstones
The web deleted a tag's row outright (delete, and merge's source), so the change feed never carried it and linked devices kept the tag. A device's delete left a tombstone that the web still listed, matched by name on create and rename, minted #tags onto, and accepted in a picker. - labeling.tombstone_label is the one way a tag is deleted: drop its links, set purged_at. REST delete, merge and sync's op=delete all use it. - labeling.live(owner) is the one definition of a tag that exists; every catalog read uses it (list, lookup, create/rename matching, #tag minting, picker ids, export, and sync's name-clash check). - 0040: (owner_id, name) is unique among live tags only, so a tombstone gives its name back and #grocery can be made again, on the web or from a device. Fixes #5382. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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
|
||
|
|
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> |
||
|
|
5d08d8a7a6 |
The first account can only be made in a 30-minute setup window
CI & Build / Python lint (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 4s
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
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
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 1m4s
CI & Build / Build & push image (push) Successful in 38s
Family idea #5105, practice 8 (Scribe #5113), the operator's choice of setup window over a setup code. Before this, whoever reached /register first on an empty server became its admin. On a fresh server at a public address, that could be a stranger, and a new DNS name is found within minutes. Now the first registration is refused once 30 minutes have passed since the server started (create_app records STARTED_AT). A restart opens the window again. It is a constant rather than a Setting, because there is no admin yet to change one. Once an account exists it no longer matters, so existing servers are unaffected. public-hosting.md says so, and two integration tests cover both sides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
65f004029b |
Signed-in app downloads are cached privately, not publicly
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 3s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / Web typecheck and unit tests (push) Successful in 10s
CI & Build / Python tests (push) Successful in 15s
CI & Build / integration (push) Successful in 1m6s
CI & Build / Build & push image (push) Successful in 49s
Quart's send_file marks every file it sends Cache-Control: public. The app download is behind a login, so a shared cache or proxy could have kept one account's copy and handed it to anyone. send_artifact now marks it private, as the attachment route already does. Family idea #5105, practice 11 (Scribe #5113). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
97b04f9f92 |
Close the gaps family idea #5103 found in how Inkwell distributes its app
CI & Build / Python lint (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 5s
Android / Build, or is the channel already serving this? (push) Successful in 6s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 7s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / Python tests (push) Successful in 18s
CI & Build / integration (push) Successful in 1m13s
CI & Build / Build & push image (push) Skipped
CI & Build / Web typecheck and unit tests (push) Successful in 12s
Android / Core and FFI clippy and tests (push) Successful in 45s
Android / Kotlin + Rust (APK) (push) Successful in 9m50s
Android / Build the server image (push) Successful in 2s
Three of the idea's practices this project still owed (Scribe #5118): Practice 3, CI fails on the wrong signer. The signing step printed the certificate and went on. It now fails unless the APK has exactly one signer and that signer is the release certificate (SHA-256 408a5835…, pinned from run 8753). The steps that publish come after it in the same job, so a wrongly signed build is never staged or published. Practice 6, app downloads are throttled and carry a sha256 ETag. - The download route counts per account and answers 429 with Retry-After past the limit. The limit is a new Settings → Security value, "App downloads per account per hour" (default 30), live like the sign-in limits. - The ETag is the sidecar's sha256, not Quart's mtime-and-path, so a phone resuming a download across a redeploy is not told its partial copy is stale. Quart's own ETag and conditional handling are off, and the route runs the conditional pass after setting the ETag, so Range and If-Range are judged against the content. Practice 9, the update offer and debug builds. - The install-permission notice re-reads the grant each time the app comes back, as ReminderNotice does. Read once, it stayed up after someone granted the permission in Settings and came back. - A debuggable build says it can't update itself and checks for nothing. Android would refuse the release-signed APK over a debug signature anyway. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
646a115701 |
server: one credential check, one coerce_bool, one top-position query, and the shared response helpers
CI & Build / Python lint (push) Successful in 3s
Android / Kotlin + Rust (APK) (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 12s
CI & Build / integration (push) Successful in 1m11s
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
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Skipped
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 49s
From the audit (#5179, server half). - auth: login and device-login share _check_credentials (dummy hash for a missing account, throttle bookkeeping, the failure log line) and _bad_credentials. - settings uses common.coerce_bool. Its private copy differed only in treating a non-string as its truthiness, which the shared one now does. - notes: create and import share helpers.top_position. - auth, settings_api, sync and client_dist return errors through responses.json_error / not_found, and parse ids with parse_uuid. - sync: push replies are built by _result(id, entity, status, **extra). Already merged by earlier steps, so nothing to do here: attachment storage (store_attachment), the preview upsert (only unfurl_queue writes one now), and _serialize_note (delegates to _serialize_notes). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b8f13cfc4a |
tests: the share ACL test's final body no longer expects the removed add-item route's eggs
Android / Kotlin + Rust (APK) (push) Skipped
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Python tests (push) Successful in 11s
CI & Build / integration (push) Successful in 1m5s
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 2s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / Web typecheck and unit tests (push) Successful in 8s
CI & Build / Build & push image (push) Successful in 47s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e2e0740b06 |
tests: stop posting to the removed add-item route in the share ACL test
CI & Build / Python lint (push) Successful in 4s
CI & Build / Build now, or wait for Android? (push) Successful in 4s
Android / Build, or is the channel already serving this? (push) Successful in 4s
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
CI & Build / Web typecheck and unit tests (push) Successful in 10s
CI & Build / Python tests (push) Successful in 13s
Android / Kotlin + Rust (APK) (push) Skipped
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / integration (push) Failing after 1m7s
CI & Build / Build & push image (push) Skipped
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
5f3cfe8bb7 |
groups: admin-managed groups, sharing a note with one, and membership in the feed
/api/groups (admin) creates, renames and deletes groups and adds or removes members. The member directory lists every group, and a share may name a group_id instead of a user_id; a note's shares answer with `member` or `group`. A note shared with a group reaches whoever is in it now, so membership is what the feed follows: joining grants each of the group's notes to the new member's devices, and leaving (or the group being deleted) revokes them unless a direct share or another group still reaches that person. recipients() never counts the note's own owner, who may sit in a group it is shared with. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
63955bbe97 |
sync: recipients keep their own pin, archive and place on shared notes
A note_user_state row per (note, recipient) holds what used to be the owner's columns as far as anyone else could tell. The board filters and orders through the viewer's own state; PATCH and reorder write it for a note shared at any level; the feed's revision for a shared note is the later of the note's and the caller's row, so a recipient's pin reaches their devices and no one else's. Push takes the three with their own `state_at` stamp (protocol 7, `shared_state`), so pinning a copy whose text is behind never makes that text win over the owner's edit. A body is only stamped as an edit when it changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
75928c7afd |
sync: shared notes in the feed, revocations, and text pushes from an edit share
Android / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
CI & Build / Python lint (push) Successful in 2s
Android / Kotlin + Rust (APK) (push) Skipped
CI & Build / Python tests (push) Successful in 15s
CI & Build / Web typecheck and unit tests (push) Successful in 9s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / integration (push) Successful in 1m4s
CI & Build / Build & push image (push) Successful in 54s
The change feed answers `?shares=1` with every note the caller can see, each saying how it is held, plus a `revoked` list of notes that left them. Granting a share moves the note past the recipient's cursor; ending one, or the owner deleting the note, leaves a revocation on the same cursor. A recipient at edit may push the note's text, and nothing else. Protocol 6, feature `shares`; opt-in, so the floor stays at 3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2e2d8667dd |
password reset by email: Settings → Email, Forgot password?, and a test-email button
CI & Build / Python tests (push) Successful in 15s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / integration (push) Successful in 1m16s
Android / Build, or is the channel already serving this? (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Skipped
CI & Build / Build now, or wait for Android? (push) Successful in 2s
CI & Build / Web typecheck and unit tests (push) Successful in 11s
CI & Build / Python lint (push) Successful in 2s
CI & Build / Build & push image (push) Successful in 1m15s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m49s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m26s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m26s
Desktop (Tauri) / Update manifest (push) Successful in 4s
The operator asked for self-service reset over SMTP. It reuses #5173's password_resets table, /reset-password page, one-hour single-use token and sign-out-everywhere. - Settings (rule 25, not env): a new Email group (SMTP server, port, encryption as a choice, username, password, from), General → Public address, and Security → Reset emails per account. The registry gains `choices`, `secret` (the value is never sent back, `is_set` says one is saved, an empty save keeps it) and `url` (http(s), trailing slash stripped). - mailer.py: stdlib smtplib on a worker thread, 20 s timeout, starttls | tls | none. mail_settings() is None until a server, a sender and the public address are set. Links are built from the public address because the Host header can be forged. - POST /api/auth/forgot-password: the same answer at the same speed for any address. The link is made and mailed off the request (send_later). It is throttled like a sign-in per visitor address, and capped per typed email by reset_emails_per_account; past the cap it answers the same and sends nothing. - POST /api/settings/test-email: mails the admin with the saved settings and shows the server's error if it fails. - Public config `password_reset_by_email`. Sign-in shows "Forgot password?" only then, linking to a new /forgot-password page. - docs/public-hosting.md: an "Email and forgotten passwords" section. Tests: the secret stays server-side; emailed link → reset; the same answer for unknown addresses; the cap; test email success and failure; validation units. #5266. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ca242e59a6 |
tests: adding a checklist item answers 201
Android / Kotlin + Rust (APK) (push) Skipped
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 59s
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
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 1m48s
CI & Build / Build & push image (push) Successful in 1m2s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m24s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m7s
Desktop (Tauri) / Update manifest (push) Successful in 4s
The edit-share test expected 200 from POST …/items, which creates. #5174. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f53d377766 |
sharing: share a note from the web, at view or edit, with anyone on the instance
CI & Build / Python lint (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 4s
Android / Build, or is the channel already serving this? (push) Successful in 4s
CI & Build / Python tests (push) Successful in 14s
Android / Kotlin + Rust (APK) (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 / integration (push) Failing after 54s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m6s
Desktop (Tauri) / Update manifest (push) Canceled after 0s
Desktop (Tauri) / Tauri desktop (Linux) (push) Canceled after 2m33s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Canceled after 2m25s
The ACL has gated every read since M0, but nothing could write a share. Server: - shares_api: GET /api/users/directory (everyone but you, signed-in only), GET/POST /api/notes/<id>/shares and DELETE …/shares/<share_id>, owner only. Sharing again with the same person changes the permission (ON CONFLICT on the new unique index). - acl.visible_to_user takes permission=; granted_to and shared_ids feed the serializer. - Edit covers body and checklist (_get_editable). Everything else stays _get_owned. A view share's write is a 404 like a stranger's (#1984). An editor's PATCH naming anything but body is a 403. - Serialized notes carry permission, shared and shared_by. A recipient never gets the owner's labels, and a #tag an editor types files under the owner's (it always went to note.owner_id). - ?shared=with_me, also allowed in saved views. Trash and reminders are the owner's. purge_note drops the note's shares. - Migration 0034: one share per note and person (and per group), permission limited to view and edit, an index for "shared with me". Web: - ShareDialog (one, mounted by the shell): pick a member, Can view or Can edit, change or remove existing shares, with loading, error and empty states. - Card: "Shared by X" or "Shared" chip; owner-only actions and reminder buttons hidden for recipients; checkboxes inert at view. - Editor: read-only at view; text and checklist only at edit; Share button for the owner. - FilterBar: Shared with me. Repo seam gains `shares`; the offline desktop shows none of it (#5175 brings sharing there). Tests: owner, recipient and stranger across reads, every write at view and edit, tag filing, unshare, trash, delete and validation; web unit tests for the facet and permission helpers. #5174. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f100e5ef85 |
tests: the self-revoke routing check reads the URL map; its 400 moves to Postgres
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 / Kotlin + Rust (APK) (push) Skipped
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
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 51s
CI & Build / Build & push image (push) Successful in 50s
test_devices signed a fake account into a session and relied on
login_required answering without the database. Since
|
||
|
|
3dd0b44cb9 |
password reset: an admin makes a one-hour link, and using it signs the account out everywhere
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 / 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) Failing after 12s
CI & Build / integration (push) Successful in 49s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 1m41s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m4s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m5s
Desktop (Tauri) / Update manifest (push) Successful in 5s
There is no mail path, so a forgotten password needed a hand on the database (#2939 §2). Settings → People lists the accounts; Reset password makes a link that works once within an hour, shown once for the admin to hand over. Making another link for the same account closes the earlier one. Using it (/reset-password) sets the password, deletes the account's device tokens, and moves users.session_epoch on. Sessions are signed cookies the server can't delete, so each now carries the epoch it signed in under and login_required reads the account's epoch by primary key. A cookie from before this has no epoch and reads as 0, the starting value, so the upgrade signs nobody out. A deleted account's session now stops working too. The one-time link reveal moves out of InviteList into OneTimeLink, and the link-building into router/links.ts, shared by invites and resets. Migration 0033. #5173. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
28fa8badcb |
invites: an admin lets one person register while registration stays closed
CI & Build / Python lint (push) Successful in 2s
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 3s
CI & Build / Build now, or wait for Android? (push) Successful in 2s
Android / Kotlin + Rust (APK) (push) Skipped
CI & Build / Web typecheck and unit tests (push) Successful in 8s
CI & Build / Python tests (push) Successful in 11s
CI & Build / integration (push) Successful in 47s
CI & Build / Build & push image (push) Successful in 54s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 2m8s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 2m28s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m14s
Until now adding a second person meant re-opening registration to the whole internet while they signed up (#2939 §1). An admin now makes an invite in Settings: a link that works once, expires (7 days by default, 1 to 30), and can be pinned to one email address. Only the token's hash is stored, so the link is shown once. POST /api/auth/register takes `invite`. Redemption is one conditional UPDATE inside the transaction that creates the account, so two people racing one link can't both get in, and a taken email leaves the invite unused. Every refusal says "invalid or expired invite". The register page reads ?invite= and opens even while registration is closed. Refs #5172 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5989ffc1c6 |
desktop: Import and Export work offline; the menu toggle is reachable; errors say why
CI & Build / Build now, or wait for Android? (push) Successful in 4s
Android / Build, or is the channel already serving this? (push) Successful in 4s
CI & Build / Python lint (push) Successful in 2s
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 13s
CI & Build / integration (push) Successful in 48s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 4m26s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m10s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m32s
Desktop (Tauri) / Update manifest (push) Successful in 4s
Android / Kotlin + Rust (APK) (push) Successful in 12m42s
Export and Import were a link to the server and a reject("needs a server") on the
desktop. Both now run in the core with no server:
- core/src/local/portable.rs builds the same zip the server writes (notes.json,
a Markdown file per note, each attachment this device holds) and reads either
export marker or a Google Keep Takeout zip, with the server's decompression
budget and an all-or-nothing transaction. Export saves to Downloads (no new
plugin) and the sidebar says where; Import takes the archive as raw IPC bytes.
- core/testdata/portable.json pins the format for both copies: the server runs
its Keep and native readers against it (test_portable_fixture.py) and checks
its real export's keys (test_integration.py); the core runs the same cases.
- Found on the way: both importers skipped a Keep note that is only a photo as
"empty". It now imports, on the server and in the core.
- New dependency, approved: `zip` (deflate only) plus `flate2` on its pure-Rust
backend, both already in the lockfile.
The AppImage applications-menu toggle moves from Account, which the desktop
never shows, to the Sync page; the first-run prompt now says so.
errorMessage (#5236) replaces the hand-rolled `.error ?? …` / `.message ?? e`
reads at the remaining catch sites, so a desktop failure shows its real reason.
Task #5170.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
2b2ceaa82e |
attachments sync: attach offline, upload when linked, removals stick (#5168)
CI & Build / Build now, or wait for Android? (push) Successful in 4s
CI & Build / Python lint (push) Successful in 3s
Android / Build, or is the channel already serving this? (push) Successful in 4s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 2s
CI & Build / Python tests (push) Successful in 11s
CI & Build / integration (push) Successful in 35s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Windows installer (cross-compiled) (push) Skipped
Desktop (Tauri) / Update manifest (push) Skipped
CI & Build / Web typecheck and unit tests (push) Successful in 9s
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Failing after 2m23s
Desktop (Tauri) / Tauri desktop (Linux) (push) Skipped
Android / Kotlin + Rust (APK) (push) Successful in 7m17s
Desktop could not create an attachment at all, and a removed attachment or dismissed preview came back on the next pull. Now: - core: add_attachment keeps the bytes in the blob store and queues the row (schema v10: attachments.uploaded / upload_error). Push uploads it once its note has landed. A refusal that retrying won't fix (too large, id clash, hash mismatch) is recorded on the file and not re-sent every cycle; the editor shows it. - core: removing a synced attachment or dismissing a preview leaves a tombstone in pending_deletes; push sends it as an `attachment`/`preview` delete, and a pull while it waits doesn't put the row back. A pull also keeps files still waiting to upload instead of replacing them wholesale. - server: PUT /api/sync/attachments/<id> (raw body, sha256-checked, idempotent, size-capped) and child deletes in push, which apply regardless of LWW and answer noop for rows the caller can't see. One store_attachment helper for the upload route, the importer and sync. Protocol 5, feature attachment_sync; the client sends neither to a server without it. - server: migration 0031 makes a link preview's insert/delete bump its note, so background-fetched previews and web dismissals reach linked devices. - desktop: Attach and paste-image work offline (raw-bytes IPC command). - SVG is served as a download by the desktop blob scheme too (as #1981 did for the web), and drawn as a file chip on both. - autosync: drop the catch_unwind; release builds abort on panic, so it only ever worked in debug builds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
535331c5b2 |
tests: one fixture for the note grammar, run by the core, the server and the web
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
CI & Build / integration (push) Failing after 27s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 1s
CI & Build / Web typecheck and unit tests (push) Successful in 9s
CI & Build / Python tests (push) Failing after 12s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Web tests, clippy, Rust tests and rustfmt (push) Successful in 4m25s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 3m28s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 4m19s
Desktop (Tauri) / Update manifest (push) Successful in 9s
Android / Kotlin + Rust (APK) (push) Canceled after 11m21s
The checklist grammar and the #tag rule are implemented three times (derive.rs, checklist.py/tags.py, markdown.ts), and the tag colour twice (colors.ts, DerivedTint.kt). Only Rust and Kotlin had tests. core/testdata/grammar.json now holds one set of cases (task lines, rendered items, tags, standalone-tag lifts and the tint hashes), and every suite reads it. - web: vitest, a dev dependency approved for #5166, with `npm test`. grammar.test.ts runs the fixture, and titles.test.ts pins #5165's palette fix. - ci.yml runs the web tests in the job the image build needs. desktop.yml's verify job runs them too, because the installers embed this frontend and can't see ci.yml's verdict (rule 177). - core: derive.rs reads the fixture. server: tests/test_grammar_fixture.py. - Android keeps its hand-written tint values; its doc now points at the fixture. The server is expected red here, on purpose. tags.py only takes a tag after whitespace and lets it start with a digit or `_`, while the core (the definition) takes any non-tag boundary and needs a letter. So `(#todo)` is a label on the phone and plain text on the server. The fix follows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
1a2f71e381 |
tests: every note-text write path is pinned, and two of them fail today
Integration tests for the edit sequence at each door: create, PATCH, ticking an item, restoring a revision and sync push. Two fail on today's code, on purpose: - restoring a revision never queues link previews, so a restored link stays a bare URL (#5164, audit B5); - a pushed note whose standalone #tag gets lifted stores the server's clock as its edit time instead of the client's, because the lift's second flush lets the column's onupdate overwrite it. The fix follows in the next commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |