Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
105 commits
Select commit Hold shift + click to select a range
840f3c7
Offline PDF library list matches the app's, sizes and all
epheterson Oct 20, 2026
3361d20
README: drop the hand-typed test count and the Scorecard badge
epheterson Oct 20, 2026
6a5a6eb
README: ask for a star under the intro
epheterson Oct 20, 2026
abfd8b5
README: tighter star line
epheterson Oct 20, 2026
4d9abc6
Voices measure how fast each engine is on this machine
epheterson Oct 20, 2026
866f304
Test all voices, as a reading uses them; Automatic reads with one tha…
epheterson Oct 20, 2026
c4f56d0
Voices: call the test what it measures, voice speed
epheterson Oct 20, 2026
a8d3f6c
Voice speed test: quickest first, every cell filled, Device timed too
epheterson Oct 20, 2026
3daedf9
Voice speed test reads a real paragraph; each voice's word comes from it
epheterson Oct 20, 2026
6006b4e
Red and Dark: text never takes its background's colour (#119)
epheterson Oct 20, 2026
8a1435e
Dictionary in Red: Say is stroked like Save and Lists
epheterson Oct 20, 2026
b5e908b
Voice speed test: spinners and dimmed old numbers, last tested date; …
epheterson Oct 20, 2026
02c6fee
Read aloud: any speed with any voice; the voice's name says it is too…
epheterson Oct 20, 2026
11d91fc
Almanac tables tests: a print test no longer sees the last test's PDF
epheterson Oct 20, 2026
86a3cc9
Voice speed table: Warm start and Cold start
epheterson Oct 20, 2026
8428a90
Voice speed table: Cold start first, then Warm start, then Top speed
epheterson Oct 20, 2026
41b6d6d
Read aloud: any speed from 0.5x to 3x, a step of 0.05 with - and +
epheterson Oct 20, 2026
f8f81b0
Tests: hash passwords with 1,000 rounds in-process
epheterson Oct 13, 2026
d07a6dd
Read aloud: the speed is a slider, settling at 1x, 1.5x and 2x
epheterson Oct 21, 2026
29d8e6a
Raise dependency floors past known vulnerabilities
epheterson Oct 21, 2026
01414af
Attest build provenance for the PyPI wheels and sdists
epheterson Oct 21, 2026
72cc828
Release notes: a Highlights block becomes the release body
epheterson Oct 21, 2026
a049795
Read aloud reads along: the sentence is marked, the page follows, a s…
epheterson Oct 21, 2026
d1352f9
Tests: read-along text mapping, sentence placement, following, select…
epheterson Oct 21, 2026
8e50bc0
Read aloud cuts sentences as people read them
epheterson Oct 21, 2026
60ceef3
Numbers, dates, sizes, sorting and the week follow Zimi's language
epheterson Oct 21, 2026
657f165
Tests: the locale helpers (numbers, dates, percent, sorting, week start)
epheterson Oct 21, 2026
af1668b
One loader for the modules fetched on first use
epheterson Oct 21, 2026
65c4a56
Almanac: the calendar's arrows point the way the line reads in RTL
epheterson Oct 21, 2026
7a3e6c6
Read aloud moves to readaloud.js (1 of 3): the voices and chunks
epheterson Oct 21, 2026
00e8c33
Read aloud moves to readaloud.js (2 of 3): read-along and speaking
epheterson Oct 21, 2026
dd33239
Strings: Read aloud says when it was never cached for offline use
epheterson Oct 21, 2026
6d3b527
Read aloud moves to readaloud.js (3 of 3): fetched on first use
epheterson Oct 21, 2026
52ca5bf
Offline: the worker keeps readaloud.js with the shell
epheterson Oct 21, 2026
38d5c93
RTL: spacing and positions in logical properties, not overrides
epheterson Oct 21, 2026
a57feba
Voices: espeak's fictional and test voices are left out
epheterson Oct 21, 2026
c14ec88
Read aloud from the ... menu closes the menu; three languages named
epheterson Oct 21, 2026
0da4deb
PDF: two pages side by side wherever two fit tall
epheterson Oct 21, 2026
8e6e922
Read aloud waits for a voice as long as it was measured starting
epheterson Oct 21, 2026
b650fa0
Read along follows on an iPhone; one Highlight, updated in place
epheterson Oct 21, 2026
5a98b4e
Read aloud: no citation marks or infobox, a chunk per block, old mark…
epheterson Oct 21, 2026
78b3b63
Book: the chapter in the footer opens the contents
epheterson Oct 21, 2026
e495d09
Dictionary test: the warm stub takes the engine warm() is now given
epheterson Oct 21, 2026
dc96d99
Strings: previous and next sentence, for Read aloud's bar
epheterson Oct 21, 2026
da870d2
Read aloud: a sentence back and on, from the reading bar
epheterson Oct 21, 2026
349b470
Read aloud: a moment's wait is not slow; a toast clears the bar
epheterson Oct 21, 2026
7c9d287
Sign-in spec: a poll that lands mid-reload waits, not fails
epheterson Oct 21, 2026
5b84b31
Sign-in sheet no longer takes focus from a field being typed in
epheterson Oct 21, 2026
1268738
Sign-in sheet keeps a name typed before it opened
epheterson Oct 22, 2026
e6d38d3
Sign-in sheet: the password starts empty each time it opens
epheterson Oct 22, 2026
1202e3b
Read aloud: first tap plays on iPhone; selection Read; stale voice ev…
epheterson Oct 22, 2026
e4f12ac
Read aloud and Voices: second-review fixes
epheterson Oct 22, 2026
1de6ebe
1.14.1: version and changelog
epheterson Oct 22, 2026
83b9a44
Sign-in redraws the home page from the new list
epheterson Oct 22, 2026
adc113d
Changelog: sign-in redraws the library
epheterson Oct 22, 2026
81fe19f
Settings follow the account: per-key merge, and the admin has an account
epheterson Oct 22, 2026
cab0132
Read aloud: Automatic never prefers a measured-slow voice; the bar na…
epheterson Oct 22, 2026
a5d363e
Strings: drop next sentence, the reading bar has one way back
epheterson Oct 22, 2026
c9c705a
Read aloud: a tap on a phone is read from the touch; one way back a s…
epheterson Oct 22, 2026
945750f
Read aloud: the sentence is marked when reading starts or moves, not …
epheterson Oct 22, 2026
f98ece0
Settings follow the account in every browser, the admin's too
epheterson Oct 22, 2026
8ba28f1
My data: say that settings follow any signed-in account
epheterson Oct 22, 2026
998659b
Dictionary: the word of the day is said ahead, so Say plays at once
epheterson Oct 22, 2026
360d764
Merge branch 'settings-sync' into v1.14.1
epheterson Oct 22, 2026
3fc43a9
Open in apps test: find the key anywhere in _PREF_KEYS, which grew
epheterson Oct 22, 2026
4f4ae3d
Changelog: settings sync, word of the day, phone taps, Automatic's voice
epheterson Oct 22, 2026
2de29e5
Read aloud: back restarts the sentence, a second tap goes to the one …
epheterson Oct 22, 2026
0ad6fda
Word of the day: only Discover's word is said ahead
epheterson Oct 22, 2026
9a50c89
Changelog: 1.14.1, short
epheterson Oct 22, 2026
e77fba2
Read aloud: a tap the page took for itself is not a tap; review tidying
epheterson Oct 22, 2026
49f6deb
Discover: the word's speaker is a 48px target; the circle stays 32px
epheterson Oct 22, 2026
483bd6b
Settings sync: the server orders changes; settings kept apart
epheterson Oct 22, 2026
886dc19
Strings: what this browser saved signed out, offered to an account
epheterson Oct 22, 2026
fe30567
Settings sync in the browser: pending changes, an owner, no clock
epheterson Oct 22, 2026
47f0114
Merge branch 'settings-sync' into v1.14.1
epheterson Oct 22, 2026
26d5560
Almanac overlays: the tip stays on the screen; a press outside, Escap…
epheterson Oct 22, 2026
8ac04c4
Settings sync: a call names its account; password off is everyone's
epheterson Oct 22, 2026
706aa2b
Settings sync: no retry loop; a stale tab asks whose account it is
epheterson Oct 22, 2026
8d61de4
Merge branch 'almanac-overlays' into v1.14.1
epheterson Oct 22, 2026
23afcd4
Merge branch 'settings-sync' into v1.14.1
epheterson Oct 22, 2026
9c9abfd
Almanac overlays spec: sets the password itself, so a fresh server ru…
epheterson Oct 22, 2026
25ce9f7
Zimipedia picture test: measure the large copy once it has loaded
epheterson Oct 22, 2026
2dcc54f
Voices: Basic's cold start is its warm start, not a dash
epheterson Oct 22, 2026
0f8be0e
Read aloud: the next sentence plays from memory; the ring only for a …
epheterson Oct 22, 2026
3762ab2
Read aloud: Zimipedia's Quick facts label is not read
epheterson Oct 22, 2026
6237b0f
Read aloud: the first chunk's cap never cuts a sentence in two
epheterson Oct 22, 2026
6963e41
Release: a failed attestation stops the upload
epheterson Oct 22, 2026
3a3fa03
Contributing: Highlights are one-line bullets
epheterson Oct 22, 2026
7ec9cb8
Read aloud: the speed slider and its marks stay left to right in ever…
epheterson Oct 22, 2026
f4c9d49
French: the signed-out saved-items line says signed out, not offline
epheterson Oct 22, 2026
c75170f
Earth: Escape from the settings panel returns focus to the gear
epheterson Oct 22, 2026
7a37ece
Read aloud: a superseded reading gives back the clips it fetched ahead
epheterson Oct 22, 2026
c3e4285
Offline: a replaced carried ZIM drops its cached catalog listing
epheterson Oct 22, 2026
db6acda
Userdata: password-off ownership of the admin's data needs management on
epheterson Oct 22, 2026
ea08df6
Voices: the fingerprint is cached and timings are flushed by a timer,…
epheterson Oct 22, 2026
f7cd285
PDF: turning the phone brings the bars back, the page at its top
epheterson Oct 22, 2026
7b82bc5
Merge branch 'cr-fixes' into v1.14.1
epheterson Oct 22, 2026
d19da58
PDF: the page slider in Zimi's look, not the phone's
epheterson Oct 22, 2026
9d0bdea
PDF: Dark pages is offered only in a dark Zimi
epheterson Oct 22, 2026
7bc4c8f
Review: a flush disarms the timer; the x returns focus to its opener
epheterson Oct 23, 2026
04f141d
Tests: read app.js and readaloud.js with a Windows checkout's line ends
epheterson Oct 23, 2026
9847ba1
Users: a data file's path is checked to stay in its folder; names quo…
epheterson Oct 23, 2026
4f2671d
Offline: a kept library's catalog listing is refreshed, never dropped
epheterson Oct 23, 2026
37a4524
CI: Pillow in the test job, as requirements.txt has it
epheterson Oct 23, 2026
4269937
Users: the folder check in the form CodeQL recognises (normpath, star…
epheterson Oct 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 8 additions & 13 deletions .github/workflows/auto-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Auto Release
# ── Single-draft release flow (there must be exactly ONE draft per tag) ──────
# When a push to main changes the version in pyproject.toml:
# 1. this workflow tags vX.Y.Z and creates/updates the draft release, with
# notes pulled from CHANGELOG.md;
# notes pulled from CHANGELOG.md (scripts/release_notes.py);
# 2. the tag push triggers desktop-release.yml, which builds every platform,
# signs the update enclosures, refreshes the appcasts, and attaches the
# built assets to the SAME draft (softprops matches the release by tag).
Expand Down Expand Up @@ -75,26 +75,21 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Extract the section for this version from CHANGELOG.md
NOTES=$(awk "/^## \[${VERSION}\]/{found=1; next} /^## \[/{if(found) exit} found{print}" CHANGELOG.md)
if [ -z "$NOTES" ]; then
NOTES="Release v${VERSION}"
fi
# The Highlights block (or, without one, the whole section) for this
# version; the rules live in scripts/release_notes.py.
python3 scripts/release_notes.py "${VERSION}" > /tmp/release-notes.md
# Append upgrade instructions
cat > /tmp/release-notes.md << ENDNOTES
${NOTES}
cat >> /tmp/release-notes.md << 'ENDNOTES'

### Upgrade

\`\`\`bash
```bash
docker pull epheterson/zimi:latest
docker compose down && docker compose up -d
\`\`\`
```

Or \`pip install --upgrade zimi\` for Python installs.
Or `pip install --upgrade zimi` for Python installs.
ENDNOTES
# Trim leading whitespace from heredoc
sed -i 's/^ //' /tmp/release-notes.md
# One draft per tag: update an existing release (curated draft or a
# re-run) instead of creating a duplicate. `gh release view` finds
# drafts by tag too.
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@ jobs:
# no upper bound while requirements.txt correctly pinned <2.0: anyone
# running `pip install zimi[mcp]` got mcp 2.x, which dropped the vendored
# FastMCP, and their MCP server refused to start.
# Pillow as requirements.txt has it (favicons; the Red contrast test reads pixels).
- name: Install package
run: pip install -e ".[pdf,mcp]" pytest
run: pip install -e ".[pdf,mcp]" pytest "Pillow>=12.3.0"

# A real browser, so the paint gate actually runs somewhere.
#
Expand Down
15 changes: 14 additions & 1 deletion .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,20 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # OIDC: the provenance attestation
attestations: write # store the provenance attestation
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.11"
- run: pip install build twine
- run: python -m build
# Verify a download with: gh attestation verify <file> --repo epheterson/Zimi
- name: Attest build provenance
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: dist/*
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
password: ${{ secrets.PYPI_API_TOKEN }}
Expand All @@ -38,14 +45,20 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
id-token: write # trusted publishing, and the provenance attestation
attestations: write # store the provenance attestation
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.11"
- run: pip install build
- run: python packaging/zimi-mcp/build.py --outdir dist-mcp
# Verify a download with: gh attestation verify <file> --repo epheterson/Zimi
- name: Attest build provenance
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: dist-mcp/*
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
packages-dir: dist-mcp/
Expand Down
38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,44 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/),
and this project adheres to [Semantic Versioning](https://semver.org/).

## [1.14.1] - 2026-10-22

### Highlights

- Read aloud marks the sentence being read and follows it.
- Settings follow your account across browsers.

### Added

- Read aloud: the sentence being read is marked and followed; tap a sentence or select text to start there.
- Read aloud: back button (once: this sentence again; twice: the one before).
- Read aloud: speed slider, 0.5× to 3×.
- Settings, Voices: speed test for each voice.
- Settings follow the signed-in account to every browser; the admin has an account.
- PDFs: two pages side by side when they fit.
- Books: the chapter in the footer opens the contents.
- Discover's word of the day is voiced in advance.
- Build provenance for PyPI wheels and sdists.

### Changed

- Numbers, dates, sizes, sorting and week start follow Zimi's language.
- Read aloud skips citation marks, infoboxes, tables, captions and references.
- Read aloud loads on first use.
- Dependency floors raised for known vulnerabilities.

### Fixed

- Red and Dark: text no longer matches its background ([#119](https://github.com/epheterson/Zimi/issues/119)).
- Private mode: sign-in could refuse the right password.
- Signing in shows your own library without a reload.
- Change password could keep the old password in the field.
- Read aloud: first tap on iPhone, taps on every page, menu closes, no false "slow" message.
- Read aloud: Automatic avoids voices measured too slow.
- Arabic and Hebrew: spacing and arrow direction.
- Offline PDF library list matches the app's.
- espeak's test voices are hidden.

## [1.14.0] - 2026-10-19

### Added
Expand Down
6 changes: 5 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,13 @@ Before tagging, verify these on the feature branch:
- [ ] `node -c zimi/static/app.js` passes (no syntax errors)
- [ ] `python3 -m pytest tests/ -q` passes
- [ ] `./scripts/release-gate.sh` passes — boots real servers against real ZIM files and drives every headline feature over HTTP, which is the only way to catch a feature that is correct in every module and still dead end to end (cross-ZIM link resolution once shipped serving an empty domain map, and no unit test noticed)
- [ ] CHANGELOG updated, README current, screenshots current
- [ ] CHANGELOG updated (with a `### Highlights` block, see Release notes below), README current, screenshots current
- [ ] No debug print statements or console.logs that shouldn't ship

### Release notes

Auto Release builds the draft's body from the version's `## [X.Y.Z]` section in `CHANGELOG.md` (`scripts/release_notes.py`). The section may open with a `### Highlights` block of one to three short one-line bullets, before `### Added` and the rest. When it does, the release body is those bullets plus a link to the full entry in the CHANGELOG at the version's tag. Without the block the whole section is the body. Highlights are one-line factual bullets, no headings or stories; the CHANGELOG itself keeps them as its first subsection. A draft that already exists is never overwritten, so a curated body wins.

### Phase 1: Tag and build

1. Squash merge feature branch to `main`
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
# Zimi

[![CI](https://github.com/epheterson/Zimi/actions/workflows/ci.yml/badge.svg)](https://github.com/epheterson/Zimi/actions/workflows/ci.yml)
[![Tests](https://img.shields.io/badge/tests-4343-brightgreen)](#)
[![Lighthouse Accessibility](https://img.shields.io/badge/Lighthouse%20a11y-100%2F100-success?logo=lighthouse&logoColor=white)](docs/features/reading.md)
[![WCAG 2.1 AA](https://img.shields.io/badge/WCAG%202.1-AA-blue)](docs/features/reading.md)
[![i18n](https://img.shields.io/badge/i18n-10%20languages-blueviolet)](#languages)
[![Docker Pulls](https://img.shields.io/docker/pulls/epheterson/zimi)](https://hub.docker.com/r/epheterson/zimi)
[![PyPI](https://img.shields.io/pypi/v/zimi)](https://pypi.org/project/zimi/)
[![License: GPL-3.0-or-later](https://img.shields.io/badge/license-GPL--3.0--or--later-blue.svg)](LICENSE)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/epheterson/Zimi/badge)](https://scorecard.dev/viewer/?uri=github.com/epheterson/Zimi)

A modern experience for your ZIM files.

[Kiwix](https://kiwix.org) packages the world's knowledge into ZIM files. Zimi makes them feel like the real internet with a rich web UI, fast JSON API, and an MCP server for AI agents. Everything works offline, in your language.

**If you ❤️ Zimi, leave a ⭐️ to help others find it.**

Use as much as you need:

- **Search for agents.** `pip install zimi-mcp` searches and reads every ZIM over MCP. 280 KB.
Expand Down
7 changes: 4 additions & 3 deletions desktop/requirements-desktop.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
libzim>=3.1.0
PyMuPDF>=1.23.0,<2
certifi>=2024.1.1
certifi>=2026.10.19
pywebview>=4.0.0
Pillow>=10.0.0
Pillow>=13.0.0; python_version >= "3.11"
Pillow>=12.3.0; python_version < "3.11"
pyinstaller>=6.0.0
zeroconf>=0.130.0
zeroconf>=0.149.16
# Windows desktop backend. pywebview 5.x/6.x drives the native Edge WebView2
# runtime through .NET, which it reaches via pythonnet (+ clr_loader). pywebview
# declares `pythonnet` transitively, but we pin both explicitly with an env
Expand Down
4 changes: 2 additions & 2 deletions packaging/zimi-mcp/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ classifiers = [
# Pinned as the main package pins them (mcp<2.0: see its pyproject).
dependencies = [
"libzim>=3.1.0",
"mcp>=1.0,<2.0",
"certifi>=2024.1.1",
"mcp>=1.30.0,<2.0",
"certifi>=2026.10.19",
]

[project.optional-dependencies]
Expand Down
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "zimi"
version = "1.14.0"
version = "1.14.1"
description = "Offline knowledge server for ZIM files — search and read Wikipedia, Stack Overflow and 50+ sources with no internet"
readme = "README.md"
license = {text = "GPL-3.0-or-later"}
Expand Down Expand Up @@ -36,10 +36,10 @@ classifiers = [
]
dependencies = [
"libzim>=3.1.0",
"certifi>=2024.1.1",
"certifi>=2026.10.19",
# LAN peer discovery (mDNS/Zeroconf). Pure-Python; the peer ZIM-sharing
# transport runs over plain HTTP so it needs no extra binary.
"zeroconf>=0.130.0",
"zeroconf>=0.149.16",
# BitTorrent accelerator: attempted at install, optional at runtime.
#
# libtorrent publishes NO sdist. So on any platform where a wheel is
Expand Down Expand Up @@ -75,7 +75,7 @@ desktop = ["pywebview>=4.0.0"]
# distribution. Without the ceiling, `pip install zimi[mcp]` resolves to 2.x and
# the MCP server refuses to start -- issue #52, which was fixed in
# requirements.txt and missed HERE, where PyPI users actually get their pins.
mcp = ["mcp>=1.0,<2.0"]
mcp = ["mcp>=1.30.0,<2.0"]
# The rendered capture engine (zimi/renderer.py): a headless Chromium run as a
# CHILD PROCESS of the server, for pages whose content is built in JavaScript.
# Deliberately not in `all`: this package is only half the install — the browser
Expand All @@ -88,7 +88,7 @@ browser = ["playwright>=1.40"]
# Docker image's pins). Without them Zimi sets them up itself, in a venv under
# its data directory, the first time someone downloads a voice.
voices = ["piper-tts>=1.8,<2", "kokoro-onnx>=0.6,<0.7", "misaki[zh]>=0.9.4,<0.10", "num2words>=0.5.14,<0.6"]
all = ["PyMuPDF>=1.23.0,<2", "mcp>=1.0,<2.0", "libtorrent==2.0.*"]
all = ["PyMuPDF>=1.23.0,<2", "mcp>=1.30.0,<2.0", "libtorrent==2.0.*"]

[project.scripts]
zimi = "zimi.server:main"
Expand Down
9 changes: 5 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ PyMuPDF>=1.23.0,<2
# (moved it to a separate `fastmcp` distribution), so a bare `mcp` resolves to
# 2.0 and the MCP server crashes on import (issue #52, hit via OpenWebUI). Pin to
# the 1.x line that still ships FastMCP until the server is ported.
mcp>=1.0,<2.0
certifi>=2024.1.1
zeroconf>=0.130.0
mcp>=1.30.0,<2.0
certifi>=2026.10.19
zeroconf>=0.149.16
# Rescales a captured site's icon into the 48x48 ZIM illustration. Without it
# has_image_support() is False and every created ZIM ships iconless — which is
# exactly what happened on the Docker image, where Pillow was absent (Eric: CNN
# capture "no favicon" on prod, present locally where Pillow is installed).
Pillow>=10.0.0
Pillow>=13.0.0; python_version >= "3.11"
Pillow>=12.3.0; python_version < "3.11"
# BitTorrent accelerator. Marker-gated below 3.14 (no wheel there yet, and no
# sdist) so a plain `pip install -r requirements.txt` never breaks on new
# interpreters; missing = HTTP-only. Wheels cover manylinux + musllinux, macOS
Expand Down
80 changes: 80 additions & 0 deletions scripts/release_notes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""The GitHub release body for a version, from CHANGELOG.md.

Auto Release calls this: ``release_notes.py 1.14.1 > notes.md``.

A version's section may open with a ``### Highlights`` block of one to three
short sentences. The release body is then that block plus one line linking the
full entry in the CHANGELOG at the version's tag. A section without the block
is used whole, as it always was, and a version with no section at all gets a
bare "Release vX.Y.Z".
"""

import re
import sys

REPO_URL = "https://github.com/epheterson/zimi"
HIGHLIGHTS_HEADING = "### Highlights"


def _section(changelog, version):
"""The lines under ``## [version]``, up to the next ``## [`` heading, and
the heading itself; ``(None, [])`` when the version has no section."""
heading, body = None, []
for line in changelog.splitlines():
if heading is None:
if line.startswith("## [%s]" % version):
heading = line
elif line.startswith("## ["):
break
else:
body.append(line)
return heading, body


def _highlights(body):
"""The text of a leading ``### Highlights`` block, or ``""``."""
lines = iter(body)
for line in lines:
if line.strip():
if line.strip() != HIGHLIGHTS_HEADING:
return ""
break
text = []
for line in lines:
if line.startswith("### "):
break
text.append(line)
return "\n".join(text).strip()


def heading_anchor(heading):
"""The fragment GitHub generates for a heading: lowercase, anything but
letters, digits, spaces, hyphens and underscores dropped, spaces to
hyphens. ``## [1.14.1] - 2026-10-20`` becomes ``1141---2026-10-20``."""
text = heading.lstrip("#").strip().lower()
return re.sub(r"[^\w\- ]", "", text).replace(" ", "-")


def release_body(changelog, version, repo_url=REPO_URL):
heading, body = _section(changelog, version)
if heading is None:
return "Release v%s" % version
highlights = _highlights(body)
if not highlights:
return "\n".join(body).strip() or "Release v%s" % version
link = "%s/blob/v%s/CHANGELOG.md#%s" % (repo_url, version, heading_anchor(heading))
return "%s\n\n[Full changelog for v%s](%s)" % (highlights, version, link)


def main(argv):
if len(argv) not in (2, 3):
sys.stderr.write("usage: release_notes.py VERSION [CHANGELOG.md]\n")
return 2
with open(argv[2] if len(argv) == 3 else "CHANGELOG.md", encoding="utf-8") as f:
print(release_body(f.read(), argv[1]))
return 0


if __name__ == "__main__":
sys.exit(main(sys.argv))
13 changes: 13 additions & 0 deletions tests/app_source.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,16 @@ module.exports = function appSource() {
const table = JSON.parse(fs.readFileSync(path.join(root, 'assets', 'lang-codes.json'), 'utf8'));
return src.replace(MARK, JSON.stringify(table));
};

// localeHelpers(src): app.js's number, date, sort and week helpers (fmtNum,
// fmtDate, cmpText, cmpTitle, _weekFirstDay), to run in a vm context before
// any part that formats or sorts. Zimi's language is the context's
// _currentLang, English when it has none.
module.exports.localeHelpers = function localeHelpers(src) {
const a = src.indexOf('function _uiLocale()'), b = src.indexOf('// Localized pluralization: picks');
if (a < 0 || b < a) throw new Error('app.js locale helpers moved');
return "if (typeof _currentLang === 'undefined') var _currentLang = 'en';\n" +
"var _uiLangPrimary = typeof _uiLangPrimary === 'function' ? _uiLangPrimary : function() { return String(_currentLang || 'en').split('-')[0]; };\n" +
"var _langPrimary = typeof _langPrimary === 'function' ? _langPrimary : function(t) { return String(t || '').split('-')[0]; };\n" +
src.slice(a, b);
};
18 changes: 18 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,24 @@
os.environ.setdefault("ZIMI_BEST_ANSWER", "1")


@pytest.fixture(autouse=True, scope="session")
def _cheap_password_hashing():
"""Every password set or checked costs 600,000 PBKDF2 rounds, a fraction
of a second by design, and the access tests do it hundreds of times. In
this process a hash is made and checked with the same count, so the
tests lose nothing at 1,000. Servers a test starts as `zimi serve` keep
the real count; none of them reads a password this process wrote."""
try:
from zimi import manage
except Exception:
yield
return
real = manage._PW_ITERATIONS
manage._PW_ITERATIONS = 1_000
yield
manage._PW_ITERATIONS = real


@pytest.fixture(autouse=True)
def _fresh_archive_pools():
try:
Expand Down
2 changes: 1 addition & 1 deletion tests/playwright.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { defineConfig } from '@playwright/test';
export default defineConfig({
// This config lives in tests/; specs sit beside it, artifacts stay at repo root.
testDir: '.',
testMatch: ['visual_validation.spec.mjs', 'test_password_flow.mjs', 'test_interlang.mjs', 'screenshots.mjs', 'test_tabs.mjs', 'test_almanac_hero_clock.spec.mjs', 'test_login_navigation.spec.mjs', 'test_private_mode_login.spec.mjs', 'test_admin_private_library.spec.mjs', 'test_backup_hub.spec.mjs', 'test_bookmarks_folders.spec.mjs', 'test_reader_bm_panel.spec.mjs', 'test_reader_captions.spec.mjs', 'test_manage_boot_race.spec.mjs', 'test_offline_state.spec.mjs', 'test_export_download.spec.mjs', 'test_create_flow.spec.mjs', 'test_create_folder.spec.mjs', 'test_activity_view.spec.mjs', 'test_zim_about.spec.mjs', 'test_manage_ux.spec.mjs', 'test_almanac_earth_settings.spec.mjs', 'test_almanac_ux.spec.mjs', 'test_almanac_secbar.spec.mjs', 'test_almanac_clocks.spec.mjs'],
testMatch: ['visual_validation.spec.mjs', 'test_password_flow.mjs', 'test_interlang.mjs', 'screenshots.mjs', 'test_tabs.mjs', 'test_almanac_hero_clock.spec.mjs', 'test_login_navigation.spec.mjs', 'test_private_mode_login.spec.mjs', 'test_admin_private_library.spec.mjs', 'test_backup_hub.spec.mjs', 'test_bookmarks_folders.spec.mjs', 'test_reader_bm_panel.spec.mjs', 'test_reader_captions.spec.mjs', 'test_manage_boot_race.spec.mjs', 'test_offline_state.spec.mjs', 'test_export_download.spec.mjs', 'test_create_flow.spec.mjs', 'test_create_folder.spec.mjs', 'test_activity_view.spec.mjs', 'test_zim_about.spec.mjs', 'test_manage_ux.spec.mjs', 'test_almanac_earth_settings.spec.mjs', 'test_almanac_ux.spec.mjs', 'test_almanac_secbar.spec.mjs', 'test_almanac_clocks.spec.mjs', 'test_settings_sync.spec.mjs', 'test_almanac_overlays.spec.mjs'],
timeout: 60000,
expect: { timeout: 10000 },
fullyParallel: false, // Run sequentially — some tests depend on server state
Expand Down
Loading
Loading