Compare commits

...
Author SHA1 Message Date
Richie 29db42d31f feat(ci): add dedicated Nix cache prebuild runner
treefmt / nix fmt (pull_request) Successful in 3s
build_systems / prebuild-common-x86-64-v3 (pull_request) Canceled after 0s
build_systems / build-bob (pull_request) Canceled after 0s
build_systems / build-brain (pull_request) Canceled after 0s
build_systems / build-jeeves (pull_request) Canceled after 0s
build_systems / build-portal-1 (pull_request) Canceled after 0s
build_systems / build-rhapsody-in-green (pull_request) Canceled after 0s
pytest / pytest (pull_request) Canceled after 3m32s
test ebook search / test-ebook-search (pull_request) Canceled after 3m32s
2026-09-19 10:43:17 -04:00
Richie 50d4aa383f Clean up overlays and remove obsolete dependencies
treefmt / nix fmt (pull_request) Successful in 3s
test ebook search / test-ebook-search (pull_request) Failing after 8h50m21s
pytest / pytest (pull_request) Failing after 8h50m22s
build_systems / build-rhapsody-in-green (pull_request) Failing after 8h50m23s
build_systems / build-portal-1 (pull_request) Failing after 8h50m24s
build_systems / build-jeeves (pull_request) Failing after 8h50m24s
build_systems / build-brain (pull_request) Failing after 8h50m24s
build_systems / build-bob (pull_request) Failing after 8h50m24s
2026-09-18 19:59:12 -04:00
Richie f2611d25a3 overlays: consolidate Abseil BMI2 workaround under patches
treefmt / nix fmt (pull_request) Successful in 3s
pytest / pytest (pull_request) Successful in 1m1s
test ebook search / test-ebook-search (pull_request) Successful in 1m18s
build_systems / build-portal-1 (pull_request) Successful in 1h14m39s
build_systems / build-brain (pull_request) Successful in 1h39m5s
build_systems / build-jeeves (pull_request) Successful in 1h55m23s
build_systems / build-bob (pull_request) Successful in 3h55m16s
build_systems / build-rhapsody-in-green (pull_request) Failing after 12h0m1s
Extract the header fix into a standalone patch and colocate the
Electron, Deno, and Signal overrides in patches/abseil. Preserve the
x86-64-v3 restriction and document the accepted upstream fix while
waiting for bundled dependency updates.

Import the Abseil overrides through the patches overlay and register
the torchcodec override. Leave PostgreSQL output checks in
x86-64-v3-workarounds.nix.
2026-09-18 13:11:04 -04:00
Richie 24cbf74ff9 fix(scipy): allow rounding differences in STFT tests
treefmt / nix fmt (pull_request) Successful in 3s
build_systems / build-rhapsody-in-green (pull_request) Failing after 1h42m42s
build_systems / build-portal-1 (pull_request) Failing after 1h56m54s
build_systems / build-brain (pull_request) Failing after 2h2m2s
test ebook search / test-ebook-search (pull_request) Successful in 2h6m28s
pytest / pytest (pull_request) Successful in 3h32m2s
build_systems / build-jeeves (pull_request) Successful in 5h36m55s
build_systems / build-bob (pull_request) Successful in 7h36m46s
Keep STFT tests enabled with precision-appropriate tolerances for
x86-64-v3 builds. Add the override under overlays/patches/scipy and
remove the commented-out test exclusions.
2026-09-18 12:09:42 -04:00
Richie 5a59c4d0f5 fix(gnutls): replace UDP test skip with readiness polling
Wait for the UDP socket to bind before starting the client, detect
server exit, and clean up on skip. Preserve both handshake checks.

Add regression checks and document the rationale for upstream submission.

Validated on x86-64-v3: 796 passes, 131 existing skips, zero failures.
All seven focused checks and the delayed-start reproduction passed.
2026-09-18 12:08:10 -04:00
Richie 3614428e3d (feat)removing firefox
treefmt / nix fmt (pull_request) Successful in 4s
build_systems / build-portal-1 (pull_request) Successful in 31s
build_systems / build-brain (pull_request) Successful in 53s
build_systems / build-bob (pull_request) Successful in 54s
build_systems / build-jeeves (pull_request) Successful in 58s
pytest / pytest (pull_request) Successful in 1m6s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m12s
test ebook search / test-ebook-search (pull_request) Successful in 1m18s
treefmt / nix fmt (push) Successful in 4s
build_systems / build-jeeves (push) Successful in 7s
build_systems / build-portal-1 (push) Successful in 19s
build_systems / build-brain (push) Successful in 28s
build_systems / build-bob (push) Successful in 31s
build_systems / build-rhapsody-in-green (push) Successful in 40s
pytest / pytest (push) Successful in 53s
test ebook search / test-ebook-search (push) Successful in 1m15s
im not using firefox any more and with the move to x86 v3 1 less browser build is nice
2026-09-18 09:38:53 -04:00
Richie 7cc87ff278 more x86-64-v3 workarounds and text exclushions
treefmt / nix fmt (pull_request) Successful in 4s
test ebook search / test-ebook-search (pull_request) Successful in 1m5s
pytest / pytest (pull_request) Successful in 48s
build_systems / build-portal-1 (pull_request) Successful in 18s
build_systems / build-brain (pull_request) Successful in 26s
build_systems / build-bob (pull_request) Successful in 27s
build_systems / build-jeeves (pull_request) Successful in 31s
build_systems / build-rhapsody-in-green (pull_request) Successful in 38s
treefmt / nix fmt (push) Successful in 3s
build_systems / build-portal-1 (push) Successful in 21s
build_systems / build-brain (push) Successful in 31s
build_systems / build-bob (push) Successful in 33s
build_systems / build-jeeves (push) Successful in 38s
build_systems / build-rhapsody-in-green (push) Successful in 47s
pytest / pytest (push) Successful in 56s
test ebook search / test-ebook-search (push) Successful in 1m14s
2026-09-17 08:44:18 -04:00
Richie 224c75768b moved modbus to extraComponents
modbus intgration requres more then just pymodbus bus now
2026-09-17 08:35:35 -04:00
Richie 562c92ae77 more test issues 2026-09-17 08:35:35 -04:00
Richie dd4a0e1f9c more test-exclusions 2026-09-17 08:35:35 -04:00
Richie c56aa0ab3d fix(overlays): stabilize timing-sensitive tests 2026-09-17 08:35:35 -04:00
Richie 21066116b0 testing numprocesses to fix pytest-xdist tests 2026-09-17 08:35:35 -04:00
Richie 0b333552e6 fix(overlays): exclude failing x86-64-v3 tests
Skip the flaky GnuTLS UDP readiness test and the SciPy FFT tests whose
precision differences are acceptable for our workloads.
2026-09-17 08:35:35 -04:00
Richie b1c21438d9 removed hostPlatform from portal-1 2026-09-17 08:35:35 -04:00
Richie 0f794411fd testing x86_v3 2026-09-17 08:35:35 -04:00
Richie f9049353e2 removing open_webui
treefmt / nix fmt (pull_request) Successful in 4s
build_systems / build-portal-1 (pull_request) Successful in 27s
build_systems / build-brain (pull_request) Successful in 48s
build_systems / build-bob (pull_request) Successful in 52s
build_systems / build-jeeves (pull_request) Successful in 53s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m4s
pytest / pytest (pull_request) Successful in 1m17s
test ebook search / test-ebook-search (pull_request) Successful in 1m21s
build_systems / build-brain (push) Successful in 34s
build_systems / build-jeeves (push) Successful in 44s
build_systems / build-rhapsody-in-green (push) Successful in 50s
pytest / pytest (push) Successful in 1m1s
treefmt / nix fmt (push) Successful in 2s
build_systems / build-portal-1 (push) Successful in 22s
build_systems / build-bob (push) Successful in 38s
test ebook search / test-ebook-search (push) Successful in 1m15s
2026-09-16 20:26:47 -04:00
Richie f905b6e270 removing camofox-browser.nix
treefmt / nix fmt (pull_request) Successful in 4s
build_systems / build-portal-1 (pull_request) Successful in 36s
build_systems / build-brain (pull_request) Successful in 1m6s
pytest / pytest (pull_request) Successful in 1m8s
build_systems / build-bob (pull_request) Successful in 1m12s
test ebook search / test-ebook-search (pull_request) Successful in 1m23s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m30s
build_systems / build-jeeves (pull_request) Successful in 1m32s
treefmt / nix fmt (push) Successful in 3s
build_systems / build-portal-1 (push) Successful in 33s
build_systems / build-brain (push) Successful in 52s
build_systems / build-bob (push) Successful in 57s
build_systems / build-jeeves (push) Successful in 1m5s
pytest / pytest (push) Successful in 1m16s
build_systems / build-rhapsody-in-green (push) Successful in 1m20s
test ebook search / test-ebook-search (push) Successful in 1m23s
2026-09-13 12:32:35 -04:00
Richie f1f027abfe removed steve
pytest / pytest (push) Successful in 1m17s
build_systems / build-brain (pull_request) Successful in 1m7s
pytest / pytest (pull_request) Successful in 1m6s
build_systems / build-bob (pull_request) Successful in 1m37s
test ebook search / test-ebook-search (pull_request) Successful in 1m22s
build_systems / build-portal-1 (pull_request) Successful in 37s
treefmt / nix fmt (pull_request) Successful in 4s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m46s
build_systems / build-jeeves (pull_request) Successful in 1m52s
treefmt / nix fmt (push) Successful in 5s
build_systems / build-portal-1 (push) Successful in 40s
build_systems / build-brain (push) Successful in 1m0s
build_systems / build-bob (push) Successful in 1m9s
test ebook search / test-ebook-search (push) Successful in 1m17s
build_systems / build-jeeves (push) Successful in 1m43s
build_systems / build-rhapsody-in-green (push) Successful in 1m38s
2026-09-12 11:20:15 -04:00
Richie 0243da86f2 refactor(home): trim user packages and move GUI modules into Richie's config 2026-09-12 11:20:15 -04:00
Richie 1803da420e feat(jeeves): limit builder CPU usage and tune Nix parallelism
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 57s
build_systems / build-portal-1 (pull_request) Successful in 1m4s
test ebook search / test-ebook-search (pull_request) Successful in 1m11s
build_systems / build-brain (pull_request) Successful in 1m49s
build_systems / build-bob (pull_request) Successful in 1m52s
build_systems / build-rhapsody-in-green (pull_request) Successful in 2m14s
build_systems / build-jeeves (pull_request) Successful in 2m32s
treefmt / nix fmt (push) Successful in 4s
build_systems / build-jeeves (push) Successful in 8s
build_systems / build-portal-1 (push) Successful in 18s
build_systems / build-brain (push) Successful in 30s
build_systems / build-bob (push) Successful in 34s
build_systems / build-rhapsody-in-green (push) Successful in 45s
pytest / pytest (push) Successful in 54s
test ebook search / test-ebook-search (push) Successful in 1m10s
2026-09-12 09:47:35 -04:00
Richie 2fb3a0d3ba feat(gitea): allow 12-hour runner builds
treefmt / nix fmt (pull_request) Successful in 4s
pytest / pytest (pull_request) Successful in 1m12s
treefmt / nix fmt (push) Successful in 5s
test ebook search / test-ebook-search (pull_request) Successful in 1m34s
build_systems / build-portal-1 (pull_request) Successful in 1m38s
build_systems / build-portal-1 (push) Successful in 38s
build_systems / build-brain (pull_request) Successful in 2m7s
pytest / pytest (push) Successful in 46s
build_systems / build-bob (pull_request) Successful in 2m9s
build_systems / build-brain (push) Successful in 51s
build_systems / build-bob (push) Successful in 56s
test ebook search / test-ebook-search (push) Successful in 54s
build_systems / build-rhapsody-in-green (pull_request) Successful in 2m18s
build_systems / build-rhapsody-in-green (push) Successful in 1m7s
build_systems / build-jeeves (pull_request) Successful in 2m40s
build_systems / build-jeeves (push) Successful in 1m23s
2026-09-12 09:09:30 -04:00
Richie 4154640637 feat(jeeves): move container state to dedicated ZFS dataset
treefmt / nix fmt (pull_request) Successful in 4s
pytest / pytest (pull_request) Successful in 1m30s
test ebook search / test-ebook-search (pull_request) Successful in 1m28s
build_systems / build-portal-1 (pull_request) Successful in 2m51s
build_systems / build-bob (pull_request) Successful in 3m43s
build_systems / build-jeeves (pull_request) Successful in 3m43s
build_systems / build-brain (pull_request) Successful in 3m29s
build_systems / build-rhapsody-in-green (pull_request) Successful in 3m54s
treefmt / nix fmt (push) Successful in 4s
build_systems / build-portal-1 (push) Successful in 24s
pytest / pytest (push) Successful in 33s
build_systems / build-brain (push) Successful in 41s
test ebook search / test-ebook-search (push) Successful in 38s
build_systems / build-bob (push) Successful in 46s
build_systems / build-jeeves (push) Successful in 1m2s
build_systems / build-rhapsody-in-green (push) Successful in 1m5s
- add a performance-tuned media/containers dataset
- store Docker and NixOS container state on the dataset
- prepare storage directories after ZFS mounts
- bind NixOS container state to its required path
- configure snapshot retention for container storage
2026-09-11 14:58:41 -04:00
Richie 78641f78dd updating snapshot config
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 1m2s
test ebook search / test-ebook-search (pull_request) Successful in 1m10s
build_systems / build-portal-1 (pull_request) Successful in 1m15s
build_systems / build-brain (pull_request) Successful in 2m5s
build_systems / build-bob (pull_request) Successful in 2m10s
build_systems / build-jeeves (pull_request) Successful in 2m32s
build_systems / build-rhapsody-in-green (pull_request) Successful in 2m34s
treefmt / nix fmt (push) Successful in 6s
build_systems / build-jeeves (push) Successful in 17s
build_systems / build-rhapsody-in-green (push) Successful in 23s
build_systems / build-portal-1 (push) Successful in 39s
pytest / pytest (push) Successful in 51s
build_systems / build-brain (push) Successful in 57s
test ebook search / test-ebook-search (push) Successful in 59s
build_systems / build-bob (push) Successful in 1m1s
2026-09-11 12:37:47 -04:00
Richie 0e09b1db34 feat(zfs): mount dedicated Nix build dataset
treefmt / nix fmt (pull_request) Successful in 4s
build_systems / build-portal-1 (pull_request) Successful in 33s
pytest / pytest (pull_request) Successful in 34s
test ebook search / test-ebook-search (pull_request) Successful in 39s
build_systems / build-brain (pull_request) Successful in 59s
build_systems / build-jeeves (pull_request) Successful in 1m16s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m16s
build_systems / build-bob (pull_request) Successful in 1m4s
treefmt / nix fmt (push) Successful in 5s
build_systems / build-rhapsody-in-green (push) Successful in 18s
build_systems / build-portal-1 (push) Successful in 49s
pytest / pytest (push) Successful in 1m1s
test ebook search / test-ebook-search (push) Successful in 1m18s
build_systems / build-bob (push) Successful in 1m33s
build_systems / build-brain (push) Successful in 1m46s
build_systems / build-jeeves (push) Successful in 2m30s
Mount root_pool/nix_build at /nix/var/nix/builds on existing hosts and
generate the same layout for new installations.
2026-09-11 12:07:24 -04:00
Richie 82a3bdf562 temp v3 suport 2026-09-11 12:00:43 -04:00
Richie 46cb80f809 feat(installer) adding root_pool/nix_build 2026-09-11 10:47:52 -04:00
Richie bce4ccb754 config(docker): disable live restore
treefmt / nix fmt (pull_request) Successful in 7s
pytest / pytest (pull_request) Successful in 1m40s
test ebook search / test-ebook-search (pull_request) Successful in 1m40s
build_systems / build-portal-1 (pull_request) Successful in 2m40s
build_systems / build-brain (pull_request) Successful in 3m43s
build_systems / build-bob (pull_request) Successful in 4m0s
build_systems / build-rhapsody-in-green (pull_request) Successful in 4m19s
build_systems / build-jeeves (pull_request) Successful in 4m37s
treefmt / nix fmt (push) Successful in 3s
build_systems / build-portal-1 (push) Successful in 22s
pytest / pytest (push) Successful in 31s
test ebook search / test-ebook-search (push) Successful in 32s
build_systems / build-brain (push) Successful in 35s
build_systems / build-bob (push) Successful in 39s
build_systems / build-rhapsody-in-green (push) Successful in 48s
build_systems / build-jeeves (push) Successful in 52s
2026-09-10 07:26:34 -04:00
Richie 7c7740fe90 remove dov
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 57s
test ebook search / test-ebook-search (pull_request) Successful in 1m1s
build_systems / build-portal-1 (pull_request) Successful in 1m7s
build_systems / build-brain (pull_request) Successful in 1m25s
build_systems / build-bob (pull_request) Successful in 1m26s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m33s
build_systems / build-jeeves (pull_request) Successful in 1m45s
treefmt / nix fmt (push) Successful in 4s
build_systems / build-jeeves (push) Successful in 7s
build_systems / build-portal-1 (push) Successful in 24s
test ebook search / test-ebook-search (push) Successful in 31s
pytest / pytest (push) Successful in 32s
build_systems / build-brain (push) Successful in 35s
build_systems / build-bob (push) Successful in 38s
build_systems / build-rhapsody-in-green (push) Successful in 50s
2026-09-09 11:06:44 -04:00
Richie a59ccd293d feat(jeeves): enable x86-64 v2 and v3 system features
treefmt / nix fmt (pull_request) Successful in 6s
build_systems / build-portal-1 (pull_request) Successful in 31s
pytest / pytest (pull_request) Successful in 33s
test ebook search / test-ebook-search (pull_request) Successful in 36s
build_systems / build-brain (pull_request) Successful in 53s
build_systems / build-bob (pull_request) Successful in 53s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m5s
build_systems / build-jeeves (pull_request) Successful in 1m8s
treefmt / nix fmt (push) Successful in 5s
build_systems / build-jeeves (push) Successful in 10s
build_systems / build-portal-1 (push) Successful in 22s
pytest / pytest (push) Successful in 32s
test ebook search / test-ebook-search (push) Successful in 34s
build_systems / build-bob (push) Successful in 41s
build_systems / build-brain (push) Successful in 36s
build_systems / build-rhapsody-in-green (push) Successful in 51s
2026-09-08 19:59:14 -04:00
Richie 5514047eda feat(jeeves): share one system closure across builders
treefmt / nix fmt (pull_request) Successful in 5s
build_systems / build-portal-1 (pull_request) Successful in 32s
pytest / pytest (pull_request) Successful in 32s
test ebook search / test-ebook-search (pull_request) Successful in 36s
build_systems / build-brain (pull_request) Successful in 54s
build_systems / build-bob (pull_request) Successful in 55s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m6s
build_systems / build-jeeves (pull_request) Successful in 1m58s
treefmt / nix fmt (push) Successful in 5s
build_systems / build-portal-1 (push) Successful in 24s
pytest / pytest (push) Successful in 32s
build_systems / build-brain (push) Successful in 37s
test ebook search / test-ebook-search (push) Successful in 35s
build_systems / build-bob (push) Successful in 41s
build_systems / build-rhapsody-in-green (push) Successful in 50s
build_systems / build-jeeves (push) Successful in 53s
Reuse a single NixOS container build for all runners while preserving
unique runtime hostnames and existing Gitea registrations.
2026-09-08 19:37:18 -04:00
Richie 810f9a66f6 flake update 09-08-26
treefmt / nix fmt (pull_request) Successful in 5s
build_systems / build-portal-1 (pull_request) Successful in 30s
pytest / pytest (pull_request) Successful in 32s
test ebook search / test-ebook-search (pull_request) Successful in 37s
build_systems / build-brain (pull_request) Successful in 49s
build_systems / build-bob (pull_request) Successful in 55s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m6s
build_systems / build-jeeves (pull_request) Successful in 2m33s
treefmt / nix fmt (push) Successful in 5s
build_systems / build-portal-1 (push) Successful in 24s
pytest / pytest (push) Successful in 32s
build_systems / build-brain (push) Successful in 38s
test ebook search / test-ebook-search (push) Successful in 36s
build_systems / build-bob (push) Successful in 41s
build_systems / build-rhapsody-in-green (push) Successful in 52s
build_systems / build-jeeves (push) Successful in 2m9s
2026-09-08 19:26:26 -04:00
Richie 3aa66c48af removing zerotierone from jeeves
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 39s
build_systems / build-portal-1 (pull_request) Successful in 45s
test ebook search / test-ebook-search (pull_request) Successful in 46s
build_systems / build-brain (pull_request) Successful in 1m1s
build_systems / build-bob (pull_request) Successful in 1m1s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m10s
build_systems / build-jeeves (pull_request) Successful in 2m29s
treefmt / nix fmt (push) Successful in 5s
build_systems / build-portal-1 (push) Successful in 24s
pytest / pytest (push) Successful in 32s
build_systems / build-brain (push) Successful in 37s
build_systems / build-bob (push) Successful in 37s
test ebook search / test-ebook-search (push) Successful in 37s
build_systems / build-rhapsody-in-green (push) Successful in 50s
build_systems / build-jeeves (push) Successful in 2m2s
2026-09-08 18:12:20 -04:00
Richie 97d63eecda add portal-1 to build_system
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 35s
test ebook search / test-ebook-search (pull_request) Successful in 41s
build_systems / build-portal-1 (pull_request) Successful in 57s
build_systems / build-brain (pull_request) Successful in 57s
build_systems / build-bob (pull_request) Successful in 59s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m12s
build_systems / build-jeeves (pull_request) Successful in 2m29s
treefmt / nix fmt (push) Successful in 6s
build_systems / build-portal-1 (push) Successful in 26s
pytest / pytest (push) Successful in 34s
test ebook search / test-ebook-search (push) Successful in 35s
build_systems / build-brain (push) Successful in 39s
build_systems / build-bob (push) Successful in 39s
build_systems / build-rhapsody-in-green (push) Successful in 58s
build_systems / build-jeeves (push) Successful in 2m21s
2026-09-08 16:40:45 -04:00
Richie 1d0ced8fd4 removed pirate-weather from home_assistant
test ebook search / test-ebook-search (push) Successful in 45s
treefmt / nix fmt (pull_request) Successful in 6s
treefmt / nix fmt (push) Successful in 6s
pytest / pytest (pull_request) Successful in 35s
test ebook search / test-ebook-search (pull_request) Successful in 47s
pytest / pytest (push) Successful in 42s
build_systems / build-brain (pull_request) Successful in 59s
build_systems / build-bob (push) Successful in 58s
build_systems / build-bob (pull_request) Successful in 1m1s
build_systems / build-brain (push) Successful in 55s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m15s
build_systems / build-rhapsody-in-green (push) Successful in 1m10s
build_systems / build-jeeves (pull_request) Successful in 2m52s
build_systems / build-jeeves (push) Successful in 2m50s
2026-09-08 16:38:21 -04:00
Richie 3ebead7663 feat(van_weather): remove python van_weather and apscheduler
build_systems / build-rhapsody-in-green (push) Successful in 48s
build_systems / build-jeeves (push) Successful in 2m0s
pytest / pytest (pull_request) Successful in 44s
test ebook search / test-ebook-search (pull_request) Successful in 47s
build_systems / build-brain (pull_request) Successful in 1m0s
build_systems / build-jeeves (pull_request) Successful in 2m59s
treefmt / nix fmt (pull_request) Successful in 6s
build_systems / build-bob (pull_request) Successful in 1m1s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m35s
pytest / pytest (push) Successful in 32s
build_systems / build-bob (push) Successful in 35s
treefmt / nix fmt (push) Successful in 6s
build_systems / build-brain (push) Successful in 8s
test ebook search / test-ebook-search (push) Successful in 32s
2026-09-08 14:44:39 -04:00
Richie 2882960a8e feat(gitea): replace GITEA_TOKEN with JEEVES_BOT_TOKEN in workflows and script
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 32s
build_systems / build-brain (push) Successful in 36s
build_systems / build-rhapsody-in-green (push) Successful in 47s
build_systems / build-jeeves (push) Successful in 1m59s
test ebook search / test-ebook-search (push) Successful in 31s
build_systems / build-bob (push) Successful in 36s
test ebook search / test-ebook-search (pull_request) Successful in 35s
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 30s
build_systems / build-brain (pull_request) Successful in 47s
build_systems / build-bob (pull_request) Successful in 49s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m0s
build_systems / build-jeeves (pull_request) Successful in 2m15s
2026-08-29 09:38:36 -04:00
Richie bdf67e96f0 feat(monitoring): add Portal-1 and Vultr dashboards
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 30s
test ebook search / test-ebook-search (pull_request) Successful in 34s
build_systems / build-brain (pull_request) Successful in 49s
build_systems / build-jeeves (pull_request) Successful in 2m14s
build_systems / build-bob (pull_request) Successful in 50s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m2s
build_systems / build-brain (push) Successful in 43s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 36s
test ebook search / test-ebook-search (push) Successful in 37s
build_systems / build-bob (push) Successful in 43s
build_systems / build-rhapsody-in-green (push) Successful in 55s
build_systems / build-jeeves (push) Successful in 2m7s
- scrape Portal-1 node metrics over Tailscale
- add a hardened Vultr billing and bandwidth exporter
- provision host health and infrastructure cost dashboards
2026-08-27 17:49:56 -04:00
Richie d95b8b99ce feat(jeeves): centralize firewall configuration
test ebook search / test-ebook-search (pull_request) Successful in 34s
build_systems / build-brain (pull_request) Successful in 48s
build_systems / build-bob (pull_request) Successful in 49s
build_systems / build-jeeves (pull_request) Successful in 2m11s
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 29s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m1s
build_systems / build-bob (push) Successful in 9s
pytest / pytest (push) Successful in 28s
test ebook search / test-ebook-search (push) Successful in 34s
build_systems / build-jeeves (push) Successful in 1m59s
treefmt / nix fmt (push) Successful in 6s
build_systems / build-brain (push) Successful in 33s
build_systems / build-rhapsody-in-green (push) Successful in 46s
- split networking into interface and firewall modules
- define named service ports in one location
- remove scattered service-level firewall rules
2026-08-27 16:59:44 -04:00
Richie 7e6908ef3d feat(networking): migrate host services from ZeroTier to Tailscale
test ebook search / test-ebook-search (pull_request) Successful in 35s
build_systems / build-bob (pull_request) Successful in 52s
build_systems / build-brain (pull_request) Successful in 52s
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 30s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m3s
build_systems / build-brain (push) Successful in 35s
build_systems / build-bob (push) Successful in 36s
build_systems / build-jeeves (pull_request) Successful in 2m20s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 29s
test ebook search / test-ebook-search (push) Successful in 34s
build_systems / build-rhapsody-in-green (push) Successful in 49s
build_systems / build-jeeves (push) Successful in 2m2s
- replace legacy IP addresses with Tailscale MagicDNS names
- route Prometheus scraping and exporter firewall access through Tailscale
- wait for Tailscale before starting Prometheus
- restrict service UI ports to the Tailscale interface
- update PostgreSQL and Transmission access rules
- remove Jeeves ZeroTier configuration and Docker host overrides
2026-08-27 12:25:27 -04:00
Richie dd51e89abf fix(haproxy): use IPv6 wildcard for dual-stack listeners
pytest / pytest (pull_request) Successful in 29s
test ebook search / test-ebook-search (pull_request) Successful in 34s
treefmt / nix fmt (pull_request) Successful in 5s
build_systems / build-rhapsody-in-green (pull_request) Successful in 59s
build_systems / build-bob (pull_request) Successful in 49s
build_systems / build-brain (pull_request) Successful in 49s
build_systems / build-jeeves (pull_request) Successful in 2m17s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 30s
build_systems / build-brain (push) Successful in 35s
test ebook search / test-ebook-search (push) Successful in 34s
build_systems / build-bob (push) Successful in 35s
build_systems / build-rhapsody-in-green (push) Successful in 47s
build_systems / build-jeeves (push) Successful in 2m1s
Bind HTTP, HTTPS, and Gitea SSH to [::] with v4v6 support.
2026-08-27 09:08:18 -04:00
Richie 677364fe74 removed zerotier.nix for common and removed zerotier from all systems except jeeves
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 29s
test ebook search / test-ebook-search (pull_request) Successful in 35s
build_systems / build-bob (pull_request) Successful in 49s
build_systems / build-brain (pull_request) Successful in 51s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m1s
build_systems / build-jeeves (pull_request) Successful in 2m15s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 29s
build_systems / build-bob (push) Successful in 37s
test ebook search / test-ebook-search (push) Successful in 33s
build_systems / build-brain (push) Successful in 35s
build_systems / build-rhapsody-in-green (push) Successful in 48s
build_systems / build-jeeves (push) Successful in 1m58s
2026-08-26 13:11:34 -04:00
Richie f5a0038ab7 feat(portal): proxy Gitea SSH through HAProxy
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 31s
test ebook search / test-ebook-search (pull_request) Successful in 35s
build_systems / build-brain (pull_request) Successful in 50s
build_systems / build-bob (pull_request) Successful in 55s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m3s
build_systems / build-jeeves (pull_request) Successful in 2m20s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 32s
test ebook search / test-ebook-search (push) Successful in 33s
build_systems / build-bob (push) Successful in 37s
build_systems / build-brain (push) Successful in 36s
build_systems / build-rhapsody-in-green (push) Successful in 47s
build_systems / build-jeeves (push) Successful in 2m1s
- expose Gitea SSH through Portal on port 2223
- advertise the correct Gitea SSH user and domain
- add the Gitea host to Richie’s SSH config
- correct the local ACME challenge backend port
2026-08-26 10:43:21 -04:00
Richie b406b92622 fix(networking): restore Gitea HTTP port in allowedTCPPorts
pytest / pytest (pull_request) Successful in 29s
test ebook search / test-ebook-search (pull_request) Successful in 33s
build_systems / build-brain (pull_request) Successful in 48s
build_systems / build-bob (pull_request) Successful in 51s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m0s
treefmt / nix fmt (pull_request) Successful in 6s
build_systems / build-jeeves (pull_request) Successful in 2m16s
2026-08-26 08:37:22 -04:00
Richie e0c14973b0 refactor(haproxy): consolidate ACME configuration
build_systems / build-rhapsody-in-green (pull_request) Failing after 7m11s
build_systems / build-jeeves (pull_request) Failing after 7m19s
build_systems / build-brain (pull_request) Failing after 7m19s
test ebook search / test-ebook-search (pull_request) Failing after 7m18s
build_systems / build-bob (pull_request) Failing after 7m23s
pytest / pytest (pull_request) Failing after 7m26s
treefmt / nix fmt (pull_request) Failing after 7m21s
Merge the ACME module into the HAProxy configuration and start HAProxy only after certificates and Tailscale connectivity are available.
2026-08-26 08:28:41 -04:00
Richie 515cbae3f8 fix(audiobookshelf): updated host
test ebook search / test-ebook-search (pull_request) Failing after 7m8s
build_systems / build-jeeves (pull_request) Failing after 7m13s
pytest / pytest (pull_request) Failing after 7m17s
build_systems / build-bob (pull_request) Failing after 7m27s
build_systems / build-rhapsody-in-green (pull_request) Failing after 7m13s
treefmt / nix fmt (pull_request) Failing after 7m12s
build_systems / build-brain (pull_request) Failing after 7m18s
2026-08-26 07:39:19 -04:00
Richie 219aea9b5f fix(acme) used the correct ip for acme 2026-08-26 07:33:31 -04:00
Richie df8908b708 feat(services): remove haproxy from validate_system.toml 2026-08-26 07:33:31 -04:00
Richie cb83fc6ea5 fix 2 imports 2026-08-26 07:33:31 -04:00
Richie 7f08a6f788 feat(portal): move edge proxy services from Jeeves
- move HAProxy and ACME certificate management to Portal
- route application backends to Jeeves over Tailscale
- restrict Jeeves backend ports to the Tailscale interface
- expose Gems for remote proxy access
2026-08-26 07:33:31 -04:00
Richie 8245d0f82e feat(tailscale): enable mesh networking across NixOS hosts
- enable Tailscale on Bob, Brain, Jeeves, and Rhapsody
- configure the Jeeves binary cache through Tailscale MagicDNS
- trust the existing workshop cache signing key
2026-08-26 07:33:31 -04:00
Richie 1c4e0c0828 renamed portal_1 to portal-1 2026-08-26 07:33:31 -04:00
Richie 5bc9ee5de9 feat(home): expand Richie’s minimal profile for Portal
- move essential CLI tools into the minimal package set
- remove duplicated packages from the full programs profile
- add the portal-1 SSH host on port 278
2026-08-26 07:33:31 -04:00
Richie 6fa93e1b96 feat(portal): add Vultr NixOS host configuration
- use the shared global base and Tailscale module
- configure Portal SSH and VM memory settings
- add Richie's minimal Home Manager profile
- document SOPS and nixos-anywhere deployment
2026-08-26 07:33:31 -04:00
Richie b8b0605279 refactor(nixos): make ZFS configuration optional
- move ZFS and snapshot manager settings out of the global base
- group snapshot files under common/optional/zfs
- enable the ZFS module on existing ZFS hosts
- make the global tmpfs setting overridable
2026-08-26 07:33:31 -04:00
Richie 03537310cb feat(tailscale): add Tailscale service configuration and update secrets 2026-08-26 07:33:31 -04:00
Richie a23c91c4ac feat(global): refactor configuration by moving settings to minimal.nix 2026-08-26 07:33:31 -04:00
Richie f24a14920e feat(flake): add disko to flake.nix for nixos-anywhere 2026-08-26 07:33:31 -04:00
Richie 48806c84fe fix(default.nix): remove permitted insecure packages
pytest / pytest (pull_request) Successful in 29s
test ebook search / test-ebook-search (pull_request) Successful in 35s
build_systems / build-rhapsody-in-green (pull_request) Successful in 58s
treefmt / nix fmt (pull_request) Successful in 5s
build_systems / build-brain (pull_request) Successful in 47s
build_systems / build-bob (pull_request) Successful in 51s
build_systems / build-jeeves (pull_request) Successful in 2m15s
treefmt / nix fmt (push) Successful in 5s
build_systems / build-brain (push) Successful in 36s
build_systems / build-rhapsody-in-green (push) Successful in 47s
build_systems / build-bob (push) Successful in 9s
pytest / pytest (push) Successful in 31s
test ebook search / test-ebook-search (push) Successful in 32s
build_systems / build-jeeves (push) Successful in 2m1s
2026-08-25 16:13:38 -04:00
Richie 03c3b36f9c feat(home_assistant): add energy meter sensors filter
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m2s
treefmt / nix fmt (pull_request) Successful in 6s
test ebook search / test-ebook-search (pull_request) Successful in 39s
pytest / pytest (pull_request) Successful in 40s
build_systems / build-bob (pull_request) Successful in 49s
build_systems / build-brain (pull_request) Successful in 50s
build_systems / build-jeeves (pull_request) Successful in 2m14s
test ebook search / test-ebook-search (push) Successful in 33s
build_systems / build-rhapsody-in-green (push) Successful in 47s
build_systems / build-jeeves (push) Successful in 1m59s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 29s
build_systems / build-brain (push) Successful in 35s
build_systems / build-bob (push) Successful in 37s
2026-08-25 12:11:06 -04:00
Richie 507b23f6ee feat(van-weather): port weather service to Rust
treefmt / nix fmt (push) Successful in 6s
pytest / pytest (push) Successful in 30s
test ebook search / test-ebook-search (push) Successful in 33s
build_systems / build-brain (push) Successful in 35s
build_systems / build-bob (push) Successful in 9s
build_systems / build-rhapsody-in-green (push) Successful in 46s
build_systems / build-jeeves (push) Successful in 2m16s
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 33s
test ebook search / test-ebook-search (pull_request) Successful in 38s
build_systems / build-bob (pull_request) Successful in 57s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m6s
build_systems / build-brain (pull_request) Successful in 1m47s
build_systems / build-jeeves (pull_request) Successful in 2m27s
Add a Rust implementation with privacy-masked locations, HTTP retries,
and Home Assistant sensor publishing. Package it with Nix and switch the
systemd service from Python to the compiled binary.
2026-08-18 22:39:52 -04:00
Richie e5132e2a0b fix(home_assistant): update monarch package name in extraPackages
treefmt / nix fmt (push) Successful in 5s
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 30s
test ebook search / test-ebook-search (pull_request) Successful in 37s
build_systems / build-bob (pull_request) Successful in 50s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m4s
build_systems / build-jeeves (pull_request) Successful in 2m24s
build_systems / build-brain (pull_request) Successful in 6m21s
test ebook search / test-ebook-search (push) Successful in 30s
pytest / pytest (push) Successful in 33s
build_systems / build-brain (push) Successful in 37s
build_systems / build-bob (push) Successful in 39s
build_systems / build-rhapsody-in-green (push) Successful in 56s
build_systems / build-jeeves (push) Successful in 2m16s
2026-08-18 21:25:46 -04:00
Richie 107b4f24d1 fix(home_assistant): update firewall settings and remove redundant http server port
treefmt / nix fmt (pull_request) Successful in 6s
build_systems / build-brain (pull_request) Failing after 31s
pytest / pytest (pull_request) Successful in 29s
test ebook search / test-ebook-search (pull_request) Successful in 35s
build_systems / build-bob (pull_request) Successful in 48s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m2s
build_systems / build-jeeves (pull_request) Successful in 2m32s
2026-08-18 21:24:18 -04:00
Richie b48f5da6c8 flake update
treefmt / nix fmt (pull_request) Successful in 6s
build_systems / build-brain (pull_request) Failing after 1m26s
test ebook search / test-ebook-search (pull_request) Successful in 5m25s
pytest / pytest (pull_request) Successful in 5m58s
build_systems / build-jeeves (pull_request) Successful in 44m39s
build_systems / build-bob (pull_request) Successful in 49m42s
build_systems / build-rhapsody-in-green (pull_request) Successful in 57m20s
2026-08-18 15:44:41 -04:00
Richie cb3eb83935 feat(home_assistant): remove Home Assistant configuration and references
treefmt / nix fmt (pull_request) Successful in 8s
test ebook search / test-ebook-search (pull_request) Successful in 46s
build_systems / build-brain (pull_request) Successful in 1m2s
build_systems / build-bob (pull_request) Successful in 1m2s
build_systems / build-jeeves (pull_request) Successful in 2m32s
pytest / pytest (pull_request) Successful in 40s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m13s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 36s
build_systems / build-brain (push) Successful in 39s
test ebook search / test-ebook-search (push) Successful in 37s
build_systems / build-bob (push) Successful in 42s
build_systems / build-rhapsody-in-green (push) Successful in 51s
build_systems / build-jeeves (push) Successful in 2m16s
2026-08-18 15:40:35 -04:00
Richie ac884c069b feat(home_assistant): add garmin_connect to customComponents
treefmt / nix fmt (pull_request) Successful in 7s
pytest / pytest (pull_request) Successful in 33s
test ebook search / test-ebook-search (pull_request) Successful in 41s
build_systems / build-brain (pull_request) Successful in 53s
build_systems / build-jeeves (pull_request) Successful in 2m50s
build_systems / build-rhapsody-in-green (pull_request) Successful in 27m23s
build_systems / build-bob (pull_request) Successful in 31m19s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 30s
build_systems / build-brain (push) Successful in 35s
test ebook search / test-ebook-search (push) Successful in 36s
build_systems / build-bob (push) Successful in 38s
build_systems / build-rhapsody-in-green (push) Successful in 48s
build_systems / build-jeeves (push) Successful in 2m10s
2026-08-17 09:45:27 -04:00
Richie 1406148517 fix(ruff): add ignore rule for missing license in files
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 28s
test ebook search / test-ebook-search (pull_request) Successful in 34s
build_systems / build-brain (pull_request) Successful in 45s
build_systems / build-bob (pull_request) Successful in 46s
build_systems / build-rhapsody-in-green (pull_request) Successful in 59s
build_systems / build-jeeves (pull_request) Successful in 2m18s
treefmt / nix fmt (push) Successful in 6s
pytest / pytest (push) Successful in 30s
build_systems / build-brain (push) Successful in 35s
build_systems / build-bob (push) Successful in 36s
test ebook search / test-ebook-search (push) Successful in 34s
build_systems / build-rhapsody-in-green (push) Successful in 46s
build_systems / build-jeeves (push) Successful in 2m2s
2026-08-13 12:54:32 -04:00
Richie 8eeacc33d6 adding rust tools
treefmt / nix fmt (pull_request) Failing after 5s
test ebook search / test-ebook-search (pull_request) Successful in 38s
pytest / pytest (pull_request) Successful in 41s
build_systems / build-bob (pull_request) Successful in 1m31s
build_systems / build-brain (pull_request) Successful in 2m25s
build_systems / build-jeeves (pull_request) Successful in 2m31s
build_systems / build-rhapsody-in-green (pull_request) Successful in 2m31s
2026-08-13 12:41:36 -04:00
Richie c1cee8dbcd adding hardware design tools
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 30s
test ebook search / test-ebook-search (pull_request) Successful in 35s
build_systems / build-brain (pull_request) Successful in 52s
build_systems / build-bob (pull_request) Successful in 52s
build_systems / build-rhapsody-in-green (pull_request) Successful in 2m16s
build_systems / build-jeeves (pull_request) Successful in 2m23s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 34s
build_systems / build-brain (push) Successful in 38s
test ebook search / test-ebook-search (push) Successful in 38s
build_systems / build-bob (push) Successful in 41s
build_systems / build-rhapsody-in-green (push) Successful in 55s
build_systems / build-jeeves (push) Successful in 2m30s
2026-08-05 21:28:39 -04:00
Richie bd50bd8262 removing cuda workaround
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 30s
test ebook search / test-ebook-search (pull_request) Successful in 35s
build_systems / build-brain (pull_request) Successful in 47s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m3s
build_systems / build-jeeves (pull_request) Successful in 4m3s
build_systems / build-bob (pull_request) Successful in 23m53s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 29s
build_systems / build-brain (push) Successful in 32s
build_systems / build-bob (push) Successful in 35s
build_systems / build-rhapsody-in-green (push) Successful in 46s
test ebook search / test-ebook-search (push) Successful in 34s
build_systems / build-jeeves (push) Successful in 2m5s
2026-08-03 23:06:44 -04:00
Richie 47e753f5b9 fix(flake.lock): update locked versions and hashes for dependencies
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 2m51s
test ebook search / test-ebook-search (pull_request) Successful in 2m55s
build_systems / build-jeeves (pull_request) Successful in 31m24s
build_systems / build-brain (pull_request) Successful in 32m14s
build_systems / build-rhapsody-in-green (pull_request) Successful in 43m4s
build_systems / build-bob (pull_request) Successful in 48m11s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 30s
build_systems / build-brain (push) Successful in 32s
test ebook search / test-ebook-search (push) Successful in 36s
build_systems / build-bob (push) Successful in 38s
build_systems / build-rhapsody-in-green (push) Successful in 45s
build_systems / build-jeeves (push) Successful in 2m5s
2026-08-03 21:44:06 -04:00
Richie 48a7e3a54c feat(zfs): enhance command handling with run_zfs and run_zpool functions
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 30s
test ebook search / test-ebook-search (pull_request) Successful in 36s
build_systems / build-brain (pull_request) Successful in 46s
build_systems / build-bob (pull_request) Successful in 47s
build_systems / build-rhapsody-in-green (pull_request) Successful in 58s
build_systems / build-jeeves (pull_request) Successful in 2m23s
pytest / pytest (push) Successful in 33s
test ebook search / test-ebook-search (push) Successful in 42s
build_systems / build-jeeves (push) Successful in 2m26s
treefmt / nix fmt (push) Successful in 5s
build_systems / build-brain (push) Successful in 9s
build_systems / build-bob (push) Successful in 40s
build_systems / build-rhapsody-in-green (push) Successful in 53s
2026-07-30 12:49:18 -04:00
Richie cc166df90f fix(open_webui): update OLLAMA_API_BASE_URL to use HTTPS
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 31s
test ebook search / test-ebook-search (pull_request) Successful in 34s
build_systems / build-jeeves (pull_request) Successful in 2m18s
build_systems / build-brain (push) Successful in 34s
build_systems / build-bob (push) Successful in 37s
build_systems / build-rhapsody-in-green (push) Successful in 47s
build_systems / build-jeeves (push) Successful in 2m8s
build_systems / build-brain (pull_request) Successful in 46s
build_systems / build-bob (pull_request) Successful in 48s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m9s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 31s
test ebook search / test-ebook-search (push) Successful in 36s
2026-07-27 21:10:48 -04:00
Richie 4384853430 feat(t3_code): add T3 Code AppImage launcher and desktop entry 2026-07-27 21:10:41 -04:00
Richie ed8b653997 feat(richie user env): adding app_image_path configuration
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 29s
test ebook search / test-ebook-search (pull_request) Successful in 33s
build_systems / build-bob (pull_request) Successful in 49s
build_systems / build-brain (pull_request) Successful in 49s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m7s
build_systems / build-jeeves (pull_request) Successful in 2m23s
2026-07-27 20:23:52 -04:00
Richie aff7398f8d fix: rename 'models' to 'modelsDir' for consistency in llms.nix configurations
treefmt / nix fmt (pull_request) Successful in 5s
test ebook search / test-ebook-search (pull_request) Successful in 42s
pytest / pytest (pull_request) Successful in 42s
build_systems / build-brain (pull_request) Successful in 49s
build_systems / build-bob (pull_request) Successful in 52s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m9s
build_systems / build-jeeves (pull_request) Successful in 2m34s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 31s
test ebook search / test-ebook-search (push) Successful in 32s
build_systems / build-brain (push) Successful in 34s
build_systems / build-bob (push) Successful in 37s
build_systems / build-rhapsody-in-green (push) Successful in 49s
build_systems / build-jeeves (push) Successful in 2m6s
2026-07-27 20:15:35 -04:00
Richie e259526c38 feat(settings): add configuration to disable YAML schema detection for specific workflow files
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 32s
test ebook search / test-ebook-search (pull_request) Successful in 38s
build_systems / build-bob (pull_request) Successful in 53s
build_systems / build-brain (pull_request) Successful in 51s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m5s
build_systems / build-jeeves (pull_request) Successful in 1m57s
treefmt / nix fmt (push) Successful in 23s
pytest / pytest (push) Successful in 36s
test ebook search / test-ebook-search (push) Successful in 40s
build_systems / build-brain (push) Successful in 53s
build_systems / build-bob (push) Successful in 54s
build_systems / build-rhapsody-in-green (push) Successful in 1m5s
build_systems / build-jeeves (push) Successful in 2m17s
2026-07-25 18:44:13 -04:00
Richie e57895cc6e patch cuda
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 32s
test ebook search / test-ebook-search (pull_request) Successful in 46s
build_systems / build-brain (pull_request) Successful in 54s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m15s
build_systems / build-jeeves (pull_request) Successful in 4m38s
build_systems / build-bob (pull_request) Successful in 28m10s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 31s
test ebook search / test-ebook-search (push) Successful in 36s
build_systems / build-brain (push) Successful in 38s
build_systems / build-bob (push) Successful in 38s
build_systems / build-rhapsody-in-green (push) Successful in 51s
build_systems / build-jeeves (push) Successful in 2m12s
2026-07-25 14:21:07 -04:00
Richie 89e24c45a0 chore: remove poetry from Python packages list
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 31s
test ebook search / test-ebook-search (pull_request) Successful in 35s
build_systems / build-bob (pull_request) Failing after 5m8s
build_systems / build-rhapsody-in-green (pull_request) Successful in 7m20s
build_systems / build-brain (pull_request) Successful in 7m45s
build_systems / build-jeeves (pull_request) Successful in 8m12s
2026-07-25 13:59:24 -04:00
Richie 2e7b51ce1d flake update
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 30s
test ebook search / test-ebook-search (pull_request) Successful in 39s
build_systems / build-bob (pull_request) Failing after 4m49s
build_systems / build-brain (pull_request) Failing after 5m46s
build_systems / build-jeeves (pull_request) Failing after 6m44s
build_systems / build-rhapsody-in-green (pull_request) Failing after 7m40s
2026-07-25 13:47:36 -04:00
Richie 8eee5faf72 feat(chat): add optional response format parameter to request_chat_completion
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 30s
test ebook search / test-ebook-search (pull_request) Successful in 36s
build_systems / build-brain (pull_request) Successful in 51s
build_systems / build-bob (pull_request) Successful in 52s
build_systems / build-jeeves (pull_request) Successful in 2m23s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m3s
treefmt / nix fmt (push) Successful in 6s
pytest / pytest (push) Successful in 31s
build_systems / build-brain (push) Successful in 37s
test ebook search / test-ebook-search (push) Successful in 38s
build_systems / build-bob (push) Successful in 40s
build_systems / build-rhapsody-in-green (push) Successful in 53s
build_systems / build-jeeves (push) Successful in 2m11s
2026-07-24 11:38:51 -04:00
Richie c135821534 feat(ingest): ensure session rollback on ingestion error and log mention count 2026-07-24 11:38:51 -04:00
Richie 62bcc4e156 feat(judge_ngrams): enhance phrase judgment with alias filtering and strict boolean handling 2026-07-24 11:38:51 -04:00
Richie 31ecad881f feat(extraction): add deduplication limit parameter to YAKE extractor 2026-07-24 11:38:51 -04:00
Richie 68c9693711 feat(config): add new scoring parameters for phrase extraction and matching 2026-07-24 11:38:51 -04:00
Richie 6a0e71a30d feat(ebook-search): implement async file path resolution and EPUB discovery 2026-07-24 11:38:51 -04:00
Richie 8073144e2b feat(prompts): implement TOML-backed prompt loading and refactor message generation 2026-07-24 11:38:51 -04:00
Richie 94f18722e4 feat(search): enhance phrase matching and reranking logic with improved async handling 2026-07-24 11:38:51 -04:00
Richie e510c94b95 feat(dependencies): update database engine dependencies to support async operations 2026-07-24 11:38:51 -04:00
Richie 9f126fedc7 feat(ebook-search): implement async phrase matching for chunks and add ChunkPhraseHit model 2026-07-24 11:38:51 -04:00
Richie e010756e09 refactor(ebook-search): simplify search and phrase matching 2026-07-24 11:38:51 -04:00
Richie 0028237579 perf(ebook-search): run phrase detection in parallel with retrieval
Move protected phrase detection into the retrieval gather so it runs
concurrently with vector and BM25 candidates instead of sequentially
before them. Make the search API accept real bool form fields for
rerank/phrase_matching, gate phrase matching on both the request and
config kill switch, and reflow log f-strings for readability.
2026-07-24 11:38:51 -04:00
Richie 8f1a69529c Refactor logging statements to use f-strings for improved readability and consistency across the codebase. This change enhances the clarity of log messages by directly embedding variable values, making it easier to trace and debug application behavior. 2026-07-24 11:38:51 -04:00
Richie 5210f00587 feat(dependencies): update sqlalchemy to use asyncio and add aiosqlite and pytest-asyncio 2026-07-24 11:38:51 -04:00
Richie 58be234d7f fix(protected-phrases): isolate phrase generation per book
Run full-book candidate generation inside worker-owned sessions so each book commits independently during backfills. Abort recalculation when a book has no indexed chapters to preserve existing phrase data, and update admin/UI tests for the new generation flow.
2026-07-24 11:38:50 -04:00
Richie 29a51eb1b8 feat(admin): simplify phrase generation by removing missing phrases endpoint 2026-07-24 11:38:50 -04:00
Richie da78914a9f refactor(ebook): remove spaCy-ner attributes from PhraseCandidate and related functions 2026-07-24 11:38:50 -04:00
Richie c852f9136a feat(dependencies): update sqlalchemy to use asyncio and add aiosqlite and pytest-asyncio to dev dependencies 2026-07-24 11:38:50 -04:00
Richie 565119ee45 test(ebook): cover protected phrases and migrate suite to async
Add test_protected_phrases.py covering phrase-matching behavior in the
RAG engine, and update the existing ebook_search tests to use the async
SQLAlchemy engine/session (create_async_engine, AsyncSession) and async
HTTP paths.
2026-07-24 11:38:50 -04:00
Richie 56c9bb2520 feat(ebook): add additional tokens to junk tokens configuration 2026-07-24 11:38:50 -04:00
Richie f69c84e7b6 feat(vscode): add new words to spell checker configuration 2026-07-24 11:38:50 -04:00
Richie 5c826088c5 feat(ebook): improve search UX with grid actions and Enter-to-submit
Add a two-column grid layout for the admin protected-phrases actions
and submit the search form on Enter (Shift+Enter for newline).
2026-07-24 11:38:50 -04:00
Richie 2706c4417d feat(ebook): migrate to async DB/HTTP and parallelize phrase pipeline
Convert the ebook-search web app to async end to end and add concurrency
to the protected-phrase extraction and judging pipeline so large books no
longer block the event loop or the UI.

ORM / infra:
- Add get_async_postgres_engine and factor shared URL/connect_args building
  into build_postgres_url (reused by the sync and async engine builders)
- Add async FastAPI session helpers (get_async_db, AsyncDbSession) with
  expire_on_commit=False to avoid implicit IO under asyncio

App:
- Use AsyncEngine/AsyncSession throughout routes, search, ingest, embeddings,
  answer, rerank and LLM calls; convert handlers to async
- Share a single httpx.AsyncClient in app state for LLM requests; size the
  connection pool for concurrent phrase-judging workers
- Add judge_tasks: run per-book judging as tracked background tasks so a
  book already being judged isn't double-queued

Protected phrases:
- Add a process pool (pool.py) and worker-count config
  (extraction/judge book/phrase workers) to parallelize candidate generation
  and judging
- Split admin actions into all/missing variants for generation and judging

Config:
- Add protected_phrase_extraction_workers, phrase_judge_book_workers,
  phrase_judge_phrase_workers
2026-07-24 11:38:50 -04:00
Richie 38c01ec121 feat(extraction): add cached YAKE extractor for improved performance 2026-07-24 11:38:50 -04:00
Richie a9311a2f9e feat(ebook): add junk tokens for improved phrase matching 2026-07-24 11:38:50 -04:00
Richie 584b209dfa feat(orm): add pool_size parameter to get_postgres_engine for connection management 2026-07-24 11:38:50 -04:00
Richie 4861f58f27 Add models and database persistence for protected phrase extraction
- Introduced dataclasses for phrase candidates, judgments, and matches in `models.py`.
- Implemented database operations for candidate and protected phrases in `store.py`, including loading, saving, and deleting phrases.
- Enhanced text normalization functions in `text_normalization.py` with detailed docstrings.
- Refactored search functionality to utilize new models and methods for detecting protected phrases.
2026-07-24 11:38:50 -04:00
Richie 34e7823517 feat(ebook): add phrase matching display and update search result structure 2026-07-24 11:38:50 -04:00
Richie 11b5d5db3c refactor(protected-phrases): extract config and text normalization helpers 2026-07-24 11:38:50 -04:00
Richie 8e2ca365c2 feat(ebook): update protected phrases with additional tokens and phrases 2026-07-24 11:38:50 -04:00
Richie 4c71508e73 feat(ebook): enhance phrase judgment logging with failure tracking 2026-07-24 11:38:50 -04:00
Richie 70bf8627a2 feat(ebook): implement phrase matching functionality and UI enhancements 2026-07-24 11:38:50 -04:00
Richie a12e7461c5 feat(ebook): add admin and book-detail UI for protected phrase pipeline
Expose the protected phrase extraction pipeline through the web UI:

- Admin routes: POST /admin/build-phrases, /admin/generate-ngrams, and
  /admin/judge-ngrams, each wrapping the protected_phrases.lib backfill
  helpers, committing on success, rolling back and rendering an error
  partial on failure, and reporting per-book/candidate/mention counts.
- Book detail page: show candidate, judged, and protected phrase counts,
  list top candidate n-grams (with kept/rejected status) and protected
  phrases, and add a POST /books/{id}/recalculate-phrases action that
  clears and regenerates candidates, then redirects back with a status
  message.
- Admin template: add Generate/Judge n-gram buttons.

Also reflows admin.html to 2-space HTML formatting.
2026-07-24 11:38:50 -04:00
Richie 6c8a4bfea7 feat(ebook): add protected phrase extraction library with config-driven tuning
Refactor protected phrase handling from a single module into a
python/ebook_search/protected_phrases package covering extraction,
storage, and runtime matching. Phrase filtering is now data-driven via
bundled TOML files: ignored_phrases, bad_starts, bad_ends, and
most_common_words.

Add phrase-tuning settings to EbookSearchConfig so candidate generation,
scoring, LLM judging, and matching are configurable rather than hardcoded:
token bounds, entity token limit, raw n-gram min count, frequency and
chapter-spread score thresholds, candidate/LLM/target caps, confidence
threshold, nesting defaults, and the phrase hit boost.
2026-07-24 11:38:50 -04:00
Richie fbb1ebfd56 fix(ebook): enhance EPUB ingestion with error handling and incrmental commits 2026-07-24 11:38:50 -04:00
Richie 9ba8200673 feat(ebook): add phrase metadata tables for protected phrase matching
Introduce four ORM models and their Alembic migration to support
phrase-based query matching in the ebook RAG engine:

- EbookCandidatePhrase: high-recall phrase candidates extracted per book,
  with source flags (ngram/yake/spacy/capitalized/metadata), scoring, and
  LLM judge results.
- EbookProtectedPhrase: phrases accepted by the LLM judge, with canonical
  id, importance, and nesting controls.
- EbookPhraseAlias: normalized aliases mapping to protected phrases.
- EbookChunkPhraseMention: precomputed phrase occurrences within chunks.

Export the new models from python.orm.richie and add a JSON_DOCUMENT
helper (JSON with JSONB postgres variant) for storing sample contexts.
2026-07-24 11:38:50 -04:00
Richie add7a6a848 feat(gems): add multiplayer gem game with custom content packs
treefmt / nix fmt (pull_request) Successful in 6s
test ebook search / test-ebook-search (pull_request) Successful in 39s
pytest / pytest (pull_request) Successful in 40s
build_systems / build-brain (pull_request) Successful in 1m0s
build_systems / build-bob (pull_request) Successful in 1m2s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m33s
build_systems / build-jeeves (pull_request) Successful in 2m25s
test ebook search / test-ebook-search (push) Successful in 35s
build_systems / build-brain (push) Successful in 37s
build_systems / build-bob (push) Successful in 37s
build_systems / build-jeeves (push) Successful in 2m17s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 29s
build_systems / build-rhapsody-in-green (push) Successful in 49s
- implement FastAPI and HTMX lobby and game interfaces
- support up to four human and AI-controlled players
- add configurable rules, victory conditions, and expansion modules
- support validated JSON cards, patrons, objectives, and outposts
2026-07-22 20:47:03 -04:00
Richie 247e951a27 Remove legacy HTML templates and API service configuration for contact management system
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 32s
test ebook search / test-ebook-search (pull_request) Successful in 35s
build_systems / build-brain (pull_request) Successful in 50s
build_systems / build-bob (pull_request) Successful in 51s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m5s
build_systems / build-jeeves (pull_request) Successful in 2m26s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 29s
test ebook search / test-ebook-search (push) Successful in 30s
build_systems / build-brain (push) Successful in 36s
build_systems / build-bob (push) Successful in 36s
build_systems / build-rhapsody-in-green (push) Successful in 49s
build_systems / build-jeeves (push) Successful in 2m9s
- Deleted base template (base.html) and all related contact templates (contact_detail.html, contact_form.html, contact_list.html, graph.html, need_list.html).
- Removed partial templates for managing contacts and needs (contact_table.html, manage_needs.html, manage_relationships.html, need_items.html).
- Eliminated contact API service configuration (contact_api.nix) from the NixOS setup.
2026-07-15 15:28:15 -04:00
Richie 8c3de690c9 feat(ebook-search): set up uv cache management and pruning service
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 27s
test ebook search / test-ebook-search (pull_request) Successful in 34s
build_systems / build-brain (pull_request) Successful in 48s
build_systems / build-bob (pull_request) Successful in 50s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m3s
build_systems / build-jeeves (pull_request) Successful in 2m14s
treefmt / nix fmt (push) Successful in 4s
test ebook search / test-ebook-search (push) Successful in 27s
pytest / pytest (push) Successful in 30s
build_systems / build-brain (push) Successful in 34s
build_systems / build-bob (push) Successful in 37s
build_systems / build-jeeves (push) Successful in 2m9s
build_systems / build-rhapsody-in-green (push) Successful in 48s
2026-07-12 17:26:55 -04:00
Richie 91575e3ab2 feat(ebook-search): update test workflow to use nix for dependency management 2026-07-12 17:10:47 -04:00
Richie 4799054601 ci(ebook-search): run tests via uv on the runner instead of docker
build_systems / build-brain (pull_request) Successful in 44s
build_systems / build-jeeves (pull_request) Successful in 2m54s
test ebook search / test-ebook-search (pull_request) Failing after 19s
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 27s
build_systems / build-bob (pull_request) Successful in 47s
build_systems / build-rhapsody-in-green (pull_request) Successful in 56s
The gitea runner containers have no docker access, so build the test
env with uv from the existing lockfile and run pytest directly:

- test_ebook_search workflow: uv sync --locked + uv run pytest, with
  UV_PYTHON_DOWNLOADS=never so uv uses the nix-provided python 3.14
- add uv to the runner hostPackages (needs a jeeves rebuild to apply)
- ignore nested **/.venv in .dockerignore (uv sync creates one in
  python/ebook_search/docker)
- document the uv test commands in the docker README; the docker test
  image remains for validating the image itself
2026-07-12 16:40:56 -04:00
Richie e9636d19de feat(ebook-search): add containerized test environment
pytest / pytest (pull_request) Successful in 26s
build_systems / build-brain (pull_request) Successful in 45s
build_systems / build-bob (pull_request) Successful in 45s
build_systems / build-rhapsody-in-green (pull_request) Successful in 57s
build_systems / build-jeeves (pull_request) Successful in 2m17s
treefmt / nix fmt (pull_request) Successful in 5s
test ebook search / test-ebook-search (pull_request) Failing after 4s
2026-07-12 16:12:40 -04:00
Richie 2518ec8551 feat(python-env): remove unused dependencies and clean up package list 2026-07-12 16:12:40 -04:00
Richie f5ec88f1e5 feat(ebook): add Docker packaging and lifecycle tooling
Add a self-contained docker/ package for running the ebook search app
against the existing Postgres database on jeeves:

- Dockerfile: python:3.14-slim image, non-root user, runs the FastAPI
  app on port 8070
- docker-compose.yml: service definition with library volume mount,
  BM25 index volume, .env loading, and a /health healthcheck
- containers.py: Typer CLI (ebook-search-containers) for build/start/
  stop/restart/logs/ps lifecycle management
- README.md: usage and configuration docs
2026-07-12 16:12:40 -04:00
Richie f4c4b11ff8 updated dependencies and added .dockerignore 2026-07-12 16:12:40 -04:00
Richie 37f37c41ac feat(common): add get_repo_dir function and corresponding tests
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 24s
build_systems / build-brain (pull_request) Successful in 44s
build_systems / build-bob (pull_request) Successful in 44s
build_systems / build-rhapsody-in-green (pull_request) Successful in 57s
build_systems / build-jeeves (pull_request) Successful in 2m17s
treefmt / nix fmt (push) Successful in 5s
build_systems / build-brain (push) Successful in 8s
pytest / pytest (push) Successful in 23s
build_systems / build-bob (push) Successful in 30s
build_systems / build-rhapsody-in-green (push) Successful in 42s
build_systems / build-jeeves (push) Successful in 2m2s
2026-07-12 14:24:05 -04:00
Richie 80a521f297 refactor: extract signal_alert into its own module
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 24s
build_systems / build-bob (pull_request) Successful in 45s
build_systems / build-brain (pull_request) Successful in 45s
build_systems / build-rhapsody-in-green (pull_request) Successful in 58s
build_systems / build-jeeves (pull_request) Successful in 2m15s
treefmt / nix fmt (push) Successful in 5s
build_systems / build-jeeves (push) Successful in 9s
pytest / pytest (push) Successful in 24s
build_systems / build-brain (push) Successful in 30s
build_systems / build-bob (push) Successful in 32s
build_systems / build-rhapsody-in-green (push) Successful in 44s
Move signal_alert out of python/common.py into a dedicated
python/signal_alert.py module and update its importers
(validate_system.py, snapshot_manager.py) to the new path.

Relocate the signal_alert tests from tests/test_common.py into
tests/test_signal_alert.py, repatching python.signal_alert.logger and
python.signal_alert.Apprise to match the new module.
2026-07-12 13:16:28 -04:00
Richie 4a410dbdf8 feat(postgres): add trust authentication for richie on 172.16.0.0/12
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 23s
build_systems / build-brain (pull_request) Successful in 44s
build_systems / build-bob (pull_request) Successful in 45s
build_systems / build-jeeves (pull_request) Successful in 2m20s
build_systems / build-rhapsody-in-green (pull_request) Successful in 56s
treefmt / nix fmt (push) Successful in 5s
build_systems / build-rhapsody-in-green (push) Successful in 11s
pytest / pytest (push) Successful in 24s
build_systems / build-brain (push) Successful in 30s
build_systems / build-bob (push) Successful in 33s
build_systems / build-jeeves (push) Successful in 2m7s
2026-07-12 13:06:44 -04:00
Richie 138b79bf97 feat(flake): update lock file
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 1m12s
build_systems / build-brain (pull_request) Successful in 2m46s
build_systems / build-bob (pull_request) Successful in 3m4s
build_systems / build-rhapsody-in-green (pull_request) Successful in 7m42s
build_systems / build-jeeves (pull_request) Successful in 7m44s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 23s
build_systems / build-brain (push) Successful in 30s
build_systems / build-bob (push) Successful in 32s
build_systems / build-rhapsody-in-green (push) Successful in 45s
build_systems / build-jeeves (push) Successful in 2m10s
2026-07-12 12:49:57 -04:00
Richie 47f77443dd feat(networking): configure unused network ports to prevent errors and optimize boot time
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 49s
build_systems / build-bob (pull_request) Successful in 1m9s
build_systems / build-brain (pull_request) Successful in 1m12s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m15s
treefmt / nix fmt (push) Successful in 6s
pytest / pytest (push) Successful in 25s
build_systems / build-jeeves (pull_request) Successful in 2m59s
build_systems / build-brain (push) Successful in 32s
build_systems / build-bob (push) Successful in 33s
build_systems / build-rhapsody-in-green (push) Successful in 46s
build_systems / build-jeeves (push) Successful in 2m26s
2026-07-09 10:45:08 -04:00
Richie 8548f4ff64 feat(programs): add codebase-memory-mcp to home packages
treefmt / nix fmt (pull_request) Successful in 8s
pytest / pytest (pull_request) Successful in 1m50s
build_systems / build-brain (pull_request) Successful in 9m47s
build_systems / build-rhapsody-in-green (pull_request) Successful in 24m1s
build_systems / build-bob (pull_request) Successful in 31m2s
build_systems / build-jeeves (pull_request) Successful in 6m55s
treefmt / nix fmt (push) Successful in 6s
build_systems / build-brain (push) Successful in 33s
build_systems / build-bob (push) Successful in 39s
pytest / pytest (push) Successful in 42s
build_systems / build-rhapsody-in-green (push) Successful in 51s
build_systems / build-jeeves (push) Successful in 2m25s
2026-07-02 16:08:12 -04:00
Richie 682960b4c9 feat(installer): refactor NixOS configurations to dynamically generate host systems
pytest / pytest (pull_request) Successful in 25s
build_systems / build-brain (pull_request) Successful in 45s
build_systems / build-bob (pull_request) Successful in 46s
build_systems / build-jeeves (pull_request) Successful in 2m30s
build_systems / build-rhapsody-in-green (pull_request) Successful in 56s
treefmt / nix fmt (pull_request) Successful in 5s
treefmt / nix fmt (push) Successful in 9s
pytest / pytest (push) Successful in 29s
build_systems / build-brain (push) Successful in 35s
build_systems / build-bob (push) Successful in 35s
build_systems / build-rhapsody-in-green (push) Successful in 48s
build_systems / build-jeeves (push) Successful in 2m28s
2026-07-02 14:33:45 -04:00
Richie ddea13b93a feat(installer): enhance NixOS installer with SSH access and encryption password input 2026-07-02 14:17:49 -04:00
Richie b252b0bf6e feat(installer): add one-file installer build and custom install ISO
Replace old_installer.py with a curses TUI installer packaged as a
one-file PyInstaller binary (python/installer/build.py, wrapped by
python/installer/package.nix). The default .#installer is patched to run
on foreign Linux live media; the new .#installer-nixos variant keeps its
Nix store interpreter so it runs on NixOS.

Add systems/iso, a minimal NixOS install CD with kernel 6.18 and ZFS 2.4
matching the deployed systems and the installer on PATH; build it with
nix build .#iso. Shared logging and subprocess helpers move out of
common.py into python/logging_config.py and python/process.py.
2026-07-02 14:17:49 -04:00
Richie fbf288649a feat(haproxy): simplify Gitea rate limiting and fix rule ordering
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 25s
build_systems / build-brain (pull_request) Successful in 43s
build_systems / build-bob (pull_request) Successful in 45s
build_systems / build-rhapsody-in-green (pull_request) Successful in 58s
build_systems / build-jeeves (pull_request) Successful in 2m31s
build_systems / build-rhapsody-in-green (push) Successful in 13s
build_systems / build-jeeves (push) Successful in 2m15s
treefmt / nix fmt (push) Failing after 13m42s
build_systems / build-brain (push) Failing after 18m43s
build_systems / build-bob (push) Failing after 18m43s
pytest / pytest (push) Failing after 23m43s
- Drop the session-cookie rate-limit bypass: Gitea hands the cookie to
  anonymous visitors too, so any cookie-accepting crawler could exempt
  itself after one request
- Loosen the general Gitea limit from 10 to 50 req/10s since logged-in
  users are now rate-limited as well
- Serve robots.txt above the rate-limit rules so throttled crawlers can
  always read the Crawl-delay, and capture Host/User-Agent before the
  deny so 429s are logged with them
- Match the compare/diff cap on every repo case-insensitively instead
  of only /Richie/dotfiles
- Remove the Jellyfin health check; it never ran (server had no check
  flag) and is not wanted
2026-07-01 16:54:07 -04:00
Richie db199f884d feat(comms): add Slack to communication packages
treefmt / nix fmt (pull_request) Successful in 7s
pytest / pytest (pull_request) Successful in 31s
build_systems / build-brain (pull_request) Successful in 54s
build_systems / build-bob (pull_request) Successful in 55s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m27s
build_systems / build-jeeves (pull_request) Successful in 2m48s
treefmt / nix fmt (push) Successful in 4s
pytest / pytest (push) Successful in 24s
build_systems / build-brain (push) Successful in 28s
build_systems / build-bob (push) Successful in 30s
build_systems / build-rhapsody-in-green (push) Successful in 44s
build_systems / build-jeeves (push) Successful in 2m16s
2026-07-01 12:14:23 -04:00
Richie c6645eb37c feat(home-assistant): remove status indicator configuration and related YAML file
pytest / pytest (pull_request) Successful in 25s
build_systems / build-rhapsody-in-green (pull_request) Successful in 56s
treefmt / nix fmt (pull_request) Successful in 5s
build_systems / build-brain (pull_request) Successful in 43s
build_systems / build-bob (pull_request) Successful in 44s
build_systems / build-jeeves (pull_request) Successful in 2m25s
treefmt / nix fmt (push) Successful in 4s
pytest / pytest (push) Successful in 24s
build_systems / build-brain (push) Successful in 30s
build_systems / build-bob (push) Successful in 32s
build_systems / build-rhapsody-in-green (push) Successful in 44s
build_systems / build-jeeves (push) Successful in 2m11s
2026-07-01 00:21:05 -04:00
Richie 3984081753 feat(cleanup): remove leviathan and elise configurations and references
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 25s
build_systems / build-brain (pull_request) Successful in 47s
build_systems / build-bob (pull_request) Successful in 49s
build_systems / build-rhapsody-in-green (pull_request) Successful in 56s
build_systems / build-jeeves (pull_request) Successful in 2m27s
build_systems / build-rhapsody-in-green (push) Successful in 12s
pytest / pytest (push) Successful in 25s
build_systems / build-bob (push) Successful in 32s
build_systems / build-jeeves (push) Successful in 2m11s
treefmt / nix fmt (push) Successful in 5s
build_systems / build-brain (push) Successful in 28s
2026-06-29 16:24:21 -04:00
Richie 03f610feed feat(ui): refactor HTML templates for consistent structure and improved styling
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 37s
build_systems / build-leviathan (pull_request) Successful in 1m20s
build_systems / build-jeeves (pull_request) Successful in 2m57s
build_systems / build-brain (pull_request) Successful in 3m3s
build_systems / build-rhapsody-in-green (pull_request) Successful in 3m25s
build_systems / build-bob (pull_request) Successful in 32m7s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 26s
build_systems / build-brain (push) Successful in 31s
build_systems / build-bob (push) Successful in 34s
build_systems / build-leviathan (push) Successful in 41s
build_systems / build-rhapsody-in-green (push) Successful in 46s
build_systems / build-jeeves (push) Successful in 2m10s
2026-06-29 10:23:13 -04:00
Richie 8804a8df83 added rate limits
build_systems / build-brain (pull_request) Successful in 51s
build_systems / build-leviathan (pull_request) Successful in 58s
pytest / pytest (pull_request) Successful in 26s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m3s
treefmt / nix fmt (pull_request) Successful in 5s
build_systems / build-jeeves (pull_request) Successful in 2m32s
build_systems / build-bob (pull_request) Failing after 13m10s
treefmt / nix fmt (push) Successful in 4s
pytest / pytest (push) Failing after 17m2s
build_systems / build-rhapsody-in-green (push) Failing after 17m2s
build_systems / build-leviathan (push) Failing after 17m2s
build_systems / build-jeeves (push) Failing after 17m2s
build_systems / build-brain (push) Failing after 17m2s
build_systems / build-bob (push) Failing after 17m2s
2026-06-27 23:01:03 -04:00
Richie 8daac4d98b feat(grafana): add HAProxy requests dashboard and Richie Postgres datasource
Visualize the ingested HAProxy request logs in Grafana.

- add a `richie-postgres` datasource connecting to the Richie DB over the
  local Unix socket (/run/postgresql; pg_hba trust for user richie)
- order grafana after postgresql.service
- add the "HAProxy Requests" dashboard: request rate by backend, response
  -time percentiles (p50-p99), top user-agents/IPs/endpoints, status mix
2026-06-27 23:01:03 -04:00
Richie 7606ddc514 feat(haproxy-logs): ingest HAProxy request logs into Richie DB
Add a pipeline to load HAProxy `option httplog` lines into the Richie
database so bot/crawler traffic can be analyzed.

- model: HaproxyRequest mirroring the httplog format, with a unique
  line_hash dedup key and indexes on common filter columns
- migration: create the haproxy_request table (unique line_hash + indexes)
- haproxy_logs package:
  - parser: httplog line -> columns, strips the journald prefix and
    hashes the normalized line
  - ingest: batched, idempotent insert that skips rows whose line_hash
    already exists, so re-ingesting the same logs is a no-op
  - cli: ingest-only `haproxy-logs` command reading stdin or a file
- tests: parsing of a real GPTBot line and idempotent re-ingestion
2026-06-27 23:01:03 -04:00
Richie 137bd8ed2f adding robots.txt 2026-06-27 23:01:03 -04:00
Richie f936253ad5 flake update
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 25s
build_systems / build-bob (pull_request) Successful in 34s
build_systems / build-leviathan (pull_request) Successful in 40s
build_systems / build-rhapsody-in-green (pull_request) Successful in 44s
build_systems / build-brain (pull_request) Successful in 26s
build_systems / build-jeeves (pull_request) Successful in 2m9s
treefmt / nix fmt (push) Successful in 4s
build_systems / build-leviathan (push) Successful in 11s
pytest / pytest (push) Successful in 24s
build_systems / build-rhapsody-in-green (push) Successful in 42s
build_systems / build-bob (push) Successful in 1m14s
build_systems / build-jeeves (push) Successful in 2m17s
build_systems / build-brain (push) Failing after 11m1s
2026-06-22 11:31:47 -04:00
Richie fc01fb9d0b Merge branch 'main' into feature/book-search-engine
pytest / pytest (pull_request) Successful in 27s
build_systems / build-brain (pull_request) Successful in 44s
build_systems / build-bob (pull_request) Successful in 45s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m2s
build_systems / build-leviathan (pull_request) Successful in 59s
build_systems / build-jeeves (pull_request) Successful in 2m26s
treefmt / nix fmt (pull_request) Failing after 10m20s
treefmt / nix fmt (push) Successful in 7s
pytest / pytest (push) Successful in 40s
build_systems / build-brain (push) Successful in 51s
build_systems / build-bob (push) Successful in 53s
build_systems / build-leviathan (push) Successful in 1m5s
build_systems / build-rhapsody-in-green (push) Successful in 1m8s
build_systems / build-jeeves (push) Successful in 2m49s
2026-06-21 15:04:43 -04:00
Richie 09d963ba34 fix(ebook-search): skip comment lines in gold query loader and realign tests
treefmt / nix fmt (pull_request) Successful in 10s
pytest / pytest (pull_request) Successful in 31s
build_systems / build-brain (pull_request) Successful in 52s
build_systems / build-bob (pull_request) Successful in 52s
build_systems / build-jeeves (pull_request) Successful in 2m43s
build_systems / build-leviathan (pull_request) Successful in 59s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m5s
load_gold_queries now skips blank and `//` comment lines so the committed
section separator in queries.jsonl no longer breaks dataset/load-test loading.

Update tests left stale by the search refactor (6bc3011):
- pass the now-required rank_constant to reciprocal_rank_fusion
- expect bm25_candidates to receive the full query and drop the removed
  "BM25 query preparation" timing step
- assert reranking is enabled by default
2026-06-21 14:45:31 -04:00
Richie 855af0fdff updating 10-Primary to enp97s0f1
treefmt / nix fmt (pull_request) Failing after 10m38s
build_systems / build-leviathan (pull_request) Failing after 10m39s
build_systems / build-jeeves (pull_request) Failing after 10m39s
build_systems / build-brain (pull_request) Failing after 10m39s
build_systems / build-bob (pull_request) Failing after 10m39s
pytest / pytest (pull_request) Successful in 32s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m7s
build_systems / build-bob (push) Successful in 41s
build_systems / build-brain (push) Successful in 41s
build_systems / build-leviathan (push) Successful in 53s
build_systems / build-jeeves (push) Successful in 2m20s
treefmt / nix fmt (push) Failing after 11m53s
pytest / pytest (push) Failing after 11m53s
build_systems / build-rhapsody-in-green (push) Failing after 11m54s
2026-06-21 14:25:02 -04:00
Richie fcb69cc68b adding words to spell check
build_systems / build-leviathan (pull_request) Successful in 1m6s
build_systems / build-rhapsody-in-green (pull_request) Successful in 59s
pytest / pytest (pull_request) Failing after 24s
build_systems / build-bob (pull_request) Successful in 40s
build_systems / build-brain (pull_request) Successful in 43s
build_systems / build-jeeves (pull_request) Successful in 2m22s
treefmt / nix fmt (pull_request) Failing after 2h0m25s
2026-06-18 12:55:46 -04:00
Richie 6bc30115d9 fix(ebook-search): code clean up to impove reliablty and readabilty 2026-06-18 12:45:56 -04:00
Richie 6ae1ff1f5c feat(ebook-search): add load-test CLI for the search service
Add a Typer CLI script that drives POST /search on a running server at a
configurable concurrency and reports latency percentiles (p50/p90/p95/p99),
throughput, and HTTP status distribution. Queries are drawn from the shared
eval JSONL set so load testing and evaluation exercise the same questions.
2026-06-18 12:39:55 -04:00
Richie dbc6b5b53b test(ebook-search): organize tests under dedicated package
Move ebook search tests into tests/ebook_search and standardize mocking on pytest-mock.
2026-06-16 21:47:40 -04:00
Richie a9daa60c17 moved TC001 expetop out of pyproject.toml 2026-06-16 18:31:40 -04:00
Richie 241a42b20d updated front end
treefmt / nix fmt (pull_request) Failing after 6s
pytest / pytest (pull_request) Failing after 52s
build_systems / build-brain (pull_request) Successful in 1m11s
build_systems / build-bob (pull_request) Successful in 1m13s
build_systems / build-leviathan (pull_request) Successful in 1m46s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m48s
build_systems / build-jeeves (pull_request) Successful in 3m46s
2026-06-16 00:00:13 -04:00
Richie 4640ebf8ce converted addmin.py and page.py to DbSession and AppConfig 2026-06-15 22:01:42 -04:00
Richie 0c9583c1cc moved config = load_config() to lifespan 2026-06-15 21:59:51 -04:00
Richie f71ae7d2c6 added guardrails.py to constrain responses and added validation to config.py 2026-06-15 21:57:38 -04:00
Richie 2e68c83021 added health endpoints 2026-06-15 21:21:01 -04:00
Richie b126987b63 added AppConfig and AppEngine 2026-06-15 21:08:55 -04:00
Richie 68b3a38b81 converting to pydantic-settings 2026-06-14 21:29:45 -04:00
Richie a5d7c3be4f fixed fomat issue
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 28s
build_systems / build-brain (pull_request) Successful in 46s
build_systems / build-bob (pull_request) Successful in 47s
build_systems / build-leviathan (pull_request) Successful in 53s
build_systems / build-rhapsody-in-green (pull_request) Successful in 59s
build_systems / build-jeeves (pull_request) Successful in 2m32s
2026-06-14 15:42:05 -04:00
Richie 2995a75748 fixed test
treefmt / nix fmt (pull_request) Failing after 5s
pytest / pytest (pull_request) Successful in 28s
build_systems / build-bob (pull_request) Successful in 48s
build_systems / build-brain (pull_request) Successful in 53s
build_systems / build-leviathan (pull_request) Successful in 54s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m1s
build_systems / build-jeeves (pull_request) Successful in 2m43s
2026-06-14 15:41:09 -04:00
Richie dce1838163 opning ports for testing 2026-06-14 15:41:09 -04:00
Richie 121eb979a4 added a index for the VEctor DB 2026-06-14 15:41:09 -04:00
Richie c88315e9b6 improved BM25 write 2026-06-14 15:41:09 -04:00
Richie 50795ab7fc added ZstdMiddleware to ebook_search 2026-06-14 15:41:09 -04:00
Richie e9a80a0308 added vector_engine to fix name postgres name space issue 2026-06-14 15:41:09 -04:00
Richie eb76edb740 reworked ebook_search routers 2026-06-14 15:41:09 -04:00
Richie c53afb3c70 made fastapi tools 2026-06-14 15:41:09 -04:00
Richie 8301db39e5 added proper cache invalidation to load_bm25_corpus 2026-06-14 15:40:31 -04:00
Richie 45a9e90524 updated tests 2026-06-14 15:40:31 -04:00
Richie dd67de3993 improved reranking weights 2026-06-14 15:40:31 -04:00
Richie 6e3635ca01 fixed duplicat enrichment 2026-06-14 15:40:31 -04:00
Richie 11cbe31152 improved queary for vector search 2026-06-14 15:40:31 -04:00
Richie 5544dfa61c add .ebook_search_bm25 to gitignore 2026-06-14 15:40:31 -04:00
Richie 911df63513 updated python 2026-06-14 15:40:31 -04:00
Richie 07eb170b34 setup tests 2026-06-14 15:40:04 -04:00
Richie 6715bbf0a5 build api and frountend 2026-06-14 15:40:04 -04:00
Richie 26ff1f0fd3 added answer.py and config 2026-06-14 15:40:04 -04:00
Richie 666ea97754 added __init__ 2026-06-14 15:40:04 -04:00
Richie e01c687625 made llm_interface.py 2026-06-14 15:40:04 -04:00
Richie 82a367a2b6 added rerank 2026-06-14 15:40:04 -04:00
Richie ad1834c537 built ingest 2026-06-14 15:40:04 -04:00
Richie aed1e14d95 built rag search setup 2026-06-14 15:40:04 -04:00
Richie 0a2d4c08cb set up embedding system 2026-06-14 15:40:04 -04:00
Richie db98bd3559 built BM25 search foundation 2026-06-14 15:40:04 -04:00
Richie cdded5da12 added ebook embedding to orm 2026-06-14 15:40:04 -04:00
Richie d022251a58 removed hedgedoc 2026-06-14 15:40:04 -04:00
Richie e1ef4de6a3 adding embedding Models to jeeves 2026-06-14 15:40:04 -04:00
Richie 5c230a267c ran treefmt
build_systems / build-brain (pull_request) Successful in 46s
build_systems / build-bob (pull_request) Successful in 46s
build_systems / build-leviathan (pull_request) Successful in 54s
build_systems / build-rhapsody-in-green (pull_request) Successful in 58s
build_systems / build-jeeves (pull_request) Successful in 2m33s
treefmt / nix fmt (push) Successful in 4s
build_systems / build-brain (push) Successful in 30s
build_systems / build-bob (push) Successful in 9s
pytest / pytest (push) Successful in 27s
build_systems / build-leviathan (push) Successful in 40s
build_systems / build-rhapsody-in-green (push) Successful in 44s
build_systems / build-jeeves (push) Successful in 2m25s
treefmt / nix fmt (pull_request) Successful in 4s
pytest / pytest (pull_request) Successful in 25s
2026-06-14 13:53:02 -04:00
Richie 5215d66d40 adding noqa to DbSession
build_systems / build-jeeves (pull_request) Successful in 2m38s
treefmt / nix fmt (pull_request) Failing after 5s
pytest / pytest (pull_request) Successful in 26s
build_systems / build-bob (pull_request) Successful in 46s
build_systems / build-leviathan (pull_request) Successful in 53s
build_systems / build-brain (pull_request) Successful in 55s
build_systems / build-rhapsody-in-green (pull_request) Successful in 57s
2026-06-14 13:50:49 -04:00
Richie 7ad198416b added TYPE_CHECKING to contact main.py 2026-06-14 13:50:27 -04:00
Richie 1461c2552a removed van-inventory from pyproject.toml 2026-06-14 13:48:17 -04:00
Richie 736717c2f8 added TYPE_CHECKING to middleware.py 2026-06-14 13:48:02 -04:00
Richie ab2521867e added TYPE_CHECKING to dependencies.py 2026-06-14 13:47:50 -04:00
Richie 8e0ab4190b added TYPE_CHECKING to heater main.py 2026-06-14 13:46:40 -04:00
Richie 734fd7641e moved fetch_weather to masked lat lon and 2026-06-14 13:46:03 -04:00
Richie e898e08c48 added noqa to validate system 2026-06-14 13:41:50 -04:00
Richie d916ea903c removed dead code 2026-06-14 13:39:55 -04:00
Richie d8e916dbe6 fixed type bug in get_snapshots 2026-06-14 13:38:14 -04:00
Richie 48e9f0199d deleting van_inventory 2026-06-14 13:37:55 -04:00
Richie a526420c8d fixed un needed noqa's 2026-06-14 12:58:50 -04:00
Richie 41e3e265af cleaned up audiobook.py mapped_column 2026-06-14 12:53:54 -04:00
Richie 38a17f6146 cleaned uo python dependencies 2026-06-14 12:49:22 -04:00
Richie fe48d4c1ad removing splendor 2026-06-14 12:00:39 -04:00
Richie 9290cb46ee updated series_index to float and added UniqueConstraint to audiobook and audiobook_author
treefmt / nix fmt (push) Successful in 5s
build_systems / build-bob (push) Successful in 32s
build_systems / build-leviathan (push) Successful in 41s
build_systems / build-rhapsody-in-green (push) Successful in 44s
pytest / pytest (push) Successful in 27s
build_systems / build-brain (push) Successful in 31s
build_systems / build-jeeves (push) Successful in 2m28s
pytest / pytest (pull_request) Successful in 26s
build_systems / build-bob (pull_request) Successful in 47s
treefmt / nix fmt (pull_request) Successful in 5s
build_systems / build-brain (pull_request) Successful in 44s
build_systems / build-leviathan (pull_request) Successful in 52s
build_systems / build-rhapsody-in-green (pull_request) Successful in 58s
build_systems / build-jeeves (pull_request) Successful in 2m27s
2026-06-13 22:29:56 -04:00
Richie acd3f2d3ac fixed omnibus for audio books 2026-06-13 22:29:56 -04:00
Richie 08e716f66a deleted frontend dir 2026-06-13 22:29:56 -04:00
Richie d197731af4 added llm_tool_calling.py 2026-06-13 22:29:56 -04:00
Richie 1ffc48bb02 built workflow 2026-06-13 22:29:56 -04:00
Richie b6395ef18f Add catalog.py for manually adding authors and series to the database. 2026-06-13 22:29:56 -04:00
Richie aff6f4e1bd adding audiobook data to DB 2026-06-13 22:29:56 -04:00
Richie a9a96db944 cleaned up old_installer.py
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 25s
pytest / pytest (push) Successful in 24s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m5s
treefmt / nix fmt (push) Successful in 4s
build_systems / build-brain (pull_request) Successful in 49s
build_systems / build-bob (pull_request) Successful in 50s
build_systems / build-brain (push) Successful in 44s
build_systems / build-bob (push) Successful in 45s
build_systems / build-leviathan (pull_request) Successful in 57s
build_systems / build-leviathan (push) Successful in 53s
build_systems / build-rhapsody-in-green (push) Successful in 55s
build_systems / build-jeeves (pull_request) Successful in 2m41s
build_systems / build-jeeves (push) Successful in 2m37s
2026-06-13 22:27:11 -04:00
Richie d34154541d moved installer.py to old_installer.py 2026-06-13 22:20:58 -04:00
Richie 5d3a851137 deleting data_science code
build_systems / build-bob (pull_request) Successful in 46s
build_systems / build-leviathan (pull_request) Successful in 53s
build_systems / build-rhapsody-in-green (pull_request) Successful in 57s
build_systems / build-jeeves (pull_request) Successful in 2m33s
build_systems / build-brain (pull_request) Successful in 46s
treefmt / nix fmt (push) Successful in 4s
build_systems / build-brain (push) Successful in 29s
pytest / pytest (push) Successful in 23s
build_systems / build-bob (push) Successful in 31s
build_systems / build-leviathan (push) Successful in 38s
build_systems / build-rhapsody-in-green (push) Successful in 43s
build_systems / build-jeeves (push) Successful in 2m29s
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 23s
this code was moved to https://gitea.tmmworkshop.com/Nornsight/weave
2026-06-13 21:14:42 -04:00
Richie e05e5c77bc deleting signal bot 2026-06-13 21:09:34 -04:00
Richie b0a2ebc052 deleting unneeded files 2026-06-13 20:47:55 -04:00
Richie f77c9657a3 chore: update flake.lock
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 27s
build_systems / build-brain (pull_request) Successful in 41s
build_systems / build-bob (pull_request) Successful in 46s
build_systems / build-leviathan (pull_request) Successful in 46s
build_systems / build-rhapsody-in-green (pull_request) Successful in 52s
build_systems / build-jeeves (pull_request) Successful in 2m34s
pytest / pytest (push) Successful in 26s
build_systems / build-leviathan (push) Successful in 41s
treefmt / nix fmt (push) Successful in 6s
build_systems / build-brain (push) Successful in 31s
build_systems / build-bob (push) Successful in 32s
build_systems / build-rhapsody-in-green (push) Successful in 48s
build_systems / build-jeeves (push) Successful in 2m18s
2026-06-12 13:06:18 -04:00
Richie f908f969d3 opening port for vllm
treefmt / nix fmt (pull_request) Successful in 7s
pytest / pytest (pull_request) Successful in 35s
build_systems / build-brain (pull_request) Successful in 47s
build_systems / build-bob (pull_request) Successful in 50s
build_systems / build-leviathan (pull_request) Successful in 56s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m1s
build_systems / build-jeeves (pull_request) Successful in 2m31s
treefmt / nix fmt (push) Successful in 6s
pytest / pytest (push) Successful in 25s
build_systems / build-brain (push) Successful in 31s
build_systems / build-bob (push) Successful in 34s
build_systems / build-leviathan (push) Successful in 42s
build_systems / build-rhapsody-in-green (push) Successful in 45s
build_systems / build-jeeves (push) Successful in 2m13s
2026-06-08 19:43:07 -04:00
Richie 3cf49c5479 fixing update-flake-lock.yaml permissions
pytest / pytest (pull_request) Successful in 25s
treefmt / nix fmt (pull_request) Successful in 6s
build_systems / build-bob (pull_request) Successful in 45s
build_systems / build-brain (pull_request) Successful in 46s
build_systems / build-leviathan (pull_request) Successful in 54s
build_systems / build-rhapsody-in-green (pull_request) Successful in 59s
build_systems / build-jeeves (pull_request) Successful in 2m31s
treefmt / nix fmt (push) Successful in 6s
pytest / pytest (push) Successful in 29s
build_systems / build-brain (push) Successful in 31s
build_systems / build-bob (push) Successful in 33s
build_systems / build-leviathan (push) Successful in 42s
build_systems / build-rhapsody-in-green (push) Successful in 48s
build_systems / build-jeeves (push) Successful in 2m34s
2026-06-07 11:21:10 -04:00
Richie b34354f5e5 adding storage to bob
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 26s
build_systems / build-brain (pull_request) Successful in 47s
build_systems / build-bob (pull_request) Successful in 47s
build_systems / build-leviathan (pull_request) Successful in 53s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m0s
build_systems / build-jeeves (pull_request) Successful in 2m32s
treefmt / nix fmt (push) Successful in 7s
build_systems / build-brain (push) Successful in 8s
pytest / pytest (push) Successful in 25s
build_systems / build-bob (push) Successful in 33s
build_systems / build-leviathan (push) Successful in 41s
build_systems / build-rhapsody-in-green (push) Successful in 46s
build_systems / build-jeeves (push) Successful in 2m19s
2026-06-07 10:48:47 -04:00
Richie 44826464de flake update fro claud code
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 26s
build_systems / build-brain (pull_request) Successful in 46s
build_systems / build-bob (pull_request) Successful in 48s
build_systems / build-leviathan (pull_request) Successful in 53s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m0s
build_systems / build-jeeves (pull_request) Successful in 2m33s
treefmt / nix fmt (push) Successful in 6s
pytest / pytest (push) Successful in 25s
build_systems / build-brain (push) Successful in 31s
build_systems / build-bob (push) Successful in 34s
build_systems / build-leviathan (push) Successful in 41s
build_systems / build-rhapsody-in-green (push) Successful in 46s
build_systems / build-jeeves (push) Successful in 2m19s
2026-06-07 10:01:51 -04:00
Richie 3de0ffccb0 adding workflow dispatch for gitea_flake_lock.py
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 25s
build_systems / build-brain (pull_request) Successful in 47s
build_systems / build-bob (pull_request) Successful in 49s
build_systems / build-leviathan (pull_request) Successful in 54s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m0s
pytest / pytest (push) Successful in 29s
build_systems / build-bob (push) Successful in 32s
build_systems / build-jeeves (pull_request) Successful in 2m35s
build_systems / build-leviathan (push) Successful in 44s
treefmt / nix fmt (push) Successful in 6s
build_systems / build-rhapsody-in-green (push) Successful in 18s
build_systems / build-brain (push) Successful in 32s
build_systems / build-jeeves (push) Successful in 2m19s
2026-06-06 22:56:34 -04:00
Richie c6c98b3e26 updated Primary nic
pytest / pytest (pull_request) Successful in 26s
build_systems / build-bob (pull_request) Successful in 49s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m1s
treefmt / nix fmt (pull_request) Successful in 6s
build_systems / build-brain (pull_request) Successful in 48s
build_systems / build-leviathan (pull_request) Successful in 55s
build_systems / build-jeeves (pull_request) Successful in 2m39s
treefmt / nix fmt (push) Successful in 6s
build_systems / build-rhapsody-in-green (push) Successful in 14s
pytest / pytest (push) Successful in 25s
build_systems / build-brain (push) Successful in 29s
build_systems / build-bob (push) Successful in 33s
build_systems / build-leviathan (push) Successful in 41s
build_systems / build-jeeves (push) Successful in 2m19s
2026-06-04 18:10:41 -04:00
Richie d459f3d675 adding brave 2026-06-04 18:10:41 -04:00
Richie 33e4b37cce fixing jeeves dns 2026-06-04 18:10:41 -04:00
Richie 2a8e7e7f2b updated my ssh_config.nix
treefmt / nix fmt (pull_request) Successful in 7s
build_systems / build-brain (pull_request) Successful in 50s
build_systems / build-bob (pull_request) Successful in 50s
build_systems / build-leviathan (pull_request) Successful in 1m1s
build_systems / build-jeeves (pull_request) Successful in 2m49s
pytest / pytest (pull_request) Successful in 31s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m8s
treefmt / nix fmt (push) Successful in 5s
build_systems / build-leviathan (push) Successful in 12s
pytest / pytest (push) Successful in 24s
build_systems / build-bob (push) Successful in 32s
build_systems / build-rhapsody-in-green (push) Successful in 51s
build_systems / build-jeeves (push) Successful in 2m23s
build_systems / build-brain (push) Successful in 30s
2026-06-03 22:11:19 -04:00
Richie 07759353be flake update
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 28s
build_systems / build-leviathan (pull_request) Successful in 18m28s
build_systems / build-rhapsody-in-green (pull_request) Successful in 19m7s
build_systems / build-brain (pull_request) Successful in 19m51s
build_systems / build-jeeves (pull_request) Successful in 19m54s
build_systems / build-bob (pull_request) Successful in 30s
build_systems / build-bob (push) Successful in 30s
treefmt / nix fmt (push) Successful in 5s
build_systems / build-brain (push) Successful in 29s
pytest / pytest (push) Successful in 25s
build_systems / build-leviathan (push) Successful in 39s
build_systems / build-rhapsody-in-green (push) Successful in 46s
build_systems / build-jeeves (push) Successful in 2m19s
2026-05-29 22:30:33 -04:00
Richie 38fb14520e removed --reload
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 27s
build_systems / build-brain (pull_request) Successful in 52s
build_systems / build-bob (pull_request) Successful in 54s
build_systems / build-leviathan (pull_request) Successful in 1m4s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m5s
build_systems / build-jeeves (pull_request) Successful in 2m45s
build_systems / build-bob (push) Successful in 34s
build_systems / build-brain (push) Successful in 32s
treefmt / nix fmt (push) Successful in 6s
pytest / pytest (push) Successful in 26s
build_systems / build-leviathan (push) Successful in 43s
build_systems / build-rhapsody-in-green (push) Successful in 47s
build_systems / build-jeeves (push) Successful in 2m26s
2026-05-29 20:26:32 -04:00
Richie 006ae6079a moved nornsight off my_python 2026-05-29 20:15:51 -04:00
Richie 7d507fb7e1 adding nornsight.nix
treefmt / nix fmt (pull_request) Successful in 6s
build_systems / build-brain (pull_request) Successful in 51s
build_systems / build-bob (pull_request) Successful in 56s
pytest / pytest (pull_request) Successful in 28s
build_systems / build-leviathan (pull_request) Successful in 1m24s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m30s
build_systems / build-jeeves (pull_request) Successful in 2m45s
2026-05-29 18:39:27 -04:00
Richie 0f69022e51 disabled terminal bell
treefmt / nix fmt (pull_request) Successful in 7s
pytest / pytest (pull_request) Successful in 29s
build_systems / build-brain (pull_request) Successful in 48s
build_systems / build-bob (pull_request) Successful in 48s
build_systems / build-jeeves (pull_request) Successful in 2m42s
build_systems / build-brain (push) Successful in 30s
build_systems / build-leviathan (pull_request) Successful in 1m0s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m4s
treefmt / nix fmt (push) Successful in 6s
build_systems / build-bob (push) Successful in 33s
pytest / pytest (push) Successful in 25s
build_systems / build-leviathan (push) Successful in 41s
build_systems / build-rhapsody-in-green (push) Successful in 46s
build_systems / build-jeeves (push) Successful in 2m23s
2026-05-29 13:52:46 -04:00
Richie a260ae2470 adding ffmpeg to jeeves and rhapsody-in-green
treefmt / nix fmt (pull_request) Successful in 7s
build_systems / build-bob (pull_request) Successful in 32s
pytest / pytest (pull_request) Successful in 26s
build_systems / build-brain (pull_request) Successful in 44s
build_systems / build-leviathan (pull_request) Successful in 55s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m30s
build_systems / build-jeeves (pull_request) Successful in 2m40s
treefmt / nix fmt (push) Successful in 6s
build_systems / build-bob (push) Successful in 33s
build_systems / build-brain (push) Successful in 34s
pytest / pytest (push) Successful in 26s
build_systems / build-leviathan (push) Successful in 44s
build_systems / build-rhapsody-in-green (push) Successful in 45s
build_systems / build-jeeves (push) Successful in 2m21s
2026-05-28 22:14:59 -04:00
Richie 820b4a53d2 adding photos to syncthing
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 1m16s
build_systems / build-jeeves (pull_request) Successful in 5m29s
build_systems / build-brain (pull_request) Successful in 6m4s
build_systems / build-rhapsody-in-green (pull_request) Successful in 16m47s
build_systems / build-leviathan (pull_request) Successful in 16m49s
build_systems / build-bob (pull_request) Successful in 31s
treefmt / nix fmt (push) Successful in 6s
build_systems / build-bob (push) Successful in 31s
build_systems / build-brain (push) Successful in 32s
pytest / pytest (push) Successful in 26s
build_systems / build-leviathan (push) Successful in 40s
build_systems / build-rhapsody-in-green (push) Successful in 14s
build_systems / build-jeeves (push) Successful in 2m33s
2026-05-28 22:08:46 -04:00
Richie ea77e83f06 setting forceImportRoot to false
pytest / pytest (pull_request) Successful in 53s
treefmt / nix fmt (pull_request) Successful in 9s
build_systems / build-brain (pull_request) Successful in 2m33s
build_systems / build-bob (pull_request) Successful in 2m41s
build_systems / build-leviathan (pull_request) Successful in 3m22s
build_systems / build-rhapsody-in-green (pull_request) Successful in 3m32s
build_systems / build-jeeves (pull_request) Successful in 8m52s
build_systems / build-bob (push) Successful in 33s
treefmt / nix fmt (push) Successful in 6s
build_systems / build-brain (push) Successful in 31s
pytest / pytest (push) Successful in 26s
build_systems / build-leviathan (push) Successful in 41s
build_systems / build-rhapsody-in-green (push) Successful in 47s
build_systems / build-jeeves (push) Successful in 2m28s
2026-05-14 15:12:53 -04:00
Richie a9da208bc3 added --accept-flake-config to nixos-rebuild step
treefmt / nix fmt (pull_request) Successful in 9s
pytest / pytest (pull_request) Successful in 1m17s
build_systems / build-brain (pull_request) Successful in 2m14s
build_systems / build-bob (pull_request) Successful in 2m25s
build_systems / build-leviathan (pull_request) Successful in 4m32s
build_systems / build-rhapsody-in-green (pull_request) Successful in 4m35s
build_systems / build-jeeves (pull_request) Successful in 8m45s
pytest / pytest (push) Successful in 1m1s
treefmt / nix fmt (push) Successful in 8s
build_systems / build-bob (push) Successful in 44s
build_systems / build-leviathan (push) Successful in 38s
build_systems / build-brain (push) Successful in 1m39s
build_systems / build-rhapsody-in-green (push) Successful in 3m0s
build_systems / build-jeeves (push) Successful in 7m3s
2026-05-14 13:39:13 -04:00
Richie 739d7dd28c droped whisper from my_python 2026-05-14 13:38:41 -04:00
Richie 651599796e moved ./llm_tools.nix to gui only
treefmt / nix fmt (pull_request) Successful in 9s
pytest / pytest (pull_request) Successful in 1m24s
build_systems / build-brain (pull_request) Successful in 4m7s
build_systems / build-leviathan (pull_request) Successful in 4m11s
build_systems / build-rhapsody-in-green (pull_request) Successful in 4m41s
build_systems / build-jeeves (pull_request) Successful in 8m38s
build_systems / build-bob (pull_request) Failing after 14m11s
2026-05-14 12:58:15 -04:00
Richie b9d440597c removed llm tools from gui
treefmt / nix fmt (pull_request) Successful in 9s
pytest / pytest (pull_request) Successful in 1m4s
build_systems / build-brain (pull_request) Successful in 2m31s
build_systems / build-leviathan (pull_request) Successful in 3m21s
build_systems / build-rhapsody-in-green (pull_request) Successful in 3m21s
build_systems / build-jeeves (pull_request) Successful in 6m55s
build_systems / build-bob (pull_request) Failing after 16m4s
2026-05-13 10:03:15 -04:00
Richie 311cc5d7a7 adding pi-coding-agenta
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 1m24s
build_systems / build-brain (pull_request) Successful in 6m28s
build_systems / build-leviathan (pull_request) Failing after 7m21s
build_systems / build-rhapsody-in-green (pull_request) Failing after 7m22s
build_systems / build-jeeves (pull_request) Successful in 11m47s
build_systems / build-bob (pull_request) Failing after 19m3s
2026-05-13 08:57:45 -04:00
Richie fb2519046d moved codex and opencode to master pkgs 2026-05-13 08:56:18 -04:00
Richie bc6b1585ec flake update 2026-05-10 13:49:53 -04:00
Richie d71330a85a updated firefox configPath
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 29s
build_systems / build-brain (pull_request) Successful in 5m41s
build_systems / build-leviathan (pull_request) Successful in 5m43s
build_systems / build-jeeves (pull_request) Successful in 6m58s
build_systems / build-rhapsody-in-green (pull_request) Successful in 27m16s
build_systems / build-bob (pull_request) Failing after 12m14s
2026-05-10 12:36:54 -04:00
Richie df51aa5200 removing sunshine
sunshine is a cool idea but has been causing annoying ui glitches and started preventing the display manning for starting
Its a cool idea in theory but not useful enough for me to want to debug
2026-05-10 12:31:06 -04:00
Richie e93cc816db flake update 2026-05-09 17:38:13 -04:00
Richie 19050b4cf4 removing llms from rhapsody-in-green 2026-05-07 18:06:21 -04:00
Richie 6676c15f75 adding qwen3.6:27b 2026-05-07 18:05:00 -04:00
Richie 27e487e322 removing signal_bot
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 27s
build_systems / build-bob (pull_request) Successful in 48s
build_systems / build-brain (pull_request) Successful in 46s
build_systems / build-leviathan (pull_request) Successful in 54s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m0s
build_systems / build-jeeves (pull_request) Successful in 2m34s
treefmt / nix fmt (push) Successful in 5s
build_systems / build-bob (push) Successful in 34s
build_systems / build-brain (push) Successful in 31s
pytest / pytest (push) Successful in 27s
build_systems / build-leviathan (push) Successful in 40s
build_systems / build-rhapsody-in-green (push) Successful in 43s
build_systems / build-jeeves (push) Successful in 2m31s
2026-05-03 21:23:20 -04:00
Richie 4f28050eff added nixfmt and nix
build_systems / build-bob (pull_request) Failing after 52s
build_systems / build-brain (pull_request) Failing after 50s
pytest / pytest (pull_request) Failing after 4s
treefmt / nix fmt (pull_request) Failing after 4s
build_systems / build-leviathan (pull_request) Failing after 57s
build_systems / build-rhapsody-in-green (pull_request) Failing after 52s
build_systems / build-jeeves (pull_request) Failing after 3m17s
2026-05-03 20:47:03 -04:00
Richie b58ea60557 adding hostPackages
pytest / pytest (pull_request) Failing after 10s
treefmt / nix fmt (pull_request) Failing after 13s
build_systems / build-brain (pull_request) Failing after 29s
build_systems / build-bob (pull_request) Failing after 29s
build_systems / build-rhapsody-in-green (pull_request) Failing after 46s
build_systems / build-jeeves (pull_request) Failing after 2m29s
build_systems / build-leviathan (pull_request) Failing after 35s
2026-05-03 19:16:37 -04:00
Richie e95eedffe4 updated br-nix-builder
build_systems / build-bob (pull_request) Failing after 2s
build_systems / build-brain (pull_request) Failing after 1s
build_systems / build-jeeves (pull_request) Failing after 1s
build_systems / build-leviathan (pull_request) Failing after 1s
build_systems / build-rhapsody-in-green (pull_request) Failing after 1s
treefmt / nix fmt (pull_request) Failing after 2s
pytest / pytest (pull_request) Failing after 9s
2026-05-03 16:30:51 -04:00
Richie 1abd53987c made nix_builders not ephemeral and depended on gitea 2026-05-03 16:29:56 -04:00
Richie d1a3e7338a added permittedInsecurePackages for discord-canary 2026-05-03 00:39:23 -04:00
Richie 687ef0c167 moved acme_challenge backend 2026-05-03 00:39:19 -04:00
Richie 3a86148352 working nix builder 2026-05-02 17:10:02 -04:00
Richie fe9a2912e1 added words to spell check 2026-04-30 12:46:55 -04:00
Richie 29a99fc210 flake lock update 2026-04-30 12:46:55 -04:00
Richie d7651bf588 set update.nix to gitea 2026-04-30 12:46:55 -04:00
Richie 2865dcbe9c set dbus.implementation = "dbus"; 2026-04-30 12:46:55 -04:00
Richie d920b77bab removed verilux 2026-04-30 12:46:55 -04:00
Richie 1b53167b53 updated nix builders 2026-04-30 12:46:55 -04:00
Richie 9dabb9dc07 updated actions 2026-04-30 12:46:55 -04:00
Richie 95630fe151 made Prometheus require zfs-media-database-prometheus.mount 2026-04-30 10:16:37 -04:00
Richie d3a889f100 fixed typo 2026-04-30 10:16:37 -04:00
Richie 6ce0671f51 ran treefmt 2026-04-30 10:16:37 -04:00
Richie 25ab6b2ab6 added gitlens.pushRepositories key shourtcut 2026-04-30 10:16:37 -04:00
Richie 374d7e8d38 setting up resource monitoring for bob and jeeves 2026-04-30 10:16:37 -04:00
Richie 957110b7e9 increasing kitty scrollback_lines 2026-04-28 12:07:03 -04:00
Richie e7dc60f2c3 adding tiktoken 2026-04-28 12:07:03 -04:00
Richie 353a9d6787 adding pgvector 2026-04-27 13:02:04 -04:00
Richie 9f2d3a3c89 updated .gitignore 2026-04-25 16:33:45 -04:00
Richie 73e221716f adding nornsight 2026-04-25 16:33:45 -04:00
Richie 0d0ed5445a moved models 2026-04-19 21:05:56 -04:00
Richie 9e4c6f6f56 adding qwen3.6 2026-04-19 21:05:56 -04:00
Richie 1cf4b99d18 updating signing.format for programs.git 2026-04-19 10:35:48 -04:00
Richie b536fb9f09 removed fallbackToPassword = true; 2026-04-19 08:08:33 -04:00
github-actions[bot] c41a2ce3bd flake.lock: Update
Flake lock file updates:

• Updated input 'firefox-addons':
    'gitlab:rycee/nur-expressions/81e28f4?dir=pkgs/firefox-addons' (2026-03-20)
  → 'gitlab:rycee/nur-expressions/0581568?dir=pkgs/firefox-addons' (2026-04-17)
• Updated input 'home-manager':
    'github:nix-community/home-manager/9670de2' (2026-03-20)
  → 'github:nix-community/home-manager/565e534' (2026-04-17)
• Updated input 'nixos-hardware':
    'github:nixos/nixos-hardware/2d4b471' (2026-03-20)
  → 'github:nixos/nixos-hardware/c775c27' (2026-04-06)
• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/b40629e' (2026-03-18)
  → 'github:nixos/nixpkgs/4bd9165' (2026-04-14)
• Updated input 'nixpkgs-master':
    'github:nixos/nixpkgs/8620c0b' (2026-03-21)
  → 'github:nixos/nixpkgs/025c852' (2026-04-17)
• Updated input 'sops-nix':
    'github:Mic92/sops-nix/29b6519' (2026-03-19)
  → 'github:Mic92/sops-nix/d4971dd' (2026-04-13)
2026-04-19 08:08:33 -04:00
Richie 8ef776f859 updating download-buffer-size 2026-04-18 22:12:47 -04:00
Richie d350c2d074 adding codex 2026-04-18 20:14:11 -04:00
Richie 93d6914e9d enabling appimages 2026-04-18 20:14:11 -04:00
Richie 7db063a240 setting up whisper transcriber 2026-04-18 19:09:02 -04:00
Richie dfe5997e0b removing brain_substituter.nix from bob 2026-04-18 12:00:59 -04:00
Richie 68671a1e84 adding steve 2026-04-18 11:56:56 -04:00
Richie bcc2227cfd updating syncthing phone id 2026-04-18 11:53:20 -04:00
Richie d6eec926e7 made web_services dir 2026-04-13 19:12:32 -04:00
Richie 5ddf1c4cab ran tree fmt 2026-04-13 19:12:32 -04:00
Richie 5a2171b9c7 updated gitea ssh settings 2026-04-13 19:12:32 -04:00
Richie 95c6ade154 moving off cloudflare tunnel 2026-04-13 19:12:32 -04:00
Richie a0bbc2896a added math the bob 2026-04-13 19:08:42 -04:00
Richie 736596c387 made bob a server 2026-04-13 19:08:42 -04:00
Richie 67622c0e51 setting up hedgedoc 2026-04-11 11:42:08 -04:00
Richie d2f447a1af disabling kafka 2026-04-11 11:11:21 -04:00
Richie af365fce9a setup sunshine.nix 2026-04-03 17:12:24 -04:00
Richie 6430049e92 updated postgres snapshot settings 2026-03-30 14:07:08 -04:00
Richie 26e4620f8f fixed systemd sandboxing 2026-03-30 14:07:08 -04:00
Richie 93fc700fa2 removed preStart step 2026-03-30 14:07:08 -04:00
Richie 8d1c1fc628 added mountpoint= to postgres zfs create 2026-03-30 14:07:08 -04:00
Richie dda318753b improving postgres wal 2026-03-30 14:07:08 -04:00
Richie 261ff139f7 removed ds table from richie DB 2026-03-29 15:54:54 -04:00
Richie ba8ff35109 updated ingest_congress to use congress-legislators for legislator info 2026-03-29 15:54:54 -04:00
Richie e368402eea adding LegislatorSocialMedia 2026-03-29 15:54:54 -04:00
Richie dd9329d218 fixed tests 2026-03-29 15:54:54 -04:00
Richie 89f6627bed converted session.execute(select to session.scalars(select 2026-03-29 15:54:54 -04:00
Richie c5babf8bad ran treefmt 2026-03-29 15:54:54 -04:00
Richie dae38ffd9b added ingest_congress.py 2026-03-29 15:54:54 -04:00
Richie ca62cc36a7 adding congress data to new DS DB 2026-03-29 15:54:54 -04:00
Richie 035410f39e adding nemotron-3-nano 2026-03-29 15:54:54 -04:00
Richie e40ab757ca making more generic exception handling 2026-03-29 15:54:54 -04:00
Richie 345ba94a59 ran ingest_posts 2026-03-29 15:54:54 -04:00
Richie f2084206b6 adding tables for 2023 2026-03-29 15:54:54 -04:00
Richie 50e764146a added ingest_posts.py 2026-03-29 15:54:54 -04:00
Richie ea97b5eb19 adding 2026 partitions 2026-03-29 15:54:54 -04:00
Richie 1ef2512daa adding post table 2026-03-29 15:54:54 -04:00
Richie f9a9e5395c added media/temp for fast dir when working with data 2026-03-29 15:54:54 -04:00
Richie d8e166a340 adding data_science_dev 2026-03-29 15:54:54 -04:00
Richie c266ba79f4 updated snapshot_config.toml 2026-03-29 14:12:06 -04:00
Richie f627a5ac6e enabling kafka 2026-03-26 09:59:31 -04:00
Richie a5e7d97213 adding full qwen3 2026-03-24 16:20:21 -04:00
Richie 1419deb3c6 setting up brain nix serve 2026-03-24 15:04:48 -04:00
Richie 1f06692696 adding zstd to firefix settings 2026-03-24 12:53:44 -04:00
Richie 8f8177f36e adding zstd compression to fastapi 2026-03-24 12:53:44 -04:00
Richie 8534edc285 added git key binds 2026-03-24 12:45:51 -04:00
Richie 73b28a855b fixed missed renames 2026-03-24 12:45:51 -04:00
Richie 0c0810a06b added cycle status 2026-03-24 12:45:51 -04:00
Richie 239bef975a adding availability status to HA 2026-03-24 12:45:51 -04:00
Richie 2577b791f7 removing antigravity 2026-03-24 12:41:28 -04:00
Richie b4d9562591 fixed treefmt 2026-03-22 19:07:23 -04:00
Richie 66f972ac2b removing react 2026-03-22 19:07:23 -04:00
Richie aca756f479 down grading transmision 2026-03-22 14:30:13 -04:00
Richie 7f59f7f7ac fixing brain gps data 2026-03-22 14:30:13 -04:00
github-actions[bot] 70864c620f flake.lock: Update
Flake lock file updates:

• Updated input 'firefox-addons':
    'gitlab:rycee/nur-expressions/07e1616?dir=pkgs/firefox-addons' (2026-03-06)
  → 'gitlab:rycee/nur-expressions/81e28f4?dir=pkgs/firefox-addons' (2026-03-20)
• Updated input 'home-manager':
    'github:nix-community/home-manager/daa2c22' (2026-03-06)
  → 'github:nix-community/home-manager/9670de2' (2026-03-20)
• Updated input 'nixos-hardware':
    'github:nixos/nixos-hardware/41c6b42' (2026-02-24)
  → 'github:nixos/nixos-hardware/2d4b471' (2026-03-20)
• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/80bdc1e' (2026-03-04)
  → 'github:nixos/nixpkgs/b40629e' (2026-03-18)
• Updated input 'nixpkgs-master':
    'github:nixos/nixpkgs/af5157a' (2026-03-07)
  → 'github:nixos/nixpkgs/8620c0b' (2026-03-21)
• Updated input 'sops-nix':
    'github:Mic92/sops-nix/1d9b98a' (2026-03-02)
  → 'github:Mic92/sops-nix/29b6519' (2026-03-19)
2026-03-21 08:29:18 -04:00
dependabot[bot] 304f1c8433 Bump flatted from 3.3.3 to 3.4.2 in /frontend
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.3 to 3.4.2.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-21 08:24:11 -04:00
Richie 1b5a036061 httpx conversion 2026-03-18 19:38:14 -04:00
Richie 42330ec186 changed to sa.Enum 2026-03-18 19:29:59 -04:00
Richie 3f4373d1f6 fixed tests and treeftm 2026-03-18 19:29:59 -04:00
Richie cc73dfc467 updated mats ssh key 2026-03-18 19:29:59 -04:00
Richie 976c3f9d3e move signal bot to its own DB 2026-03-18 19:29:59 -04:00
Richie 2661127426 updated _format_location to use van van_last_known_longitude and van_last_known_latitude 2026-03-18 19:29:59 -04:00
Richie 1b3e6725ea added sync_roles 2026-03-18 19:29:59 -04:00
Richie 7d2fbaea43 added bot class and rbac style auth with dynamic help msg base on roles 2026-03-18 19:29:59 -04:00
Richie a19b1c7e60 Add Signal location command backed by Home Assistant 2026-03-18 19:29:59 -04:00
Richie 76da6cbc54 set syncModels to false 2026-03-15 12:06:01 -04:00
Richie c83bbe2c24 added more data to van weatere and moved retry logic to tenacity 2026-03-15 12:06:01 -04:00
Richie 7611a3b2df fixed GPS 2026-03-15 12:06:01 -04:00
Richie aec5e3e22b adding qalculate-gtk 2026-03-15 10:39:17 -04:00
Richie 4e3273d5ec fixed tree fmt and removed chat with images 2026-03-14 11:49:44 -04:00
Richie b5ee7c2dc2 added logging 2026-03-14 11:49:44 -04:00
Richie 958b06ecf0 added auth cashe 2026-03-14 11:49:44 -04:00
Richie 71ad8ab29e removed comand prefix 2026-03-14 11:49:44 -04:00
Richie 852759c510 decreased signal_cli_rest_api version 2026-03-14 11:49:44 -04:00
Richie d684d5d62c add envvars to 2026-03-14 11:49:44 -04:00
Richie f1e394565d migrated to tanasty and added dead letter queue 2026-03-14 11:49:44 -04:00
Richie 754ced4822 added tenacity 2026-03-14 11:49:44 -04:00
Richie 5b054dfc8f added signalbot servec account 2026-03-14 11:49:44 -04:00
Richie 663833d4fa fixed tests 2026-03-14 11:49:44 -04:00
Richie 433ec9a38e fixed typo in van_inventory serviceConfig 2026-03-14 11:49:44 -04:00
Richie 3a3267ee9a fixed ruff warning 2026-03-14 11:49:44 -04:00
Richie 0497a50a43 removed repo_line_counter.py 2026-03-14 11:49:44 -04:00
Richie 6365dd8067 updated the van inventory to use the api 2026-03-14 11:49:44 -04:00
Richie a6fbbd245f fixed safety number logic 2026-03-14 11:49:44 -04:00
Richie 7ad321e5e2 moved device registry to postgresql 2026-03-14 11:49:44 -04:00
Richie 14338e34df updated BotConfig 2026-03-14 11:49:44 -04:00
Richie c73aa5c98a setup context manger for SignalClient and LLMClient 2026-03-14 11:49:44 -04:00
Richie f762f12bd2 added max retry and retry back off to run_loop 2026-03-14 11:49:44 -04:00
Richie ab5df442c6 reworked dispatch 2026-03-14 11:49:44 -04:00
Claude f11c9bed58 Remove LLMConfig, pass LLM settings directly to LLMClient
LLMConfig was an unnecessary intermediary — LLMClient now takes
model, host, and port directly as constructor args.

https://claude.ai/code/session_01AKXQBuVBsW7J1YbukDiQ7A
2026-03-14 11:49:44 -04:00
Claude ab2d8dbd51 Remove unused LLMConfig from BotConfig
LLMConfig was stored in BotConfig but never accessed after
construction — LLMClient receives it directly.

https://claude.ai/code/session_01AKXQBuVBsW7J1YbukDiQ7A
2026-03-14 11:49:44 -04:00
Claude 42ede19472 Replace polling with WebSocket for real-time Signal message reception
Switch from polling /v1/receive every 2s to a persistent WebSocket
connection at ws://.../v1/receive/<number>. Messages now arrive
instantly via the signal-cli-rest-api WebSocket endpoint.

- Add `listen()` generator to SignalClient using websockets library
- Extract `_parse_envelope()` as standalone function
- Replace `run_loop` polling with WebSocket listener + reconnect logic
- Remove `poll_interval` from BotConfig and CLI args
- Add websockets to Nix overlay and pyproject.toml dependencies

https://claude.ai/code/session_01AKXQBuVBsW7J1YbukDiQ7A
2026-03-14 11:49:44 -04:00
Claude f4f33eacc4 Run ruff format on Python files
https://claude.ai/code/session_01AKXQBuVBsW7J1YbukDiQ7A
2026-03-14 11:49:44 -04:00
Claude 51f6cd23ad add Signal command and control bot service
Python service for jeeves that communicates over Signal via signal-cli-rest-api.
Implements device verification via safety numbers (unverified devices cannot
run commands until verified over SSH), and a van inventory command that uses
an LLM on BOB (ollama) to parse receipt photos or text lists into structured
inventory data. The LLM backend is configurable to swap models easily.

https://claude.ai/code/session_01AKXQBuVBsW7J1YbukDiQ7A
2026-03-14 11:49:44 -04:00
Richie 3dadb145b7 added congress data to database 2026-03-14 11:49:44 -04:00
Richie 75a67294ea added bound checking to van invintory 2026-03-09 07:24:05 -04:00
Richie 58b25f2e89 ran treefmt 2026-03-09 07:18:01 -04:00
Richie 568bf8dd38 updated the van_inventory user and db 2026-03-09 07:18:01 -04:00
Richie 82851eb287 added van inventory serves 2026-03-09 07:18:01 -04:00
Richie b7bce0bcb9 created alembic revision for van_inventory 2026-03-09 07:18:01 -04:00
Richie 583af965ad fixed orm __init__.py 2026-03-09 07:18:01 -04:00
Richie ec80bf1c5f added commit to env.py 2026-03-09 07:18:01 -04:00
Richie bd490334f5 added van api and front end 2026-03-09 07:18:01 -04:00
Richie e893ea0f57 added python-multipart 2026-03-09 07:18:01 -04:00
Richie 18f149b831 ran treefmt 2026-03-09 07:18:01 -04:00
Richie 69f5b87e5f setup multy db suport 2026-03-09 07:18:01 -04:00
Richie 66acc010ca add typedmonarchmoney 2026-03-08 16:47:52 -04:00
Richie e8f3a563be adding nix_serve to brain 2026-03-08 16:37:25 -04:00
Richie 8f1d765cad adding llms 2026-03-08 16:23:20 -04:00
Richie 4f0ba687c4 improving kitty 2026-03-08 16:13:46 -04:00
github-actions[bot] 27891c3903 flake.lock: Update
Flake lock file updates:

• Updated input 'firefox-addons':
    'gitlab:rycee/nur-expressions/ec30ecf?dir=pkgs/firefox-addons' (2026-01-30)
  → 'gitlab:rycee/nur-expressions/07e1616?dir=pkgs/firefox-addons' (2026-03-06)
• Updated input 'home-manager':
    'github:nix-community/home-manager/4759213' (2026-01-30)
  → 'github:nix-community/home-manager/daa2c22' (2026-03-06)
• Updated input 'nixos-hardware':
    'github:nixos/nixos-hardware/a351494' (2026-01-25)
  → 'github:nixos/nixos-hardware/41c6b42' (2026-02-24)
• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/bfc1b8a' (2026-01-26)
  → 'github:nixos/nixpkgs/80bdc1e' (2026-03-04)
• Updated input 'nixpkgs-master':
    'github:nixos/nixpkgs/743e016' (2026-01-31)
  → 'github:nixos/nixpkgs/af5157a' (2026-03-07)
• Updated input 'sops-nix':
    'github:Mic92/sops-nix/c5eebd4' (2026-01-26)
  → 'github:Mic92/sops-nix/1d9b98a' (2026-03-02)
2026-03-07 14:39:24 -05:00
Richie ccdc61b4dd removed neofetch
it served us well
2026-03-07 10:43:34 -05:00
Richie 1d732bf41c removed ds_python 2026-03-05 20:47:34 -05:00
Richie 13ba118cfc testing python313 for ds_python 2026-03-05 20:11:49 -05:00
Richie 47c6f42d2f adding ds_python 2026-03-05 20:11:49 -05:00
dependabot[bot] ff9dcde5d9 Bump minimatch from 3.1.2 to 3.1.5 in /frontend
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.1.2 to 3.1.5.
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 3.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-04 22:21:17 -05:00
Richie 7de800b519 added lazy_error_count to victron_modbuss.yaml and
removed HA from haproxy.cfg
2026-03-04 22:12:42 -05:00
Richie 55767ad555 llm update 2026-03-04 22:12:27 -05:00
Richie c262ff9048 added time out and fallback to nix settings 2026-03-04 22:07:53 -05:00
dependabot[bot] 9abac2978a Bump rollup from 4.55.1 to 4.59.0 in /frontend
Bumps [rollup](https://github.com/rollup/rollup) from 4.55.1 to 4.59.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.55.1...v4.59.0)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.59.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-04 22:01:49 -05:00
Richie 70d20e55d2 fix code rabbit warnings 2026-02-18 17:12:38 -05:00
Richie f038f248a1 improved eval_warnings/main.py 2026-02-18 17:12:38 -05:00
Richie af828fc9c4 updated nix_builder pkgs 2026-02-18 13:50:53 -05:00
Richie 4d121ae9f9 created fix_eval_warnings.yml and python eval_warnings 2026-02-18 13:50:53 -05:00
Richie 959d599ff9 updated pyproject.toml 2026-02-17 23:06:03 -05:00
Richie d470243fdd moved ollama-url to a secrets 2026-02-17 23:06:03 -05:00
Richie d96c93fa17 created fix_eval_warnings.yml and python eval_warnings 2026-02-17 23:06:03 -05:00
Richie 6bea380e3d creating shared user settings 2026-02-11 20:07:08 -05:00
Richie 56c933c8cb ran treefmt 2026-02-08 13:17:33 -05:00
Richie e7dae1eb4b added retry logic to post_to_ha 2026-02-08 13:17:33 -05:00
Richie 17ebe50ac9 made van weather start after HA 2026-02-08 13:17:33 -05:00
Richie 97b35ce27b fixing van_weather_template.yaml 2026-02-08 13:17:33 -05:00
github-actions[bot] 595579fe8b flake.lock: Update
Flake lock file updates:

• Updated input 'firefox-addons':
    'gitlab:rycee/nur-expressions/91b470d?dir=pkgs/firefox-addons' (2026-01-23)
  → 'gitlab:rycee/nur-expressions/ec30ecf?dir=pkgs/firefox-addons' (2026-01-30)
• Updated input 'home-manager':
    'github:nix-community/home-manager/082a4cd' (2026-01-23)
  → 'github:nix-community/home-manager/4759213' (2026-01-30)
• Updated input 'nixos-hardware':
    'github:nixos/nixos-hardware/9f7ba89' (2026-01-22)
  → 'github:nixos/nixos-hardware/a351494' (2026-01-25)
• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/88d3861' (2026-01-21)
  → 'github:nixos/nixpkgs/bfc1b8a' (2026-01-26)
• Updated input 'nixpkgs-master':
    'github:nixos/nixpkgs/565e0c1' (2026-01-24)
  → 'github:nixos/nixpkgs/743e016' (2026-01-31)
• Updated input 'sops-nix':
    'github:Mic92/sops-nix/c7067be' (2026-01-19)
  → 'github:Mic92/sops-nix/c5eebd4' (2026-01-26)
2026-02-05 08:45:37 -05:00
Richie fcfbce4e16 fix treefmt issues 2026-02-04 20:05:32 -05:00
Richie 80af3377e6 created python heater to contron the hln heater 2026-02-04 20:05:32 -05:00
Richie 557c1a4d5d added van_weather 2026-02-04 18:59:24 -05:00
Richie 89e37249af added flatpak to rhapsody-in-green 2026-01-25 21:35:32 -05:00
github-actions[bot] ccd523b4d0 flake.lock: Update
Flake lock file updates:

• Updated input 'firefox-addons':
    'gitlab:rycee/nur-expressions/45d1193?dir=pkgs/firefox-addons' (2026-01-09)
  → 'gitlab:rycee/nur-expressions/91b470d?dir=pkgs/firefox-addons' (2026-01-23)
• Updated input 'home-manager':
    'github:nix-community/home-manager/0e4217b' (2026-01-09)
  → 'github:nix-community/home-manager/082a4cd' (2026-01-23)
• Updated input 'nixos-hardware':
    'github:nixos/nixos-hardware/40b1a28' (2025-12-31)
  → 'github:nixos/nixos-hardware/9f7ba89' (2026-01-22)
• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/3497aa5' (2026-01-08)
  → 'github:nixos/nixpkgs/88d3861' (2026-01-21)
• Updated input 'nixpkgs-master':
    'github:nixos/nixpkgs/aaae520' (2026-01-10)
  → 'github:nixos/nixpkgs/565e0c1' (2026-01-24)
• Updated input 'sops-nix':
    'github:Mic92/sops-nix/ea3adcb' (2026-01-07)
  → 'github:Mic92/sops-nix/c7067be' (2026-01-19)
2026-01-25 09:23:05 -05:00
Richie 606035432b home assistant updates 2026-01-23 10:24:54 -05:00
Richie 4d2f6831e3 adding cpp tools 2026-01-23 09:42:20 -05:00
Richie 86e72d1da0 fixed ruff errors 2026-01-22 21:26:38 -05:00
Richie 139727bf50 fixed pyproject.toml from stuff rename 2026-01-22 21:26:38 -05:00
Richie 88c2f1b139 updated contact_api.nix and how the api is 2026-01-22 21:26:38 -05:00
Richie e75a3ef9c6 renamed random to stuff 2026-01-22 21:26:38 -05:00
Richie 258f918794 reworded fastapi code 2026-01-22 21:26:38 -05:00
Richie cf4635922e added claudeCode setting 2026-01-22 21:26:38 -05:00
Richie 0615ece46a added frontend 2026-01-22 21:26:38 -05:00
Richie 8afa4fce6c added Contact api and data model 2026-01-22 21:26:38 -05:00
Richie 8bbcd37933 added fastapi-cli 2026-01-22 21:26:38 -05:00
Richie 037b2f9cf7 updated .gitignore and AGENTS.md 2026-01-22 21:26:38 -05:00
Richie 7dbc4c248f removed gaming user 2026-01-13 18:09:53 -05:00
Richie 08dffc6f6d moved jellyfin cacheDir 2026-01-13 18:07:02 -05:00
Richie 0109167b10 base of sqlalchemy alembic 2026-01-11 11:41:19 -05:00
Richie b87f6b0b34 adding Packages 2026-01-11 11:41:19 -05:00
Richie 35376c3fca added opencode 2026-01-10 09:45:13 -05:00
Richie 0c218f2551 added dolphin-llama3 2026-01-10 09:45:13 -05:00
Richie d0b66496a1 Major llm rework 2026-01-10 09:45:13 -05:00
Richie 5101da4914 added env vars 2026-01-10 09:45:13 -05:00
Richie 393545868f adding open_webui to jeeves 2026-01-10 09:45:13 -05:00
Richie 6bb7904782 updated nixfmt pkg 2026-01-10 09:35:28 -05:00
Richie 59147834f7 updated my_python to python314 2026-01-10 08:54:33 -05:00
github-actions[bot] 52235239d0 flake.lock: Update
Flake lock file updates:

• Updated input 'firefox-addons':
    'gitlab:rycee/nur-expressions/03d7d31?dir=pkgs/firefox-addons' (2025-12-26)
  → 'gitlab:rycee/nur-expressions/45d1193?dir=pkgs/firefox-addons' (2026-01-09)
• Updated input 'home-manager':
    'github:nix-community/home-manager/91cdb0e' (2025-12-25)
  → 'github:nix-community/home-manager/0e4217b' (2026-01-09)
• Updated input 'nixos-hardware':
    'github:nixos/nixos-hardware/c5db956' (2025-12-24)
  → 'github:nixos/nixos-hardware/40b1a28' (2025-12-31)
• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/3e2499d' (2025-12-25)
  → 'github:nixos/nixpkgs/3497aa5' (2026-01-08)
• Updated input 'nixpkgs-master':
    'github:nixos/nixpkgs/088b069' (2025-12-27)
  → 'github:nixos/nixpkgs/45a1530' (2026-01-09)
• Updated input 'sops-nix':
    'github:Mic92/sops-nix/9836912' (2025-12-21)
  → 'github:Mic92/sops-nix/ea3adcb' (2026-01-07)
2026-01-10 08:54:33 -05:00
github-actions[bot] 9e43c3e8b8 flake.lock: Update
Flake lock file updates:

• Updated input 'firefox-addons':
    'gitlab:rycee/nur-expressions/03d7d31?dir=pkgs/firefox-addons' (2025-12-26)
  → 'gitlab:rycee/nur-expressions/45d1193?dir=pkgs/firefox-addons' (2026-01-09)
• Updated input 'home-manager':
    'github:nix-community/home-manager/91cdb0e' (2025-12-25)
  → 'github:nix-community/home-manager/0e4217b' (2026-01-09)
• Updated input 'nixos-hardware':
    'github:nixos/nixos-hardware/c5db956' (2025-12-24)
  → 'github:nixos/nixos-hardware/40b1a28' (2025-12-31)
• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/3e2499d' (2025-12-25)
  → 'github:nixos/nixpkgs/3497aa5' (2026-01-08)
• Updated input 'nixpkgs-master':
    'github:nixos/nixpkgs/088b069' (2025-12-27)
  → 'github:nixos/nixpkgs/aaae520' (2026-01-10)
• Updated input 'sops-nix':
    'github:Mic92/sops-nix/9836912' (2025-12-21)
  → 'github:Mic92/sops-nix/ea3adcb' (2026-01-07)
2026-01-09 21:36:23 -05:00
392 changed files with 37988 additions and 4812 deletions
+28
View File
@@ -0,0 +1,28 @@
.git
.direnv
.mypy_cache
.pytest_cache
.ruff_cache
.venv
**/.venv
.env
.cache
.claude
.coverage
.vscode
.stfolder
.literotica_data
esphome
htmlcov
data
ebooks
__pycache__
**/__pycache__
*.pyc
*.pyo
.ebook_search_bm25
result
result-*
*.egg-info
dist
build
+17 -2
View File
@@ -8,8 +8,23 @@ on:
- cron: "0 22 * * *"
jobs:
prebuild-common:
name: prebuild-common-x86-64-v3
runs-on: nix-cache-builder
steps:
- uses: actions/checkout@v4
# portal-1 is the smallest system closure: 95% of its derivations are
# shared by all five systems, so it is a maintainable common cache seed.
# Keep going so one failing package does not stop unrelated cache entries
# from being built.
- name: Build common packages
run: nixos-rebuild build --keep-going --accept-flake-config --flake ./#portal-1
- name: Copy common packages to nix-cache
run: nix copy --accept-flake-config --to unix:///host-nix/var/nix/daemon-socket/socket .#nixosConfigurations.portal-1.config.system.build.toplevel
build:
name: build-${{ matrix.system }}
needs: prebuild-common
runs-on: self-hosted
strategy:
matrix:
@@ -17,12 +32,12 @@ jobs:
- "bob"
- "brain"
- "jeeves"
- "leviathan"
- "rhapsody-in-green"
- "portal-1"
continue-on-error: true
steps:
- uses: actions/checkout@v4
- name: Build default package
run: "nixos-rebuild build --flake ./#${{ matrix.system }}"
run: "nixos-rebuild build --accept-flake-config --flake ./#${{ matrix.system }}"
- name: copy to nix-cache
run: nix copy --accept-flake-config --to unix:///host-nix/var/nix/daemon-socket/socket .#nixosConfigurations.${{ matrix.system }}.config.system.build.toplevel
+7 -13
View File
@@ -6,24 +6,18 @@ on:
jobs:
merge:
runs-on: ubuntu-latest
runs-on: self-hosted
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: merge_flake_lock_update
run: |
pr_number=$(gh pr list --state open --author RichieCahill --label flake_lock_update --json number --jq '.[0].number')
echo "pr_number=$pr_number" >> $GITHUB_ENV
if [ -n "$pr_number" ]; then
gh pr merge "$pr_number" --rebase
else
echo "No open PR found with label flake_lock_update"
fi
run: >-
nix develop .#devShells.x86_64-linux.default -c
python -m python.gitea_flake_lock merge
--repo "${{ github.repository }}"
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN_FOR_UPDATES }}
JEEVES_BOT_TOKEN: ${{ secrets.JEEVES_BOT_TOKEN }}
GITEA_URL: https://gitea.tmmworkshop.com
+1 -1
View File
@@ -1,13 +1,13 @@
name: pytest
on:
workflow_dispatch:
push:
branches:
- main
pull_request:
branches:
- main
merge_group:
jobs:
pytest:
+26
View File
@@ -0,0 +1,26 @@
name: test ebook search
on:
workflow_dispatch:
push:
branches:
- main
pull_request:
branches:
- main
env:
UV_PYTHON_DOWNLOADS: never
UV_CACHE_DIR: /var/cache/uv
UV_LINK_MODE: copy
jobs:
test-ebook-search:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: nix develop .#devShells.x86_64-linux.ebook-search -c uv sync --locked --project python/ebook_search/docker
- name: Run ebook search tests
run: nix develop .#devShells.x86_64-linux.ebook-search -c uv run --project python/ebook_search/docker --no-sync pytest tests/ebook_search --override-ini addopts="-n auto -ra"
+14 -11
View File
@@ -6,18 +6,21 @@ on:
jobs:
lockfile:
runs-on: ubuntu-latest
runs-on: self-hosted
permissions:
actions: write
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@main
- name: Update flake.lock
uses: DeterminateSystems/update-flake-lock@main
with:
token: ${{ secrets.GH_TOKEN_FOR_UPDATES }}
pr-title: "Update flake.lock"
pr-labels: |
dependencies
automated
flake_lock_update
run: nix flake update
- name: Create or update flake.lock PR
env:
JEEVES_BOT_TOKEN: ${{ secrets.JEEVES_BOT_TOKEN }}
GITEA_URL: https://gitea.tmmworkshop.com
run: >-
nix develop .#devShells.x86_64-linux.default -c
python -m python.gitea_flake_lock update
--repo "${{ github.repository }}"
+11
View File
@@ -165,3 +165,14 @@ test.*
# syncthing
.stfolder
# Frontend build output
frontend/dist/
frontend/node_modules/
# data from testing llms
data/*
.ebook_search_bm25
# gems data
.gems
+5 -2
View File
@@ -1,14 +1,17 @@
# Generate AGE keys from SSH keys with:
# ssh-keygen -A
# nix-shell -p ssh-to-age --run 'cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age'
# update keys after addin/removing a key
# nix-shell -p sops --run "sops updatekeys users/secrets.yaml" users/secrets.yaml
keys:
- &admin_richie age1u8zj599elqqvcmhxn8zuwrufsz8w8w366d3ayrljjejljt2q45kq8mxw9c # cspell:disable-line
- &system_bob age1q47vup0tjhulkg7d6xwmdsgrw64h4ax3la3evzqpxyy4adsmk9fs56qz3y # cspell:disable-line
- &system_brain age1jhf7vm0005j60mjq63696frrmjhpy8kpc2d66mw044lqap5mjv4snmwvwm # cspell:disable-line
- &system_jeeves age13lmqgc3jvkyah5e3vcwmj4s5wsc2akctcga0lpc0x8v8du3fxprqp4ldkv # cspell:disable-line
- &system_leviathan age1l272y8udvg60z7edgje42fu49uwt4x2gxn5zvywssnv9h2krms8s094m4k # cspell:disable-line
- &system_rhapsody age1ufnewppysaq2wwcl4ugngjz8pfzc5a35yg7luq0qmuqvctajcycs5lf6k4 # cspell:disable-line
- &system_portal_1 age1vyav6kxtvt3z4vtnkkjj38eu8hlts5m7ygyckhskvalg2gpjk52su53d0a # cspell:disable-line
creation_rules:
- path_regex: users/secrets\.yaml$
@@ -18,5 +21,5 @@ creation_rules:
- *system_bob
- *system_brain
- *system_jeeves
- *system_leviathan
- *system_rhapsody
- *system_portal_1
+14 -5
View File
@@ -10,7 +10,6 @@
"aiounifi",
"alsa",
"apiclient",
"apscheduler",
"archlinux",
"ashift",
"asrouter",
@@ -40,7 +39,6 @@
"cgroupdriver",
"charliermarsh",
"Checkpointing",
"cloudflared",
"codellama",
"codezombiech",
"compactmode",
@@ -72,11 +70,13 @@
"ehci",
"emerg",
"endlessh",
"ents",
"errorlens",
"esbenp",
"esphome",
"extest",
"fadvise",
"fastfetch",
"fastforwardteam",
"FASTFOX",
"ffmpegthumbnailer",
@@ -166,13 +166,14 @@
"mypy",
"ncdu",
"nemo",
"neofetch",
"nerdfonts",
"netdev",
"netdevs",
"Networkd",
"networkmanager",
"newtabpage",
"ngram",
"ngrams",
"nixfmt",
"nixos",
"nixpkgs",
@@ -204,6 +205,7 @@
"peerconnection",
"PESKYFOX",
"PGID",
"pgvector",
"pipewire",
"pkgs",
"plugdev",
@@ -232,6 +234,7 @@
"pyopenweathermap",
"pyownet",
"pytest",
"qalculate",
"quicksuggest",
"radarr",
"readahead",
@@ -241,6 +244,7 @@
"referer",
"REFERERS",
"relatime",
"rerank",
"Rhosts",
"ripgrep",
"roboto",
@@ -256,6 +260,7 @@
"sessionmaker",
"sessionstore",
"shellcheck",
"signalbot",
"signon",
"Signons",
"skia",
@@ -287,6 +292,7 @@
"topstories",
"treefmt",
"twimg",
"typedmonarchmoney",
"typer",
"uaccess",
"ubiquiti",
@@ -294,7 +300,9 @@
"uiprotect",
"uitour",
"unifi",
"unjudged",
"unrar",
"unstorable",
"unsubmitted",
"uptimekuma",
"urlbar",
@@ -304,6 +312,8 @@
"useragent",
"usernamehw",
"userprefs",
"vaninventory",
"vdev",
"vfat",
"victron",
"virt",
@@ -320,13 +330,12 @@
"xcursorgen",
"xdist",
"xhci",
"yake",
"yazi",
"yubikey",
"yubioath",
"yzhang",
"zeroconf",
"zerotier",
"zerotierone",
"zoxide",
"zram",
"zstd"
-5
View File
@@ -1,5 +0,0 @@
## Dev environment tips
- use treefmt to format all files
- make python code ruff compliant
- use pytest to test python code
Generated
+1686
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
[workspace]
resolver = "2"
members = ["rust/*"]
+50
View File
@@ -1 +1,51 @@
# dotfiles
## Installer ISO
Build a bootable NixOS ISO with the installer preinstalled:
```sh
nix build .#iso
```
Write `result/iso/nixos-zfs-installer.iso` to a USB stick (for example with `dd`) or boot it in a VM. The image is the minimal NixOS installation CD with ZFS enabled and `nixos-installer` on `PATH`. SSH is enabled and the `nixos` and `root` accounts use the password `nixos`, so you can also run the installer remotely. Once booted:
```sh
sudo nixos-installer
```
The ISO bundles the `.#installer-nixos` package, a variant of the binary that keeps its Nix store linkage instead of being patched for foreign distributions.
## Installer binary
Build the self-contained installer executable with:
```sh
nix build .#installer
```
The flake package (defined in `python/installer/package.nix`) uses the Python builder in `python/installer/build.py`, which stages only the installer modules before running PyInstaller. You can also call it directly when `pyinstaller` and `patchelf` are on `PATH`:
```sh
python -m python.installer.build --output ./nixos-installer
```
Copy `result/bin/nixos-installer` to the installer USB stick and run it as root from the NixOS live environment:
```sh
sudo ./nixos-installer
```
Validate the live environment first with:
```sh
./nixos-installer --check
```
Paste a value into the TUI encryption password field to enable LUKS during install, or set `ENCRYPT_KEY`:
```sh
sudo env ENCRYPT_KEY='change-me' ./nixos-installer
```
The binary bundles the Python runtime and only the installer modules it imports. It still expects the NixOS installer environment to provide system install tools such as `parted`, `zfs`, `zpool`, `cryptsetup`, `nixos-generate-config`, and `nixos-install`.
+6 -14
View File
@@ -17,13 +17,11 @@
./nix.nix
./programs.nix
./ssh.nix
./snapshot_manager.nix
];
boot = {
tmp.useTmpfs = true;
tmp.useTmpfs = lib.mkDefault true;
kernelPackages = lib.mkDefault pkgs.linuxPackages_6_12;
zfs.package = lib.mkDefault pkgs.zfs_2_4;
};
hardware.enableRedistributableFirmware = true;
@@ -37,22 +35,16 @@
nixpkgs = {
overlays = builtins.attrValues outputs.overlays;
config.allowUnfree = true;
config = {
allowUnfree = true;
};
};
services = {
dbus.implementation = "dbus";
# firmware update
fwupd.enable = true;
snapshot_manager = {
enable = lib.mkDefault true;
PYTHONPATH = "${inputs.self}/";
};
zfs = {
trim.enable = lib.mkDefault true;
autoScrub.enable = lib.mkDefault true;
};
};
powerManagement.powertop.enable = lib.mkDefault true;
+7
View File
@@ -31,8 +31,15 @@ in
"flakes"
"ca-derivations"
];
system-features = lib.mkAfter [
"gccarch-x86-64-v2"
"gccarch-x86-64-v3"
];
warn-dirty = false;
flake-registry = ""; # disable global flake registries
connect-timeout = 10;
download-buffer-size = 536870912;
fallback = true;
};
# Add each flake input as a registry and nix_path
+6
View File
@@ -0,0 +1,6 @@
{
nix.settings = {
trusted-substituters = [ "http://192.168.95.35:5000" ];
substituters = [ "http://192.168.95.35:5000/?priority=1&want-mass-query=true" ];
};
}
+1
View File
@@ -17,6 +17,7 @@
logDriver = "local";
storageDriver = "overlay2";
daemon.settings = {
live-restore = false;
experimental = true;
exec-opts = [ "native.cgroupdriver=systemd" ];
log-opts = {
+256
View File
@@ -0,0 +1,256 @@
{
config,
lib,
pkgs,
...
}:
let
monitoringInterface = "tailscale0";
nodeTextfileDir = "/var/lib/prometheus-node-exporter-textfile";
mkProcessNameTemplate =
perPid: template: if perPid then "${template}:{{.PID}}:{{.StartTime}}" else template;
mkProcessMatchers = perPid: [
{
name = mkProcessNameTemplate perPid "{{.Username}}:{{.Matches.Module}}";
cmdline = [ "^/nix/store[^ ]*/bin/python[^ ]* -m (?P<Module>[^ ]+)" ];
}
{
name = mkProcessNameTemplate perPid "{{.Username}}:{{.Matches.Wrapped}}";
cmdline = [
"^/nix/store[^ ]*/bin/python[^ ]* /nix/store[^ ]*/bin/\\.?(?P<Wrapped>[^ /]+?)(?:-wrapped)?(?:\\s|$)"
];
}
{
name = mkProcessNameTemplate perPid "{{.Username}}:{{.Matches.Wrapped}}";
cmdline = [
"^/nix/store[^ ]*/bin/node /nix/store[^ ]*-(?P<Wrapped>[A-Za-z0-9._+-]+)-[0-9][^ /]*/"
];
}
{
name = mkProcessNameTemplate perPid "{{.Username}}:{{.Matches.Wrapped}}";
cmdline = [ "^/nix/store[^ ]*/(?:bin/|lib/[^ ]*/)?\\.?(?P<Wrapped>[^ /]+?)(?:-wrapped)?(?:\\s|$)" ];
}
{
name = mkProcessNameTemplate perPid "{{.Username}}:{{.ExeBase}}";
cmdline = [ ".+" ];
}
];
perPidConfig = pkgs.writeText "process-exporter-per-pid.yaml" (
builtins.toJSON {
process_names = mkProcessMatchers true;
}
);
zpoolLatencyScript = pkgs.writeShellScript "zpool-latency-exporter" ''
set -euo pipefail
out_dir=${lib.escapeShellArg nodeTextfileDir}
host=${lib.escapeShellArg config.networking.hostName}
tmp_file="$(mktemp "$out_dir/zpool.prom.XXXXXX")"
trap 'rm -f "$tmp_file"' EXIT
pools="$(zpool list -H -o name | paste -sd, -)"
cat >"$tmp_file" <<'EOF'
# HELP zpool_iostat_total_wait_read_ns Average total read wait time reported by zpool iostat.
# TYPE zpool_iostat_total_wait_read_ns gauge
# HELP zpool_iostat_total_wait_write_ns Average total write wait time reported by zpool iostat.
# TYPE zpool_iostat_total_wait_write_ns gauge
# HELP zpool_iostat_disk_wait_read_ns Average disk read wait time reported by zpool iostat.
# TYPE zpool_iostat_disk_wait_read_ns gauge
# HELP zpool_iostat_disk_wait_write_ns Average disk write wait time reported by zpool iostat.
# TYPE zpool_iostat_disk_wait_write_ns gauge
# HELP zpool_iostat_syncq_wait_read_ns Average synchronous queue read wait time reported by zpool iostat.
# TYPE zpool_iostat_syncq_wait_read_ns gauge
# HELP zpool_iostat_syncq_wait_write_ns Average synchronous queue write wait time reported by zpool iostat.
# TYPE zpool_iostat_syncq_wait_write_ns gauge
# HELP zpool_iostat_asyncq_wait_read_ns Average asynchronous queue read wait time reported by zpool iostat.
# TYPE zpool_iostat_asyncq_wait_read_ns gauge
# HELP zpool_iostat_asyncq_wait_write_ns Average asynchronous queue write wait time reported by zpool iostat.
# TYPE zpool_iostat_asyncq_wait_write_ns gauge
EOF
zpool iostat -Hplvy -y 1 1 | awk -F '\t' -v host="$host" -v pools="$pools" '
function esc(str, out) {
out = str
gsub(/\\/, "\\\\", out)
gsub(/"/, "\\\"", out)
return out
}
function emit(metric, pool, vdev, value) {
if (value == "" || value == "-") {
return
}
printf "%s{host=\"%s\",pool=\"%s\",vdev=\"%s\"} %s\n",
metric,
esc(host),
esc(pool),
esc(vdev),
value
}
BEGIN {
split(pools, pool_names, ",")
for (idx in pool_names) {
if (pool_names[idx] != "") {
known_pools[pool_names[idx]] = 1
}
}
}
NF == 0 {
next
}
{
row_name = $1
if (row_name in known_pools) {
current_pool = row_name
current_vdev = "_pool"
} else if (current_pool == "") {
next
} else {
current_vdev = row_name
}
emit("zpool_iostat_total_wait_read_ns", current_pool, current_vdev, $8)
emit("zpool_iostat_total_wait_write_ns", current_pool, current_vdev, $9)
emit("zpool_iostat_disk_wait_read_ns", current_pool, current_vdev, $10)
emit("zpool_iostat_disk_wait_write_ns", current_pool, current_vdev, $11)
emit("zpool_iostat_syncq_wait_read_ns", current_pool, current_vdev, $12)
emit("zpool_iostat_syncq_wait_write_ns", current_pool, current_vdev, $13)
emit("zpool_iostat_asyncq_wait_read_ns", current_pool, current_vdev, $14)
emit("zpool_iostat_asyncq_wait_write_ns", current_pool, current_vdev, $15)
}
' >>"$tmp_file"
mv "$tmp_file" "$out_dir/zpool.prom"
trap - EXIT
'';
in
{
networking.firewall.interfaces.${monitoringInterface}.allowedTCPPorts = [
9100
9134
9256
9257
9633
];
services.prometheus.exporters = {
node = {
enable = true;
enabledCollectors = [
"pressure"
"processes"
"systemd"
];
extraFlags = [ "--collector.textfile.directory=${nodeTextfileDir}" ];
};
process = {
enable = true;
user = "root";
group = "root";
settings.process_names = mkProcessMatchers false;
extraFlags = [
"-gather-smaps=false"
"-remove-empty-groups=true"
"-threads=false"
];
};
smartctl.enable = true;
zfs.enable = true;
};
programs.atop = {
enable = true;
atopService.enable = true;
atopRotateTimer.enable = true;
atopacctService.enable = true;
settings.interval = 30;
};
systemd = {
services = {
prometheus-process-pid-exporter = {
description = "Prometheus process exporter with per-PID naming";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
ExecStart = ''
${pkgs.prometheus-process-exporter}/bin/process-exporter \
--web.listen-address 0.0.0.0:9257 \
--config.path ${perPidConfig} \
-children=false \
-gather-smaps=false \
-remove-empty-groups=true \
-threads=false
'';
User = "root";
Group = "root";
Restart = "always";
WorkingDirectory = "/tmp";
CapabilityBoundingSet = [ "" ];
DeviceAllow = [ "" ];
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
UMask = "0077";
};
};
zpool-latency-exporter = {
description = "Exports ZFS latency metrics for node_exporter textfile collection";
after = [ "zfs-import.target" ];
requires = [ "zfs-import.target" ];
path = [
config.boot.zfs.package
pkgs.coreutils
pkgs.gawk
];
serviceConfig = {
Type = "oneshot";
ExecStart = zpoolLatencyScript;
};
};
};
timers.zpool-latency-exporter = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "2m";
OnUnitActiveSec = "60s";
Unit = "zpool-latency-exporter.service";
};
};
tmpfiles.rules = [ "d ${nodeTextfileDir} 0755 root root - -" ];
};
}
+1 -1
View File
@@ -12,7 +12,7 @@
brain.id = "SSCGIPI-IV3VYKB-TRNIJE3-COV4T2H-CDBER7F-I2CGHYA-NWOEUDU-3T5QAAN"; # cspell:disable-line
ipad.id = "KI76T3X-SFUGV2L-VSNYTKR-TSIUV5L-SHWD3HE-GQRGRCN-GY4UFMD-CW6Z6AX"; # cspell:disable-line
jeeves.id = "ICRHXZW-ECYJCUZ-I4CZ64R-3XRK7CG-LL2HAAK-FGOHD22-BQA4AI6-5OAL6AG"; # cspell:disable-line
phone.id = "TBRULKD-7DZPGGZ-F6LLB7J-MSO54AY-7KLPBIN-QOFK6PX-W2HBEWI-PHM2CQI"; # cspell:disable-line
phone.id = "JPVQKQW-CFXOJXT-Q5G5F3H-QIDHDRE-GKHPTQB-GXZUQSP-U7FR7F7-INP3AAH"; # cspell:disable-line
rhapsody-in-green.id = "ASL3KC4-3XEN6PA-7BQBRKE-A7JXLI6-DJT43BY-Q4WPOER-7UALUAZ-VTPQ6Q4"; # cspell:disable-line
};
};
+37
View File
@@ -0,0 +1,37 @@
{
config,
inputs,
...
}:
{
nix.settings = {
trusted-substituters = [ "http://jeeves:5000" ];
substituters = [ "http://jeeves:5000/?priority=1&want-mass-query=true" ];
trusted-public-keys = [ "cache.tmmworkshop.com:jHffkpgbmEdstQPoihJPYW9TQe6jnQbWR2LqkNGV3iA=" ];
};
services.tailscale = {
enable = true;
openFirewall = true;
authKeyFile = config.sops.secrets.tailscale_auth_key.path;
# OAuth client secrets create ephemeral nodes by default. NixOS machines
# are persistent and should enroll without interactive device approval.
authKeyParameters = {
ephemeral = false;
preauthorized = true;
};
extraUpFlags = [ "--advertise-tags=tag:nixos" ];
};
sops = {
age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
secrets.tailscale_auth_key = {
sopsFile = "${inputs.self}/users/secrets.yaml";
owner = "root";
mode = "0400";
};
};
}
+1 -1
View File
@@ -4,7 +4,7 @@
flags = [ "--accept-flake-config" ];
randomizedDelaySec = "1h";
persistent = true;
flake = "github:RichieCahill/dotfiles";
flake = "git+https://gitea.tmmworkshop.com/richie/dotfiles?ref=main";
allowReboot = true;
dates = "Sat *-*-* 06:00:00";
};
+9
View File
@@ -0,0 +1,9 @@
{
nixpkgs.hostPlatform = {
system = "x86_64-linux";
gcc = {
arch = "x86-64-v3";
tune = "generic";
};
};
}
-11
View File
@@ -1,11 +0,0 @@
{
services.zerotierone = {
enable = true;
joinNetworks = [ "e4da7455b2ae64ca" ];
};
nix.settings = {
trusted-substituters = [ "http://192.168.90.40:5000" ];
substituters = [ "http://192.168.90.40:5000/?priority=1&want-mass-query=true" ];
trusted-public-keys = [ "cache.tmmworkshop.com:jHffkpgbmEdstQPoihJPYW9TQe6jnQbWR2LqkNGV3iA=" ];
};
}
+26
View File
@@ -0,0 +1,26 @@
{
inputs,
lib,
pkgs,
...
}:
{
imports = [ ./snapshot.nix ];
boot.zfs = {
package = lib.mkDefault pkgs.zfs_2_4;
forceImportRoot = lib.mkDefault false;
};
services = {
snapshot_manager = {
enable = lib.mkDefault true;
PYTHONPATH = "${inputs.self}/";
};
zfs = {
trim.enable = lib.mkDefault true;
autoScrub.enable = lib.mkDefault true;
};
};
}
@@ -22,6 +22,12 @@ hourly = 0
daily = 0
monthly = 0
["root_pool/nix_build"]
15_min = 1
hourly = 0
daily = 0
monthly = 0
["root_pool/var"]
15_min = 8
hourly = 24
+76
View File
@@ -0,0 +1,76 @@
# ZFS failed root import recovery
## Fast path
If the machine fails to boot because ZFS refuses to import `root_pool`:
### GRUB
1. At the bootloader menu, select the normal NixOS entry.
2. Press `e`.
3. Find the line that starts with `linux`.
4. Append this to the end of that line:
```text
zfs_force=1
```
5. Boot once with `Ctrl+x` or `F10`.
### systemd-boot
1. At the bootloader menu, highlight the normal NixOS entry.
2. Press `e`.
3. Append this to the end of the options line:
```text
zfs_force=1
```
4. Press `Enter` to boot once.
## After boot
Run:
```bash
sudo zpool status
sudo zpool import
journalctl -b | rg "ZFS|zfs|import|root_pool"
```
## Expected result
`sudo zpool status` should show `root_pool` as `ONLINE`.
## Reboot test
Run:
```bash
sudo reboot
```
Do not add `zfs_force=1` the second time.
## If it still fails
Boot once more with:
```text
zfs_force=1
```
Then run:
```bash
sudo zpool status -v
sudo zpool history | tail -n 50
journalctl -b | rg "ZFS|zfs|import|root_pool"
```
## Notes
- Root pool name is `root_pool`.
- This is a one-time recovery path after disk moves, controller changes, dirty exports, or interrupted imports.
- Some hosts also need the LUKS unlock USB key inserted before boot.
File diff suppressed because one or more lines are too long
Generated
+50 -36
View File
@@ -1,25 +1,23 @@
{
"nodes": {
"firefox-addons": {
"disko": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"dir": "pkgs/firefox-addons",
"lastModified": 1766762570,
"narHash": "sha256-Nevsj5NYurwp3I6nSMeh3uirwoinVSbCldqOXu4smms=",
"owner": "rycee",
"repo": "nur-expressions",
"rev": "03d7d310ea91d6e4b47ed70aa86c781fcc5b38e1",
"type": "gitlab"
"lastModified": 1781152676,
"narHash": "sha256-RxWs5ND31KzTG7wvMM+PMfUjyNpmIEr999lqNARaM5o=",
"owner": "nix-community",
"repo": "disko",
"rev": "ff8702b4de27f72b4c78573dfb89ec74e36abdf1",
"type": "github"
},
"original": {
"dir": "pkgs/firefox-addons",
"owner": "rycee",
"repo": "nur-expressions",
"type": "gitlab"
"owner": "nix-community",
"repo": "disko",
"type": "github"
}
},
"home-manager": {
@@ -29,11 +27,11 @@
]
},
"locked": {
"lastModified": 1766682973,
"narHash": "sha256-GKO35onS711ThCxwWcfuvbIBKXwriahGqs+WZuJ3v9E=",
"lastModified": 1788651960,
"narHash": "sha256-v9wJd32eZ2bvhBzVOd7TIjLQd011P7nwOhjKtWlci5I=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "91cdb0e2d574c64fae80d221f4bf09d5592e9ec2",
"rev": "2c0350c759688177331b8f5242311fae8877bdb3",
"type": "github"
},
"original": {
@@ -43,12 +41,15 @@
}
},
"nixos-hardware": {
"inputs": {
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1766568855,
"narHash": "sha256-UXVtN77D7pzKmzOotFTStgZBqpOcf8cO95FcupWp4Zo=",
"lastModified": 1788860136,
"narHash": "sha256-MhPMOFV4pVkygWEbQ8t1De/uQ9cWF1u++tRe2L5tG48=",
"owner": "nixos",
"repo": "nixos-hardware",
"rev": "c5db9569ac9cc70929c268ac461f4003e3e5ca80",
"rev": "62173785b9a18c78b4a15aca2623d02bceb9d077",
"type": "github"
},
"original": {
@@ -60,27 +61,24 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1766651565,
"narHash": "sha256-QEhk0eXgyIqTpJ/ehZKg9IKS7EtlWxF3N7DXy42zPfU=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "3e2499d5539c16d0d173ba53552a4ff8547f4539",
"type": "github"
"lastModified": 1767892417,
"narHash": "sha256-8bW3q88CEg2u4hSP66Vf4lpbLonHz7hqDNBMcCY7E9U=",
"rev": "3497aa5c9457a9d88d71fa93a4a8368816fbeeba",
"type": "tarball",
"url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre924538.3497aa5c9457/nixexprs.tar.xz"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
"type": "tarball",
"url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"
}
},
"nixpkgs-master": {
"locked": {
"lastModified": 1766794443,
"narHash": "sha256-Q8IyTQ3Lu8vX/iqO3U+E4pjLbP1NsqFih6uElf8OYrQ=",
"lastModified": 1788892992,
"narHash": "sha256-cIMFh9gyU4/aLeB3JCcsWM3tTAvD9pAq9Smr1Wa8aIU=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "088b069b8270ee36d83533c86b9f91d924d185d9",
"rev": "dff6994123e257ec9901c271bc2b52e64d7c8f05",
"type": "github"
},
"original": {
@@ -106,12 +104,28 @@
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1788752844,
"narHash": "sha256-VaWGJ6+cIYN2erfSecbRV+4ljI185Ty2wUrXyvQbgOw=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "dc5d91f840324650bac8c379428c7037a416959a",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"firefox-addons": "firefox-addons",
"disko": "disko",
"home-manager": "home-manager",
"nixos-hardware": "nixos-hardware",
"nixpkgs": "nixpkgs",
"nixpkgs": "nixpkgs_2",
"nixpkgs-master": "nixpkgs-master",
"nixpkgs-stable": "nixpkgs-stable",
"sops-nix": "sops-nix",
@@ -125,11 +139,11 @@
]
},
"locked": {
"lastModified": 1766289575,
"narHash": "sha256-BOKCwOQQIP4p9z8DasT5r+qjri3x7sPCOq+FTjY8Z+o=",
"lastModified": 1788337237,
"narHash": "sha256-gkSH8VUtCo6hnysNmb9DbTuDepH2t5pv+QWjP75xKAk=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "9836912e37aef546029e48c8749834735a6b9dad",
"rev": "fbf759290e0cb0a98dfc813a4eb7d53ad1dacb57",
"type": "github"
},
"original": {
+46 -36
View File
@@ -26,13 +26,13 @@
inputs.nixpkgs.follows = "nixpkgs";
};
firefox-addons = {
url = "gitlab:rycee/nur-expressions?dir=pkgs/firefox-addons";
sops-nix = {
url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
sops-nix = {
url = "github:Mic92/sops-nix";
disko = {
url = "github:nix-community/disko";
inputs.nixpkgs.follows = "nixpkgs";
};
};
@@ -65,38 +65,48 @@
devShells = forEachSystem (pkgs: import ./shell.nix { inherit pkgs; });
formatter = forEachSystem (pkgs: pkgs.treefmt);
packages = forEachSystem (
pkgs:
let
installer = pkgs.callPackage ./python/installer/package.nix { };
installer-nixos = pkgs.callPackage ./python/installer/package.nix { patchElf = false; };
in
{
inherit installer installer-nixos;
default = installer;
}
// lib.optionalAttrs (pkgs.stdenv.hostPlatform.system == "x86_64-linux") {
iso = self.nixosConfigurations.iso.config.system.build.isoImage;
}
);
apps = forEachSystem (
pkgs:
let
system = pkgs.stdenv.hostPlatform.system;
installer = {
type = "app";
program = "${self.packages.${system}.installer}/bin/nixos-installer";
meta.description = "One-file NixOS ZFS installer.";
};
in
{
inherit installer;
default = installer;
}
);
nixosConfigurations = {
bob = lib.nixosSystem {
modules = [
./systems/bob
];
specialArgs = { inherit inputs outputs; };
};
brain = lib.nixosSystem {
modules = [
./systems/brain
];
specialArgs = { inherit inputs outputs; };
};
jeeves = lib.nixosSystem {
modules = [
./systems/jeeves
];
specialArgs = { inherit inputs outputs; };
};
rhapsody-in-green = lib.nixosSystem {
modules = [
./systems/rhapsody-in-green
];
specialArgs = { inherit inputs outputs; };
};
leviathan = lib.nixosSystem {
modules = [
./systems/leviathan
];
specialArgs = { inherit inputs outputs; };
};
};
nixosConfigurations =
let
hosts = builtins.attrNames (
lib.filterAttrs (_: type: type == "directory") (builtins.readDir ./systems)
);
mkHost =
name:
lib.nixosSystem {
modules = [ ./systems/${name} ];
specialArgs = { inherit inputs outputs; };
};
in
lib.genAttrs hosts mkHost;
};
}
+18 -7
View File
@@ -15,26 +15,37 @@
};
};
patches = import ./patches;
test-exclusions = import ./test-exclusions.nix;
# x86-64-v3-workarounds = import ./x86-64-v3-workarounds.nix;
python-env = final: _prev: {
my_python = final.python313.withPackages (
my_python = final.python314.withPackages (
ps: with ps; [
alembic
apprise
apscheduler
fastapi
fastapi-cli
httpx
jinja2
mypy
polars
pgvector
psycopg
pydantic
pyfakefs
pytest
pytest-cov
pytest-mock
pytest-xdist
requests
python-multipart
pydantic-settings
ruff
scalene
sqlalchemy
textual
tenacity
tinytuya
typer
types-requests
uvicorn
websockets
]
);
};
+46
View File
@@ -0,0 +1,46 @@
# Package patches
Each package follows the [GnuTLS layout](gnutls/README.md):
- `default.nix` applies the patch through the package overlay.
- A descriptive `.patch` file contains the standalone upstream source change.
- `README.md` explains the problem, scope, reproduction, upstream status,
Nix integration, and recorded validation limits.
- Companion `verify-*` tools live beside the patch when needed; otherwise
the README gives commands for the package's existing tests.
Keep package-specific evidence in its directory. Patch headers explain the
change independently of Nix, and `default.nix` preserves existing patches.
| Package | Repair |
| --- | --- |
| [Abseil](abseil/README.md) | Public BMI2 header in Electron, Deno, and Signal's vendored copies |
| [Backrefs](backrefs/README.md) | Match the regex timeout's CPU clock |
| [GnuTLS](gnutls/README.md) | Wait for the UDP server socket before connecting |
| [Jupyter Server](jupyter-server/README.md) | Exercise the correct shared future during reconnect |
| [Prometheus](prometheus/README.md) | Complete parsing before inspecting the test editor state |
| [pytest-xdist](pytest-xdist/README.md) | Check worker replacements despite concurrent crashes |
| [SciPy](scipy/README.md) | Account for floating-point rounding in STFT tests |
| [Sentry SDK](sentry-sdk/README.md) | Isolate SDK thread mocks from Python's threading module |
| [Torchaudio](torchaudio/README.md) | Compare pitch-shift batches at appropriate precision |
| [TorchCodec](torchcodec/README.md) | Match the reference MP3 encoder's sample format |
## Local NixOS integration
[`../default.nix`](../default.nix) imports this directory's
[`default.nix`](default.nix), which wires each package's override into the
package set. Abseil repairs several vendored copies and is gated on
`x86-64-v3`; Prometheus patches its separate assets derivation; Python
packages use `pythonPackagesExtensions`.
[`../test-exclusions.nix`](../test-exclusions.nix) retains only pytest-xdist's
outer-worker limit and inner-worker startup allowance. It adds no skipped
tests. Existing nixpkgs exclusions remain separate from these repairs.
The test-exclusion review used Python 3.14.7 and the pinned x86-64-v3 package
set. Host-flake evaluation verified patch wiring, Python install checks,
removal of the local skips, and Prometheus's reference to the patched assets.
Jupyter and Sentry package tests used the preceding dependency set with the
new package patch to avoid unrelated rebuilds after pytest-xdist changed.
No complete NixOS rebuild was performed. Individual READMEs distinguish
package builds, focused tests, and checks that have not been run.
+59
View File
@@ -0,0 +1,59 @@
# Abseil BMI2 public header
Vendored Abseil includes `bmi2intrin.h` directly when `__BMI2__` is enabled.
Compilers reject that internal header without the umbrella-header setup.
`bmi2-public-header.patch` includes `immintrin.h` instead, allowing builds
that enable BMI2 through `-march=x86-64-v3`.
## Scope and behavior
The patch changes one include in
`third_party/abseil-cpp/absl/container/internal/raw_hash_set.h`.
`default.nix` applies it to Electron 43's unwrapped package, Deno's
`librusty_v8`, and Signal's WebRTC dependency. It also supplies the patched
Electron package to Signal. These overrides apply only to `x86-64-v3`.
The shared file path is relative to each vendoring project's source root,
not the root of a standalone Abseil checkout. No hash-table algorithm or
test exclusion changes.
## Reproduction and focused checks
From this directory, check and apply the patch to each vendored source tree:
```sh
patch --dry-run --fuzz=0 -d /path/to/vendor-source -p1 < bmi2-public-header.patch
patch --fuzz=0 -d /path/to/vendor-source -p1 < bmi2-public-header.patch
```
A small compiler check isolates the header requirement. With GCC or Clang
on x86-64, compile `#include <bmi2intrin.h>` using `-march=x86-64-v3`; the
compiler rejects the direct include. Changing it to `#include <immintrin.h>`
should compile. The full consumer builds below check integration with their
actual toolchains.
## Upstream status
Abseil addressed this issue through
[PR #2071](https://github.com/abseil/abseil-cpp/pull/2071), imported by its
upstream workflow. That change uses `x86gprintrin.h`; this local variant uses
the public `immintrin.h` umbrella header for the vendored toolchains.
Keep the workaround until all three bundled copies include a compatible fix.
This file is a local adaptation, not a verbatim copy of the upstream diff.
## Local NixOS integration and build results
[`../default.nix`](../default.nix) merges this directory's overlay fragment
because it repairs multiple packages. From the repository root, the consumer
build commands are:
```sh
nix build --no-link -L .#nixosConfigurations.jeeves.pkgs.deno
nix build --no-link -L .#nixosConfigurations.jeeves.pkgs.electron_43
nix build --no-link -L .#nixosConfigurations.jeeves.pkgs.signal-desktop
```
The earlier extraction checked the vendored header snapshots and evaluated
all three patch attachments. Those records do not establish successful full
consumer rebuilds. No new compiler or consumer build was run for the layout
change; the patch and override are unchanged.
@@ -0,0 +1,20 @@
Subject: [PATCH] abseil: include BMI2 intrinsics through the public header
GCC and Clang reject direct inclusion of bmi2intrin.h. Include immintrin.h
instead so that the compiler supplies the required intrinsic setup when
BMI2 is enabled, including builds targeting x86-64-v3.
This patch is shared by the vendored Abseil copies in Electron, rusty_v8
(Deno), and Signal's WebRTC build.
--- a/third_party/abseil-cpp/absl/container/internal/raw_hash_set.h
+++ b/third_party/abseil-cpp/absl/container/internal/raw_hash_set.h
@@ -226,7 +226,7 @@
#endif
#ifdef __BMI2__
-#include <bmi2intrin.h>
+#include <immintrin.h>
#endif // __BMI2__
namespace absl {
+38
View File
@@ -0,0 +1,38 @@
# Abseil accepted the upstream fix: https://github.com/abseil/abseil-cpp/pull/2071
# Keep this workaround until Electron, Deno's rusty_v8, and Signal's WebRTC
# update their bundled Abseil copies to include it.
{ prev }:
let
patchAbseilBmi2Include =
package:
package.overrideAttrs (old: {
# GCC and Clang require the public umbrella header for BMI2 intrinsics.
patches = (old.patches or [ ]) ++ [ ./bmi2-public-header.patch ];
});
electron43Unwrapped = patchAbseilBmi2Include prev.electron_43.unwrapped;
electron43 = prev.electron_43.override {
electron-unwrapped = electron43Unwrapped;
};
signalCallPackage =
path: args:
let
package = prev.callPackage path args;
in
if builtins.baseNameOf path == "webrtc.nix" then patchAbseilBmi2Include package else package;
in
prev.lib.optionalAttrs ((prev.stdenv.hostPlatform.gcc.arch or null) == "x86-64-v3") {
deno =
let
librusty_v8 = patchAbseilBmi2Include prev.deno.passthru.librusty_v8;
in
prev.deno.override { inherit librusty_v8; };
electron_43 = electron43;
signal-desktop = prev.signal-desktop.override {
electron_43 = electron43;
callPackage = signalCallPackage;
};
}
+15
View File
@@ -0,0 +1,15 @@
_final: prev:
(import ./abseil { inherit prev; })
// {
gnutls = import ./gnutls { inherit (prev) gnutls; };
prometheus = import ./prometheus { inherit (prev) prometheus; };
pythonPackagesExtensions = prev.pythonPackagesExtensions ++ [
(_pythonFinal: pythonPrev: {
backrefs = import ./backrefs { inherit (pythonPrev) backrefs; };
pytest-xdist = import ./pytest-xdist { inherit (pythonPrev) pytest-xdist; };
sentry-sdk = import ./sentry-sdk { inherit (pythonPrev) sentry-sdk; };
torchcodec = import ./torchcodec { inherit (pythonPrev) torchcodec; };
})
];
}
+160
View File
@@ -0,0 +1,160 @@
# GnuTLS UDP server readiness
Under load, the test client can start before `gnutls-serv` binds its UDP
socket, and the first handshake fails with `Connection refused`.
`serv-udp.sh` currently waits a fixed four seconds; elapsed time does not
establish server readiness. `udp-server-readiness.patch` replaces that wait
with polling for the local IPv4 UDP endpoint.
## Scope and waiting behavior
The patch changes the existing `wait_udp_server()` and adds a new
`check_if_udp_port_bound()` beside it in `tests/scripts/common.sh`.
`serv-udp.sh` is its only caller in 3.8.13. The TCP helpers `wait_server()`
and `wait_for_port()`, including their existing sleeps, are unchanged.
Both original DTLS handshake checks remain unchanged.
Each iteration checks process liveness and the socket **before sleeping**.
A ready socket returns immediately. An unsuccessful check sleeps two
seconds only if another attempt remains: at most 90 attempts, consistent
with the existing `wait_server()` budget implemented by `wait_for_port()`,
with no sleep after the final check. Server exit fails early; exhausting the
budget fails and terminates the server. No handshake is retried, and no
protocol timeout is changed. Once bound, the kernel can queue datagrams
while the server is scheduled; the probe itself sends no packets.
The existing `have_port_finder()` prefers `ss`, then `netstat`. If neither
exists, it prints `neither ss nor netstat found` and exits **77 (skip)**.
In the normal test flow, port selection calls it before launching a server.
The probe runs in a subshell so that, even if this skip occurs after launch,
the waiting helper can terminate and reap the server before exiting 77.
## Why an IPv4 socket is expected
This is specific to the server used by this test, not a general rule that
IPv6 sockets cannot serve IPv4 clients. The client explicitly uses
`127.0.0.1`. The server's `--udp` path calls `udp_server()`, which calls
`listen_socket(..., SOCK_DGRAM)`. That function iterates the wildcard
addresses returned by `getaddrinfo(NULL, ..., AI_PASSIVE)`:
| Server build / Linux setting | Binding behavior |
| --- | --- |
| IPv6 enabled, `net.ipv6.bindv6only=0` | Requests `IPV6_V6ONLY=1` on the IPv6 socket, binds `[::]:PORT`, and separately binds `0.0.0.0:PORT`. It overrides the system's dual-stack default. |
| IPv6 enabled, `net.ipv6.bindv6only=1` | The same explicit socket option and separate IPv4/IPv6 binds. |
| `HAVE_IPV6` undefined | Skips every address family except `AF_INET`; only the IPv4 wildcard is attempted. |
`udp_server()` uses `wait_for_connection()`, which puts **every listener**
from that list into `select()` and returns a readable socket for `recvfrom()`;
it does not permanently choose one socket based on `getaddrinfo()` order.
The first two cases were traced with the actual GnuTLS 3.8.13 binary in
separate Linux network namespaces: `setsockopt(IPV6_V6ONLY, [1])` and both
UDP binds returned success under each setting. The no-IPv6 case was checked
in source, not by building a second binary. The same bind implementation
was checked directly on GitLab master.
Thus, successful normal startup for this invocation provides an explicit
IPv4 socket; a lone IPv6 wildcard is not the expected success path.
There is one portability caveat: upstream discards the return value of
`setsockopt(IPV6_V6ONLY)`. On a platform where that call fails and the server
ends up with only a dual-stack socket, this helper would time out despite
IPv4 reachability. Such a platform needs additional handling before this
patch can claim support. Blindly accepting every IPv6 wildcard would also
accept IPv6-only sockets before the separate IPv4 bind finishes.
Source: [`src/serv.c`, `listen_socket()`](https://gitlab.com/gnutls/gnutls/-/blob/master/src/serv.c#L937),
[`src/udp-serv.c`](https://gitlab.com/gnutls/gnutls/-/blob/master/src/udp-serv.c),
and [`tests/serv-udp.sh`](https://gitlab.com/gnutls/gnutls/-/blob/master/tests/serv-udp.sh).
## Port matching and ownership limit
Only `-an` is passed to the socket-listing tool: BSD `netstat -u` selects
Unix-domain sockets, whereas Linux `netstat -u` selects UDP. The parser
handles the extra state column in `ss`, Linux colon-separated endpoints,
and BSD dot-separated endpoints, including `*.PORT`. It matches the full
local port and rejects TCP, IPv6 entries, peer ports, and longer numbers.
A live PID plus a bound port does **not** prove that PID owns the socket.
Existing `GETPORT` selection checks for an unused port and uses a test
port-lock directory; `launch_bare_server()` also calls
`wait_for_free_port()` before starting the process. These are advisory:
the launcher does not enforce the latter's result, and another process
can bind between the check and launch. The patch does not close that race
or add nonportable PID parsing. An unrelated process can satisfy the
socket check; the real handshakes remain the functional check and may
fail (or reach the wrong server). This is a startup-order fix, not a
socket-ownership guarantee.
## Reproduction and focused checks
Apply the patch to an unpacked source tree, then run the companion checks
with Python's standard library and a shell:
```sh
patch --fuzz=0 -d /path/to/gnutls -p1 < udp-server-readiness.patch
SHELL=/bin/sh python3 verify-readiness.py /path/to/gnutls/tests/scripts/common.sh -v
```
Set `NETSTAT=/path/to/netstat` to exercise one outside `PATH`. The checks
cover Linux/BSD output samples, false matches, immediate readiness,
missing tools, process exit, timeout cleanup, and real IPv4 UDP sockets
whose bind is delayed six seconds. The missing-tools fixture is skipped
if an absolute fallback `ss` path cannot be hidden with `PATH`. Native
BSD execution remains untested.
To reproduce with GnuTLS itself, run `tests/serv-udp.sh` with `SERV` pointing
to a wrapper that sleeps six seconds, then `exec`s `gnutls-serv` with all
arguments. Set `CLI` to the matching `gnutls-cli`, `srcdir` to the source
`tests` directory, and `abs_top_builddir` to a writable build directory.
With GnuTLS 3.8.13, the original helper failed the first handshake with
`Connection refused`; the patched helper passed both with the same binaries.
## GnuTLS submission
Development and merge requests are on [GitLab](https://gitlab.com/gnutls/gnutls).
[`CONTRIBUTING.md` on master](https://gitlab.com/gnutls/gnutls/-/blob/master/CONTRIBUTING.md)
was read directly for this review. It requires the contributor's DCO
`Signed-off-by`, successful and failure test coverage, consistent coding
style, and adequate documentation; GitLab CI runs for merge requests.
Its commenting guidance asks for comments explaining non-obvious behavior
or protocol expectations. It does not prescribe an additional special
test-suite comment. The patch now explains its IPv4 binding assumption
next to the probe.
The submission will contain the shell patch, without the Python verifier
or a new Python test dependency. The existing `serv-udp.sh` supplies the
functional success check. Running it through the six-second startup
wrapper supplies a reproducible regression case: it fails before the fix
and passes after it. The local verifier was used to validate socket-output
parsing and the helper's success, process-exit, skip-cleanup, and timeout
branches. Those branch checks are local evidence, not new automated
coverage in the upstream suite; the MR must state that distinction.
No dedicated unit-test harness for these shell helpers was found in the
3.8.13 tests inspected. That does not establish that Python cannot be used
upstream; keeping this submission dependency-free is a scope choice. Use
the existing test and before/after reproduction as the submission's
coverage argument, retaining the platform limitations above. Apply the
patch in an upstream checkout and include those results with the
contributor's own sign-off. No MR or sign-off has been created.
## Local NixOS integration and build results
`overlays/default.nix` imports the `overlays/patches` overlay, which loads
`gnutls/default.nix` to apply the patch and keep `serv-udp.sh` enabled.
The patch itself has no Nix dependencies and applies to 3.8.13 and GitLab
master without fuzz.
The final patch was rebuilt with:
```sh
nix build --no-link -L .#nixosConfigurations.jeeves.pkgs.gnutls
```
That x86-64-v3 build passed: 927 tests, 796 passes, 131 existing skips,
zero failures/errors, and `PASS: serv-udp.sh`. The patch bytes in the built
derivation were compared with the repository artifact; both have SHA-256
`59013d47fd446f2dd065012a2259ccc1898fedc8a053a630e13efa0076368760`.
All seven local checks passed, including skip cleanup and exactly 90
probes with 89 sleeps on timeout. The six-second before/after reproduction
was also repeated successfully with the final helper.
+6
View File
@@ -0,0 +1,6 @@
{ gnutls }:
gnutls.overrideAttrs (old: {
# Keep the UDP handshake test enabled on loaded builders by waiting for
# the server to bind its socket. Kept as a standalone patch for upstream.
patches = (old.patches or [ ]) ++ [ ./udp-server-readiness.patch ];
})
@@ -0,0 +1,70 @@
Subject: [PATCH] tests: wait for the UDP server socket before connecting
A fixed four-second sleep does not guarantee that gnutls-serv has bound
its UDP socket on a busy builder. Poll the local IPv4 UDP endpoint using
the existing ss/netstat discovery, with the same retry budget as the TCP
helper. Fail early if the server exits, and retain the original handshake
checks in serv-udp.sh.
Use flags common to ss and BSD/Linux netstat. Match the local endpoint
and complete port number, excluding TCP, IPv6-only and peer endpoints.
--- a/tests/scripts/common.sh
+++ b/tests/scripts/common.sh
@@ -185,10 +185,55 @@
fi
}
+check_if_udp_port_bound() {
+ local PORT=$1
+ have_port_finder
+ # Use only -an, which is shared by ss and BSD/Linux netstat. UDP has
+ # no LISTEN state. Match the local IPv4 endpoint, not a peer port or
+ # a longer port number. serv-udp.sh connects to 127.0.0.1;
+ # listen_socket() in serv.c binds IPv4 separately and requests
+ # IPV6_V6ONLY=1 for its IPv6 socket.
+ $PFCMD -an | awk -v port="$PORT" '
+ $1 == "udp" || $1 == "udp4" {
+ # ss includes a state column; netstat does not.
+ address = ($2 == "UNCONN" || $2 == "ESTAB") ? $5 : $4
+ if (address ~ ("^[0-9.]+[.:]" port "$") ||
+ address == "*." port)
+ found = 1
+ }
+ END { exit !found }
+ '
+}
+
wait_udp_server() {
local PID=$1
+ local ret
trap "test -n \"${PID}\" && kill ${PID};exit 1" 1 15 2
- sleep 4
+ local i=0
+ # Use the same retry budget as wait_for_port(), but also stop if the
+ # server exits before binding its socket.
+ while test $i -lt 90; do
+ if ! kill -0 "$PID" 2>/dev/null; then
+ fail "" "UDP server $PID exited before binding port $PORT"
+ fi
+ # Contain have_port_finder's exit so a skip also stops the server.
+ if (check_if_udp_port_bound "$PORT"); then
+ return 0
+ else
+ ret=$?
+ if test "$ret" = 77; then
+ kill "$PID" 2>/dev/null || :
+ wait "$PID" 2>/dev/null || :
+ exit 77
+ fi
+ fi
+ i=$((i + 1))
+ if test $i -lt 90; then
+ echo "try $i: waiting for UDP port $PORT"
+ sleep 2
+ fi
+ done
+ fail "$PID" "UDP server $PORT did not come up"
}
create_testdir() {
+180
View File
@@ -0,0 +1,180 @@
#!/usr/bin/env python3
"""Exercise patched common.sh without building GnuTLS (Python standard library only).
Usage: python3 verify-readiness.py /path/to/patched/tests/scripts/common.sh
Set SHELL to test another shell, and NETSTAT to test a netstat outside PATH.
"""
# Use unittest so this upstream companion tool needs no pytest installation.
# ruff: noqa: PT009
import os
import shutil
import socket
import subprocess
import sys
import tempfile
import time
import unittest
from pathlib import Path
COMMON = str(Path(sys.argv.pop(1)).resolve())
SHELL = os.environ.get("SHELL", "/bin/sh")
class ReadinessTests(unittest.TestCase):
"""Check endpoint parsing and the server startup lifecycle."""
def setUp(self) -> None:
"""Create a socket-listing fixture for each check."""
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.root = Path(self.tmp.name)
self.fixture = self.root / "sockets"
self.fixture.write_text("")
self.finder = self.root / "port-finder"
self.finder.write_text('#!/bin/sh\ncat "$SOCKET_FIXTURE"\n')
self.finder.chmod(0o755)
def run_shell(self, body: str, **env: str) -> subprocess.CompletedProcess[str]:
"""Source the actual helper and run a shell scenario."""
return subprocess.run(
[SHELL, "-c", '. "$COMMON"\n' + body],
env={
**os.environ,
"COMMON": COMMON,
"SOCKET_FIXTURE": str(self.fixture),
"PFCMD": str(self.finder),
"PORT": "12345",
**env,
},
capture_output=True,
text=True,
timeout=20,
check=False,
)
def test_socket_formats_and_false_matches(self) -> None:
"""Accept IPv4 UDP local endpoints and reject unrelated sockets."""
cases = [
("udp UNCONN 0 0 0.0.0.0:12345 0.0.0.0:*", True),
("udp UNCONN 0 0 127.0.0.1:12345 0.0.0.0:*", True),
("udp 0 0 0.0.0.0:12345 0.0.0.0:*", True),
("udp4 0 0 *.12345 *.*", True),
("udp 0 0 127.0.0.1.12345 *.*", True),
("udp 0 0 *.12345 *.*", True),
("udp UNCONN 0 0 0.0.0.0:123456 0.0.0.0:*", False),
("udp 0 0 0.0.0.0:123456 0.0.0.0:*", False),
("udp ESTAB 0 0 127.0.0.1:54321 127.0.0.1:12345", False),
("udp 0 0 127.0.0.1:54321 127.0.0.1:12345", False),
("tcp LISTEN 0 128 0.0.0.0:12345 0.0.0.0:*", False),
("tcp 0 0 0.0.0.0:12345 0.0.0.0:* LISTEN", False),
("udp UNCONN 0 0 [::]:12345 [::]:*", False),
("udp UNCONN 0 0 *:12345 *:*", False),
("udp6 0 0 :::12345 :::*", False),
("udp6 0 0 *.12345 *.*", False),
("", False),
]
for row, ready in cases:
with self.subTest(row=row):
self.fixture.write_text(row + "\n")
result = self.run_shell('check_if_udp_port_bound "$PORT"')
self.assertEqual(result.returncode, 0 if ready else 1, result.stderr)
def test_exited_server_fails_immediately(self) -> None:
"""Fail without sleeping when the server has already exited."""
result = self.run_shell(
'true &\npid=$!\nwait "$pid"\nsleep() { echo "unexpected sleep" >&2; }\nwait_udp_server "$pid"'
)
self.assertEqual(result.returncode, 1)
self.assertIn("exited before binding", result.stderr)
self.assertNotIn("unexpected sleep", result.stderr)
def test_ready_socket_does_not_sleep(self) -> None:
"""Check readiness before the first sleep."""
self.fixture.write_text("udp UNCONN 0 0 0.0.0.0:12345 0.0.0.0:*\n")
result = self.run_shell('sleep() { echo "unexpected sleep" >&2; }\nwait_udp_server "$$"')
self.assertEqual(result.returncode, 0, result.stderr)
self.assertNotIn("unexpected sleep", result.stderr)
def test_missing_port_finders_skip(self) -> None:
"""Skip and stop the live server when no finder is available."""
# have_port_finder also tries these paths independently of PATH.
if any(os.access(f"{directory}/ss", os.X_OK) for directory in ("/sbin", "/usr/sbin", "/usr/local/sbin")):
self.skipTest("an absolute ss path cannot be hidden by this PATH-only fixture")
with subprocess.Popen(["sleep", "60"]) as server:
try:
result = self.run_shell(
'unset PFCMD\nPATH=/nonexistent\nwait_udp_server "$SERVER_PID"',
SERVER_PID=str(server.pid),
)
self.assertEqual(result.returncode, 77)
self.assertIn("neither ss nor netstat found", result.stderr)
server.wait(timeout=3)
self.assertLess(server.returncode, 0)
finally:
if server.poll() is None:
server.kill()
def test_timeout_is_bounded_and_cleans_up(self) -> None:
"""Stop polling after the retry budget and terminate the server."""
# Only accelerate the polling delay; keep a real live server process.
self.finder.write_text('#!/bin/sh\necho probe >&2\ncat "$SOCKET_FIXTURE"\n')
with subprocess.Popen(["sleep", "60"]) as server:
try:
result = self.run_shell(
'sleep() { echo polling-sleep; }\nwait_udp_server "$SERVER_PID"',
SERVER_PID=str(server.pid),
)
self.assertEqual(result.returncode, 1)
self.assertIn("did not come up", result.stderr)
self.assertEqual(result.stderr.count("probe\n"), 90)
self.assertEqual(result.stdout.count("polling-sleep"), 89)
server.wait(timeout=3)
self.assertLess(server.returncode, 0)
finally:
if server.poll() is None:
server.kill()
def test_server_exits_while_waiting(self) -> None:
"""Detect a startup failure that happens after polling begins."""
result = self.run_shell('sleep 1 &\npid=$!\nwait_udp_server "$pid"')
self.assertEqual(result.returncode, 1)
self.assertIn("exited before binding", result.stderr)
self.assertIn("waiting for UDP port", result.stdout)
def test_real_socket_delayed_beyond_four_seconds(self) -> None:
"""Wait for a real delayed bind with each installed port finder."""
finders = [shutil.which("ss"), os.environ.get("NETSTAT") or shutil.which("netstat")]
finders = [finder for finder in finders if finder]
if not finders:
self.skipTest("neither ss nor netstat available")
for finder in finders:
with self.subTest(finder=finder):
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
code = (
"import socket,time,sys; time.sleep(6); "
"s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); "
"s.bind(('127.0.0.1',int(sys.argv[1]))); time.sleep(30)"
)
with subprocess.Popen([sys.executable, "-c", code, str(port)]) as server:
try:
started = time.monotonic()
result = self.run_shell(
'wait_udp_server "$SERVER_PID"',
SERVER_PID=str(server.pid),
PORT=str(port),
PFCMD=finder,
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertGreaterEqual(time.monotonic() - started, 6)
self.assertIsNone(server.poll())
finally:
server.terminate()
server.wait(timeout=3)
if __name__ == "__main__":
unittest.main()
+66
View File
@@ -0,0 +1,66 @@
# Prometheus complete test parsing
CodeMirror gives editor-state creation a 20 ms synchronous parsing budget.
The shared `createEditorState()` test helper can therefore return an
incomplete syntax tree when the process is descheduled. The completion and
vector-matching tests immediately inspect that tree.
## Scope and behavior
`complete-test-parsing.patch` changes only
`module/codemirror-promql/src/test/utils-test.ts` inside `web/ui`. It completes
the small test expression with `ensureSyntaxTree(..., Infinity)` and publishes
the completed parse through an empty transaction so `syntaxTree(state)` sees
it. Failure to obtain a tree raises an error.
The original assertions remain enabled, including `autocomplete topk params 2`
and `foo * on(test,blub) bar`. The unlimited budget applies to the test helper;
production editor parsing budgets are unchanged.
## Reproduction and focused checks
Use a disposable Prometheus 3.14.0 checkout. The patch root is `web/ui`, matching
the Nix assets derivation. From this directory:
```sh
patch --fuzz=0 -d /path/to/prometheus/web/ui -p1 < complete-test-parsing.patch
cd /path/to/prometheus/web/ui
pnpm install --frozen-lockfile
pnpm --filter @prometheus-io/lezer-promql build
pnpm --filter @prometheus-io/codemirror-promql test
```
To force the scheduling condition, temporarily append this clock to
`module/codemirror-promql/setupJest.cjs` in the disposable checkout:
```js
let parseClock = 0;
Date.now = () => (parseClock += 25);
```
Each clock read crosses the editor's initial parsing budget. Against the
original helper, the hybrid and vector suites have 186 failures, including
both locally excluded cases. With the patch, all 386 CodeMirror tests pass
under that same clock. Remove the injected clock before normal builds.
## Upstream status
This is a standalone test-helper patch for Prometheus 3.14.0. No upstream
submission was made during this work. Recheck the helper when updating
Prometheus or CodeMirror, including how an ensured parse becomes visible
through the editor state.
## Local NixOS integration and build results
[`../default.nix`](../default.nix) loads `default.nix`, which patches the
separate assets derivation. It updates both `passthru.assets` and the main
Prometheus build's reference to those assets. From the repository root:
```sh
nix build --no-link -L .#nixosConfigurations.jeeves.pkgs.prometheus.assets
```
The full x86-64-v3 assets build passed with the normal clock, including the
CodeMirror and UI suites. Host-flake evaluation confirmed that the main
Prometheus derivation refers to these patched assets. The Go server package
was not rebuilt for this test-helper change.
@@ -0,0 +1,36 @@
Subject: [PATCH] tests: finish parsing before inspecting editor state
EditorState creation has a 20 ms parsing budget. A descheduled test can
therefore observe an incomplete tree. Finish these small test documents
without an interactive deadline and publish the result with a transaction.
Keep the original completion and vector-matching assertions enabled.
--- a/module/codemirror-promql/src/test/utils-test.ts
+++ b/module/codemirror-promql/src/test/utils-test.ts
@@ -13,7 +13,7 @@
import { parser } from '@prometheus-io/lezer-promql';
import { EditorState } from '@codemirror/state';
-import { LRLanguage } from '@codemirror/language';
+import { ensureSyntaxTree, LRLanguage } from '@codemirror/language';
import nock from 'nock';
import path from 'path';
import { fileURLToPath } from 'url';
@@ -23,10 +23,16 @@
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export function createEditorState(expr: string): EditorState {
- return EditorState.create({
+ const state = EditorState.create({
doc: expr,
extensions: lightPromQLSyntax,
});
+ // These tests need a complete tree, independent of the editor's time budget.
+ if (!ensureSyntaxTree(state, state.doc.length, Infinity)) {
+ throw new Error('Unable to parse the test expression');
+ }
+ // Publish the completed parse so syntaxTree(state) sees it too.
+ return state.update({}).state;
}
export function mockPrometheusServer(): void {
+17
View File
@@ -0,0 +1,17 @@
{ prometheus }:
prometheus.overrideAttrs (
old:
let
assets = old.passthru.assets.overrideAttrs (assetsOld: {
patches = (assetsOld.patches or [ ]) ++ [ ./complete-test-parsing.patch ];
});
in
{
postPatch = builtins.replaceStrings [ "${old.passthru.assets}" ] [ "${assets}" ] (
builtins.unsafeDiscardStringContext old.postPatch
);
passthru = old.passthru // {
inherit assets;
};
}
)
+67
View File
@@ -0,0 +1,67 @@
# pytest-xdist concurrent worker crashes
With two workers and a restart limit of three, the fourth worker crash
requests shutdown while another test can still be running. That test may
also crash. The original queued-work test requires exactly four failures,
even though five failures can occur without exceeding the replacement limit.
## Scope and behavior
`concurrent-worker-crashes.patch` changes the assertions in
`TestNodeFailure.test_max_worker_restart_tests_queued` in
`testing/acceptance_test.py`. It requires exactly three replacements, four or
five failed tests, the failed-tests exit status, the limit message, and no
internal error. It retains the two-worker workload and ten queued tests.
The existing nixpkgs pytest-9 compatibility patches remain in place.
Production scheduling and worker-restart behavior are unchanged.
## Reproduction and focused checks
Use a disposable pytest-xdist 3.8.0 checkout with its test dependencies and
the nixpkgs pytest-9 compatibility patches where required. From this directory:
```sh
patch --fuzz=0 -d /path/to/pytest-xdist -p1 < concurrent-worker-crashes.patch
cd /path/to/pytest-xdist
python -m pytest testing/acceptance_test.py \
-k test_max_worker_restart_tests_queued -q
```
Twenty unmodified runs passed during the review. To force the failing
schedule, modify the generated crashing test in a disposable checkout to
accept `worker_id`: make `gw3` wait for a marker created by `gw4`, and make
`gw4` pause 0.1 seconds after creating the marker. Then both have in-flight
tests when shutdown starts. Bound the marker wait so a reproduction failure
cannot hang the suite. The original assertion fails on five reported
failures; the patched test passes.
## Remaining resource settings
[`../../test-exclusions.nix`](../../test-exclusions.nix) runs the outer suite
with one worker and sets the inner-worker wait to 60 seconds. These settings
limit nested process pools and allow worker startup on loaded builders.
A separate reproduction inserts an 11-second `pytest_sessionstart` delay
into the child created by `test_basic_collect_and_runtests` in
`testing/test_remote.py`. The original 10-second channel wait fails; the
60-second wait passes. This is a worker-startup bound, not a product deadline.
## Upstream status
This is a standalone test patch for pytest-xdist 3.8.0. No upstream submission
was made during this work. Recheck the allowed in-flight failures and
replacement count when updating the scheduler or shutdown behavior.
## Local NixOS integration and build results
[`../default.nix`](../default.nix) loads `default.nix` through
`pythonPackagesExtensions`. From the repository root:
```sh
nix build --no-link -L .#nixosConfigurations.jeeves.pkgs.python314Packages.pytest-xdist
```
The patched x86-64-v3 package passed 185 tests, with 6 existing skips and
10 expected failures. The forced concurrent-crash reproduction passed after
the fix, and the focused test passed again after formatting the assertion.
@@ -0,0 +1,29 @@
Subject: [PATCH] tests: count replacements when checking the worker restart limit
With two workers, another in-flight test may crash after the fourth
crash requests shutdown. Either four or five failed tests is valid.
Require exactly three replacements and the failed-tests exit status,
while preserving the queued-work and no-internal-error assertions.
--- a/testing/acceptance_test.py
+++ b/testing/acceptance_test.py
@@ -1011,9 +1011,18 @@
"worker*crashed while running*",
"worker*crashed while running*",
"* xdist: maximum crashed workers reached: 3 *",
- "* 4 failed in *",
]
)
+ # A second in-flight test may crash after shutdown is requested.
+ # The restart limit constrains replacements, not concurrent failures.
+ replacements = sum(
+ line.startswith("replacing crashed worker ") for line in res.stdout.lines
+ )
+ assert replacements == 3
+ failed = res.parseoutcomes()["failed"]
+ assert failed in (4, 5)
+ res.assert_outcomes(failed=failed)
+ assert res.ret == pytest.ExitCode.TESTS_FAILED
assert "INTERNALERROR" not in res.stdout.str()
def test_max_worker_restart_die(self, pytester: pytest.Pytester) -> None:
@@ -0,0 +1,4 @@
{ pytest-xdist }:
pytest-xdist.overridePythonAttrs (old: {
patches = (old.patches or [ ]) ++ [ ./concurrent-worker-crashes.patch ];
})
+58
View File
@@ -0,0 +1,58 @@
# Sentry SDK thread-metadata test isolation
The fallback tests globally mock `threading.current_thread` while a worker
is running. Python 3.14's `Thread.join()` also calls that function. A one-use
mock can therefore be consumed by the wrong caller or raise `StopIteration`
when the main thread joins the worker.
## Scope and behavior
`isolate-threading-mocks.patch` changes three neighboring thread-metadata
tests in `tests/test_utils.py`, including the formerly excluded
`test_get_current_thread_meta_main_thread`.
Each test replaces only `sentry_sdk.utils.threading`, wraps the real module
for unmocked operations, and sets the SDK lookup's return value. The real
`Thread.join()` continues using Python's unmodified `threading` module.
The fallback-result assertions remain; SDK production code is unchanged.
## Reproduction and focused checks
Use a disposable Sentry SDK 2.66.0 checkout and its Python test dependencies.
From this directory:
```sh
patch --fuzz=0 -d /path/to/sentry-python -p1 < isolate-threading-mocks.patch
cd /path/to/sentry-python
python -m pytest tests/test_utils.py -k get_current_thread_meta -q
```
To reproduce the race, hold the worker inside its mock just after
`get_current_thread_meta()` returns, signal that point to the main thread,
and call `Thread.join()` before releasing the worker. Use an independent
bounded release so the patched join can finish. The original test raises
`StopIteration` in `join`; the patched test passes under the same schedule.
Perform this scheduling instrumentation only in a disposable checkout.
## Upstream status
This is a standalone test patch for Sentry SDK 2.66.0. No upstream submission
was made during this work. Recheck mock isolation and Python threading
behavior when upgrading the SDK or interpreter.
## Local NixOS integration and build results
[`../default.nix`](../default.nix) loads `default.nix` through
`pythonPackagesExtensions`. From the repository root:
```sh
nix build --no-link -L .#nixosConfigurations.jeeves.pkgs.python314Packages.sentry-sdk
```
The patched package passed 2,356 tests with 116 existing skips on Python
3.14.7. The controlled join reproduction failed before the fix and passed
after it.
That package build used the preceding dependency set with this patch to avoid
unrelated rebuilds after pytest-xdist changed. The integrated host derivation
was evaluated; a complete NixOS rebuild was not performed.
+4
View File
@@ -0,0 +1,4 @@
{ sentry-sdk }:
sentry-sdk.overridePythonAttrs (old: {
patches = (old.patches or [ ]) ++ [ ./isolate-threading-mocks.patch ];
})
@@ -0,0 +1,41 @@
Subject: [PATCH] tests: isolate SDK thread lookup mocks from Python threading
Thread.join also calls threading.current_thread on Python 3.14. A global
single-use side effect can be consumed by join instead of the SDK, or
raise StopIteration in join after the SDK consumes it. Patch the SDK's
module binding and delegate unmocked operations to the real module.
Apply the same isolation to the adjacent invalid-thread fallback tests.
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -914,7 +914,8 @@
results = Queue(maxsize=1)
def target():
- with mock.patch("threading.current_thread", side_effect=["fake thread"]):
+ with mock.patch("sentry_sdk.utils.threading", wraps=threading) as sdk_threading:
+ sdk_threading.current_thread.return_value = "fake thread"
results.put(get_current_thread_meta())
thread = threading.Thread(target=target)
@@ -930,7 +931,9 @@
def target():
# mock that somehow the current thread doesn't exist
- with mock.patch("threading.current_thread", side_effect=[None]):
+ # Keep the real threading module intact for concurrent Thread.join calls.
+ with mock.patch("sentry_sdk.utils.threading", wraps=threading) as sdk_threading:
+ sdk_threading.current_thread.return_value = None
results.put(get_current_thread_meta())
main_thread = threading.main_thread()
@@ -945,7 +948,8 @@
results = Queue(maxsize=1)
def target():
- with mock.patch("threading.current_thread", return_value="fake thread"):
+ with mock.patch("sentry_sdk.utils.threading", wraps=threading) as sdk_threading:
+ sdk_threading.current_thread.return_value = "fake thread"
results.put(get_current_thread_meta())
main_thread = threading.main_thread()
+27
View File
@@ -0,0 +1,27 @@
# Test resource settings for the locally rebuilt x86-64-v3 package set.
#
# Selecting x86-64-v3 changes every affected derivation, so the normal
# nixpkgs binary cache cannot be used and upstream test suites run locally.
# The jeeves builder uses /tmp/nix-builds so filesystem tests run on tmpfs
# instead of ZFS with normalization=formD and utf8only=on; those tests remain
# enabled. This overlay no longer excludes any tests. The remaining settings
# bound nested worker concurrency and allow time for worker startup under load.
# Test repairs and their validation are indexed in patches/README.md.
_final: prev: {
pythonPackagesExtensions = prev.pythonPackagesExtensions ++ [
(_pythonFinal: pythonPrev: {
pytest-xdist = pythonPrev.pytest-xdist.overridePythonAttrs (old: {
# The suite exercises its own worker pools. Run the outer suite with one
# worker and allow inner workers more time on heavily loaded builders.
postPatch = (old.postPatch or "") + ''
substituteInPlace testing/test_remote.py \
--replace-fail "WAIT_TIMEOUT = 10.0" "WAIT_TIMEOUT = 60.0"
'';
preCheck = builtins.replaceStrings [ "--numprocesses=$NIX_BUILD_CORES" ] [ "--numprocesses=1" ] (
old.preCheck or ""
);
});
})
];
}
+20
View File
@@ -0,0 +1,20 @@
# Output-validation workarounds for packages rebuilt with x86-64-v3.
_final: prev:
let
removeSiblingOutputChecks =
package:
package.overrideAttrs (old: {
# Nix 2.34 can validate a partial multi-output rebuild against only the
# outputs still being realised. PostgreSQL's checks then reject valid
# sibling names such as "out" and "lib". Keep the test suite and
# disallowed-requisite checks; accept the loss of cross-output checks.
outputChecks = builtins.mapAttrs (
_output: checks: builtins.removeAttrs checks [ "disallowedReferences" ]
) (old.outputChecks or { });
});
in
prev.lib.optionalAttrs ((prev.stdenv.hostPlatform.gcc.arch or null) == "x86-64-v3") {
postgresql = removeSiblingOutputChecks prev.postgresql;
postgresql_18 = removeSiblingOutputChecks prev.postgresql_18;
}
+53 -8
View File
@@ -3,27 +3,56 @@ name = "system_tools"
version = "0.1.0"
description = ""
authors = [{ name = "Richie Cahill", email = "richie@tmmworkshop.com" }]
requires-python = "~=3.13.0"
requires-python = "~=3.14.0"
readme = "README.md"
license = "MIT"
# these dependencies are a best effort and aren't guaranteed to work
dependencies = ["apprise", "apscheduler", "polars", "requests", "typer"]
# for up-to-date dependencies, see overlays/default.nix
dependencies = [
"alembic",
"apprise",
"beautifulsoup4",
"bm25s",
"ebooklib",
"fastapi",
"fastapi-cli",
"httpx",
"jinja2",
"pgvector",
"psycopg[binary]",
"pydantic",
"pydantic-settings",
"python-multipart",
"sqlalchemy[asyncio]",
"tenacity",
"tiktoken",
"tinytuya",
"typer",
"uvicorn",
"websockets",
"yake",
]
[project.scripts]
database = "python.database_cli:app"
whisper-transcribe = "python.tools.whisper.transcribe:main"
[dependency-groups]
dev = [
"aiosqlite",
"mypy",
"pyfakefs",
"pytest-asyncio",
"pytest-cov",
"pytest-mock",
"pytest-xdist",
"pytest",
"ruff",
"types-requests",
]
[tool.ruff]
target-version = "py313"
target-version = "py314"
line-length = 120
@@ -33,26 +62,40 @@ lint.ignore = [
"COM812", # (TEMP) conflicts when used with the formatter
"ISC001", # (TEMP) conflicts when used with the formatter
"S603", # (PERM) This is known to cause a false positive
"S607", # (PERM) This is becoming a consistent annoyance
"CPY001", # (PERM) I don't include the license in every file
]
[tool.ruff.lint.per-file-ignores]
"tests/**" = [
"S101", # (perm) pytest needs asserts
"ANN", # (perm) type annotations not needed in tests
"D", # (perm) docstrings not needed in tests
"PLR2004", # (perm) magic values are fine in test assertions
"S101", # (perm) pytest needs asserts
]
"python/random/**" = [
"python/stuff/**" = [
"T201", # (perm) I don't care about print statements dir
]
"python/testing/**" = [
"T201", # (perm) I don't care about print statements dir
"ERA001", # (perm) I don't care about print statements dir
]
"python/splendor/**" = [
"S311", # (perm) there is no security issue here
"T201", # (perm) I don't care about print statements dir
"PLR2004", # (temps) need to think about this
]
"python/orm/**" = [
"TC003", # (perm) this creates issues because sqlalchemy uses these at runtime
]
"python/congress_tracker/**" = [
"TC003", # (perm) this creates issues because sqlalchemy uses these at runtime
]
"python/alembic/**" = [
"INP001", # (perm) this creates LSP issues for alembic
]
[tool.ruff.lint.pydocstyle]
convention = "google"
@@ -75,5 +118,7 @@ exclude_lines = [
]
[tool.pytest.ini_options]
addopts = "-n auto -ra"
addopts = "-n auto -ra --ignore=tests/ebook_search"
asyncio_mode = "auto"
testpaths = ["tests"]
# --cov=system_tools --cov-report=term-missing --cov-report=xml --cov-report=html --cov-branch
+122
View File
@@ -0,0 +1,122 @@
"""Alembic."""
from __future__ import annotations
import logging
import sys
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal
from alembic import context
from alembic.script import write_hooks
from sqlalchemy.schema import CreateSchema
from python.common import bash_wrapper
from python.orm.common import get_postgres_engine
if TYPE_CHECKING:
from collections.abc import MutableMapping
from sqlalchemy.orm import DeclarativeBase
config = context.config
base_class: type[DeclarativeBase] = config.attributes.get("base")
if base_class is None:
error = "No base class provided. Use the database CLI to run alembic commands."
raise RuntimeError(error)
target_metadata = base_class.metadata
logging.basicConfig(
level="DEBUG",
datefmt="%Y-%m-%dT%H:%M:%S%z",
format="%(asctime)s %(levelname)s %(filename)s:%(lineno)d - %(message)s",
handlers=[logging.StreamHandler(sys.stdout)],
)
@write_hooks.register("dynamic_schema")
def dynamic_schema(filename: str, _options: dict[Any, Any]) -> None:
"""Dynamic schema."""
original_file = Path(filename).read_text()
schema_name = base_class.schema_name
dynamic_schema_file_part1 = original_file.replace(f"schema='{schema_name}'", "schema=schema")
dynamic_schema_file = dynamic_schema_file_part1.replace(f"'{schema_name}.", "f'{schema}.")
Path(filename).write_text(dynamic_schema_file)
@write_hooks.register("import_postgresql")
def import_postgresql(filename: str, _options: dict[Any, Any]) -> None:
"""Add postgresql dialect import when postgresql types are used."""
content = Path(filename).read_text()
if "postgresql." in content and "from sqlalchemy.dialects import postgresql" not in content:
content = content.replace(
"import sqlalchemy as sa\n",
"import sqlalchemy as sa\nfrom sqlalchemy.dialects import postgresql\n",
)
Path(filename).write_text(content)
@write_hooks.register("ruff")
def ruff_check_and_format(filename: str, _options: dict[Any, Any]) -> None:
"""Docstring for ruff_check_and_format."""
bash_wrapper(f"ruff check --fix {filename}")
bash_wrapper(f"ruff format {filename}")
def include_name(
name: str | None,
type_: Literal["schema", "table", "column", "index", "unique_constraint", "foreign_key_constraint"],
_parent_names: MutableMapping[Literal["schema_name", "table_name", "schema_qualified_table_name"], str | None],
) -> bool:
"""Filter tables to be included in the migration.
Args:
name (str): The name of the table.
type_ (str): The type of the table.
_parent_names (MutableMapping): The names of the parent tables.
Returns:
bool: True if the table should be included, False otherwise.
"""
if type_ == "schema":
# allows a database with multiple schemas to have separate alembic revisions
return name == target_metadata.schema
return True
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
env_prefix = config.attributes.get("env_prefix", "POSTGRES")
connectable = get_postgres_engine(name=env_prefix)
with connectable.connect() as connection:
schema = base_class.schema_name
if not connectable.dialect.has_schema(connection, schema):
answer = input(f"Schema {schema!r} does not exist. Create it? [y/N] ")
if answer.lower() != "y":
error = f"Schema {schema!r} does not exist. Exiting."
raise SystemExit(error)
connection.execute(CreateSchema(schema))
connection.commit()
context.configure(
connection=connection,
target_metadata=target_metadata,
include_schemas=True,
version_table_schema=schema,
include_name=include_name,
)
with context.begin_transaction():
context.run_migrations()
connection.commit()
run_migrations_online()
@@ -0,0 +1,113 @@
"""created contact api.
Revision ID: edd7dd61a3d2
Revises:
Create Date: 2026-01-11 15:45:59.909266
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import sqlalchemy as sa
from alembic import op
from python.orm import RichieBase
if TYPE_CHECKING:
from collections.abc import Sequence
# revision identifiers, used by Alembic.
revision: str = "edd7dd61a3d2"
down_revision: str | None = None
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
schema = RichieBase.schema_name
def upgrade() -> None:
"""Upgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"contact",
sa.Column("name", sa.String(), nullable=False),
sa.Column("age", sa.Integer(), nullable=True),
sa.Column("bio", sa.String(), nullable=True),
sa.Column("current_job", sa.String(), nullable=True),
sa.Column("gender", sa.String(), nullable=True),
sa.Column("goals", sa.String(), nullable=True),
sa.Column("legal_name", sa.String(), nullable=True),
sa.Column("profile_pic", sa.String(), nullable=True),
sa.Column("safe_conversation_starters", sa.String(), nullable=True),
sa.Column("self_sufficiency_score", sa.Integer(), nullable=True),
sa.Column("social_structure_style", sa.String(), nullable=True),
sa.Column("ssn", sa.String(), nullable=True),
sa.Column("suffix", sa.String(), nullable=True),
sa.Column("timezone", sa.String(), nullable=True),
sa.Column("topics_to_avoid", sa.String(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("pk_contact")),
schema=schema,
)
op.create_table(
"need",
sa.Column("name", sa.String(), nullable=False),
sa.Column("description", sa.String(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("pk_need")),
schema=schema,
)
op.create_table(
"contact_need",
sa.Column("contact_id", sa.Integer(), nullable=False),
sa.Column("need_id", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
["contact_id"],
[f"{schema}.contact.id"],
name=op.f("fk_contact_need_contact_id_contact"),
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["need_id"], [f"{schema}.need.id"], name=op.f("fk_contact_need_need_id_need"), ondelete="CASCADE"
),
sa.PrimaryKeyConstraint("contact_id", "need_id", name=op.f("pk_contact_need")),
schema=schema,
)
op.create_table(
"contact_relationship",
sa.Column("contact_id", sa.Integer(), nullable=False),
sa.Column("related_contact_id", sa.Integer(), nullable=False),
sa.Column("relationship_type", sa.String(length=100), nullable=False),
sa.Column("closeness_weight", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
["contact_id"],
[f"{schema}.contact.id"],
name=op.f("fk_contact_relationship_contact_id_contact"),
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["related_contact_id"],
[f"{schema}.contact.id"],
name=op.f("fk_contact_relationship_related_contact_id_contact"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("contact_id", "related_contact_id", name=op.f("pk_contact_relationship")),
schema=schema,
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("contact_relationship", schema=schema)
op.drop_table("contact_need", schema=schema)
op.drop_table("need", schema=schema)
op.drop_table("contact", schema=schema)
# ### end Alembic commands ###
@@ -0,0 +1,135 @@
"""add congress tracker tables.
Revision ID: 3f71565e38de
Revises: edd7dd61a3d2
Create Date: 2026-02-12 16:36:09.457303
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import sqlalchemy as sa
from alembic import op
from python.orm import RichieBase
if TYPE_CHECKING:
from collections.abc import Sequence
# revision identifiers, used by Alembic.
revision: str = "3f71565e38de"
down_revision: str | None = "edd7dd61a3d2"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
schema = RichieBase.schema_name
def upgrade() -> None:
"""Upgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"bill",
sa.Column("congress", sa.Integer(), nullable=False),
sa.Column("bill_type", sa.String(), nullable=False),
sa.Column("number", sa.Integer(), nullable=False),
sa.Column("title", sa.String(), nullable=True),
sa.Column("title_short", sa.String(), nullable=True),
sa.Column("official_title", sa.String(), nullable=True),
sa.Column("status", sa.String(), nullable=True),
sa.Column("status_at", sa.Date(), nullable=True),
sa.Column("sponsor_bioguide_id", sa.String(), nullable=True),
sa.Column("subjects_top_term", sa.String(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("pk_bill")),
sa.UniqueConstraint("congress", "bill_type", "number", name="uq_bill_congress_type_number"),
schema=schema,
)
op.create_index("ix_bill_congress", "bill", ["congress"], unique=False, schema=schema)
op.create_table(
"legislator",
sa.Column("bioguide_id", sa.Text(), nullable=False),
sa.Column("thomas_id", sa.String(), nullable=True),
sa.Column("lis_id", sa.String(), nullable=True),
sa.Column("govtrack_id", sa.Integer(), nullable=True),
sa.Column("opensecrets_id", sa.String(), nullable=True),
sa.Column("fec_ids", sa.String(), nullable=True),
sa.Column("first_name", sa.String(), nullable=False),
sa.Column("last_name", sa.String(), nullable=False),
sa.Column("official_full_name", sa.String(), nullable=True),
sa.Column("nickname", sa.String(), nullable=True),
sa.Column("birthday", sa.Date(), nullable=True),
sa.Column("gender", sa.String(), nullable=True),
sa.Column("current_party", sa.String(), nullable=True),
sa.Column("current_state", sa.String(), nullable=True),
sa.Column("current_district", sa.Integer(), nullable=True),
sa.Column("current_chamber", sa.String(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("pk_legislator")),
schema=schema,
)
op.create_index(op.f("ix_legislator_bioguide_id"), "legislator", ["bioguide_id"], unique=True, schema=schema)
op.create_table(
"vote",
sa.Column("congress", sa.Integer(), nullable=False),
sa.Column("chamber", sa.String(), nullable=False),
sa.Column("session", sa.Integer(), nullable=False),
sa.Column("number", sa.Integer(), nullable=False),
sa.Column("vote_type", sa.String(), nullable=True),
sa.Column("question", sa.String(), nullable=True),
sa.Column("result", sa.String(), nullable=True),
sa.Column("result_text", sa.String(), nullable=True),
sa.Column("vote_date", sa.Date(), nullable=False),
sa.Column("yea_count", sa.Integer(), nullable=True),
sa.Column("nay_count", sa.Integer(), nullable=True),
sa.Column("not_voting_count", sa.Integer(), nullable=True),
sa.Column("present_count", sa.Integer(), nullable=True),
sa.Column("bill_id", sa.Integer(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(["bill_id"], [f"{schema}.bill.id"], name=op.f("fk_vote_bill_id_bill")),
sa.PrimaryKeyConstraint("id", name=op.f("pk_vote")),
sa.UniqueConstraint("congress", "chamber", "session", "number", name="uq_vote_congress_chamber_session_number"),
schema=schema,
)
op.create_index("ix_vote_congress_chamber", "vote", ["congress", "chamber"], unique=False, schema=schema)
op.create_index("ix_vote_date", "vote", ["vote_date"], unique=False, schema=schema)
op.create_table(
"vote_record",
sa.Column("vote_id", sa.Integer(), nullable=False),
sa.Column("legislator_id", sa.Integer(), nullable=False),
sa.Column("position", sa.String(), nullable=False),
sa.ForeignKeyConstraint(
["legislator_id"],
[f"{schema}.legislator.id"],
name=op.f("fk_vote_record_legislator_id_legislator"),
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["vote_id"], [f"{schema}.vote.id"], name=op.f("fk_vote_record_vote_id_vote"), ondelete="CASCADE"
),
sa.PrimaryKeyConstraint("vote_id", "legislator_id", name=op.f("pk_vote_record")),
schema=schema,
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("vote_record", schema=schema)
op.drop_index("ix_vote_date", table_name="vote", schema=schema)
op.drop_index("ix_vote_congress_chamber", table_name="vote", schema=schema)
op.drop_table("vote", schema=schema)
op.drop_index(op.f("ix_legislator_bioguide_id"), table_name="legislator", schema=schema)
op.drop_table("legislator", schema=schema)
op.drop_index("ix_bill_congress", table_name="bill", schema=schema)
op.drop_table("bill", schema=schema)
# ### end Alembic commands ###
@@ -0,0 +1,58 @@
"""adding SignalDevice for DeviceRegistry for signal bot.
Revision ID: 4c410c16e39c
Revises: 3f71565e38de
Create Date: 2026-03-09 14:51:24.228976
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
from python.orm import RichieBase
if TYPE_CHECKING:
from collections.abc import Sequence
# revision identifiers, used by Alembic.
revision: str = "4c410c16e39c"
down_revision: str | None = "3f71565e38de"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
schema = RichieBase.schema_name
def upgrade() -> None:
"""Upgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"signal_device",
sa.Column("phone_number", sa.String(length=50), nullable=False),
sa.Column("safety_number", sa.String(), nullable=False),
sa.Column(
"trust_level",
postgresql.ENUM("VERIFIED", "UNVERIFIED", "BLOCKED", name="trust_level", schema=schema),
nullable=False,
),
sa.Column("last_seen", sa.DateTime(timezone=True), nullable=False),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("pk_signal_device")),
sa.UniqueConstraint("phone_number", name=op.f("uq_signal_device_phone_number")),
schema=schema,
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("signal_device", schema=schema)
# ### end Alembic commands ###
@@ -0,0 +1,41 @@
"""fixed safety number logic.
Revision ID: 99fec682516c
Revises: 4c410c16e39c
Create Date: 2026-03-09 16:25:25.085806
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import sqlalchemy as sa
from alembic import op
from python.orm import RichieBase
if TYPE_CHECKING:
from collections.abc import Sequence
# revision identifiers, used by Alembic.
revision: str = "99fec682516c"
down_revision: str | None = "4c410c16e39c"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
schema = RichieBase.schema_name
def upgrade() -> None:
"""Upgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column("signal_device", "safety_number", existing_type=sa.VARCHAR(), nullable=True, schema=schema)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column("signal_device", "safety_number", existing_type=sa.VARCHAR(), nullable=False, schema=schema)
# ### end Alembic commands ###
@@ -0,0 +1,54 @@
"""add dead_letter_message table.
Revision ID: a1b2c3d4e5f6
Revises: 99fec682516c
Create Date: 2026-03-10 12:00:00.000000
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
from python.orm import RichieBase
if TYPE_CHECKING:
from collections.abc import Sequence
# revision identifiers, used by Alembic.
revision: str = "a1b2c3d4e5f6"
down_revision: str | None = "99fec682516c"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
schema = RichieBase.schema_name
def upgrade() -> None:
"""Upgrade."""
op.create_table(
"dead_letter_message",
sa.Column("source", sa.String(), nullable=False),
sa.Column("message", sa.Text(), nullable=False),
sa.Column("received_at", sa.DateTime(timezone=True), nullable=False),
sa.Column(
"status",
postgresql.ENUM("UNPROCESSED", "PROCESSED", name="message_status", schema=schema),
nullable=False,
),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("pk_dead_letter_message")),
schema=schema,
)
def downgrade() -> None:
"""Downgrade."""
op.drop_table("dead_letter_message", schema=schema)
op.execute(sa.text(f"DROP TYPE IF EXISTS {schema}.message_status"))
@@ -0,0 +1,66 @@
"""adding roles to signal devices.
Revision ID: 2ef7ba690159
Revises: a1b2c3d4e5f6
Create Date: 2026-03-16 19:22:38.020350
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import sqlalchemy as sa
from alembic import op
from python.orm import RichieBase
if TYPE_CHECKING:
from collections.abc import Sequence
# revision identifiers, used by Alembic.
revision: str = "2ef7ba690159"
down_revision: str | None = "a1b2c3d4e5f6"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
schema = RichieBase.schema_name
def upgrade() -> None:
"""Upgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"role",
sa.Column("name", sa.String(length=50), nullable=False),
sa.Column("id", sa.SmallInteger(), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("pk_role")),
sa.UniqueConstraint("name", name=op.f("uq_role_name")),
schema=schema,
)
op.create_table(
"device_role",
sa.Column("device_id", sa.Integer(), nullable=False),
sa.Column("role_id", sa.SmallInteger(), nullable=False),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(
["device_id"], [f"{schema}.signal_device.id"], name=op.f("fk_device_role_device_id_signal_device")
),
sa.ForeignKeyConstraint(["role_id"], [f"{schema}.role.id"], name=op.f("fk_device_role_role_id_role")),
sa.PrimaryKeyConstraint("id", name=op.f("pk_device_role")),
sa.UniqueConstraint("device_id", "role_id", name="uq_device_role_device_role"),
schema=schema,
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("device_role", schema=schema)
op.drop_table("role", schema=schema)
# ### end Alembic commands ###
@@ -0,0 +1,171 @@
"""seprating signal_bot database.
Revision ID: 6b275323f435
Revises: 2ef7ba690159
Create Date: 2026-03-18 08:34:28.785885
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
from python.orm import RichieBase
if TYPE_CHECKING:
from collections.abc import Sequence
# revision identifiers, used by Alembic.
revision: str = "6b275323f435"
down_revision: str | None = "2ef7ba690159"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
schema = RichieBase.schema_name
def upgrade() -> None:
"""Upgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("device_role", schema=schema)
op.drop_table("signal_device", schema=schema)
op.drop_table("role", schema=schema)
op.drop_table("dead_letter_message", schema=schema)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"dead_letter_message",
sa.Column("source", sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column("message", sa.TEXT(), autoincrement=False, nullable=False),
sa.Column("received_at", postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=False),
sa.Column(
"status",
postgresql.ENUM("UNPROCESSED", "PROCESSED", name="message_status", schema=schema),
autoincrement=False,
nullable=False,
),
sa.Column("id", sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column(
"created",
postgresql.TIMESTAMP(timezone=True),
server_default=sa.text("now()"),
autoincrement=False,
nullable=False,
),
sa.Column(
"updated",
postgresql.TIMESTAMP(timezone=True),
server_default=sa.text("now()"),
autoincrement=False,
nullable=False,
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_dead_letter_message")),
schema=schema,
)
op.create_table(
"role",
sa.Column("name", sa.VARCHAR(length=50), autoincrement=False, nullable=False),
sa.Column(
"id",
sa.SMALLINT(),
server_default=sa.text(f"nextval('{schema}.role_id_seq'::regclass)"),
autoincrement=True,
nullable=False,
),
sa.Column(
"created",
postgresql.TIMESTAMP(timezone=True),
server_default=sa.text("now()"),
autoincrement=False,
nullable=False,
),
sa.Column(
"updated",
postgresql.TIMESTAMP(timezone=True),
server_default=sa.text("now()"),
autoincrement=False,
nullable=False,
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_role")),
sa.UniqueConstraint(
"name", name=op.f("uq_role_name"), postgresql_include=[], postgresql_nulls_not_distinct=False
),
schema=schema,
)
op.create_table(
"signal_device",
sa.Column("phone_number", sa.VARCHAR(length=50), autoincrement=False, nullable=False),
sa.Column("safety_number", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column(
"trust_level",
postgresql.ENUM("VERIFIED", "UNVERIFIED", "BLOCKED", name="trust_level", schema=schema),
autoincrement=False,
nullable=False,
),
sa.Column("last_seen", postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=False),
sa.Column("id", sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column(
"created",
postgresql.TIMESTAMP(timezone=True),
server_default=sa.text("now()"),
autoincrement=False,
nullable=False,
),
sa.Column(
"updated",
postgresql.TIMESTAMP(timezone=True),
server_default=sa.text("now()"),
autoincrement=False,
nullable=False,
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_signal_device")),
sa.UniqueConstraint(
"phone_number",
name=op.f("uq_signal_device_phone_number"),
postgresql_include=[],
postgresql_nulls_not_distinct=False,
),
schema=schema,
)
op.create_table(
"device_role",
sa.Column("device_id", sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column("role_id", sa.SMALLINT(), autoincrement=False, nullable=False),
sa.Column("id", sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column(
"created",
postgresql.TIMESTAMP(timezone=True),
server_default=sa.text("now()"),
autoincrement=False,
nullable=False,
),
sa.Column(
"updated",
postgresql.TIMESTAMP(timezone=True),
server_default=sa.text("now()"),
autoincrement=False,
nullable=False,
),
sa.ForeignKeyConstraint(
["device_id"], [f"{schema}.signal_device.id"], name=op.f("fk_device_role_device_id_signal_device")
),
sa.ForeignKeyConstraint(["role_id"], [f"{schema}.role.id"], name=op.f("fk_device_role_role_id_role")),
sa.PrimaryKeyConstraint("id", name=op.f("pk_device_role")),
sa.UniqueConstraint(
"device_id",
"role_id",
name=op.f("uq_device_role_device_role"),
postgresql_include=[],
postgresql_nulls_not_distinct=False,
),
schema=schema,
)
# ### end Alembic commands ###
@@ -0,0 +1,187 @@
"""removed ds table from richie DB.
Revision ID: c8a794340928
Revises: 6b275323f435
Create Date: 2026-03-29 15:29:23.643146
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
from python.orm import RichieBase
if TYPE_CHECKING:
from collections.abc import Sequence
# revision identifiers, used by Alembic.
revision: str = "c8a794340928"
down_revision: str | None = "6b275323f435"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
schema = RichieBase.schema_name
def upgrade() -> None:
"""Upgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("vote_record", schema=schema)
op.drop_index(op.f("ix_vote_congress_chamber"), table_name="vote", schema=schema)
op.drop_index(op.f("ix_vote_date"), table_name="vote", schema=schema)
op.drop_index(op.f("ix_legislator_bioguide_id"), table_name="legislator", schema=schema)
op.drop_table("legislator", schema=schema)
op.drop_table("vote", schema=schema)
op.drop_index(op.f("ix_bill_congress"), table_name="bill", schema=schema)
op.drop_table("bill", schema=schema)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"vote",
sa.Column("congress", sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column("chamber", sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column("session", sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column("number", sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column("vote_type", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column("question", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column("result", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column("result_text", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column("vote_date", sa.DATE(), autoincrement=False, nullable=False),
sa.Column("yea_count", sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column("nay_count", sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column("not_voting_count", sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column("present_count", sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column("bill_id", sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column("id", sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column(
"created",
postgresql.TIMESTAMP(timezone=True),
server_default=sa.text("now()"),
autoincrement=False,
nullable=False,
),
sa.Column(
"updated",
postgresql.TIMESTAMP(timezone=True),
server_default=sa.text("now()"),
autoincrement=False,
nullable=False,
),
sa.ForeignKeyConstraint(["bill_id"], [f"{schema}.bill.id"], name=op.f("fk_vote_bill_id_bill")),
sa.PrimaryKeyConstraint("id", name=op.f("pk_vote")),
sa.UniqueConstraint(
"congress",
"chamber",
"session",
"number",
name=op.f("uq_vote_congress_chamber_session_number"),
postgresql_include=[],
postgresql_nulls_not_distinct=False,
),
schema=schema,
)
op.create_index(op.f("ix_vote_date"), "vote", ["vote_date"], unique=False, schema=schema)
op.create_index(op.f("ix_vote_congress_chamber"), "vote", ["congress", "chamber"], unique=False, schema=schema)
op.create_table(
"vote_record",
sa.Column("vote_id", sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column("legislator_id", sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column("position", sa.VARCHAR(), autoincrement=False, nullable=False),
sa.ForeignKeyConstraint(
["legislator_id"],
[f"{schema}.legislator.id"],
name=op.f("fk_vote_record_legislator_id_legislator"),
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["vote_id"], [f"{schema}.vote.id"], name=op.f("fk_vote_record_vote_id_vote"), ondelete="CASCADE"
),
sa.PrimaryKeyConstraint("vote_id", "legislator_id", name=op.f("pk_vote_record")),
schema=schema,
)
op.create_table(
"legislator",
sa.Column("bioguide_id", sa.TEXT(), autoincrement=False, nullable=False),
sa.Column("thomas_id", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column("lis_id", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column("govtrack_id", sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column("opensecrets_id", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column("fec_ids", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column("first_name", sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column("last_name", sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column("official_full_name", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column("nickname", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column("birthday", sa.DATE(), autoincrement=False, nullable=True),
sa.Column("gender", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column("current_party", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column("current_state", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column("current_district", sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column("current_chamber", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column("id", sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column(
"created",
postgresql.TIMESTAMP(timezone=True),
server_default=sa.text("now()"),
autoincrement=False,
nullable=False,
),
sa.Column(
"updated",
postgresql.TIMESTAMP(timezone=True),
server_default=sa.text("now()"),
autoincrement=False,
nullable=False,
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_legislator")),
schema=schema,
)
op.create_index(op.f("ix_legislator_bioguide_id"), "legislator", ["bioguide_id"], unique=True, schema=schema)
op.create_table(
"bill",
sa.Column("congress", sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column("bill_type", sa.VARCHAR(), autoincrement=False, nullable=False),
sa.Column("number", sa.INTEGER(), autoincrement=False, nullable=False),
sa.Column("title", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column("title_short", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column("official_title", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column("status", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column("status_at", sa.DATE(), autoincrement=False, nullable=True),
sa.Column("sponsor_bioguide_id", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column("subjects_top_term", sa.VARCHAR(), autoincrement=False, nullable=True),
sa.Column("id", sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column(
"created",
postgresql.TIMESTAMP(timezone=True),
server_default=sa.text("now()"),
autoincrement=False,
nullable=False,
),
sa.Column(
"updated",
postgresql.TIMESTAMP(timezone=True),
server_default=sa.text("now()"),
autoincrement=False,
nullable=False,
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_bill")),
sa.UniqueConstraint(
"congress",
"bill_type",
"number",
name=op.f("uq_bill_congress_type_number"),
postgresql_include=[],
postgresql_nulls_not_distinct=False,
),
schema=schema,
)
op.create_index(op.f("ix_bill_congress"), "bill", ["congress"], unique=False, schema=schema)
# ### end Alembic commands ###
@@ -0,0 +1,93 @@
"""adding audiobook libreary metadata.
Revision ID: d7864d1ffc17
Revises: c8a794340928
Create Date: 2026-06-03 20:24:09.200837
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import sqlalchemy as sa
from alembic import op
from python.orm import RichieBase
if TYPE_CHECKING:
from collections.abc import Sequence
# revision identifiers, used by Alembic.
revision: str = "d7864d1ffc17"
down_revision: str | None = "c8a794340928"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
schema = RichieBase.schema_name
def upgrade() -> None:
"""Upgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"audiobook_author",
sa.Column("name", sa.String(), nullable=False),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("pk_audiobook_author")),
sa.UniqueConstraint("name", name=op.f("uq_audiobook_author_name")),
schema=schema,
)
op.create_table(
"audiobook_series",
sa.Column("name", sa.String(), nullable=False),
sa.Column("author_id", sa.Integer(), nullable=False),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(
["author_id"],
[f"{schema}.audiobook_author.id"],
name=op.f("fk_audiobook_series_author_id_audiobook_author"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_audiobook_series")),
sa.UniqueConstraint("author_id", "name", name=op.f("uq_audiobook_series_author_id")),
schema=schema,
)
op.create_table(
"audiobook",
sa.Column("title", sa.String(), nullable=False),
sa.Column("author_id", sa.Integer(), nullable=False),
sa.Column("series_id", sa.Integer(), nullable=True),
sa.Column("series_index", sa.Integer(), nullable=False),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(
["author_id"],
[f"{schema}.audiobook_author.id"],
name=op.f("fk_audiobook_author_id_audiobook_author"),
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["series_id"],
[f"{schema}.audiobook_series.id"],
name=op.f("fk_audiobook_series_id_audiobook_series"),
ondelete="SET NULL",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_audiobook")),
schema=schema,
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("audiobook", schema=schema)
op.drop_table("audiobook_series", schema=schema)
op.drop_table("audiobook_author", schema=schema)
# ### end Alembic commands ###
@@ -0,0 +1,200 @@
"""add ebook search tables.
Revision ID: 2db132cace1a
Revises: b3c60cc5beb5
Create Date: 2026-06-10 22:10:54.379159
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import pgvector
import sqlalchemy as sa
from alembic import op
from python.orm import RichieBase
if TYPE_CHECKING:
from collections.abc import Sequence
# revision identifiers, used by Alembic.
revision: str = "2db132cace1a"
down_revision: str | None = "b3c60cc5beb5"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
schema = RichieBase.schema_name
def upgrade() -> None:
"""Upgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"ebook_embedding_model",
sa.Column("name", sa.String(), nullable=False),
sa.Column("dimension", sa.Integer(), nullable=False),
sa.Column("is_default", sa.Boolean(), nullable=False),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("pk_ebook_embedding_model")),
sa.UniqueConstraint("name", name=op.f("uq_ebook_embedding_model_name")),
schema=schema,
)
op.create_table(
"ebook_source",
sa.Column("title", sa.String(), nullable=False),
sa.Column("author", sa.String(), nullable=True),
sa.Column("language", sa.String(), nullable=True),
sa.Column("publisher", sa.String(), nullable=True),
sa.Column("identifier", sa.String(), nullable=True),
sa.Column("file_path", sa.String(), nullable=False),
sa.Column("file_sha256", sa.String(length=64), nullable=False),
sa.Column("file_mtime", sa.DateTime(timezone=True), nullable=False),
sa.Column("file_size", sa.BigInteger(), nullable=False),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("pk_ebook_source")),
sa.UniqueConstraint("file_path", name=op.f("uq_ebook_source_file_path")),
sa.UniqueConstraint("file_sha256", name=op.f("uq_ebook_source_file_sha256")),
schema=schema,
)
op.create_table(
"ebook_chapter",
sa.Column("source_id", sa.Integer(), nullable=False),
sa.Column("spine_index", sa.Integer(), nullable=False),
sa.Column("title", sa.String(), nullable=True),
sa.Column("href", sa.String(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(
["source_id"],
[f"{schema}.ebook_source.id"],
name=op.f("fk_ebook_chapter_source_id_ebook_source"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_ebook_chapter")),
sa.UniqueConstraint("source_id", "spine_index", name=op.f("uq_ebook_chapter_source_id")),
schema=schema,
)
op.create_table(
"ebook_chunk",
sa.Column("source_id", sa.Integer(), nullable=False),
sa.Column("chapter_id", sa.Integer(), nullable=True),
sa.Column("chunk_index", sa.Integer(), nullable=False),
sa.Column("text", sa.String(), nullable=False),
sa.Column("token_start", sa.Integer(), nullable=False),
sa.Column("token_count", sa.Integer(), nullable=False),
sa.Column("page_label", sa.String(), nullable=True),
sa.Column("content_sha256", sa.String(length=64), nullable=False),
sa.Column("search_text", sa.String(), nullable=False),
sa.Column("id", sa.BigInteger(), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(
["chapter_id"],
[f"{schema}.ebook_chapter.id"],
name=op.f("fk_ebook_chunk_chapter_id_ebook_chapter"),
ondelete="SET NULL",
),
sa.ForeignKeyConstraint(
["source_id"],
[f"{schema}.ebook_source.id"],
name=op.f("fk_ebook_chunk_source_id_ebook_source"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_ebook_chunk")),
sa.UniqueConstraint("source_id", "chunk_index", name="uq_ebook_chunk_source_id_chunk_index"),
sa.UniqueConstraint("source_id", "content_sha256", name="uq_ebook_chunk_source_id_content_sha256"),
schema=schema,
)
op.create_table(
"ebook_chunk_embedding_1024",
sa.Column("chunk_id", sa.BigInteger(), nullable=False),
sa.Column("model_id", sa.Integer(), nullable=False),
sa.Column("embedding", pgvector.sqlalchemy.vector.VECTOR(dim=1024), nullable=False),
sa.Column("id", sa.BigInteger(), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(
["chunk_id"],
[f"{schema}.ebook_chunk.id"],
name=op.f("fk_ebook_chunk_embedding_1024_chunk_id_ebook_chunk"),
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["model_id"],
[f"{schema}.ebook_embedding_model.id"],
name=op.f("fk_ebook_chunk_embedding_1024_model_id_ebook_embedding_model"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_ebook_chunk_embedding_1024")),
sa.UniqueConstraint("chunk_id", "model_id", name=op.f("uq_ebook_chunk_embedding_1024_chunk_id")),
schema=schema,
)
op.create_table(
"ebook_chunk_embedding_2560",
sa.Column("chunk_id", sa.BigInteger(), nullable=False),
sa.Column("model_id", sa.Integer(), nullable=False),
sa.Column("embedding", pgvector.sqlalchemy.vector.VECTOR(dim=2560), nullable=False),
sa.Column("id", sa.BigInteger(), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(
["chunk_id"],
[f"{schema}.ebook_chunk.id"],
name=op.f("fk_ebook_chunk_embedding_2560_chunk_id_ebook_chunk"),
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["model_id"],
[f"{schema}.ebook_embedding_model.id"],
name=op.f("fk_ebook_chunk_embedding_2560_model_id_ebook_embedding_model"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_ebook_chunk_embedding_2560")),
sa.UniqueConstraint("chunk_id", "model_id", name=op.f("uq_ebook_chunk_embedding_2560_chunk_id")),
schema=schema,
)
op.create_table(
"ebook_chunk_embedding_4096",
sa.Column("chunk_id", sa.BigInteger(), nullable=False),
sa.Column("model_id", sa.Integer(), nullable=False),
sa.Column("embedding", pgvector.sqlalchemy.vector.VECTOR(dim=4096), nullable=False),
sa.Column("id", sa.BigInteger(), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(
["chunk_id"],
[f"{schema}.ebook_chunk.id"],
name=op.f("fk_ebook_chunk_embedding_4096_chunk_id_ebook_chunk"),
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["model_id"],
[f"{schema}.ebook_embedding_model.id"],
name=op.f("fk_ebook_chunk_embedding_4096_model_id_ebook_embedding_model"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_ebook_chunk_embedding_4096")),
sa.UniqueConstraint("chunk_id", "model_id", name=op.f("uq_ebook_chunk_embedding_4096_chunk_id")),
schema=schema,
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("ebook_chunk_embedding_4096", schema=schema)
op.drop_table("ebook_chunk_embedding_2560", schema=schema)
op.drop_table("ebook_chunk_embedding_1024", schema=schema)
op.drop_table("ebook_chunk", schema=schema)
op.drop_table("ebook_chapter", schema=schema)
op.drop_table("ebook_source", schema=schema)
op.drop_table("ebook_embedding_model", schema=schema)
# ### end Alembic commands ###
@@ -0,0 +1,63 @@
"""updated series_index to float and added UniqueConstraint to audiobook and audiobook_author.
Revision ID: b3c60cc5beb5
Revises: d7864d1ffc17
Create Date: 2026-06-10 20:02:43.073725
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import sqlalchemy as sa
from alembic import op
from python.orm import RichieBase
if TYPE_CHECKING:
from collections.abc import Sequence
# revision identifiers, used by Alembic.
revision: str = "b3c60cc5beb5"
down_revision: str | None = "d7864d1ffc17"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
schema = RichieBase.schema_name
def upgrade() -> None:
"""Upgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column(
"audiobook",
"series_index",
existing_type=sa.INTEGER(),
type_=sa.Float(),
existing_nullable=False,
schema=schema,
)
op.create_unique_constraint(
op.f("uq_audiobook_author_id"),
"audiobook",
["author_id", "series_id", "title"],
schema=schema,
postgresql_nulls_not_distinct=True,
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(op.f("uq_audiobook_author_id"), "audiobook", schema=schema, type_="unique")
op.alter_column(
"audiobook",
"series_index",
existing_type=sa.Float(),
type_=sa.INTEGER(),
existing_nullable=False,
schema=schema,
)
# ### end Alembic commands ###
@@ -0,0 +1,54 @@
"""add 1024 ebook embedding cosine index.
Revision ID: c460105682d2
Revises: 2db132cace1a
Create Date: 2026-06-13 19:53:45.680289
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from alembic import op
from python.orm import RichieBase
if TYPE_CHECKING:
from collections.abc import Sequence
# revision identifiers, used by Alembic.
revision: str = "c460105682d2"
down_revision: str | None = "2db132cace1a"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
schema = RichieBase.schema_name
def upgrade() -> None:
"""Upgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_index(
"ix_ebook_chunk_embedding_1024_embedding_cosine",
"ebook_chunk_embedding_1024",
["embedding"],
unique=False,
schema=schema,
postgresql_using="hnsw",
postgresql_ops={"embedding": "vector_cosine_ops"},
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(
"ix_ebook_chunk_embedding_1024_embedding_cosine",
table_name="ebook_chunk_embedding_1024",
schema=schema,
postgresql_using="hnsw",
postgresql_ops={"embedding": "vector_cosine_ops"},
)
# ### end Alembic commands ###
@@ -0,0 +1,103 @@
"""adding haproxy data.
Revision ID: 96d72c748c24
Revises: c460105682d2
Create Date: 2026-06-23 16:37:17.768851
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import sqlalchemy as sa
from alembic import op
from python.orm import RichieBase
if TYPE_CHECKING:
from collections.abc import Sequence
# revision identifiers, used by Alembic.
revision: str = "96d72c748c24"
down_revision: str | None = "c460105682d2"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
schema = RichieBase.schema_name
def upgrade() -> None:
"""Upgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"haproxy_request",
sa.Column("line_hash", sa.String(), nullable=False),
sa.Column("requested_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("client_ip", sa.String(), nullable=False),
sa.Column("client_port", sa.Integer(), nullable=False),
sa.Column("frontend", sa.String(), nullable=False),
sa.Column("ssl", sa.Boolean(), nullable=False),
sa.Column("backend", sa.String(), nullable=False),
sa.Column("server", sa.String(), nullable=False),
sa.Column("time_request", sa.Integer(), nullable=False),
sa.Column("time_queue", sa.Integer(), nullable=False),
sa.Column("time_connect", sa.Integer(), nullable=False),
sa.Column("time_response", sa.Integer(), nullable=False),
sa.Column("time_total", sa.Integer(), nullable=False),
sa.Column("status_code", sa.Integer(), nullable=False),
sa.Column("bytes_read", sa.BigInteger(), nullable=False),
sa.Column("termination_state", sa.String(), nullable=False),
sa.Column("active_connections", sa.Integer(), nullable=False),
sa.Column("frontend_connections", sa.Integer(), nullable=False),
sa.Column("backend_connections", sa.Integer(), nullable=False),
sa.Column("server_connections", sa.Integer(), nullable=False),
sa.Column("retries", sa.Integer(), nullable=False),
sa.Column("server_queue", sa.Integer(), nullable=False),
sa.Column("backend_queue", sa.Integer(), nullable=False),
sa.Column("host", sa.String(), nullable=True),
sa.Column("user_agent", sa.String(), nullable=True),
sa.Column("method", sa.String(), nullable=False),
sa.Column("target", sa.String(), nullable=False),
sa.Column("path", sa.String(), nullable=False),
sa.Column("query", sa.String(), nullable=True),
sa.Column("http_version", sa.String(), nullable=False),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("pk_haproxy_request")),
sa.UniqueConstraint("line_hash", name=op.f("uq_haproxy_request_line_hash")),
schema=schema,
)
op.create_index(op.f("ix_haproxy_request_backend"), "haproxy_request", ["backend"], unique=False, schema=schema)
op.create_index(op.f("ix_haproxy_request_client_ip"), "haproxy_request", ["client_ip"], unique=False, schema=schema)
op.create_index(op.f("ix_haproxy_request_host"), "haproxy_request", ["host"], unique=False, schema=schema)
op.create_index(op.f("ix_haproxy_request_path"), "haproxy_request", ["path"], unique=False, schema=schema)
op.create_index(
op.f("ix_haproxy_request_requested_at"), "haproxy_request", ["requested_at"], unique=False, schema=schema
)
op.create_index(
op.f("ix_haproxy_request_status_code"), "haproxy_request", ["status_code"], unique=False, schema=schema
)
op.create_index(
op.f("ix_haproxy_request_time_response"), "haproxy_request", ["time_response"], unique=False, schema=schema
)
op.create_index(
op.f("ix_haproxy_request_user_agent"), "haproxy_request", ["user_agent"], unique=False, schema=schema
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f("ix_haproxy_request_user_agent"), table_name="haproxy_request", schema=schema)
op.drop_index(op.f("ix_haproxy_request_time_response"), table_name="haproxy_request", schema=schema)
op.drop_index(op.f("ix_haproxy_request_status_code"), table_name="haproxy_request", schema=schema)
op.drop_index(op.f("ix_haproxy_request_requested_at"), table_name="haproxy_request", schema=schema)
op.drop_index(op.f("ix_haproxy_request_path"), table_name="haproxy_request", schema=schema)
op.drop_index(op.f("ix_haproxy_request_host"), table_name="haproxy_request", schema=schema)
op.drop_index(op.f("ix_haproxy_request_client_ip"), table_name="haproxy_request", schema=schema)
op.drop_index(op.f("ix_haproxy_request_backend"), table_name="haproxy_request", schema=schema)
op.drop_table("haproxy_request", schema=schema)
# ### end Alembic commands ###
@@ -0,0 +1,206 @@
"""adding Phrase metadata tables.
Revision ID: dddee09eddcc
Revises: 96d72c748c24
Create Date: 2026-06-29 00:49:07.344159
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
from python.orm import RichieBase
if TYPE_CHECKING:
from collections.abc import Sequence
# revision identifiers, used by Alembic.
revision: str = "dddee09eddcc"
down_revision: str | None = "96d72c748c24"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
schema = RichieBase.schema_name
def upgrade() -> None:
"""Upgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"candidate_phrases",
sa.Column("book_id", sa.Integer(), nullable=False),
sa.Column("series_id", sa.Integer(), nullable=True),
sa.Column("phrase_text", sa.Text(), nullable=False),
sa.Column("phrase_norm", sa.Text(), nullable=False),
sa.Column("token_count", sa.Integer(), nullable=False),
sa.Column("source_raw_ngram", sa.Boolean(), nullable=False),
sa.Column("source_yake", sa.Boolean(), nullable=False),
sa.Column("source_spacy_ner", sa.Boolean(), nullable=False),
sa.Column("source_spacy_noun_chunk", sa.Boolean(), nullable=False),
sa.Column("source_capitalized", sa.Boolean(), nullable=False),
sa.Column("source_metadata", sa.Boolean(), nullable=False),
sa.Column("spacy_label", sa.String(), nullable=True),
sa.Column("raw_count", sa.Integer(), nullable=False),
sa.Column("chapter_count", sa.Integer(), nullable=False),
sa.Column("yake_score", sa.Float(), nullable=True),
sa.Column("candidate_score", sa.Float(), nullable=False),
sa.Column(
"sample_contexts",
sa.JSON().with_variant(postgresql.JSONB(astext_type=sa.Text()), "postgresql"),
nullable=True,
),
sa.Column("llm_judged", sa.Boolean(), nullable=False),
sa.Column("llm_keep", sa.Boolean(), nullable=True),
sa.Column("llm_confidence", sa.Float(), nullable=True),
sa.Column("llm_category", sa.String(), nullable=True),
sa.Column("llm_reason", sa.Text(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(
["book_id"],
[f"{schema}.ebook_source.id"],
name=op.f("fk_candidate_phrases_book_id_ebook_source"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_candidate_phrases")),
sa.UniqueConstraint("book_id", "phrase_norm", name="uq_candidate_phrases_book_id_phrase_norm"),
schema=schema,
)
op.create_index(
"candidate_phrases_book_norm_idx", "candidate_phrases", ["book_id", "phrase_norm"], unique=False, schema=schema
)
op.create_index(
"candidate_phrases_book_score_idx",
"candidate_phrases",
["book_id", "candidate_score"],
unique=False,
schema=schema,
)
op.create_table(
"protected_phrases",
sa.Column("book_id", sa.Integer(), nullable=True),
sa.Column("series_id", sa.Integer(), nullable=True),
sa.Column("phrase_text", sa.Text(), nullable=False),
sa.Column("phrase_norm", sa.Text(), nullable=False),
sa.Column("canonical_id", sa.String(), nullable=False),
sa.Column("phrase_type", sa.String(), nullable=True),
sa.Column("token_count", sa.Integer(), nullable=False),
sa.Column("confidence", sa.Float(), nullable=False),
sa.Column("importance", sa.Float(), nullable=False),
sa.Column("allow_nested", sa.Boolean(), nullable=False),
sa.Column("suppress_children", sa.Boolean(), nullable=False),
sa.Column("source_candidate_id", sa.Integer(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(
["book_id"],
[f"{schema}.ebook_source.id"],
name=op.f("fk_protected_phrases_book_id_ebook_source"),
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["source_candidate_id"],
[f"{schema}.candidate_phrases.id"],
name=op.f("fk_protected_phrases_source_candidate_id_candidate_phrases"),
ondelete="SET NULL",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_protected_phrases")),
sa.UniqueConstraint("book_id", "phrase_norm", name="uq_protected_phrases_book_id_phrase_norm"),
schema=schema,
)
op.create_index(
"protected_phrases_book_norm_idx", "protected_phrases", ["book_id", "phrase_norm"], unique=False, schema=schema
)
op.create_index("protected_phrases_norm_idx", "protected_phrases", ["phrase_norm"], unique=False, schema=schema)
op.create_index(
"protected_phrases_series_norm_idx",
"protected_phrases",
["series_id", "phrase_norm"],
unique=False,
schema=schema,
)
op.create_table(
"chunk_phrase_mentions",
sa.Column("chunk_id", sa.BigInteger(), nullable=False),
sa.Column("phrase_id", sa.Integer(), nullable=False),
sa.Column("book_id", sa.Integer(), nullable=True),
sa.Column("series_id", sa.Integer(), nullable=True),
sa.Column("start_char", sa.Integer(), nullable=False),
sa.Column("end_char", sa.Integer(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(
["book_id"],
[f"{schema}.ebook_source.id"],
name=op.f("fk_chunk_phrase_mentions_book_id_ebook_source"),
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["chunk_id"],
[f"{schema}.ebook_chunk.id"],
name=op.f("fk_chunk_phrase_mentions_chunk_id_ebook_chunk"),
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["phrase_id"],
[f"{schema}.protected_phrases.id"],
name=op.f("fk_chunk_phrase_mentions_phrase_id_protected_phrases"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_chunk_phrase_mentions")),
sa.UniqueConstraint("chunk_id", "phrase_id", "start_char", name="uq_chunk_phrase_mentions_chunk_phrase_start"),
schema=schema,
)
op.create_index(
"chunk_phrase_mentions_chunk_idx", "chunk_phrase_mentions", ["chunk_id"], unique=False, schema=schema
)
op.create_index(
"chunk_phrase_mentions_phrase_idx", "chunk_phrase_mentions", ["phrase_id"], unique=False, schema=schema
)
op.create_table(
"phrase_aliases",
sa.Column("phrase_id", sa.Integer(), nullable=False),
sa.Column("alias_text", sa.Text(), nullable=False),
sa.Column("alias_norm", sa.Text(), nullable=False),
sa.Column("confidence", sa.Float(), nullable=False),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(
["phrase_id"],
[f"{schema}.protected_phrases.id"],
name=op.f("fk_phrase_aliases_phrase_id_protected_phrases"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_phrase_aliases")),
sa.UniqueConstraint("phrase_id", "alias_norm", name="uq_phrase_aliases_phrase_id_alias_norm"),
schema=schema,
)
op.create_index("phrase_aliases_norm_idx", "phrase_aliases", ["alias_norm"], unique=False, schema=schema)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index("phrase_aliases_norm_idx", table_name="phrase_aliases", schema=schema)
op.drop_table("phrase_aliases", schema=schema)
op.drop_index("chunk_phrase_mentions_phrase_idx", table_name="chunk_phrase_mentions", schema=schema)
op.drop_index("chunk_phrase_mentions_chunk_idx", table_name="chunk_phrase_mentions", schema=schema)
op.drop_table("chunk_phrase_mentions", schema=schema)
op.drop_index("protected_phrases_series_norm_idx", table_name="protected_phrases", schema=schema)
op.drop_index("protected_phrases_norm_idx", table_name="protected_phrases", schema=schema)
op.drop_index("protected_phrases_book_norm_idx", table_name="protected_phrases", schema=schema)
op.drop_table("protected_phrases", schema=schema)
op.drop_index("candidate_phrases_book_score_idx", table_name="candidate_phrases", schema=schema)
op.drop_index("candidate_phrases_book_norm_idx", table_name="candidate_phrases", schema=schema)
op.drop_table("candidate_phrases", schema=schema)
# ### end Alembic commands ###
@@ -0,0 +1,55 @@
"""remove spaCy-ner.
Revision ID: 751260fc3228
Revises: dddee09eddcc
Create Date: 2026-07-09 23:03:39.554083
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import sqlalchemy as sa
from alembic import op
from python.orm import RichieBase
if TYPE_CHECKING:
from collections.abc import Sequence
# revision identifiers, used by Alembic.
revision: str = "751260fc3228"
down_revision: str | None = "dddee09eddcc"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
schema = RichieBase.schema_name
def upgrade() -> None:
"""Upgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("candidate_phrases", "source_spacy_noun_chunk", schema=schema)
op.drop_column("candidate_phrases", "source_spacy_ner", schema=schema)
op.drop_column("candidate_phrases", "spacy_label", schema=schema)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"candidate_phrases", sa.Column("spacy_label", sa.VARCHAR(), autoincrement=False, nullable=True), schema=schema
)
op.add_column(
"candidate_phrases",
sa.Column("source_spacy_ner", sa.BOOLEAN(), autoincrement=False, nullable=False),
schema=schema,
)
op.add_column(
"candidate_phrases",
sa.Column("source_spacy_noun_chunk", sa.BOOLEAN(), autoincrement=False, nullable=False),
schema=schema,
)
# ### end Alembic commands ###
+36
View File
@@ -0,0 +1,36 @@
"""${message}.
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import sqlalchemy as sa
from alembic import op
from python.orm import ${config.attributes["base"].__name__}
if TYPE_CHECKING:
from collections.abc import Sequence
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: str | None = ${repr(down_revision)}
branch_labels: str | Sequence[str] | None = ${repr(branch_labels)}
depends_on: str | Sequence[str] | None = ${repr(depends_on)}
schema=${config.attributes["base"].__name__}.schema_name
def upgrade() -> None:
"""Upgrade."""
${upgrades if upgrades else "pass"}
def downgrade() -> None:
"""Downgrade."""
${downgrades if downgrades else "pass"}
@@ -0,0 +1,80 @@
"""starting van invintory.
Revision ID: 15e733499804
Revises:
Create Date: 2026-03-08 00:18:20.759720
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import sqlalchemy as sa
from alembic import op
from python.orm import VanInventoryBase
if TYPE_CHECKING:
from collections.abc import Sequence
# revision identifiers, used by Alembic.
revision: str = "15e733499804"
down_revision: str | None = None
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
schema = VanInventoryBase.schema_name
def upgrade() -> None:
"""Upgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"items",
sa.Column("name", sa.String(), nullable=False),
sa.Column("quantity", sa.Float(), nullable=False),
sa.Column("unit", sa.String(), nullable=False),
sa.Column("category", sa.String(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("pk_items")),
sa.UniqueConstraint("name", name=op.f("uq_items_name")),
schema=schema,
)
op.create_table(
"meals",
sa.Column("name", sa.String(), nullable=False),
sa.Column("instructions", sa.String(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("pk_meals")),
sa.UniqueConstraint("name", name=op.f("uq_meals_name")),
schema=schema,
)
op.create_table(
"meal_ingredients",
sa.Column("meal_id", sa.Integer(), nullable=False),
sa.Column("item_id", sa.Integer(), nullable=False),
sa.Column("quantity_needed", sa.Float(), nullable=False),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.Column("updated", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False),
sa.ForeignKeyConstraint(["item_id"], [f"{schema}.items.id"], name=op.f("fk_meal_ingredients_item_id_items")),
sa.ForeignKeyConstraint(["meal_id"], [f"{schema}.meals.id"], name=op.f("fk_meal_ingredients_meal_id_meals")),
sa.PrimaryKeyConstraint("id", name=op.f("pk_meal_ingredients")),
sa.UniqueConstraint("meal_id", "item_id", name=op.f("uq_meal_ingredients_meal_id")),
schema=schema,
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("meal_ingredients", schema=schema)
op.drop_table("meals", schema=schema)
op.drop_table("items", schema=schema)
# ### end Alembic commands ###
+9 -34
View File
@@ -3,28 +3,23 @@
from __future__ import annotations
import logging
import sys
from datetime import UTC, datetime
from os import getenv
from pathlib import Path
from subprocess import PIPE, Popen
from apprise import Apprise
from python.logging_config import configure_logger as _configure_logger
logger = logging.getLogger(__name__)
def configure_logger(level: str = "INFO") -> None:
"""Configure the logger.
def get_repo_dir() -> Path:
"""Return the repository root directory."""
return Path(__file__).resolve().parents[1]
Args:
level (str, optional): The logging level. Defaults to "INFO".
"""
logging.basicConfig(
level=level,
datefmt="%Y-%m-%dT%H:%M:%S%z",
format="%(asctime)s %(levelname)s %(filename)s:%(lineno)d - %(message)s",
handlers=[logging.StreamHandler(sys.stdout)],
)
def configure_logger(level: str = "INFO") -> None:
"""Configure the logger."""
_configure_logger(level)
def bash_wrapper(command: str) -> tuple[str, int]:
@@ -47,26 +42,6 @@ def bash_wrapper(command: str) -> tuple[str, int]:
return output.decode(), process.returncode
def signal_alert(body: str, title: str = "") -> None:
"""Send a signal alert.
Args:
body (str): The body of the alert.
title (str, optional): The title of the alert. Defaults to "".
"""
apprise_client = Apprise()
from_phone = getenv("SIGNAL_ALERT_FROM_PHONE")
to_phone = getenv("SIGNAL_ALERT_TO_PHONE")
if not from_phone or not to_phone:
logger.info("SIGNAL_ALERT_FROM_PHONE or SIGNAL_ALERT_TO_PHONE not set")
return
apprise_client.add(f"signal://localhost:8989/{from_phone}/{to_phone}")
apprise_client.notify(title=title, body=body)
def utcnow() -> datetime:
"""Get the current UTC time."""
return datetime.now(tz=UTC)
+103
View File
@@ -0,0 +1,103 @@
"""CLI wrapper around alembic for multi-database support.
Usage:
database <db_name> <command> [args...]
Examples:
database richie check
database richie upgrade head
database richie downgrade head-1
database richie revision --autogenerate -m "add meals table"
"""
from __future__ import annotations
from dataclasses import dataclass
from importlib import import_module
from typing import TYPE_CHECKING, Annotated
import typer
from alembic.config import CommandLine, Config
if TYPE_CHECKING:
from sqlalchemy.orm import DeclarativeBase
@dataclass(frozen=True)
class DatabaseConfig:
"""Configuration for a database."""
env_prefix: str
version_location: str
base_module: str
base_class_name: str
models_module: str
script_location: str = "python/alembic"
file_template: str = "%%(year)d_%%(month).2d_%%(day).2d-%%(slug)s_%%(rev)s"
def get_base(self) -> type[DeclarativeBase]:
"""Import and return the Base class."""
module = import_module(self.base_module)
return getattr(module, self.base_class_name)
def import_models(self) -> None:
"""Import ORM models so alembic autogenerate can detect them."""
import_module(self.models_module)
def alembic_config(self) -> Config:
"""Build an alembic Config for this database."""
cfg = Config()
cfg.set_main_option("script_location", self.script_location)
cfg.set_main_option("file_template", self.file_template)
cfg.set_main_option("prepend_sys_path", ".")
cfg.set_main_option("version_path_separator", "os")
cfg.set_main_option("version_locations", self.version_location)
cfg.set_main_option("revision_environment", "true")
cfg.set_section_option("post_write_hooks", "hooks", "dynamic_schema,import_postgresql,ruff")
cfg.set_section_option("post_write_hooks", "dynamic_schema.type", "dynamic_schema")
cfg.set_section_option("post_write_hooks", "import_postgresql.type", "import_postgresql")
cfg.set_section_option("post_write_hooks", "ruff.type", "ruff")
cfg.attributes["base"] = self.get_base()
cfg.attributes["env_prefix"] = self.env_prefix
self.import_models()
return cfg
DATABASES: dict[str, DatabaseConfig] = {
"richie": DatabaseConfig(
env_prefix="RICHIE",
version_location="python/alembic/richie/versions",
base_module="python.orm.richie.base",
base_class_name="RichieBase",
models_module="python.orm.richie",
),
}
app = typer.Typer(help="Multi-database alembic wrapper.")
@app.command(
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
)
def main(
ctx: typer.Context,
db_name: Annotated[str, typer.Argument(help=f"Database name. Options: {', '.join(DATABASES)}")],
command: Annotated[str, typer.Argument(help="Alembic command (upgrade, downgrade, revision, check, etc.)")],
) -> None:
"""Run an alembic command against the specified database."""
db_config = DATABASES.get(db_name)
if not db_config:
typer.echo(f"Unknown database: {db_name!r}. Available: {', '.join(DATABASES)}", err=True)
raise typer.Exit(code=1)
alembic_cfg = db_config.alembic_config()
cmd_line = CommandLine()
options = cmd_line.parser.parse_args([command, *ctx.args])
cmd_line.run_cmd(alembic_cfg, options)
if __name__ == "__main__":
app()
+1
View File
@@ -0,0 +1 @@
"""EPUB search package."""
+50
View File
@@ -0,0 +1,50 @@
"""Grounded answer generation."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from python.ebook_search.llm_interface import request_chat_completion
from python.ebook_search.prompts import load_prompt
if TYPE_CHECKING:
import httpx
from python.ebook_search.config import EbookSearchConfig
from python.ebook_search.search import SearchResult
logger = logging.getLogger(__name__)
async def answer_query(
client: httpx.AsyncClient,
query: str,
results: list[SearchResult],
config: EbookSearchConfig,
) -> str:
"""Answer a question using only retrieved chunks."""
if not config.answer_enabled:
logger.info("ebook_answer_skipped_disabled")
return "Answer generation is disabled. Source chunks are shown below."
if not results:
logger.info("ebook_answer_skipped_no_results")
return "No relevant sources were found."
logger.info(
f"ebook_answer_request_start {config.vllm_base_url=} {config.chat_model=} sources={len(results)} "
f"query_length={len(query)}"
)
context = "\n\n".join(
f"[{index}] {result.source_title}{' - ' + result.chapter_title if result.chapter_title else ''}\n{result.text}"
for index, result in enumerate(results, start=1)
)
content = await request_chat_completion(
client,
config,
load_prompt("answer").messages(query=query, context=context),
)
logger.info(f"ebook_answer_request_complete {config.chat_model=} answer_length={len(content)}")
return content or "The model returned an empty answer."
+1
View File
@@ -0,0 +1 @@
"""Web and external API adapters for EPUB search."""
+70
View File
@@ -0,0 +1,70 @@
"""Background BM25 refresh tasks for the web app.
The refresh is scheduled on the event loop instead of a thread because the async psycopg
driver only works from the loop; a bare thread cannot open a session on the async engine.
"""
from __future__ import annotations
import asyncio
import logging
from typing import TYPE_CHECKING
from sqlalchemy.ext.asyncio import AsyncSession
from python.ebook_search.bm25_corpus import load_bm25_corpus, refresh_bm25_corpus
if TYPE_CHECKING:
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import AsyncEngine
from python.ebook_search.config import EbookSearchConfig
logger = logging.getLogger(__name__)
def schedule_bm25_refresh(app: FastAPI) -> None:
"""Schedule a delayed BM25 corpus refresh, replacing any pending refresh.
Only called from route handlers, so a running event loop is guaranteed.
"""
cancel_bm25_refresh(app)
loop = asyncio.get_running_loop()
def start_refresh() -> None:
app.state.bm25_refresh_task = loop.create_task(refresh_bm25_for_app(app))
app.state.bm25_refresh_timer = loop.call_later(app.state.config.bm25_refresh_delay_seconds, start_refresh)
logger.info(f"ebook_bm25_refresh_scheduled {app.state.config.bm25_refresh_delay_seconds=}")
def cancel_bm25_refresh(app: FastAPI) -> None:
"""Cancel any pending BM25 corpus refresh timer and in-flight refresh task."""
existing_timer = getattr(app.state, "bm25_refresh_timer", None)
if existing_timer is not None:
existing_timer.cancel()
app.state.bm25_refresh_timer = None
logger.info("ebook_bm25_refresh_cancelled")
existing_task = getattr(app.state, "bm25_refresh_task", None)
if existing_task is not None:
if not existing_task.done():
existing_task.cancel()
app.state.bm25_refresh_task = None
async def refresh_bm25_for_app(app: FastAPI) -> None:
"""Refresh the BM25 corpus using the app engine and config."""
try:
await refresh_bm25_for_engine(app.state.engine, app.state.config)
except Exception:
logger.exception("ebook_bm25_refresh_failed")
async def refresh_bm25_for_engine(engine: AsyncEngine, config: EbookSearchConfig) -> None:
"""Refresh the BM25 corpus using an async SQLAlchemy engine."""
async with AsyncSession(engine) as session:
await refresh_bm25_corpus(session, config)
load_bm25_corpus.cache_clear()
logger.info("ebook_bm25_corpus_cache_cleared_after_refresh")
+24
View File
@@ -0,0 +1,24 @@
"""FastAPI dependencies for the EPUB search app."""
from __future__ import annotations
from typing import Annotated
import httpx
from fastapi import Depends, Request
from python.ebook_search.config import EbookSearchConfig
def get_config(request: Request) -> EbookSearchConfig:
"""Get the loaded search config from app state."""
return request.app.state.config
def get_http_client(request: Request) -> httpx.AsyncClient:
"""Get the shared LLM HTTP client from app state."""
return request.app.state.http_client
AppConfig = Annotated[EbookSearchConfig, Depends(get_config)]
AppHttpClient = Annotated[httpx.AsyncClient, Depends(get_http_client)]
+127
View File
@@ -0,0 +1,127 @@
"""Background phrase-judging tasks for the web app.
Judging a book sends one LLM request per candidate phrase, which can take minutes, so it must
not run inside the request where it would block the UI. Judgments run as async FastAPI
background tasks, awaited on the event loop after the response is sent, and are tracked per
book in app state so a second judge request for a book that is already being judged is
rejected instead of doubling the work.
State is loop-confined: every read and mutation happens on the event loop (async route
handlers and async background tasks) and no critical section contains an ``await``, so each
mutation is atomic per loop iteration and no locking is needed.
"""
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
from python.ebook_search.protected_phrases.judge_ngrams import judge_candidate_phrases_for_books
if TYPE_CHECKING:
from fastapi import BackgroundTasks, FastAPI
logger = logging.getLogger(__name__)
@dataclass
class JudgeTaskState:
"""Running book judgments and last outcome messages, keyed by book id."""
running_book_ids: set[int] = field(default_factory=set)
outcome_messages: dict[int, str] = field(default_factory=dict)
def get_judge_task_state(app: FastAPI) -> JudgeTaskState:
"""Return the app's judge task state, creating it on first use.
Args:
app (FastAPI): App whose state holds the judge task registry.
Returns:
JudgeTaskState: The shared judge task state for this app.
"""
state = getattr(app.state, "judge_tasks", None)
if state is None:
state = JudgeTaskState()
app.state.judge_tasks = state
return state
def start_book_phrase_judgment(app: FastAPI, background_tasks: BackgroundTasks, source_id: int) -> bool:
"""Queue judging of one book's candidate phrases as a FastAPI background task.
The book is claimed before the response returns, so a repeated judge request cannot queue
a second run while one is pending or running.
Args:
app (FastAPI): App supplying the engine, config, and judge task state.
background_tasks (BackgroundTasks): Request's background tasks to queue the judgment on.
source_id (int): Book to judge candidates for.
Returns:
bool: True when a judgment was queued, False when one is already running for this book.
"""
state = get_judge_task_state(app)
if source_id in state.running_book_ids:
logger.info(f"ebook_book_phrase_judgment_already_running {source_id=}")
return False
state.running_book_ids.add(source_id)
state.outcome_messages.pop(source_id, None)
background_tasks.add_task(judge_book_phrases_for_app, app, source_id)
logger.info(f"ebook_book_phrase_judgment_queued {source_id=}")
return True
async def judge_book_phrases_for_app(app: FastAPI, source_id: int) -> None:
"""Judge one book using the app engine and config, recording the outcome message.
Args:
app (FastAPI): App supplying the engine, config, and judge task state.
source_id (int): Book to judge candidates for.
"""
state = get_judge_task_state(app)
try:
result = await judge_candidate_phrases_for_books(app.state.engine, app.state.config, source_ids=[source_id])
logger.info(
f"ebook_book_phrase_judgment_complete {source_id=} {result.candidates_judged=} {result.protected_phrases=} "
f"{result.phrase_mentions=} {result.books_failed=}"
)
if result.books_failed:
message = "Judging failed; see server logs for details"
else:
message = (
f"Judged {result.candidates_judged} candidates; {result.protected_phrases} protected phrases promoted"
)
except Exception:
logger.exception(f"ebook_book_phrase_judgment_task_failed {source_id=}")
message = "Judging failed; see server logs for details"
state.running_book_ids.discard(source_id)
state.outcome_messages[source_id] = message
def is_judging_book(app: FastAPI, source_id: int) -> bool:
"""Report whether a judgment is currently queued or running for one book.
Args:
app (FastAPI): App supplying the judge task state.
source_id (int): Book to check.
Returns:
bool: True while the book's judgment is pending or running.
"""
return source_id in get_judge_task_state(app).running_book_ids
def pop_book_judgment_outcome(app: FastAPI, source_id: int) -> str | None:
"""Return and clear the outcome message from one book's last finished judgment.
Args:
app (FastAPI): App supplying the judge task state.
source_id (int): Book to fetch the outcome for.
Returns:
str | None: The outcome message, or None when there is nothing new to report.
"""
return get_judge_task_state(app).outcome_messages.pop(source_id, None)
+91
View File
@@ -0,0 +1,91 @@
"""FastAPI HTMX app for EPUB search."""
from __future__ import annotations
import logging
from contextlib import asynccontextmanager
from typing import TYPE_CHECKING, Annotated
import httpx
import typer
import uvicorn
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from sqlalchemy.ext.asyncio import AsyncSession
from python.common import configure_logger
from python.ebook_search.api.bm25_tasks import cancel_bm25_refresh
from python.ebook_search.api.routes import admin_router, health_router, page_router, search_router
from python.ebook_search.api.web import STATIC_DIR
from python.ebook_search.bm25_corpus import ensure_bm25_corpus
from python.ebook_search.config import load_config
from python.ebook_search.protected_phrases.pool import shutdown_extraction_pool
from python.fastapi_tools import ZstdMiddleware
from python.orm.common import get_async_postgres_engine
if TYPE_CHECKING:
from collections.abc import AsyncIterator
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Manage application startup and shutdown resources."""
logger.info("ebook_search_startup")
config = load_config()
app.state.config = config
logger.info(
f"ebook_search_config_loaded {config.top_k=} {config.embedding_model=} {config.embedding_base_url=} "
f"{config.vllm_base_url=} {config.rerank.enabled=} {config.phrase_matching_enabled=} {config.answer_enabled=} "
f"library_paths={len(config.library_paths)}"
)
if not config.library_paths:
logger.warning("ebook_search_no_library_paths_configured")
# Concurrent phrase judging opens one session per book worker on this engine, so size the pool
# to cover those plus headroom for ordinary web requests.
app.state.engine = get_async_postgres_engine(
name="RICHIE",
vector_engine=True,
pool_size=config.phrase_judge_book_workers + 10,
)
app.state.http_client = httpx.AsyncClient()
async with AsyncSession(app.state.engine, expire_on_commit=False) as session:
await ensure_bm25_corpus(session, config)
try:
yield
finally:
logger.info("ebook_search_shutdown")
cancel_bm25_refresh(app)
shutdown_extraction_pool()
await app.state.http_client.aclose()
await app.state.engine.dispose()
def create_app() -> FastAPI:
"""Create the EPUB search web app."""
app = FastAPI(title="EPUB Search", lifespan=lifespan)
app.add_middleware(ZstdMiddleware)
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
app.include_router(admin_router)
app.include_router(health_router)
app.include_router(page_router)
app.include_router(search_router)
return app
def serve(
host: Annotated[str, typer.Option("--host", "-h", help="Host to bind to")] = "127.0.0.1",
port: Annotated[int, typer.Option("--port", "-p", help="Port to bind to")] = 8070,
log_level: Annotated[str, typer.Option("--log-level", "-l", help="Log level")] = "INFO",
) -> None:
"""Start the EPUB search server."""
configure_logger(log_level)
uvicorn.run(create_app(), host=host, port=port)
if __name__ == "__main__":
typer.run(serve)
@@ -0,0 +1,13 @@
"""EPUB search web route modules."""
from python.ebook_search.api.routes.admin import router as admin_router
from python.ebook_search.api.routes.health import router as health_router
from python.ebook_search.api.routes.page import router as page_router
from python.ebook_search.api.routes.search import router as search_router
__all__ = [
"admin_router",
"health_router",
"page_router",
"search_router",
]
+204
View File
@@ -0,0 +1,204 @@
"""Admin routes for the EPUB search web UI."""
from __future__ import annotations
import logging
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from python.ebook_search.api.bm25_tasks import schedule_bm25_refresh
from python.ebook_search.api.dependencies import ( # noqa: TC001 FastAPI resolves these annotated dependencies at runtime
AppConfig,
AppHttpClient,
)
from python.ebook_search.api.web import error_response, templates
from python.ebook_search.embeddings import embed_missing_chunks, embedding_model_stats
from python.ebook_search.ingest import ingest_configured_paths
from python.ebook_search.protected_phrases.generate_ngrams import generate_candidate_phrases_for_books
from python.ebook_search.protected_phrases.judge_ngrams import judge_candidate_phrases_for_books
from python.ebook_search.protected_phrases.store import book_ids_pending_first_judgment, corpus_phrase_stats
from python.fastapi_tools import ( # noqa: TC001 FastAPI resolves these annotated dependencies at runtime
AppAsyncEngine,
AsyncDbSession,
)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/admin")
@router.get("", response_class=HTMLResponse)
async def admin(request: Request, config: AppConfig, session: AsyncDbSession) -> HTMLResponse:
"""Render the admin page."""
stats = await embedding_model_stats(session)
phrase_stats = await corpus_phrase_stats(session)
logger.info(
f"ebook_admin_page_loaded models={len(stats)} {phrase_stats.candidate_phrases=} "
f"{phrase_stats.protected_phrases=}"
)
return templates.TemplateResponse(
request,
"admin.html",
{"config": config, "stats": stats, "phrase_stats": phrase_stats},
)
@router.post("/scan", response_class=HTMLResponse)
async def scan_library(request: Request, config: AppConfig, session: AsyncDbSession) -> HTMLResponse:
"""Scan configured library paths for EPUB changes."""
try:
count = await ingest_configured_paths(session, config)
await session.commit()
except Exception as error:
logger.exception("ebook_admin_scan_failed")
return error_response(request, error)
logger.info(f"ebook_admin_scan_complete {count=}")
if count > 0:
schedule_bm25_refresh(request.app)
return templates.TemplateResponse(request, "partials/admin_status.html", {"message": f"Indexed {count} EPUBs"})
@router.post("/phrases/generate-all", response_class=HTMLResponse)
async def generate_all_phrases(request: Request, config: AppConfig, engine: AppAsyncEngine) -> HTMLResponse:
"""Regenerate candidate phrases for every indexed book without LLM judging."""
try:
result = await generate_candidate_phrases_for_books(engine, config)
except Exception as error:
logger.exception("ebook_admin_generate_phrases_failed")
return error_response(request, error)
logger.info(
f"ebook_admin_generate_phrases_complete {result.books_seen=} {result.books_built=} {result.candidate_phrases=}"
)
return templates.TemplateResponse(
request,
"partials/admin_status.html",
{
"message": (
f"Generated phrases for {result.books_built} of {result.books_seen} books; "
f"{result.candidate_phrases} candidates stored"
)
},
)
@router.post("/phrases/judge-all", response_class=HTMLResponse)
async def judge_all_phrases(request: Request, engine: AppAsyncEngine, config: AppConfig) -> HTMLResponse:
"""Judge unjudged candidate phrases across every indexed book."""
return await run_phrase_judgment(request, engine, config, source_ids=None)
@router.post("/phrases/judge-missing", response_class=HTMLResponse)
async def judge_missing_phrases(
request: Request,
engine: AppAsyncEngine,
config: AppConfig,
session: AsyncDbSession,
) -> HTMLResponse:
"""Judge candidate phrases only for books where judging has never run."""
source_ids = await book_ids_pending_first_judgment(session)
if not source_ids:
return templates.TemplateResponse(
request,
"partials/admin_status.html",
{"message": "All books with candidate phrases have been judged"},
)
return await run_phrase_judgment(request, engine, config, source_ids=source_ids)
async def run_phrase_judgment(
request: Request,
engine: AppAsyncEngine,
config: AppConfig,
*,
source_ids: list[int] | None,
) -> HTMLResponse:
"""Run LLM judging for candidate phrases and render the outcome as an admin status partial.
Args:
request (Request): Current request, for template rendering.
engine (AppAsyncEngine): Engine used to open per-book judging sessions.
config (AppConfig): Runtime phrase-tuning settings.
source_ids (list[int] | None): Books to judge; ``None`` judges every indexed book.
Returns:
HTMLResponse: Status partial describing the judging outcome.
"""
try:
result = await judge_candidate_phrases_for_books(engine, config, source_ids=source_ids)
except Exception as error:
logger.exception("ebook_admin_judge_phrases_failed")
return error_response(request, error)
logger.info(
f"ebook_admin_judge_phrases_complete {result.books_seen=} {result.books_judged=} {result.books_failed=} "
f"{result.candidates_judged=} {result.protected_phrases=} {result.phrase_mentions=}"
)
return templates.TemplateResponse(
request,
"partials/admin_status.html",
{
"message": (
f"Judged {result.candidates_judged} candidates across {result.books_judged} of "
f"{result.books_seen} books; {result.protected_phrases} protected phrases, "
f"{result.phrase_mentions} mentions"
+ (f"; {result.books_failed} books failed" if result.books_failed else "")
)
},
)
@router.post("/embed-missing", response_class=HTMLResponse)
async def embed_missing(
request: Request,
config: AppConfig,
session: AsyncDbSession,
client: AppHttpClient,
) -> HTMLResponse:
"""Embed chunks missing vectors for the configured model."""
try:
count = await embed_missing_chunks(session, client, config)
await session.commit()
except Exception as error:
logger.exception("ebook_admin_embed_missing_failed")
return error_response(request, error)
logger.info(f"ebook_admin_embed_missing_complete {count=}")
return templates.TemplateResponse(
request,
"partials/admin_status.html",
{"message": f"Embedded {count} chunks"},
)
@router.post("/embed-all", response_class=HTMLResponse)
async def embed_all(
request: Request,
config: AppConfig,
session: AsyncDbSession,
client: AppHttpClient,
) -> HTMLResponse:
"""Embed all chunks missing vectors in fixed-size batches."""
total = 0
batches = 0
try:
while True:
count = await embed_missing_chunks(session, client, config)
if count == 0:
break
await session.commit()
total += count
batches += 1
logger.info(f"ebook_admin_embed_all_batch_complete {batches=} {count=} {total=}")
except Exception as error:
logger.exception(f"ebook_admin_embed_all_failed {batches=} {total=}")
return error_response(request, f"Embed all failed after {total} chunks in {batches} batches: {error}")
logger.info(f"ebook_admin_embed_all_complete {batches=} {total=}")
return templates.TemplateResponse(
request,
"partials/admin_status.html",
{"message": f"Embedded {total} chunks in {batches} batches of {config.embedding_batch_size}"},
)
+92
View File
@@ -0,0 +1,92 @@
"""Liveness and readiness routes for the EPUB search service."""
from __future__ import annotations
import logging
from http import HTTPStatus
from typing import TYPE_CHECKING
from fastapi import APIRouter
from fastapi.responses import JSONResponse
from sqlalchemy import literal, select
from sqlalchemy.exc import SQLAlchemyError
from python.ebook_search.api.dependencies import ( # noqa: TC001 FastAPI resolves these annotated dependencies at runtime
AppConfig,
AppHttpClient,
)
from python.ebook_search.bm25_corpus import bm25_index_exists, bm25_index_path, read_bm25_manifest
from python.ebook_search.llm_interface import check_chat_endpoint, check_embedding_endpoint
from python.fastapi_tools import AsyncDbSession # noqa: TC001 FastAPI resolves this annotated dependency at runtime
if TYPE_CHECKING:
import httpx
from sqlalchemy.ext.asyncio import AsyncSession
from python.ebook_search.config import EbookSearchConfig
logger = logging.getLogger(__name__)
router = APIRouter()
@router.get("/health")
async def health() -> dict[str, str]:
"""Liveness probe that returns ok without touching dependencies."""
return {"status": "ok"}
@router.get("/ready")
async def ready(config: AppConfig, session: AsyncDbSession, client: AppHttpClient) -> JSONResponse:
"""Readiness probe reporting database, embedding endpoint, and BM25 index status."""
database_ok = await check_database(session)
embedding_ok = await check_embedding_endpoint(client, config)
chat_status = await chat_endpoint_status(client, config)
bm25_status = check_bm25_status(config)
checks = {
"database": "ok" if database_ok else "fail",
"embedding": "ok" if embedding_ok else "fail",
"chat": chat_status,
"bm25": bm25_status,
}
if not database_ok:
status = "unavailable"
status_code = HTTPStatus.SERVICE_UNAVAILABLE
elif not embedding_ok or chat_status == "fail" or bm25_status == "missing":
status = "degraded"
status_code = HTTPStatus.OK
else:
status = "ready"
status_code = HTTPStatus.OK
logger.info(f"ebook_ready_check {status=} {database_ok=} {embedding_ok=} {chat_status=} {bm25_status=}")
return JSONResponse(content={"status": status, "checks": checks}, status_code=status_code)
async def chat_endpoint_status(client: httpx.AsyncClient, config: EbookSearchConfig) -> str:
"""Return the answering chat endpoint status, or disabled when answers are off."""
if not config.answer_enabled:
return "disabled"
return "ok" if await check_chat_endpoint(client, config) else "fail"
async def check_database(session: AsyncSession) -> bool:
"""Return whether the database answers a trivial query."""
try:
await session.execute(select(literal(1)))
except SQLAlchemyError as error:
logger.warning(f"ebook_ready_database_unavailable {error=}")
return False
return True
def check_bm25_status(config: EbookSearchConfig) -> str:
"""Return the persisted BM25 index status without loading it into memory."""
index_path = bm25_index_path(config)
manifest = read_bm25_manifest(index_path)
if manifest is None or not bm25_index_exists(index_path, manifest):
return "missing"
if manifest.chunk_count == 0:
return "empty"
return "ok"
+187
View File
@@ -0,0 +1,187 @@
"""Page routes for the EPUB search web UI."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from sqlalchemy import func, select
from python.ebook_search.api.dependencies import (
AppConfig, # noqa: TC001 FastAPI resolves this annotated dependency at runtime
)
from python.ebook_search.api.judge_tasks import is_judging_book, pop_book_judgment_outcome, start_book_phrase_judgment
from python.ebook_search.api.web import templates
from python.ebook_search.protected_phrases.generate_ngrams import recalculate_candidate_phrases_for_book
from python.ebook_search.protected_phrases.store import count_protected_phrases
from python.fastapi_tools import AsyncDbSession # noqa: TC001 FastAPI resolves this annotated dependency at runtime
from python.orm.richie import EbookCandidatePhrase, EbookChapter, EbookChunk, EbookProtectedPhrase, EbookSource
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession
logger = logging.getLogger(__name__)
router = APIRouter()
@router.get("/", response_class=HTMLResponse)
async def index(request: Request, config: AppConfig) -> HTMLResponse:
"""Render the search page."""
return templates.TemplateResponse(request, "search.html", {"config": config})
@router.get("/books", response_class=HTMLResponse)
async def books(request: Request, session: AsyncDbSession) -> HTMLResponse:
"""Render the indexed books page."""
sources = list((await session.scalars(select(EbookSource).order_by(EbookSource.title))).all())
logger.info(f"ebook_books_page_loaded count={len(sources)}")
return templates.TemplateResponse(request, "books.html", {"sources": sources})
async def get_chapter_count(session: AsyncSession, book_id: int) -> int:
"""Return the number of indexed chapters for one book."""
return await session.scalar(select(func.count(EbookChapter.id)).where(EbookChapter.source_id == book_id)) or 0
async def get_chunk_count(session: AsyncSession, book_id: int) -> int:
"""Return the number of indexed chunks for one book."""
return await session.scalar(select(func.count(EbookChunk.id)).where(EbookChunk.source_id == book_id)) or 0
async def get_candidate_count(session: AsyncSession, book_id: int) -> int:
"""Return the number of indexed candidates for one book."""
return (
await session.scalar(select(func.count(EbookCandidatePhrase.id)).where(EbookCandidatePhrase.book_id == book_id))
or 0
)
async def get_judged_candidate_count(session: AsyncSession, book_id: int) -> int:
"""Return the number of judged candidates for one book."""
return (
await session.scalar(
select(func.count(EbookCandidatePhrase.id)).where(
EbookCandidatePhrase.book_id == book_id,
EbookCandidatePhrase.llm_judged.is_(True),
)
)
or 0
)
async def get_candidates(session: AsyncSession, book_id: int) -> list[EbookCandidatePhrase]:
"""Return the indexed candidates for one book."""
return list(
await session.scalars(
select(EbookCandidatePhrase)
.where(EbookCandidatePhrase.book_id == book_id)
.order_by(EbookCandidatePhrase.candidate_score.desc())
.limit(100)
)
)
async def get_protected_phrases(session: AsyncSession, book_id: int) -> list[EbookProtectedPhrase]:
"""Return the protected phrases for one book."""
return list(
await session.scalars(
select(EbookProtectedPhrase)
.where(EbookProtectedPhrase.book_id == book_id)
.order_by(EbookProtectedPhrase.importance.desc())
.limit(100)
)
)
@router.get("/books/{source_id}", response_class=HTMLResponse)
async def book_detail(source_id: int, request: Request, session: AsyncDbSession) -> HTMLResponse:
"""Render details for one indexed book."""
source = await session.get(EbookSource, source_id)
phrase_status_message = None
recalculated = request.query_params.get("phrases_recalculated")
if recalculated is not None:
phrase_status_message = f"Recalculated phrases; {recalculated} candidates generated"
judgment_outcome = pop_book_judgment_outcome(request.app, source_id)
if judgment_outcome is not None:
phrase_status_message = judgment_outcome
judging_in_progress = is_judging_book(request.app, source_id)
if judging_in_progress:
phrase_status_message = "Judging candidate phrases in the background; refresh to see progress"
if source is not None:
chapter_count = await get_chapter_count(session, source.id)
chunk_count = await get_chunk_count(session, source.id)
candidate_count = await get_candidate_count(session, source.id)
judged_candidate_count = await get_judged_candidate_count(session, source.id)
protected_count = await count_protected_phrases(session, source.id)
candidates = await get_candidates(session, source.id)
protected_phrases = await get_protected_phrases(session, source.id)
else:
chapter_count = 0
chunk_count = 0
candidate_count = 0
judged_candidate_count = 0
protected_count = 0
candidates = []
protected_phrases = []
logger.info(
f"ebook_book_detail_loaded {source_id=} found={source is not None} {chapter_count=} {chunk_count=} "
f"{candidate_count=} {judged_candidate_count=} {protected_count=}"
)
return templates.TemplateResponse(
request,
"book_detail.html",
{
"candidate_count": candidate_count,
"candidates": candidates,
"chapter_count": chapter_count,
"chunk_count": chunk_count,
"judged_candidate_count": judged_candidate_count,
"judging_in_progress": judging_in_progress,
"protected_count": protected_count,
"protected_phrases": protected_phrases,
"phrase_status_message": phrase_status_message,
"source": source,
},
)
@router.post("/books/{source_id}/recalculate-phrases")
async def recalculate_book_phrases(source_id: int, config: AppConfig, session: AsyncDbSession) -> RedirectResponse:
"""Clear and regenerate candidate phrases for one indexed book."""
source = await session.get(EbookSource, source_id)
if source is None:
raise HTTPException(status_code=404, detail="Book not found")
try:
result = await recalculate_candidate_phrases_for_book(session, source, config)
except ValueError as error:
raise HTTPException(status_code=409, detail=str(error)) from error
logger.info(
f"ebook_book_phrase_recalculation_complete {source_id=} {result.candidate_phrases=} "
f"{result.deleted_candidates=} {result.deleted_protected_phrases=} {result.deleted_aliases=} "
f"{result.deleted_mentions=}"
)
return RedirectResponse(
url=f"/books/{source_id}?phrases_recalculated={result.candidate_phrases}",
status_code=303,
)
@router.post("/books/{source_id}/judge-phrases")
async def judge_book_phrases(
source_id: int,
request: Request,
background_tasks: BackgroundTasks,
session: AsyncDbSession,
) -> RedirectResponse:
"""Queue background judging of one book's candidate phrases and return immediately."""
source = await session.get(EbookSource, source_id)
if source is None:
raise HTTPException(status_code=404, detail="Book not found")
started = start_book_phrase_judgment(request.app, background_tasks, source.id)
logger.info(f"ebook_book_phrase_judgment_requested {source_id=} {started=}")
return RedirectResponse(url=f"/books/{source_id}", status_code=303)
+123
View File
@@ -0,0 +1,123 @@
"""Search routes for the EPUB search web UI."""
from __future__ import annotations
import logging
from dataclasses import replace
from time import perf_counter
from typing import TYPE_CHECKING, Annotated
from fastapi import APIRouter, Form, Request
from fastapi.responses import HTMLResponse
from python.ebook_search.answer import answer_query
from python.ebook_search.api.dependencies import ( # noqa: TC001 FastAPI resolves these annotated dependencies at runtime
AppConfig,
AppHttpClient,
)
from python.ebook_search.api.web import error_response, templates
from python.ebook_search.guardrails import (
CitationReport,
is_confident,
retrieval_confidence,
validate_citations,
)
from python.ebook_search.search import SearchResponse, search_ebooks
from python.ebook_search.timing import runtime_step_from_start
from python.fastapi_tools import AppAsyncEngine # noqa: TC001 FastAPI resolves this annotated dependency at runtime
if TYPE_CHECKING:
import httpx
from python.ebook_search.config import EbookSearchConfig
logger = logging.getLogger(__name__)
router = APIRouter()
async def build_answer(
client: httpx.AsyncClient,
query: str,
response: SearchResponse,
config: EbookSearchConfig,
) -> tuple[str, bool, CitationReport | None]:
"""Generate the answer for a search, returning ``(answer, low_confidence, citation_report)``."""
if not config.answer_enabled:
logger.info("ebook_answer_skipped_disabled")
return "Answer generation is disabled. Source chunks are shown below.", False, None
if not is_confident(response.results, config):
logger.info(
f"ebook_answer_low_confidence confidence={retrieval_confidence(response.results):.4f} "
f"{config.min_retrieval_confidence=:.4f}"
)
answer = (
"Retrieval confidence is low for this query, so answer generation was skipped. "
"Source chunks are shown below."
)
return answer, True, None
try:
answer = await answer_query(client, query, response.results, config)
except RuntimeError as error:
logger.warning(f"ebook_answer_request_failed_falling_back {error=}")
return "Answer generation failed. Source chunks are still shown below.", False, None
citation_report = None
if config.validate_citations_enabled and response.results:
citation_report = validate_citations(answer, len(response.results))
if citation_report.invalid or not citation_report.grounded:
logger.warning(f"ebook_answer_citation_issue {citation_report.invalid=} {citation_report.grounded=}")
return answer, False, citation_report
@router.post("/search", response_class=HTMLResponse)
async def search(
request: Request,
config: AppConfig,
engine: AppAsyncEngine,
client: AppHttpClient,
query: Annotated[str, Form()],
*,
rerank: Annotated[bool, Form()] = False,
phrase_matching: Annotated[bool, Form()] = False,
) -> HTMLResponse:
"""Run a search and render HTMX results."""
try:
response = await search_ebooks(
engine,
client,
query,
config,
rerank=rerank,
phrase_matching=phrase_matching,
)
except Exception as error:
logger.exception("ebook_search_request_failed")
return error_response(request, error)
answer_start = perf_counter()
answer, low_confidence, citation_report = await build_answer(client, query, response, config)
answer_step_name = "Answer generation" if config.answer_enabled else "Answer skipped"
response = replace(
response,
timings=(*response.timings, runtime_step_from_start(answer_step_name, answer_start)),
)
for step in response.timings:
logger.info(f"ebook_search_timing {step.name=} {step.duration_ms=:.1f}")
logger.info(
f"ebook_search_request_complete results={len(response.results)} {response.rank_label=} "
f"{response.total_runtime_ms=:.1f}"
)
return templates.TemplateResponse(
request,
"partials/results.html",
{
"answer": answer,
"response": response,
"low_confidence": low_confidence,
"citation_report": citation_report,
},
)
+447
View File
@@ -0,0 +1,447 @@
:root {
--bg: #f4f5f7;
--surface: #ffffff;
--border: #e3e5ea;
--text: #1c1f24;
--muted: #6b7280;
--accent: #4f46e5;
--accent-soft: #eef0fe;
--danger: #b42318;
--warn-bg: #fff8eb;
--warn-border: #e0a92e;
--warn-text: #7a5008;
--radius: 12px;
--shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 1px 3px rgba(16, 24, 40, 0.08);
}
html.theme-dark {
--bg: #0f1117;
--surface: #1a1d25;
--border: #2b303b;
--text: #e6e8ec;
--muted: #9aa1ad;
--accent: #818cf8;
--accent-soft: #262b45;
--danger: #f97066;
--warn-bg: #2a2410;
--warn-border: #b9881f;
--warn-text: #e8c97a;
--shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 1px 3px rgba(0, 0, 0, 0.4);
color-scheme: dark;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.55;
}
main {
max-width: 820px;
margin: 0 auto;
padding: 32px 20px 64px;
}
/* Header / nav */
.site-header {
background: var(--surface);
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
z-index: 10;
}
.site-nav {
max-width: 820px;
margin: 0 auto;
padding: 12px 20px;
display: flex;
align-items: center;
gap: 20px;
}
.brand {
font-weight: 700;
font-size: 1.05rem;
color: var(--text);
text-decoration: none;
}
.nav-links {
display: flex;
gap: 6px;
margin-right: auto;
}
.nav-links a {
padding: 6px 12px;
border-radius: 8px;
color: var(--muted);
text-decoration: none;
font-size: 0.94rem;
transition: background 0.15s, color 0.15s;
}
.nav-links a:hover {
background: var(--accent-soft);
color: var(--accent);
}
.dev-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.85rem;
color: var(--muted);
cursor: pointer;
user-select: none;
}
.theme-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
padding: 0;
font-size: 1rem;
line-height: 1;
color: var(--text);
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
cursor: pointer;
}
.theme-toggle:hover {
border-color: var(--accent);
filter: none;
}
h1 {
font-size: 1.6rem;
margin: 0 0 20px;
}
h2 {
font-size: 1.15rem;
margin: 0 0 8px;
}
/* Cards */
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 20px;
}
/* Search form */
form {
margin: 0;
}
label {
font-weight: 600;
font-size: 0.92rem;
}
textarea {
display: block;
width: 100%;
margin: 8px 0 16px;
padding: 12px 14px;
font: inherit;
color: var(--text);
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
resize: vertical;
transition: border-color 0.15s, box-shadow 0.15s;
}
textarea:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.form-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.search-toggles {
display: flex;
flex-wrap: wrap;
gap: 14px;
}
button {
padding: 10px 20px;
font: inherit;
font-weight: 600;
color: #fff;
background: var(--accent);
border: none;
border-radius: 10px;
cursor: pointer;
transition: filter 0.15s;
}
button:hover {
filter: brightness(1.08);
}
.check {
display: inline-flex;
gap: 8px;
align-items: center;
font-weight: 500;
color: var(--muted);
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 24px;
}
.actions-grid {
display: grid;
grid-template-columns: repeat(2, max-content);
}
/* Answer + results */
#results {
display: block;
margin-top: 28px;
}
.rank-label {
font-size: 0.82rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--muted);
margin-bottom: 16px;
}
.answer {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 20px;
margin-bottom: 24px;
}
.answer p:last-child {
margin-bottom: 0;
}
.results {
list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: 16px;
}
.results > li {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 18px 20px;
}
.results h2 {
font-size: 1.05rem;
}
.results h2 a {
color: var(--text);
text-decoration: none;
}
.results h2 a:hover {
color: var(--accent);
}
.meta {
color: var(--muted);
font-size: 0.88rem;
margin: 0 0 10px;
}
.scores {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 14px 0 0;
}
.scores div {
display: inline-flex;
gap: 6px;
align-items: baseline;
padding: 3px 10px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 999px;
font-size: 0.78rem;
}
.scores dt {
font-weight: 600;
color: var(--muted);
}
.scores dd {
margin: 0;
font-variant-numeric: tabular-nums;
}
.phrase-matches {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: baseline;
margin: 10px 0 0;
font-size: 0.78rem;
}
.phrase-matches-label {
color: var(--muted);
font-weight: 600;
}
.phrase-match {
padding: 3px 10px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 999px;
color: var(--accent);
}
/* Runtime — developer diagnostics, hidden unless dev mode is on */
.runtime {
display: none;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 18px 20px;
margin-bottom: 24px;
}
html.dev .runtime {
display: block;
}
.timing-chart {
display: grid;
gap: 8px;
padding: 0;
margin: 12px 0 0;
list-style: none;
}
.timing-chart li {
display: grid;
grid-template-columns: minmax(150px, 1fr) minmax(160px, 2fr) auto auto;
gap: 10px;
align-items: center;
font-size: 0.85rem;
}
.timing-bar {
height: 8px;
overflow: hidden;
background: var(--bg);
border-radius: 999px;
}
.timing-bar span {
display: block;
height: 100%;
background: var(--accent);
border-radius: 999px;
}
.timing-value,
.timing-remaining {
color: var(--muted);
font-variant-numeric: tabular-nums;
text-align: right;
}
/* Tables */
table {
width: 100%;
border-collapse: collapse;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
}
th,
td {
padding: 10px 14px;
border-bottom: 1px solid var(--border);
text-align: left;
font-size: 0.9rem;
}
th {
font-weight: 600;
color: var(--muted);
background: var(--bg);
}
tbody tr:last-child td {
border-bottom: none;
}
dl dt {
font-weight: 600;
color: var(--muted);
font-size: 0.85rem;
}
dl dd {
margin: 0 0 12px;
}
/* States */
.error {
color: var(--danger);
font-weight: 600;
}
.notice {
margin: 12px 0;
padding: 10px 14px;
border-left: 3px solid var(--warn-border);
border-radius: 6px;
background: var(--warn-bg);
color: var(--warn-text);
font-weight: 500;
}
.status {
color: var(--muted);
}
@@ -0,0 +1,103 @@
{% extends "base.html" %} {% block title %}EPUB Admin{% endblock %} {% block
head %}
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
{% endblock %} {% block content %}
<h1>Admin</h1>
<section id="admin-status"></section>
<section class="actions">
<form hx-post="/admin/scan" hx-target="#admin-status" hx-swap="innerHTML">
<button type="submit">Scan</button>
</form>
</section>
<section>
<h2>Embeddings</h2>
<section class="actions">
<form
hx-post="/admin/embed-missing"
hx-target="#admin-status"
hx-swap="innerHTML"
>
<button type="submit">Embed</button>
</form>
<form
hx-post="/admin/embed-all"
hx-target="#admin-status"
hx-swap="innerHTML"
>
<button type="submit">Embed all</button>
</form>
</section>
<table>
<thead>
<tr>
<th>Model</th>
<th>Dimensions</th>
<th>Embedded</th>
<th>Missing</th>
<th>Total chunks</th>
</tr>
</thead>
<tbody>
{% for item in stats %}
<tr>
<td>{{ item.model_name }}</td>
<td>{{ item.dimension }}</td>
<td>{{ item.embedded_chunks }}</td>
<td>{{ item.missing_chunks }}</td>
<td>{{ item.total_chunks }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
<section>
<h2>Protected phrases</h2>
<section class="actions actions-grid">
<form
hx-post="/admin/phrases/generate-all"
hx-target="#admin-status"
hx-swap="innerHTML"
>
<button type="submit">Regenerate all phrases</button>
</form>
<form
hx-post="/admin/phrases/judge-all"
hx-target="#admin-status"
hx-swap="innerHTML"
>
<button type="submit">Judge all phrases</button>
</form>
<form
hx-post="/admin/phrases/judge-missing"
hx-target="#admin-status"
hx-swap="innerHTML"
>
<button type="submit">Judge missing phrases</button>
</form>
</section>
<table>
<thead>
<tr>
<th>Candidates</th>
<th>Judged</th>
<th>Unjudged</th>
<th>Protected</th>
<th>Books indexed</th>
<th>Books generated</th>
<th>Books fully judged</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ phrase_stats.candidate_phrases }}</td>
<td>{{ phrase_stats.judged_candidates }}</td>
<td>{{ phrase_stats.unjudged_candidates }}</td>
<td>{{ phrase_stats.protected_phrases }}</td>
<td>{{ phrase_stats.total_books }}</td>
<td>{{ phrase_stats.books_with_candidates }}</td>
<td>{{ phrase_stats.books_fully_judged }}</td>
</tr>
</tbody>
</table>
</section>
{% endblock %}
@@ -0,0 +1,71 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}EPUB Search{% endblock %}</title>
{% block head %}{% endblock %}
<link rel="stylesheet" href="/static/style.css?v={{ static_version('style.css') }}">
<script>
// Apply theme and dev mode before paint to avoid a flash of unstyled/wrong content.
(function () {
var stored = localStorage.getItem("ebook-theme");
var prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
var theme = stored || (prefersDark ? "dark" : "light");
document.documentElement.classList.add("theme-" + theme);
if (localStorage.getItem("ebook-dev-mode") === "on") {
document.documentElement.classList.add("dev");
}
})();
</script>
</head>
<body>
<header class="site-header">
<nav class="site-nav">
<a class="brand" href="/">EPUB Search</a>
<div class="nav-links">
<a href="/">Search</a>
<a href="/books">Books</a>
<a href="/admin">Admin</a>
</div>
<button type="button" id="theme-toggle" class="theme-toggle" title="Toggle light / dark theme" aria-label="Toggle theme"></button>
<label class="dev-toggle" title="Show developer diagnostics">
<input type="checkbox" id="dev-mode-toggle">
<span>Dev</span>
</label>
</nav>
</header>
<main>
{% block content %}{% endblock %}
</main>
<script>
(function () {
var toggle = document.getElementById("dev-mode-toggle");
if (toggle) {
toggle.checked = document.documentElement.classList.contains("dev");
toggle.addEventListener("change", function () {
document.documentElement.classList.toggle("dev", toggle.checked);
localStorage.setItem("ebook-dev-mode", toggle.checked ? "on" : "off");
});
}
var themeButton = document.getElementById("theme-toggle");
if (themeButton) {
var root = document.documentElement;
var sync = function () {
var isDark = root.classList.contains("theme-dark");
themeButton.textContent = isDark ? "☀️" : "🌙";
};
sync();
themeButton.addEventListener("click", function () {
var next = root.classList.contains("theme-dark") ? "light" : "dark";
root.classList.remove("theme-dark", "theme-light");
root.classList.add("theme-" + next);
localStorage.setItem("ebook-theme", next);
sync();
});
}
})();
</script>
</body>
</html>
@@ -0,0 +1,109 @@
{% extends "base.html" %}
{% block title %}{% if source %}{{ source.title }}{% else %}Book not found{% endif %}{% endblock %}
{% block content %}
{% if source %}
<h1>{{ source.title }}</h1>
<p class="meta">{{ source.author or "Unknown author" }}</p>
{% if phrase_status_message %}
<p class="status">{{ phrase_status_message }}</p>
{% endif %}
<dl class="card">
<dt>File</dt>
<dd>{{ source.file_path }}</dd>
<dt>Chapters</dt>
<dd>{{ chapter_count }}</dd>
<dt>Chunks</dt>
<dd>{{ chunk_count }}</dd>
<dt>Candidates</dt>
<dd>{{ candidate_count }}</dd>
<dt>Judged</dt>
<dd>{{ judged_candidate_count }}</dd>
<dt>Protected</dt>
<dd>{{ protected_count }}</dd>
</dl>
<form
method="post"
action="/books/{{ source.id }}/recalculate-phrases"
onsubmit="return confirm('Remove old phrases for this book and generate new candidates?');"
>
<button type="submit">Recalculate phrases</button>
</form>
<form
method="post"
action="/books/{{ source.id }}/judge-phrases"
onsubmit="return confirm('Judge candidate phrases for this book with the LLM?');"
>
<button type="submit"{% if judging_in_progress %} disabled{% endif %}>
{% if judging_in_progress %}Judging&hellip;{% else %}Judge phrases{% endif %}
</button>
</form>
<section>
<h2>Candidate n-grams</h2>
{% if candidates %}
<table>
<thead>
<tr>
<th>Phrase</th>
<th>Status</th>
<th>Score</th>
<th>Count</th>
<th>Chapters</th>
</tr>
</thead>
<tbody>
{% for candidate in candidates %}
<tr>
<td>{{ candidate.phrase_text }}</td>
<td>
{% if candidate.llm_judged %}
{% if candidate.llm_keep %}Kept{% else %}Rejected{% endif %}
{% else %}
Candidate
{% endif %}
</td>
<td>{{ "%.2f"|format(candidate.candidate_score) }}</td>
<td>{{ candidate.raw_count }}</td>
<td>{{ candidate.chapter_count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No candidate n-grams.</p>
{% endif %}
</section>
<section>
<h2>Protected phrases</h2>
{% if protected_phrases %}
<table>
<thead>
<tr>
<th>Phrase</th>
<th>Type</th>
<th>Confidence</th>
<th>Importance</th>
</tr>
</thead>
<tbody>
{% for phrase in protected_phrases %}
<tr>
<td>{{ phrase.phrase_text }}</td>
<td>{{ phrase.phrase_type or "phrase" }}</td>
<td>{{ "%.2f"|format(phrase.confidence) }}</td>
<td>{{ "%.2f"|format(phrase.importance) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No protected phrases.</p>
{% endif %}
</section>
{% else %}
<h1>Book not found</h1>
{% endif %}
{% endblock %}
@@ -0,0 +1,19 @@
{% extends "base.html" %}
{% block title %}EPUB Books{% endblock %}
{% block content %}
<h1>Books</h1>
{% if sources %}
<ol class="results">
{% for source in sources %}
<li>
<h2><a href="/books/{{ source.id }}">{{ source.title }}</a></h2>
<p class="meta">{{ source.author or "Unknown author" }}</p>
</li>
{% endfor %}
</ol>
{% else %}
<p>No EPUBs indexed.</p>
{% endif %}
{% endblock %}
@@ -0,0 +1 @@
<p class="status">{{ message }}</p>
@@ -0,0 +1 @@
<p class="error">{{ message }}</p>
@@ -0,0 +1,98 @@
<div class="rank-label">{{ response.rank_label }}</div>
{% if response.timings %}
<section class="runtime">
<h2>Runtime</h2>
<p class="meta">Total {{ "%.1f"|format(response.total_runtime_ms) }} ms</p>
<ol class="timing-chart">
{% set total = response.total_runtime_ms %}
{% set ns = namespace(remaining=total) %}
{% for step in response.timings %}
{% set width = (step.duration_ms / total * 100) if total else 0 %}
{% if step.counts_toward_total %}
{% set ns.remaining = ns.remaining - step.duration_ms %}
{% endif %}
<li>
<span class="timing-label">{{ step.name }}</span>
<span class="timing-bar"><span style="width: {{ "%.2f"|format(width) }}%"></span></span>
<span class="timing-value">{{ "%.1f"|format(step.duration_ms) }} ms</span>
<span class="timing-remaining">{{ "%.1f"|format([ns.remaining, 0]|max) }} ms left</span>
</li>
{% endfor %}
</ol>
</section>
{% endif %}
<section class="answer">
<h2>Answer</h2>
{% if low_confidence|default(false) %}
<p class="notice">Low retrieval confidence — answer generation was skipped.</p>
{% endif %}
{% set report = citation_report|default(none) %}
{% if report is not none and not report.grounded %}
<p class="notice">Unverified — no source citations were found in this answer.</p>
{% endif %}
{% if report is not none and report.invalid %}
<p class="notice">Invalid citations: {{ report.invalid|join(", ") }} (no matching source).</p>
{% endif %}
<p>{{ answer }}</p>
</section>
{% if response.results %}
<ol class="results">
{% for result in response.results %}
<li>
<h2>
{% if result.source_id %}
<a href="/books/{{ result.source_id }}">{{ result.source_title }}</a>
{% else %}
{{ result.source_title }}
{% endif %}
</h2>
<p class="meta">
{% if result.source_author %}{{ result.source_author }}{% endif %}
{% if result.chapter_title %} · {{ result.chapter_title }}{% endif %}
{% if result.page_label %} · page {{ result.page_label }}{% endif %}
</p>
<p>{{ result.text }}</p>
<dl class="scores">
<div>
<dt>final</dt>
<dd>{{ "%.3f"|format(result.score) }}</dd>
</div>
{% if result.rerank_score is not none %}
<div>
<dt>rerank</dt>
<dd>{{ "%.3f"|format(result.rerank_score) }}</dd>
</div>
{% endif %}
{% if result.vector_score is not none %}
<div>
<dt>vector cosine</dt>
<dd>{{ "%.3f"|format(result.vector_score) }}</dd>
</div>
{% endif %}
{% if result.bm25_score is not none %}
<div>
<dt>BM25</dt>
<dd>{{ "%.6f"|format(result.bm25_score) }}</dd>
</div>
{% endif %}
{% if result.fused_score is not none %}
<div>
<dt>RRF</dt>
<dd>{{ "%.3f"|format(result.fused_score) }}</dd>
</div>
{% endif %}
</dl>
{% if result.matched_phrases %}
<p class="phrase-matches">
<span class="phrase-matches-label">boosted by</span>
{% for phrase in result.matched_phrases %}
<span class="phrase-match">{{ phrase }}</span>
{% endfor %}
</p>
{% endif %}
</li>
{% endfor %}
</ol>
{% else %}
<p>No results.</p>
{% endif %}
@@ -0,0 +1,32 @@
{% extends "base.html" %}
{% block title %}EPUB Search{% endblock %}
{% block head %}<script src="https://unpkg.com/htmx.org@2.0.4"></script>{% endblock %}
{% block content %}
<h1>Search</h1>
<form class="card" hx-post="/search" hx-target="#results" hx-swap="innerHTML">
<label for="query">What are you looking for?</label>
<textarea id="query" name="query" rows="4" placeholder="Ask a question or paste a passage…" required
onkeydown="if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); this.form.requestSubmit(); }"></textarea>
<div class="form-row">
<div class="search-toggles">
<label class="check">
<input type="checkbox" name="rerank" value="true" {% if config.rerank.enabled %}checked{% endif %}>
Rerank
</label>
<label class="check">
<input
type="checkbox"
name="phrase_matching"
value="true"
{% if config.phrase_matching_enabled %}checked{% endif %}
>
Phrase matching
</label>
</div>
<button type="submit">Search</button>
</div>
</form>
<section id="results"></section>
{% endblock %}
+33
View File
@@ -0,0 +1,33 @@
"""Shared web UI resources for EPUB search."""
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING
from fastapi.templating import Jinja2Templates
if TYPE_CHECKING:
from fastapi import Request
from fastapi.responses import HTMLResponse
PACKAGE_DIR = Path(__file__).resolve().parent
TEMPLATE_DIR = PACKAGE_DIR / "templates"
STATIC_DIR = PACKAGE_DIR / "static"
def static_version(filename: str) -> int:
"""Return a cache-busting token for a static file based on its modification time."""
try:
return int((STATIC_DIR / filename).stat().st_mtime)
except OSError:
return 0
templates = Jinja2Templates(directory=TEMPLATE_DIR)
templates.env.globals["static_version"] = static_version
def error_response(request: Request, message: object) -> HTMLResponse:
"""Render the shared error partial for a failed UI request."""
return templates.TemplateResponse(request, "partials/error.html", {"message": str(message)}, status_code=500)
+274
View File
@@ -0,0 +1,274 @@
"""Persisted BM25 corpus management."""
from __future__ import annotations
import asyncio
import json
import logging
import shutil
from dataclasses import dataclass
from datetime import UTC, datetime
from functools import cache
from pathlib import Path
from typing import TYPE_CHECKING
import bm25s
from sqlalchemy import func, select, union_all
from python.ebook_search.chunk_records import CHUNK_RECORD_COLUMNS
from python.orm.richie import EbookChapter, EbookChunk, EbookSource
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession
from python.ebook_search.config import EbookSearchConfig
logger = logging.getLogger(__name__)
MANIFEST_NAME = "manifest.json"
REQUIRED_INDEX_FILES = frozenset(
{
"data.csc.index.npy",
"indices.csc.index.npy",
"indptr.csc.index.npy",
"params.index.json",
"vocab.index.json",
"corpus.jsonl",
}
)
@dataclass(frozen=True)
class BM25Manifest:
"""Metadata describing a persisted BM25 corpus."""
created_at: datetime
db_updated_at: datetime | None
chunk_count: int
@dataclass(frozen=True)
class BM25Corpus:
"""Loaded persisted BM25 corpus and retriever."""
retriever: object | None
records: tuple[dict[str, object], ...]
manifest: BM25Manifest
class BM25CorpusUnavailableError(RuntimeError):
"""Raised when the persisted BM25 corpus cannot be loaded."""
def bm25_index_path(config: EbookSearchConfig) -> Path:
"""Return the configured BM25 index root path relative to the current working directory."""
path = Path(config.bm25_index_dir).expanduser()
if path.is_absolute():
return path
return Path.cwd() / path
def get_current_bm25_index(index_path: Path) -> Path:
"""Return the live BM25 index directory."""
current_path = index_path / "current"
if current_path.exists() or current_path.is_symlink():
return current_path
return index_path
async def ensure_bm25_corpus(session: AsyncSession, config: EbookSearchConfig) -> None:
"""Create or refresh the persisted BM25 corpus when it is missing or stale."""
index_path = bm25_index_path(config)
manifest = read_bm25_manifest(index_path)
db_updated_at = await corpus_last_updated_at(session)
if not bm25_index_exists(index_path, manifest):
logger.info(f"ebook_bm25_index_missing {index_path=}")
await refresh_bm25_corpus(session, config, db_updated_at=db_updated_at)
return
if db_updated_at is not None and manifest is not None and manifest.created_at < db_updated_at:
logger.info(
f"ebook_bm25_index_stale {index_path=} created_at={manifest.created_at.isoformat()} "
f"db_updated_at={db_updated_at.isoformat()}"
)
await refresh_bm25_corpus(session, config, db_updated_at=db_updated_at)
return
logger.info(
f"ebook_bm25_index_current {index_path=} chunks={manifest.chunk_count if manifest else 0} "
f"created_at={manifest.created_at.isoformat() if manifest else None}"
)
async def refresh_bm25_corpus(
session: AsyncSession,
config: EbookSearchConfig,
*,
db_updated_at: datetime | None = None,
) -> BM25Manifest:
"""Rebuild and persist the BM25 corpus from the current database chunks.
The index build is CPU and disk work, so it runs in a worker thread.
"""
index_path = bm25_index_path(config)
records, texts = await fetch_bm25_corpus_records(session)
manifest = BM25Manifest(
created_at=datetime.now(tz=UTC),
db_updated_at=db_updated_at if db_updated_at is not None else await corpus_last_updated_at(session),
chunk_count=len(records),
)
await asyncio.to_thread(write_bm25_corpus, index_path, records, texts, manifest)
logger.info(
f"ebook_bm25_index_refreshed {index_path=} {manifest.chunk_count=} created_at={manifest.created_at.isoformat()}"
)
return manifest
@cache
def load_bm25_corpus(config: EbookSearchConfig) -> BM25Corpus:
"""Load the BM25 corpus into memory once per process.
Background refresh tasks clear this cache after rebuilding the on-disk corpus.
"""
index_path = bm25_index_path(config)
active_index_path = get_current_bm25_index(index_path)
logger.info(f"ebook_bm25_corpus_cache_load {index_path=} {active_index_path=}")
manifest = read_bm25_manifest(index_path)
if manifest is None or not bm25_index_exists(index_path, manifest):
msg = f"BM25 corpus is not available: {index_path}"
raise BM25CorpusUnavailableError(msg)
if manifest.chunk_count == 0:
return BM25Corpus(retriever=None, records=(), manifest=manifest)
retriever = bm25s.BM25.load(active_index_path, load_corpus=True, mmap=True)
records = tuple(dict(record) for record in retriever.corpus)
return BM25Corpus(retriever=retriever, records=records, manifest=manifest)
def score_bm25_corpus(query: str, corpus: BM25Corpus, *, limit: int) -> list[tuple[dict[str, object], float]]:
"""Score a query against a loaded BM25 corpus."""
if corpus.retriever is None or not corpus.records:
return []
k = min(limit, len(corpus.records))
documents, scores = corpus.retriever.retrieve(
bm25s.tokenize(query, show_progress=False),
corpus=list(corpus.records),
k=k,
show_progress=False,
)
results: list[tuple[dict[str, object], float]] = []
for document, score in zip(documents[0], scores[0], strict=True):
score_value = float(score)
if score_value <= 0:
continue
results.append((dict(document), score_value))
return results
async def fetch_bm25_corpus_records(session: AsyncSession) -> tuple[list[dict[str, object]], list[str]]:
"""Fetch persistable BM25 corpus records and their matching index texts from the database.
search_text is only needed to build the index, so it is returned separately instead of
being persisted into the corpus records, which would double the corpus size.
"""
statement = (
select(
*CHUNK_RECORD_COLUMNS,
EbookChunk.search_text.label("bm25_text"),
)
.select_from(EbookChunk)
.join(EbookSource, EbookSource.id == EbookChunk.source_id)
.outerjoin(EbookChapter, EbookChapter.id == EbookChunk.chapter_id)
.order_by(EbookChunk.id)
)
records: list[dict[str, object]] = []
texts: list[str] = []
for row in (await session.execute(statement)).mappings():
record = dict(row)
texts.append(str(record.pop("bm25_text")))
records.append(record)
return records, texts
async def corpus_last_updated_at(session: AsyncSession) -> datetime | None:
"""Return the latest source/chapter/chunk update timestamp relevant to BM25 text."""
update_times = union_all(
select(func.max(EbookSource.updated).label("updated")),
select(func.max(EbookChapter.updated).label("updated")),
select(func.max(EbookChunk.updated).label("updated")),
).subquery()
return await session.scalar(select(func.max(update_times.c.updated)))
def write_bm25_corpus(
index_path: Path,
records: list[dict[str, object]],
texts: list[str],
manifest: BM25Manifest,
) -> None:
"""Write a BM25 corpus generation and publish it through the current symlink."""
index_path.mkdir(parents=True, exist_ok=True)
generations_path = index_path / "generations"
generations_path.mkdir(exist_ok=True)
generation_path = next_bm25_generation_path(generations_path, manifest.created_at)
current_path = index_path / "current"
next_current_path = index_path / f".current.{generation_path.name}.tmp"
try:
generation_path.mkdir()
# Empty corpora publish a manifest-only generation so startup succeeds before any chunks exist.
if records:
retriever = bm25s.BM25()
retriever.index(bm25s.tokenize(texts, show_progress=False), show_progress=False)
retriever.save(generation_path, corpus=records, show_progress=False)
write_bm25_manifest(generation_path, manifest)
next_current_path.unlink(missing_ok=True)
next_current_path.symlink_to(generation_path, target_is_directory=True)
next_current_path.replace(current_path)
except Exception:
next_current_path.unlink(missing_ok=True)
shutil.rmtree(generation_path, ignore_errors=True)
raise
def read_bm25_manifest(index_path: Path) -> BM25Manifest | None:
"""Read the BM25 manifest if it exists and is valid."""
manifest_path = get_current_bm25_index(index_path) / MANIFEST_NAME
if not manifest_path.exists():
return None
body = json.loads(manifest_path.read_text(encoding="utf-8"))
return BM25Manifest(
created_at=datetime.fromisoformat(str(body["created_at"])),
db_updated_at=datetime.fromisoformat(str(body["db_updated_at"])) if body.get("db_updated_at") else None,
chunk_count=int(body["chunk_count"]),
)
def write_bm25_manifest(index_path: Path, manifest: BM25Manifest) -> None:
"""Write the BM25 manifest to an index directory."""
body = {
"created_at": manifest.created_at.isoformat(),
"db_updated_at": manifest.db_updated_at.isoformat() if manifest.db_updated_at else None,
"chunk_count": manifest.chunk_count,
}
(index_path / MANIFEST_NAME).write_text(json.dumps(body, indent=2, sort_keys=True), encoding="utf-8")
def bm25_index_exists(index_path: Path, manifest: BM25Manifest | None) -> bool:
"""Return whether a usable persisted BM25 index exists."""
active_index_path = get_current_bm25_index(index_path)
if manifest is None or not active_index_path.is_dir():
return False
if manifest.chunk_count == 0:
return True
return all((active_index_path / file_name).exists() for file_name in REQUIRED_INDEX_FILES)
def next_bm25_generation_path(generations_path: Path, created_at: datetime) -> Path:
"""Return an unused dated BM25 generation path."""
base_name = created_at.astimezone(UTC).strftime("%Y%m%dT%H%M%S.%fZ")
generation_path = generations_path / base_name
suffix = 1
while generation_path.exists():
generation_path = generations_path / f"{base_name}.{suffix}"
suffix += 1
return generation_path
+13
View File
@@ -0,0 +1,13 @@
"""Shared database columns used to build search-result records."""
from python.orm.richie import EbookChapter, EbookChunk, EbookSource
CHUNK_RECORD_COLUMNS = (
EbookChunk.id.label("chunk_id"),
EbookChunk.text.label("text"),
EbookSource.id.label("source_id"),
EbookSource.title.label("source_title"),
EbookSource.author.label("source_author"),
EbookChapter.title.label("chapter_title"),
EbookChunk.page_label.label("page_label"),
)
+145
View File
@@ -0,0 +1,145 @@
"""Configuration for the EPUB search app."""
from __future__ import annotations
from typing import Annotated, Self
from pydantic import AliasChoices, Field, field_validator, model_validator
from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict
def normalize_embedding_alias(model: str) -> str:
"""Normalize a supported embedding alias to its provider model name."""
aliases = {
"Qwen3-Embedding-0.6B": "qwen3-embedding-0.6b",
"Qwen3-Embedding-4B": "qwen3-embedding-4b",
"Qwen3-Embedding-8B": "qwen3-embedding-8b",
"Qwen/Qwen3-Embedding-0.6B": "qwen3-embedding-0.6b",
"Qwen/Qwen3-Embedding-4B": "qwen3-embedding-4b",
"Qwen/Qwen3-Embedding-8B": "qwen3-embedding-8b",
"qwen3-embedding:0.6b": "qwen3-embedding-0.6b",
"qwen3-embedding:4b": "qwen3-embedding-4b",
"qwen3-embedding:8b": "qwen3-embedding-8b",
"qwen3-embedding-0.6b": "qwen3-embedding-0.6b",
"qwen3-embedding-4b": "qwen3-embedding-4b",
"qwen3-embedding-8b": "qwen3-embedding-8b",
}
standard_model = aliases.get(model)
if standard_model is None:
error = f"Embedding model {model} is not supported. Supported models are {aliases.keys()}"
raise ValueError(error)
return standard_model
class RerankConfig(BaseSettings):
"""vLLM reranker settings."""
model_config = SettingsConfigDict(env_prefix="EBOOK_SEARCH_RERANK_", frozen=True, protected_namespaces=())
enabled: bool = True
base_url: str = "http://bob:8001"
model: str = "qwen3-reranker-06b"
candidates: int = 24
timeout_seconds: float = 30.0
score_weight: float = 0.7
hybrid_weight: float = 0.3
class EbookSearchConfig(BaseSettings):
"""Runtime settings for EPUB search."""
model_config = SettingsConfigDict(
env_prefix="EBOOK_SEARCH_",
frozen=True,
populate_by_name=True,
protected_namespaces=(),
)
rerank: RerankConfig = Field(default_factory=RerankConfig)
top_k: int = 12
library_paths: Annotated[tuple[str, ...], NoDecode] = ()
chunk_tokens: int = 700
chunk_overlap: int = 100
vllm_base_url: str = "https://ollama.com/v1"
vllm_api_key: str = Field(
default="not-needed",
validation_alias=AliasChoices("EBOOK_SEARCH_VLLM_API_KEY", "OLLAMA_API_KEY"),
)
chat_model: str = "deepseek-v4-flash"
answer_enabled: bool = True
embedding_base_url: str = "http://bob:8000/v1"
embedding_api_key: str = "not-needed"
embedding_model: str = "qwen3-embedding-0.6b"
embedding_batch_size: int = 32
embedding_timeout_seconds: float = 60.0
chat_timeout_seconds: float = 60.0
vector_candidate_multiplier: int = 4
bm25_candidate_limit: int = 120
rrf_rank_constant: int = 60
min_retrieval_confidence: float = 0.0
validate_citations_enabled: bool = True
bm25_index_dir: str = ".ebook_search_bm25"
bm25_refresh_delay_seconds: int = 60
protected_phrase_max_candidates_per_book: int = 5000
protected_phrase_llm_candidates_per_book: int = 500
protected_phrase_extraction_workers: int = 16
phrase_judge_book_workers: int = 20
phrase_judge_phrase_workers: int = 100
protected_phrase_confidence_threshold: float = 0.80
phrase_matching_enabled: bool = True
phrase_hit_boost: float = 0.25
phrase_min_tokens: int = 2
phrase_max_tokens: int = 5
phrase_max_entity_tokens: int = 8
phrase_yake_top_k: int = 1000
phrase_yake_dedup_limit: float = 0.85
phrase_raw_ngram_min_count: int = 2
phrase_raw_count_score_threshold: int = 3
phrase_raw_count_high_score_threshold: int = 10
phrase_chapter_count_score_threshold: int = 2
phrase_chapter_count_high_score_threshold: int = 5
phrase_target_protected_per_book: int = 100
phrase_default_allow_nested: bool = False
phrase_default_suppress_children: bool = True
phrase_bad_start_score_penalty: float = 10.0
phrase_bad_end_score_penalty: float = 10.0
phrase_multi_source_score_bonus: float = 2.0
phrase_multi_source_min_sources: int = 2
@field_validator("library_paths", mode="before")
@classmethod
def split_library_paths(cls, value: object) -> object:
"""Split a colon-separated library path string into a tuple of paths."""
if isinstance(value, str):
return tuple(path for path in value.split(":") if path)
return value
@field_validator("embedding_model")
@classmethod
def normalize_embedding(cls, value: str) -> str:
"""Normalize the configured embedding alias to its provider model name."""
return normalize_embedding_alias(value)
@model_validator(mode="after")
def validate_runtime_consistency(self) -> Self:
"""Reject configurations that cannot serve the features they enable."""
if not self.embedding_base_url.strip():
msg = "embedding_base_url must be set"
raise ValueError(msg)
if self.answer_enabled and (not self.vllm_base_url.strip() or not self.chat_model.strip()):
msg = "answer_enabled requires vllm_base_url and chat_model to be set"
raise ValueError(msg)
if self.rerank.enabled and not self.rerank.base_url.strip():
msg = "rerank.enabled requires rerank.base_url to be set"
raise ValueError(msg)
return self
def load_rerank_config() -> RerankConfig:
"""Load reranker config from environment variables."""
return RerankConfig()
def load_config() -> EbookSearchConfig:
"""Load EPUB search config from environment variables."""
return EbookSearchConfig()
+54
View File
@@ -0,0 +1,54 @@
FROM python:3.14-slim AS base
COPY --from=ghcr.io/astral-sh/uv:0.11.26 /uv /uvx /bin/
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
APP_DIR=/home/richie/dotfiles \
UV_PROJECT_ENVIRONMENT=/opt/venv \
UV_PYTHON_DOWNLOADS=never \
UV_NO_CACHE=1
# Separate ENV instruction so ${APP_DIR} and ${PATH} from above resolve.
ENV PYTHONPATH=${APP_DIR} \
PATH=/opt/venv/bin:${PATH}
WORKDIR ${APP_DIR}
RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential curl \
&& rm -rf /var/lib/apt/lists/*
COPY python/ebook_search/docker/pyproject.toml python/ebook_search/docker/uv.lock ./
RUN uv sync --locked --no-dev
FROM base AS test
RUN uv sync --locked
COPY python ./python
COPY tests/__init__.py ./tests/__init__.py
COPY tests/ebook_search ./tests/ebook_search
CMD ["pytest"]
FROM base AS runtime
ENV EBOOK_SEARCH_HOST=0.0.0.0 \
EBOOK_SEARCH_PORT=8070 \
EBOOK_SEARCH_BM25_INDEX_DIR=/data/bm25
COPY python ./python
RUN useradd --create-home --uid 10001 app \
&& mkdir -p /data \
&& chown -R app:app /home/richie /data
USER app
EXPOSE 8070
CMD ["sh", "-c", "exec python -m python.ebook_search.api.main --host \"${EBOOK_SEARCH_HOST}\" --port \"${EBOOK_SEARCH_PORT}\" --log-level \"${EBOOK_SEARCH_LOG_LEVEL:-INFO}\""]
+77
View File
@@ -0,0 +1,77 @@
# Ebook Search Docker
Run the EPUB search app against the existing Postgres database on `jeeves`:
```sh
python -m python.ebook_search.docker.containers start --library-path /path/to/epubs --build
```
All ebook-search Docker files live in this directory:
- `Dockerfile` — multi-stage: `test` (runs pytest) and `runtime` (default target, the app image)
- `docker-compose.yml`
- `containers.py` — Typer lifecycle CLI
- `pyproject.toml` / `uv.lock` — the container's uv-locked dependencies
The app listens on `http://localhost:8070`.
Useful lifecycle commands:
```sh
python -m python.ebook_search.docker.containers build
python -m python.ebook_search.docker.containers start --library-path /path/to/epubs
python -m python.ebook_search.docker.containers test
python -m python.ebook_search.docker.containers logs
python -m python.ebook_search.docker.containers ps
python -m python.ebook_search.docker.containers stop
```
Direct compose usage from the repo root:
```sh
docker compose -f python/ebook_search/docker/docker-compose.yml ps
```
## Dependencies
The image builds its environment with uv from `pyproject.toml` + `uv.lock` in this
directory — this is the source of truth for the container's dependencies. To add or
update a dependency, edit `pyproject.toml` here and regenerate the lock (uv is
available in the `ebook-search` dev shell):
```sh
nix develop .#ebook-search -c uv lock --project python/ebook_search/docker
```
## Tests
The main pytest suite excludes `tests/ebook_search` (its dependencies are no longer
in the nix dev shell). The `test ebook search` CI workflow runs them in a uv env
built from the lockfile in this directory — same commands work locally from the
repo root (the `--override-ini` drops the main suite's ignore):
```sh
uv sync --locked --project python/ebook_search/docker
uv run --project python/ebook_search/docker --no-sync pytest tests/ebook_search --override-ini addopts="-n auto -ra"
```
They can also run inside the Docker `test` image, which validates the image itself:
```sh
python -m python.ebook_search.docker.containers test
```
or the raw docker equivalent:
```sh
docker build --file python/ebook_search/docker/Dockerfile --target test --tag ebook-search:test .
docker run --rm ebook-search:test
```
## Configuration
The compose service loads the repo root `.env` into the container via `env_file`.
Mount your EPUB directory by setting `EBOOK_LIBRARY_HOST_PATH` in an env file or on the command line. The container sees it as `/library`, and `EBOOK_SEARCH_LIBRARY_PATHS` is set to `/library` inside the container.
Database connection settings are controlled by `RICHIE_DB`, `RICHIE_HOST`, `RICHIE_PORT`, `RICHIE_USER`, and `RICHIE_PASSWORD`. The default host is `jeeves`.
+1
View File
@@ -0,0 +1 @@
"""Docker packaging and lifecycle tooling for ebook search."""

Some files were not shown because too many files have changed in this diff Show More