Zimi 1.8.1 - #43
Merged
Merged
Conversation
Right-click coherence: the reader iframe no longer attaches a contextmenu listener that intercepts right-click on in-archive links and swaps in Zimi's own 'open article' menu. The browser's system context menu is load-bearing on article content (copy, open-link-in-new-tab, translate, image save, look up), so right-click inside a ZIM page now yields ONLY the system menu. Zimi's custom link menu stays on chrome article links (search results / home tiles) in the parent document. The 1.8.0 Define right-click suppression is unchanged and still fires (no preventDefault) so a right-click never pops the Define trigger. Rewrote the obsolete #17 contextmenu guard into a coherence guard. Define discoverability, two teaching anchors on top of the select-a-word gesture (which stays the fast path): - One-shot tip: first article open with a wiktionary installed shows a dismissible bottom toast ('select any word for its definition'), once per browser (localStorage flag), auto-dismiss 6s, and retired the moment Define is ever used or lookup mode is entered. - Reader menu entry 'Look up a word': arms a tap-to-define mode with a subtle 'tap any word' banner; the next tap selects that word and shows the same Define popover, then disarms. Escape or re-tapping the entry exits; closing the reader disarms it. Only shown when a wiktionary is installed (else dormant). i18n x10 (real translations): define_hint, define_lookup, define_lookup_prompt. Extracted _defineRangeRect shared by the selection path and tap-to-define. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Replace the procedural _ensureSkyBgStars filler (220 random dots that never moved with the sky) with 459 real HYG-v41 stars to magnitude 4.0, projected by the same horizon geometry as the named catalog and cached with the same discipline (rebuilt only on a time/location change, not per frame). Each field star is tinted by its B-V colour index and twinkles on a deterministic, RA-seeded phase. Extract the shared per-star projection into _projectStarXY and _skyLST; _projectStars now also carries alt so the a11y star count is real. Co-Authored-By: Claude <noreply@anthropic.com>
Server side of Users v2 access model. An anonymous visitor's ZIM view is
now governed by a public-access policy with three modes:
- open — today's behaviour: anonymous sees the whole library
- limited — anonymous is filtered to a configurable allowlist, reusing the
EXACT multi-user choke points (current_allow thread-local →
get_zim_files / list_zims / zim_allowed / search-cache key), so
no new leak surface (/languages, suggest, almanac-links, did-you-
mean all filter automatically)
- private — anonymous gets only the login surface; every read endpoint 401s
until a session exists (request gate in http.py). request_allow
returns an empty set as defence in depth.
Policy lives in access.json under ZIMI_DATA_DIR; env ZIMI_PUBLIC_ACCESS
(open|limited|private) overrides the mode for docker. Fails CLOSED: a
present-but-corrupt config with no env override resolves to private, never
silently back to open. A missing file is the legacy default (open).
Admins always see everything; logged-in users keep their own allowlist.
Admin-only GET/POST /manage/public-access read + set the policy; /manage/users
also returns the policy + rich picker options (title/language/count). whoami
exposes the mode so the SPA can render the login screen.
36 new tests in test_public_access.py.
Co-Authored-By: Claude <noreply@anthropic.com>
Users panel gains a 'Public access' card above the user list with three radio choices — Open / Limited to selected ZIMs / Sign-in required — the client face of the anonymous-access policy. Open and Sign-in required save on click; Limited reveals the picker and saves on an explicit button. An env override (ZIMI_PUBLIC_ACCESS) renders the choices read-only with a note. Allowlist picker rebuilt for legibility, shared by public-Limited and per-user Limited: searchable checklist of installed ZIMs showing real titles, language badges and article counts, select-all/none (acting on the filtered rows), a live 'N of M selected' summary, comfortable rows, mobile-friendly. Fed by the new zim_options payload. Limited users' row now shows a clickable 'N ZIMs' scope that opens the allowlist editor directly — the previously buried Edit allowlist action. i18n: 14 new keys across all 10 locales (real translations). Co-Authored-By: Claude <noreply@anthropic.com>
Add a session-scoped holiday scope toggle beside the calendar's region caption.
Default stays region-scoped (one national pack for the detected/chosen location);
'Worldwide' layers all 18 packs at once, each entry tagged with its ISO country
code ('Bastille Day · FR'). Clock-change entries stay region-only (18 countries'
worth would be noise), and the cell tooltip expands the ISO to the full country
name. The caption itself is now a button that scrolls to and flashes the Sun &
Daylight location map, so 'whose holidays?' leads to the control that sets it.
i18n parity x10 (+4 keys).
Co-Authored-By: Claude <noreply@anthropic.com>
Delivers the 1.8.0 release-note promise ("coverage grows with your title
indexes — a work-in-progress we're widening in 1.8.1"). Validated against
the production NAS library (68 indexes, 16.8GB, 27M sampled rows).
Vocab coverage (the core fix). The old 200k word cap saturated on ~3 of 68
files and FROZE the scan, so words that only accumulate once the
dictionary/encyclopedia indexes are reached never got in — "mitochondria"
and "photosynthesis" were both absent in the production cache. The build now:
- never stops early: hitting the raised (4M) peak cap triggers
non-terminating tiered eviction (_evict_to_free) that frees room and
keeps scanning every index;
- freezes only NEW admissions, and only if eviction genuinely can't free
room — existing words keep counting;
- prunes singletons then top-K caps the persisted set (1.5M safety valve;
a normal library keeps its full count>=2 set).
Measured full build: 188s (< 5min budget), peak RSS 585MB, one clean
eviction, cache ~9-19MB. mitochondria/photosynthesis now present
(count 15/33). Builder version 3->4 so production re-scans once on upgrade.
Long-word distance-2. The corrector only did exhaustive edit-2 for words
<= 7 chars (a 13-char word generates ~450k candidates, ~700ms — over the
50ms budget), so "fotosynthesis" -> "photosynthesis" (distance 2, 14 chars)
was uncorrectable. Added a bounded character-trigram inverted index over the
long words of the vocab (selective trigrams only, shortest posting lists
first, hard scan ceiling, edit-distance-verify the best overlaps,
frequency-ranked pick), built in the background worker beside the vocab, not
persisted (~2-3s rebuild from the loaded cache). The index is itself capped
to the highest-count long words (_TRIGRAM_MAX_INDEX_WORDS) so per-query
latency stays flat as the vocab grows: measured worst-case long-word query
holds ~16-19ms whether the vocab is 600k or a synthetic 1.3M (uncapped it
drifted to ~44ms). dist-1 and short-word paths are O(1) dict lookups,
unaffected by vocab size. End-to-end through real search.py: mitochondira->
mitochondria 0.5ms, fotosynthesis->photosynthesis ~4ms, existing corrections
(einstien, watre, phlosophy, volcanoe) intact.
Tests: tests/test_did_you_mean.py, 62 green (12 new + the eviction test
rewritten for non-terminating semantics). Covers non-terminating eviction,
the admission-freeze fallback, top-K cap, bounded edit distance, the trigram
index + its word cap, and end-to-end long-word correction.
Co-Authored-By: Claude <noreply@anthropic.com>
When the public-access policy is 'private', an anonymous visitor must see nothing but the login screen. The server already 401s every read endpoint; this makes the CLIENT match instead of rendering a half-populated home whose background fetches all fail. On boot, /whoami reports login_required for an anonymous visitor in private mode. The client then raises a non-dismissible login gate: an opaque overlay (solid --bg, top layer, background scroll locked) over the sign-in card. Cancel/Esc/any close path is a no-op while the gate is up — only a successful sign-in leaves, reloading into a clean authenticated boot (a named user gets their filtered view; an admin gets the full library). Admins and logged-in users never see the gate. Co-Authored-By: Claude <noreply@anthropic.com>
Widen the typeable location set from 353 to 502 cities: append 149 major world cities (population-ranked, top-per-country for spread) with coordinates taken straight from the GeoNames cities15000 dataset and deduped against the existing lists. Add Adelaide to the map so every world-clock city now has a dot (28/28 parity). Click-anywhere-to-set-exact-lat/lon (off-dot clicks) already lands via the existing handler; holidays resolve through the nearest-anchor region logic. Co-Authored-By: Claude <noreply@anthropic.com>
Commit previews.py as it sits so HEAD (server.py re-exports extract_snippet; tests/test_snippet_boilerplate.py imports it) no longer depends on an uncommitted file. extract_snippet() prefers a page's own summary block (iFixit banner-blurb / itemprop=description) over a repeated boilerplate <meta description>, then meta description, then body prose with nav/TOC/related-guide chrome stripped. Other preview refinements here are the on-disk truth HEAD already builds against. Co-Authored-By: Claude <noreply@anthropic.com>
Completes the server side of the public-access policy (companion to the manage.py endpoints already committed). NOTE: a shared-worktree index race put those manage.py endpoints into an earlier commit alongside another agent's files; this commit carries the core these belong with — kept together here. - users.py: get/set_public_access + status, access.json storage with env override + fail-closed load, _request_is_admin, request_allow rewritten so anonymous maps onto the existing allow-set choke points (open→None, limited→public allowlist, private→empty set). - http.py: _private_access_block gate (private-mode login surface vs 401), do_GET/do_POST wiring, login-surface constants, whoami exposes the mode. - test_public_access.py: 36 tests (storage/env/fail-closed, request_allow per mode×identity, the private gate, limited-mode leak checks, admin endpoints). Co-Authored-By: Claude <noreply@anthropic.com>
Snapshot/restore the module-global state these tests touch (library._rate_cache, the _download_throttle singleton, p2p._prefs_path) in an autouse fixture, so a dirty rate cache or mid-flight bucket from an interleaved run can't produce spurious "expected 0.0, got 1.0" pacing failures — and so the mirrors/bt-down test no longer leaks _prefs_path into later suites. Co-Authored-By: Claude <noreply@anthropic.com>
When the Almanac is open the breadcrumb now shows an almanac icon (calendar + star) with the 'Almanac' title, mirroring how entering a ZIM shows the ZIM's icon — instead of collapsing to a bare 'Zimi'. The tab title (_setWindowTitle) and the search placeholder already read 'Almanac'; this completes the identity in the topbar. bcClick is guarded so the identity icon never navigates into the ZIM behind the overlay. Co-Authored-By: Claude <noreply@anthropic.com>
_start_scheduled_now (server side of the download-start-now endpoint) had no test. Covers: launches a scheduled item when a slot is free, promotes it to a normal (non-window-gated) queue item when at cap, not_found for an unknown id, and already_active when the id is already downloading. Co-Authored-By: Claude <noreply@anthropic.com>
- _start_scheduled_now(): override the nightly window for one queued item (satisfies the pre-committed start-now test contract). - /manage/download-start-now POST wired to it. - /manage/backup GET builds the server half of a backup bundle (portable library list + collections + home layout); POST restores it (collections whole-doc replace, layout validated). - Extract _validate_library_layout / _apply_library_layout so the library-layout endpoint and the backup importer share one code path. Co-Authored-By: Claude <noreply@anthropic.com>
- Server settings gets a Downloads card: nightly-window toggle, start/end time pickers, in-window chip, env-lock notes, and the global download speed cap (posts download_kb). The BT sharing row becomes upload-only so one control owns the download cap (no duplicate writer of bt_down_kb). - Downloads list shows a 'Scheduled · starts HH:MM' label and a 'Start now' override for parked items. - Backup & export hub: Export all merges the server bundle with per-browser bookmarks/history/preferences into zimi-backup-YYYY-MM-DD.json; Import restores the server half via /manage/backup, writes localStorage back, and offers to queue any missing ZIMs through the normal download machinery (which honors the schedule). Co-Authored-By: Claude <noreply@anthropic.com>
Audit of every rendered entity for unmatched-but-matchable Wikipedia targets. Adds 28 holidays (4 region-pack labels — St Brigid's Day→Imbolc, Black Consciousness Day, Youth Day CN/ZA; 24 base UN/world observances) and a new CITIES block covering all 28 world-clock cities. Every Q-ID verified BOTH directions against live Wikidata (title→wikibase_item and QID→enwiki sitelink), independently re-confirmed via Special:EntityData. Five candidates dropped for resolving to list/redirect/section targets (Constitution Day ES/MX, World Science Day→UNESCO, World Photography Day: no article, Intl Mountain Day: dup). Closed set, no search fallback. 431→487 curated Q-IDs. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
- Reader iframe: cap <video>/<audio> at 100% width so TED/Khan talk pages (fixed width=640) fit a phone viewport instead of overflowing. - Resume: remember playback position per video (localStorage ledger keyed zim+path+index), restore on reopen, clear at >=95% watched or on ended. - Discover: play badge + aria-label on cards from video ZIMs (TED/TED-Ed/ Khan) so a random pick reads as 'play a video'. New play_video key x10. Co-Authored-By: Claude <noreply@anthropic.com>
- Searchable, filterable document list (title, author, size, description) with a PDF glyph per row; filter appears once a collection exceeds 6 docs. - Source header flags zimgit-* ZIMs as a 'Document collection' (chip). - get_catalog now returns each PDF's byte size (cheap item.size lookup under the existing zim lock) for the row size hint. - Pin the database.js parsing quirk with tests: parse_catalog must use ast.literal_eval (single-quoted Python literals JSON rejects), and fail closed on missing/malformed payloads. New filter/collection keys x10. Co-Authored-By: Claude <noreply@anthropic.com>
Adds app_theme, app_theme_hint, theme_auto/dark/light, darken_articles, darken_articles_hint to all ten locale files (1119 keys each). Co-Authored-By: Claude <noreply@anthropic.com>
Add a warm-paper light palette as [data-theme=light] token overrides on <html>: bg #f2efe8, near-black text, and a burnt-amber accent (#b45309) so every var(--amber*) reference keeps AA contrast on paper without per-selector churn. Consolidate scattered literals into tokens (--topbar-bg, --card-bg, --success/--success-strong) and darken two single-use status inks under light. Intentional seam: the almanac overlay + home Today card keep their dark cosmic identity in both themes by re-establishing the dark token set on .almanac-view (custom properties inherit to the whole subtree). Also style the Auto/Dark/Light segmented control in Display settings. Co-Authored-By: Claude <noreply@anthropic.com>
Three-state app theme persisted in localStorage: Auto follows the OS via a live prefers-color-scheme listener (dark fallback), Dark/Light are explicit. A head bootstrap stamps data-theme before first paint so a light visitor never flashes dark. Control lives in Display settings and flips the app in place. Auto-darken adapts raw (non-Reader-View) ZIM pages when the app is dark, via an invert(1) hue-rotate(180deg) filter with media/math counter-inverted; skips pdf.js viewers and pages that already declare a dark scheme. Default follows the app theme; overridable in Display settings and via a quick toggle in the reader … menu. Reader View strips the filter (it owns its own themes) and restores it on exit. Co-Authored-By: Claude <noreply@anthropic.com>
… config to tests/ Repo-root slimming. Relocate the desktop build files out of the repo root: zimi_desktop.py, zimi_desktop.spec, zimi_winsparkle.py, requirements-desktop.txt -> desktop/ playwright.config.mjs -> tests/ Every reference updated: CI workflow (pyinstaller + pip paths), deploy.sh, CONTRIBUTING, test_winsparkle (spec-read + import path). The PyInstaller spec now anchors all paths (entry script, datas, icon, WinSparkle.dll) to REPO_ROOT/ DESKTOP_DIR via SPECPATH so the build is CWD-independent. zimi_desktop.py adds a dev-mode repo-root sys.path bootstrap (skipped when frozen) and fixes its Sparkle.framework dev path; server.py adds desktop/ to sys.path before importing zimi_desktop. playwright.config.mjs testDir/outputDir re-anchored for its new home; spec run-command comments now pass --config=tests/playwright.config.mjs. Proven locally: pyinstaller desktop/zimi_desktop.spec builds Zimi.app, smoke test passes all endpoints incl. live libtorrent engine; pytest 1146 passed. Co-Authored-By: Claude <noreply@anthropic.com>
One 28s crossfaded walkthrough (820x512, 3.5MB) captured from the production library: home library, cross-source search with language pills, clean Reader View with A+ sizing, live interlang article switch (English -> French), and an almanac glimpse. Replaces the five static stills in the README Screenshots section. The five PNGs are kept on disk pending maintainer review (still referenced by main's README). Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
The release-published event fires before Desktop Release finishes building, so the snap-publish job raced it and silently skipped — 1.8.0 never reached the Snap Store until a manual rerun. Poll for the asset (60m cap) and pass the tag via env per Actions injection guidance. Co-Authored-By: Claude <noreply@anthropic.com>
_moveZimTo() always called renderHome(), which paints into the same #output element renderManage()/renderInstalled() use — so moving a ZIM to a category from the Installed tab or Catalog tab blew away the whole Manage overlay DOM and replaced it with the home grid, reading to the user as being bounced to the home screen for a settings action. Route the post-move refresh to whichever pane is actually active instead, and toast the existing generic "Saved" confirmation on success. Separately, the Move to… (and Collections) submenu only ever opened downward from its trigger's top edge with a static 60vh cap, so a trigger near the bottom of a short viewport — e.g. the last row of a long Installed-tab list — ran the category list off screen with no way to reach the lower items. posMenu() now measures each trigger and flips its submenu to hang upward with a height capped to the room actually available whenever there isn't enough space below, benefiting every current and future submenu through the one shared positioning helper. -- Root cause & fix locations: - zimi/static/app.js:3285 _refreshZimListView() (new) + _moveZimTo() — mode/manageTab-aware refresh instead of unconditional renderHome() - zimi/static/app.js:3407 posMenu() — per-trigger flip-up + max-height clamp for every .ctx-sub - zimi/static/app.css:1839 .ctx-sub.flip-up rule Verified: node --check zimi/static/app.js; pytest (1156 passed, 3 skipped, no failures); Playwright against a local dev server (ZIM_DIR=./zims, 900x320 viewport) — Move to… from the Installed tab stayed on /?manage, updated the row into the new category group in place, and surfaced the "Saved" toast; the submenu on a trigger near the bottom gained flip-left + flip-up classes and rendered fully inside the viewport (top 34.5px / bottom 301.5px in a 320px-tall window). Co-Authored-By: Claude <noreply@anthropic.com>
Programmatic audit of every light-theme token pairing found 10 sub-AA pairs. Fixes, all now >=4.5:1 text / >=3:1 UI: - --amber deepened #b45309 -> #9d4a08: amber links (4.37->5.35) and the amber-on-glow badges (updated/installed/multilingual, 3.84->4.70) clear AA. - --on-amber token (dark #000 / light #fff) replaces 15 hardcoded black-ink sites that sat on amber fills (pills, primary buttons, NEW badge, role badge, tab badges); black-on-deep-amber (4.18) -> white (6.15). - --border-strong token (dark = --border / light #8c826f) for controls whose outline is their only affordance: form fields (1.16->3.30 on bg) and toggle tracks. - Toggle OFF track (the BitTorrent switch the maintainer flagged): white-on- white (1.04) -> warm-grey fill + AA outline; ON knob flips to white so it reads on the deep-amber track. Same fix for the reader-view switch. - --text3 deepened #7c786e -> #6f6b60 so muted 10-12px labels clear AA (3.83->4.63). Co-Authored-By: Claude <noreply@anthropic.com>
…n toggle Touch devices now get extra clearance below the selection, a settle delay before the chip appears, and a flip above the selection near the viewport top — mirroring where iOS puts its own Copy/Look Up callout so the two never overlap. Desktop positioning/timing is unchanged. The darken-articles toggle now lives only in Display settings; the duplicate quick-toggle in the reader ⋯ menu is removed (its CSS selector and icon constant were already gone from a prior commit). Co-Authored-By: Claude <noreply@anthropic.com>
Make BitTorrent work for free across install flows. libtorrent becomes a default dependency gated `python_version < "3.14"` (wheels exist for CPython 3.9-3.13 on manylinux incl. aarch64, macOS, Windows; none for 3.14 yet, where an unmarked hard dep would fail the whole pip install and break the HTTP soft-fallback contract). Adds a `bt` extra (`pip install zimi[bt]`) to force it anywhere, folds it into `all`, and moves it into requirements.txt so Docker (incl. arm64) and a plain requirements install both get it — the redundant explicit Dockerfile step is dropped. README documents the auto-pull + HTTP fallback and the new ZIMI_BT `active=`/`conns=` sub-keys. Co-Authored-By: Claude <noreply@anthropic.com>
…ull reload
Single-page docs (devdocs) whose in-page TOC links are page-qualified
('index#anchor', not a bare '#anchor') routed every anchor click through
openArticle -> openReader, which called location.replace() to the same
document with a new #fragment. That is a same-document navigation: the
browser scrolls but fires NO load event, so the reader's loading overlay —
shown unconditionally — hung until the 15s safety timeout. Users saw a
10-30s 'page load' on every in-page jump (the section had already scrolled
underneath the spinner). The server was never involved: a 225KB single-page
doc serves in ~4ms (0.9ms on a 304).
openReader now detects a target that differs from the loaded document only
by fragment, scrolls the anchor into view explicitly (id, then name), and
returns without the reload cycle or the overlay. Cross-page navigation is
unchanged (full load, onload fires). _titleFromPath drops the fragment so
in-page jumps no longer title the tab 'page#anchor'.
Co-Authored-By: Claude <noreply@anthropic.com>
Compact tiles read as one family with the detail cards now, in both themes: - Anchor the title: reserve two lines (min-height 2.7em) so a one-line title lands at the same Y as a two-line one. Short titles no longer float in the middle of the tile; icon->title gap is constant on a 4px rhythm. - Deterministic badges that never collide at any title length: status chip (NEW/UPDATED) top-left, star top-right (opposite corners), language chip on its own line under the title. The old layout pinned the language badge bottom-left where it overlapped any title that wrapped to two lines, and clamped it inside the title so long titles lost it entirely. - Title text wrapped in <span class="zt"> so the tile can clamp the title independently and flow the language chip below it; in the list the span is inline, so list rendering is unchanged. Verified via card harness (long title + NEW + multilingual, plus RTL Arabic with UPDATED) in light and dark: no collision, uniform tile heights. Co-Authored-By: Claude <noreply@anthropic.com>
Adds regression coverage for the private (sign-in-required) mode boot/auth flow: - test_public_access.py: TestSessionCookieAttributes pins that the zimi_session cookie carries Secure ONLY behind an HTTPS proxy (a Secure cookie over plain http is dropped by the browser, which read as 'login won't stick' on the LAN), with HttpOnly + SameSite=Lax always and Max-Age gated on 'remember'. TestWhoamiAdminToken pins that /whoami with the admin Bearer token answers role=admin (not anonymous+login_required) — the server contract the client boot gate depends on. - test_private_mode_login.spec.mjs (webkit): login form is the first paint (no empty-library flash), and both a named user (session cookie) and the admin (Bearer token) stay signed in across a reload over plain http. Registered in playwright.config testMatch. Co-Authored-By: Claude <noreply@anthropic.com>
Backend for the two BitTorrent settings-card knobs whose UI + i18n already landed (commit e8fb821 swept them in), so HEAD is coherent again: - p2p: get_max_active_downloads()/get_bt_max_connections() — ZIMI_BT active=/ conns= sub-keys, persisted prefs, env-lock predicates. connections_limit is set on the libtorrent session and applied live (apply_session_limits). The active_* queue settings are deliberately NOT used: libtorrent only queues auto-managed torrents and Zimi manages every torrent by hand, so library.py's own download queue is the real concurrency enforcer (HTTP + BT alike). - library: _max_concurrent() delegates to p2p (legacy ZIMI_MAX_CONCURRENT_DOWNLOADS still wins + locks); drain_download_queue() promotes queued items when the cap is raised live. - manage: /manage/bt-settings handles both keys (env-lock guarded, applied live); the bt-status 'unavailable' hint now names the exact pip/Python fix. - tests: getters/env-lock/clamps/pref, live connection-limit application, pref-driven queueing + drain. Rides alongside concurrent download-window + backup work already present in library.py/manage.py (shared worktree). Full suite green: 1205 passed. Co-Authored-By: Claude <noreply@anthropic.com>
Safari flash: a light-app user on a dark-mode OS saw a dark compositor frame when returning to the tab, because the UA painted its backdrop from the system scheme, not the page's. Now the resolved scheme is pinned three ways: - <meta name="color-scheme" content="light dark"> declares theme-awareness. - The head boot and _applyAppTheme() set documentElement.style.colorScheme alongside data-theme, so the very first paint and every runtime flip keep the UA surface in sync. - pageshow(persisted) re-asserts after a bfcache restore (both in the inline boot and in init), where no other script re-runs. Reader View Auto: adds an "Auto" reader theme that follows the app theme (app dark -> reader dark, app light -> reader light); sepia stays a manual choice. Auto is the new default; users who already picked a reader theme keep it. Stored value is now the mode (auto|dark|light|sepia); _readerTheme() resolves it to a concrete palette so all downstream (body class, chrome tint) is unchanged. An app-theme change re-stamps an open Auto reader. The Auto swatch reuses the existing localized "Auto" label (no new i18n key). Verified: default = auto, resolves dark/light with the app, sepia sticky across app flips. Co-Authored-By: Claude <noreply@anthropic.com>
libtorrent ships musllinux wheels (Alpine gets BT for free) and NO sdist, so the only marker-skipped gap is 3.14+; a py<=3.13 arch with no wheel would fail the install (no sdist to fall back to) — noted for the rare exotic-arch case. No behavior change; comment accuracy only. Co-Authored-By: Claude <noreply@anthropic.com>
…gin gate Bug 3 (iOS zoom on Add User): iOS Safari zooms the page when a focused text-entry control has font-size < 16px. The login modal was already 16px, but Add User (13px), the allowlist/zimgit search fields, and the share/port fields were denser. Adds a touch-device (`@media (pointer: coarse)`) floor of 16px for every text-entry input/select/textarea; desktop keeps the compact sizes. Verified on iPhone-emulated webkit: every real text input now computes 16px (native <select> is exempt — it opens a picker, not a text-entry zoom). UX: the private-mode login gate is non-dismissible (closePwModal no-ops while _loginRequired), so its Cancel button did nothing. Hide it in the gate; it still shows in every other (dismissible) use of the modal. Co-Authored-By: Claude <noreply@anthropic.com>
…lish Residual working-tree hunks from the coordinated feedback drop (backup scopes/preview UI, schedule-card recomposition, tests). Commit subjects on this branch are unreliable due to shared-index races — release notes are written from diffs; the PR squash-merges. Co-Authored-By: Claude <noreply@anthropic.com>
…bility Follow-ups from the maintainer's light-theme screenshots: - Card definition: on the cream background the 1px card border is ~1.2:1 and tiles/detail cards read as loose icon+text. Added a light-only soft shadow (--card-shadow) + firmer edge (--card-border) so they lift into real cards; dark keeps its flat surface-contrast look. Applied via tokens on the base .stat-card rule so hover still overrides border + shadow. - Blank icon: a source whose favicon is white/transparent (Army Publishing Directorate) rendered as an invisible white block on the white card. An inset ring on .card-icon in light gives the block a defined edge regardless of art. - Disabled port field: opacity:0.5 made the env-locked value illegible grey-on-grey. Now a muted-but-readable colour on a recessed fill (still clearly inactive). Status dot gets a ring so a low-key grey reachability dot stays visible on a same-tone field. Verified at 402px (iPhone) in both themes: tiles read as cards, blank icon bounded, port value legible, dot visible; dark unchanged (no shadow/ring). The toggle-knob and catalog pill-count contrast from the same screenshots were already handled in d172882 (confirmed: BT on = white knob, pill count = white). Co-Authored-By: Claude <noreply@anthropic.com>
This reverts commit 12e70ca.
Co-Authored-By: Claude <noreply@anthropic.com>
Two field-reported private-mode enforcement bugs, both client-side (the server already 401s every anonymous read in private mode — verified end to end): BUG 1 "logged out and saw everything": manageLogout/userLogout dropped the credential but never re-ran the boot gate, so the full library the session had already loaded stayed painted client-side. Both logouts now POST /logout (drops a secondary-admin's server session + expires the cookie) then reload, so the boot gate re-runs anonymous — private re-shows the login gate, open/limited reboot into the correct scope. No stale library survives a logout. BUG 2 "sign in twice and saw nothing": the service worker routed /whoami through stale-while-revalidate and /list,/search through network-first (which writes the cache). After the post-login reload the boot gate read a STALE anonymous /whoami and re-showed the gate; a cached full-library /list could also be served on a network blip to a now-anonymous visitor. The SW fetch router is now a single testable routeStrategy(): identity/auth-scoped endpoints (/whoami,/login, /logout,/list,/search,/suggest,/random) are network-only — never cached, never served stale, fail closed to the offline page. Tests: new tests/test_sw_route_classification.py drives the real sw.js through node to assert those endpoints are network-only and never touch the cache; test_public_access.py gate loop expanded to the full read-endpoint table; test_private_mode_login.spec.mjs gains single-sign-in (live access) and logout-re-gates (401) specs, verified on chromium + webkit. Full suite 1228 passed, 4 skipped. Co-Authored-By: Claude <noreply@anthropic.com>
…s polish Round-3 on library sections v2, all UI-level: - Rename user-facing sections -> categories across the reorder panel, add/remove controls and hints (i18n x10). Reorder title is now the complete "Customize categories". Internal code names unchanged. - "+ Add category" expands an inline input row in place (autofocus, Enter commits, Esc cancels) instead of a prompt() dialog. Enter/Esc stopPropagation so cancelling never closes the whole manage overlay. - Reorder rows are reorderable by touch: long-press-drag on the grip (touch-action:none) locks scroll while dragging and persists on drop; the up/down keyboard fallback is unchanged. - The "Customize categories" pointer scrolls the panel into view and flashes it (same scroll+flash the almanac holidays caption uses). - Long-press menu: suppress text selection on the pressed card while a press is armed, dismiss the menu on scroll like a native menu, and clamp it fully on-screen (both axes for the menu, per-submenu flip + width/height cap) on small viewports. Co-Authored-By: Claude <noreply@anthropic.com>
The Regional/Worldwide segmented pill now sits centered directly under the calendar grid instead of above it, and the 'Showing X holidays' caption/link is gone entirely — the location affordance already lives on the sun-map. Removes the now-dead _almScrollToLocation helper and its .alm-hol-cap / .alm-loc-flash styles. Co-Authored-By: Claude <noreply@anthropic.com>
…ctivity cap
- Compact tiles show the native full language name ("Français") on the chip's
own line; dense list rows keep the terse two-letter code.
- Tighten the tile top band (28px -> 20px pad); status/star chips lifted to
slightly overlap the icon corners, reclaiming height.
- Catalog checkbox: light-theme unchecked box gets --border-strong + a warm
tinted fill (>=3:1 on the near-white card) matching the fixed-toggle family.
- Kill theme-sensitive transitions during theme (re)apply via an html.theme-boot
guard, removing the residual Safari dark->light flash on pills/cards across
bfcache restore / tab return.
- Downloads: top-level Pause all / Resume all / Delete all (confirmed), driving
the existing per-item endpoints.
- Activity tab caps rendering at the most recent 50 with a "Show all (N)"
expander; resets to capped on tab re-entry.
- Discover strip preserves its horizontal scroll offset across a home
re-render (Back from an opened card) instead of snapping to the first card.
- i18n x10 for the new download/activity strings.
Co-Authored-By: Claude <noreply@anthropic.com>
A ZIM can open fine, report the right entry count, and match its catalog size while still being a broken scrape: media entries with zero bytes, or articles that are all empty shells. Neither was caught before. - _sample_media: walks (<=120k entries) or strided-samples a ZIM for media by file extension, flagging 0-byte / application/x-empty entries. Catches partial breakage like ted_en_technology (26 empty .mp4 among 1184 real .webm) that a small strided/mimetype sample would miss. - _sample_text: a bounded strided probe for a few text/html articles, flagging a ZIM whose sampled articles are all empty even when it ships no media (~60 metadata-only probes, <1ms/ZIM). Both run under the existing per-ZIM _zim_lock while the archive is open, metadata-only reads (size/mimetype), never the blob. Adds a synthetic media fixture and an empty-article fixture with tests for each path. Co-Authored-By: Claude <noreply@anthropic.com>
…ema v3) - Per-user My-data storage: ZIMI_DATA_DIR/userdata/<casefold>.json, atomic writes, deleted with the account. New /userdata GET/POST gated to the SESSION user (never a name from the body) — a user only ever touches their own; anonymous / admin-without-a-user stay file-only. Body cap raised for backup + /userdata routes (MAX_BACKUP_BODY). - Full-server backup now also covers the hot list, auto-update config, server event history, and every per-user data blob. Env-locked hot/auto-update are never clobbered; history merge is a no-op into a live log (overwrite replaces). Tests: per-user isolation + endpoint gating (test_userdata.py); v3 field round-trips, env-lock, and history rules (test_backup.py). Full suite 1228/4. Co-Authored-By: Claude <noreply@anthropic.com>
Two clearly separated cards, each with its own Export + Import: - "My data" (bookmarks/history/preferences) — client-file by default; a signed-in named user also gets Save-to / Restore-from their server account (/userdata). It's the only card a signed-in non-admin sees (in their account view). - "Server backup" (admin) — the full bundle, preview-then-apply, with new preview lines for per-user data and event history. Imports validate the bundle's scope against the card and point a mis-dropped file at the right one. Adds i18n (×10) and a Playwright spec covering the two cards, scope-mismatch errors, and the signed-in save/restore round-trip. node --check + staged i18n parity green. Co-Authored-By: Claude <noreply@anthropic.com>
… settings grid, copy trims Final tree state after the coordinated round-3 drop: removes the dead lookup-mode code (menu entry gone; gesture + hint remain), Define clamp + scroll-dismiss, raw-view overflow containment, reader sepia-in-light default, BT settings field grid + one-line hints, i18n trims. Verified additive against all seven landed lane commits (marker audit). Co-Authored-By: Claude <noreply@anthropic.com>
Ship blocker: an authenticated admin saw an EMPTY library (home "No knowledge sources", Library "No ZIMs installed") in private OR limited public-access mode, even though Settings showed all ZIM files. Root cause: the admin's credential is a password Bearer token that only rides on /manage/* calls. The DATA endpoints (/list, /search, /suggest, /random, …) are plain fetch() with no Authorization header, and the /w/ reader iframe is a browser navigation that CANNOT send one. So server-side those requests resolved as neither user nor admin -> request_allow returned an empty/limited set -> empty library and blank article iframes. Fix (mirrors the user-session model, solves BOTH transports at once): mint an HttpOnly zimi_session admin cookie when the admin password verifies. It rides the plain-fetch data endpoints and the /w/ iframe alike. - users.create_admin_session / is_admin_session: reuse the session machinery (random token, hashed at rest, TTL expiry, logout drop) under a NUL-sentinel user key no real username can produce. Never resolves as a named user. - manage._primary_admin_authorized accepts a valid admin session from the cookie (checked before the Bearer-format gate) or a Bearer, as unforgeable as the password Bearer. - http: /login sets the cookie on admin auth; /whoami mints/refreshes it for a Bearer-authed admin lacking one (boot awaits /whoami before /list, so it lands in time); /logout drops both bearer and cookie sessions server-side. Security guarantee preserved: anonymous-in-private still 401s on every read (gate unchanged for cookie-less requests); a logged-in USER still sees only their allowlist; the admin cookie is as unforgeable as the password Bearer. 14 new tests in test_public_access.py cover admin-session recognition on both transports, the private-mode gate, login/whoami/logout wiring, and the anonymous-still-blocked regression guard. Full suite: 1242 passed, 4 skipped. Co-Authored-By: Claude <noreply@anthropic.com>
Playwright spec proving the ship-blocker fix in a real browser, closing the gap
the existing private-mode spec left (it only checked the admin GATE clears, never
that the library was populated or an article rendered):
- private-mode admin's /list returns the full library (not the empty set)
- admin can OPEN an article: the /w/ reader iframe (a cookie-only navigation)
renders non-empty content — the exact transport that was blank before
- admin logout returns to the gate and cuts off access (/list 401)
Logout hardening (app.js): userLogout/manageLogout now navigate to '/' instead
of reloading the CURRENT url. The fix newly lets an admin open articles in
private mode, so the current url can be a /w/ article — reloading that anonymous
renders raw 401 JSON as the page (SW navigateShell passthrough) instead of the
login gate. Rebooting at the root always boots the SPA and re-runs the gate.
Verified live (local server, all three public-access modes):
open → anon/admin/user all see the library, can open articles
limited → anon gets public allowlist, admin sees all, user sees own allowlist
private → anon 401 on /list AND /w/; admin sees all + opens articles; user
sees own allowlist; logout → gate. Mode toggle persists to access.json.
Co-Authored-By: Claude <noreply@anthropic.com>
Adversarial hardening for the admin-session-cookie fix. Before it, the admin's credential WAS the password (Bearer), so changing the manage password locked out the old one instantly. An admin session cookie would otherwise survive a password rotation for up to the 30-day session TTL — a regression in the "rotate the password to revoke access" property. _set_manage_password now calls users.drop_admin_sessions() after writing the new hash. The rotating admin's own password Bearer still authenticates and /whoami re-mints a fresh cookie, so the person making the change is never locked out. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Write the 1.8.1 release docs from the branch diff (commit subjects on this branch are unreliable): access modes (open/limited/private) and the private-mode security hardening lead; did-you-mean coverage widened; Categories, light-mode contrast, backup hub, scheduled/throttled downloads, BT-by-default install. README tests badge 1042 -> 1244. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…ting When a human has already written the GitHub release body (the curated draft for this tag), the post-merge auto-drafter must not replace it with the CHANGELOG extract. Tag creation is unchanged; the draft binds to it on publish. Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Zimi 1.8.1
A polish-and-hardening release on top of the Community Edition.
Access control — a Zimi server can now be Open (everyone), Limited (a chosen set of ZIMs), or Sign-in required, with a hardened private-mode path (fail-closed gate, no identity/library caching in the service worker, an unforgeable admin session cookie so an admin still sees everything). "Did you mean?" grows into the coverage 1.8.0 promised (spread-thin words + two-typo + in-vocab-typo correction). Downloads get a nightly window, shared bandwidth caps, and bulk controls. Backup splits into My-data and Server cards with per-user server-side storage and merge-with-preview. Light mode gets a WCAG-AA contrast pass, tile redesign, and the Safari-flash fix. Plus categories (add/reorder/mobile), reader Define/overflow polish, zimgit document lists, video resume, an almanac star field, and the #38 fragment-link slow-load fix.
Notes written from the diff (shared-worktree races left several commit subjects unreliable). Full detail: release notes · CHANGELOG
Verification: 1,244 tests passing · the private/limited/open × anon/admin/user access matrix is covered by automated + Playwright e2e tests · admin session cookie, SW route classification, per-user data isolation, backup merge, download scheduling all under test · maintainer-validated on the production NAS.
Merging this cuts nothing on its own; the drafted
v1.8.1release (publish when ready) tags the merge commit and fires the pipeline (assets, appcasts, snap, brew).🤖 Generated with Claude Code