8 Commits
Author SHA1 Message Date
Noah Masur 2ac07e2874 remove home-manager module from nixos 2026-09-12 22:29:53 +00:00
Noah Masur 985ab2b433 feat(services): integrate api service for flame 2026-09-12 21:56:19 +00:00
github-actions[bot] d6209c039b flake.lock: Update
Flake lock file updates:

• Updated input 'home-manager':
    'github:nix-community/home-manager/693e8ce0fb240a73c116a03cfd7b19269c87af88' (2026-09-04)
  → 'github:nix-community/home-manager/cd1c9e552f41894aeb5cc5cb353d5a1d61550357' (2026-09-12)
• Updated input 'nix-index-database':
    'github:nix-community/nix-index-database/dbb978fa87faf13398e90ccbc52c343d21c7dd6d' (2026-08-30)
  → 'github:nix-community/nix-index-database/116ad1c2adb642405ef8916f6a94c8626f971344' (2026-09-06)
• Updated input 'nixpkgs':
    'github:nixos/nixpkgs/801bef6abd86b91e51083066b83fb354a11fc640' (2026-09-04)
  → 'github:nixos/nixpkgs/8ce4ef6cb6f871616146b9fe26d2a5ae594e94fe' (2026-09-10)
• Updated input 'nixpkgs-stable':
    'github:nixos/nixpkgs/a5cc6f2c37bf518436dc8d1c288ccd0c43c2f4c4' (2026-09-03)
  → 'github:nixos/nixpkgs/21a67dc470149f337cecafbe965d8d252a390518' (2026-09-11)
• Updated input 'nur':
    'github:nix-community/nur/82ca52a282cd5f90afabcf7c9acd27571b253523' (2026-09-05)
  → 'github:nix-community/nur/2c789298fc525713063e3b88359d02101651144f' (2026-09-12)
• Updated input 'rust-overlay':
    'github:oxalica/rust-overlay/c361047d3a538f547f1617bb6b410411929ac9cc' (2026-09-05)
  → 'github:oxalica/rust-overlay/228ecefb6329d5a531b77b46b581a2f0c26ee056' (2026-09-12)
• Updated input 'wsl':
    'github:nix-community/NixOS-WSL/eaeb18da90024448a60eb1ec7132eafa4003404e' (2026-07-21)
  → 'github:nix-community/NixOS-WSL/72c92b11bb8289e6651c7fef29cc0a885fd6a255' (2026-09-11)
2026-09-12 08:10:45 +00:00
Noah Masur 3e246645a2 paperless: configure OIDC authentication 2026-09-07 18:16:45 +00:00
Noah Masur ed8be9dee6 jellyfin: forward client IP headers from caddy and cloudflare 2026-09-07 17:00:31 +00:00
Noah Masur ab98a202da nextcloud: implement OIDC authentication 2026-09-07 16:34:45 +00:00
Noah Masur e00560c42c post-TUI lag: two-phase autosuggestion toggle; Ctrl-b was never activated
Root cause of 'Ctrl-b does nothing': home-manager was not switched, so the
binding built into the flake was never deployed (~/.config/fish lacked
heal-autosuggest and \cb; the built config had both). Activation needs the
home rebuild, not a system rebuild.

heal-autosuggest is now two-phase (set 0; repaint; set 1; repaint) to give
the reader a disabled-state repaint, which is what clears the wedged
in_flight_autosuggest_request per reader.rs update_autosuggestion. Matches
the manual cure (which had prompt cycles between off and on); the old
back-to-back toggle and the postexec hook did not. User chose the
session-preserving toggle over exec fish.
2026-09-07 11:17:44 -04:00
Noah Masur d4e56dd190 post-TUI lag: autosuggestion culprit confirmed; add self-heal hook
A/B in a live lagging shell: disabling fish_autosuggestion_enabled cures
the lag instantly, and re-enabling does NOT bring it back — the toggle
resets the wedged reader state. __autosuggestion_unwedge (fish_postexec)
now applies that reset after every command, at the moment TUIs exit.
Builtins only, invisible, respects a deliberate manual disable. Flight
recorder stays armed until the hook is proven in real use.

Also from this investigation: wedged-thread evidence (sampler attach
cures), lag-sample tool, flight recorder in the zellij fish wrapper.
2026-09-07 11:17:44 -04:00
20 changed files with 665 additions and 74 deletions
+89 -1
View File
@@ -1,5 +1,46 @@
# Changelog # Changelog
## 2026-09-12
- **Integrated `api` service for `flame`**:
- Added `github:nmasur/api` as a flake input with `inputs.nixpkgs.follows = "nixpkgs"`.
- Added hostname `api = "api.masu.rs"`.
- Added `inputs.api.overlays.default` to system overlays and `inputs.api.nixosModules.default` to `buildNixos` and `generateImage` modules in `lib/default.nix`.
- Created preset `platforms/nixos/modules/nmasur/presets/services/api/api.nix` with options for `nmasur.presets.services.api.enable`:
- Configures `services.api.enable = true` and `services.api.hostname = hostnames.api`.
- Configures `services.api.backends.actual` with `actualServerUrl`, `apiKeysFile`, `serverPasswordFile`, and budget sync ID files for `budget1` and `budget2`.
- Added secret definitions for `api-actual-keys`, `api-actual-budget1-sync-id`, and `api-actual-budget2-sync-id` owned by `api_actual` with prefixes `API_KEYS=`, `ACTUAL_SYNC_ID_BUDGET1=`, and `ACTUAL_SYNC_ID_BUDGET2=`.
- Reused `config.secrets.actualbudget-password.dest` for upstream Actual server password.
- Ordered `systemd.services.api-actual` after and required by `postgresql-setup.service`, secret services, and `actual.service`.
- Added PostgreSQL peer authentication for `api_actual` (`local api_actual api_actual peer`) and database backup via `services.postgresqlBackup.databases = [ "api_actual" ]`.
- Added `systemd.services.postgresql-setup.preStart` hook in `postgresql.nix` to refresh collation version on `template1` and `postgres` (`ALTER DATABASE ... REFRESH COLLATION VERSION`), preventing database creation failure after glibc upgrades.
- Mounted generated `caddyRoutes` onto `nmasur.presets.services.caddy.routes`.
- Added `hostnames.api` to `services.cloudflare-dyndns.domains`.
- Added `/var/lib/api-actual` to restic backups.
- Enabled `nmasur.presets.services.api` and disabled `actualtap` in `platforms/nixos/modules/nmasur/profiles/communications.nix`.
## 2026-09-07
- **Configured OpenID Connect (OIDC) authentication for Paperless-ngx**:
- Added secret management for `paperless-oidc-secret.age` via `secrets.paperless-oidc-secret` owned by `paperless:paperless` (0440).
- Configured `systemd.services.paperless-oidc-secret-secret` to run before and be required by all Paperless units, dynamically generating `/var/private/paperless-env` with `PAPERLESS_SOCIALACCOUNT_PROVIDERS` configured for Pocket ID (`auth.masu.rs`) using `client_id = "e6ff7fce-8e67-4c66-8f32-5ec3db4740a9"`, `oauth_pkce_enabled = true`, and email authentication linking (`email_authentication = true`, `verified_email = true`) in the native `APPS` schema.
- Added `services.paperless.environmentFile = "${config.secretsDirectory}/paperless-env"` to securely pass the OIDC configuration to Paperless services without exposing secrets in the Nix store.
- Configured Paperless settings to enable `allauth.socialaccount.providers.openid_connect` in `PAPERLESS_APPS` and enabled `PAPERLESS_REDIRECT_LOGIN_TO_SSO`.
- Configured reverse proxy trust settings (`PAPERLESS_USE_X_FORWARD_HOST`, `PAPERLESS_PROXY_SSL_HEADER`, and `PAPERLESS_TRUSTED_PROXIES`) to ensure callback URIs preserve the `https://` scheme behind Caddy.
- Fixed `systemd.services.paperless-secret` to order before and be required by `paperless-scheduler.service` instead of non-existent `paperless.service`.
- **Configured real client IP forwarding for Jellyfin reverse proxy**:
- Added a `map` handler to Jellyfin's Caddy route in `platforms/nixos/modules/nmasur/presets/services/jellyfin.nix` to resolve `{client_ip}` using Cloudflare's `CF-Connecting-IP` header when available, falling back to `{http.request.remote.host}` for direct local LAN connections.
- Configured `reverse_proxy.headers.request.set` to forward `X-Real-IP`, `X-Forwarded-For`, and `X-Forwarded-Proto` with the resolved client IP and request scheme to Jellyfin.
- **Configured OpenID Connect (OIDC) authentication for Nextcloud**:
- Added `user_oidc` to `services.nextcloud.extraApps`.
- Added secret management for `nextcloud-oidc-secret.age` via `secrets.nextcloud-oidc-secret` with owner `nextcloud` and group `nextcloud` (0440).
- Configured `systemd.services.nextcloud-oidc-secret-secret` to be required by and order before `nextcloud-setup.service`.
- Set `services.nextcloud.settings.overwriteprotocol = "https"` to ensure correct scheme handling for redirect URIs behind reverse proxy.
- Configured automated idempotent upsert of the Pocket ID provider via `systemd.services.nextcloud-setup.postStart` running `nextcloud-occ user_oidc:provider pocket-id` with client ID `c8a32c58-a781-4f14-9070-f498fdfda438`, `--clientsecret-file`, discovery URI `https://${hostnames.auth}/.well-known/openid-configuration`, `--mapping-uid="preferred_username"`, and `--unique-uid=0` to connect OIDC logins directly to existing local Nextcloud accounts.
- Updated `docs/oidc-services.md` with the verified configuration and callback URI (`https://cloud.masu.rs/apps/user_oidc/code`).
## 2026-09-06 ## 2026-09-06
- **Configured OpenID Connect (OIDC) authentication for Immich**: - **Configured OpenID Connect (OIDC) authentication for Immich**:
@@ -28,7 +69,54 @@
- Configured `systemd.services.actual` to order after the decrypted secret service and granted the dynamic unit access via `SupplementaryGroups = [ "shared" ]` and `PrivateUsers = false`. - Configured `systemd.services.actual` to order after the decrypted secret service and granted the dynamic unit access via `SupplementaryGroups = [ "shared" ]` and `PrivateUsers = false`.
- Updated `docs/oidc-services.md` with the verified callback URI (`https://money.masu.rs/openid/callback`) and NixOS configuration snippet. - Updated `docs/oidc-services.md` with the verified callback URI (`https://money.masu.rs/openid/callback`) and NixOS configuration snippet.
## 2026-08-29 (root cause found and fixed) ## 2026-09-03: Ctrl-b did nothing because home-manager was never switched; two-phase toggle
- "Ctrl-b does nothing" root cause: the binding was built into the flake but never activated. The deployed `~/.config/fish/functions/` had no `heal-autosuggest.fish` and no `\cb` binding (0 matches), while the freshly-built config had both. On this setup a system rebuild does not switch home-manager — activation needs the home rebuild (`rebuild-home` / the Alt-Shift-H binding, i.e. `home-manager switch --flake`) followed by a fresh pane so `config.fish` re-runs `fish_user_key_bindings`. No fix works until it is actually activated; this should be the FIRST check next time a "did nothing" is reported.
- Made `heal-autosuggest` a two-phase toggle (chosen by the user over `exec fish`, to preserve the session): disable autosuggestions, `commandline -f repaint`, re-enable, `commandline -f repaint`. Grounded in `reader.rs` `update_autosuggestion`, which clears the wedged `in_flight_autosuggest_request` only on a repaint taken while autosuggestions are disabled. The old one-liner and the `fish_postexec` hook did a single back-to-back `set 0; set 1` with no disabled-state repaint, which likely never flushed the stuck request — matching the manual cure, which had prompt cycles between the off and the on. Still unverified against the real bug (not reproducible in a harness); confirm by activating, then pressing Ctrl-b in a live lagging shell.
- If the two-phase toggle still does not cure once activated: fall back to `exec fish` on the key (guaranteed per the user's day-one report that a fresh shell always fixes it), and capture a flight-recorder log for the upstream fish report.
## 2026-09-01: postexec hook fires but does not cure — moved heal to a keybinding
- Honest status: the `fish_postexec` self-heal hook IS registered and DOES fire (verified in the real config), yet the lag persists. So toggling `fish_autosuggestion_enabled` off/on from `fish_postexec` does not cure it, even though the user typing the same `set … 0; set … 1` at the prompt does.
- Why (code-level): variable dispatch is synchronous (`env_dispatch.rs``reader_set_autosuggestion_enabled` on every `set`), so the two sets net to no change and schedule a repaint. That repaint only has effect from inside the reader's active input loop. `fish_postexec` runs BETWEEN commands, outside that loop, so its effect is superseded before the next prompt. A key binding runs inside the loop; `fish_postexec` cannot.
- Change: bound **Ctrl-b** to a new `heal-autosuggest` function (toggle + `commandline -f repaint`) via `fish_user_key_bindings` (Ctrl-g was already taken). Verified that when `fish_user_key_bindings` runs to completion — as it does in the real config, since the existing `\cn` etc. work (`__fish_config_interactive.fish:105`) — `\cb` binds to heal-autosuggest and the toggle resets the variable. NOT verified to cure the real lag: the bug still cannot be reproduced in a harness, so only a test in a live lagging shell can confirm. The `fish_postexec`/`fish_cancel` hook is kept (harmless) but is no longer considered the fix.
- Guaranteed fallback if Ctrl-b does not cure: a fresh shell (`exec fish`), which the user has confirmed from the very start always fixes it — Ctrl-b can be rebound to that. The only path to a truly automatic fix is a flight-recorder capture (`~/.local/state/lag-triage/RECORD`) of the reader during an actual episode, then an upstream fish report.
## 2026-08-31 (evening): self-heal hook was never firing — fish cannot autoload event handlers
- The `__autosuggestion_unwedge` hook did not work because it was installed via `programs.fish.functions`, which writes to fish's **autoload** directory — and fish only registers `--on-event` handlers when a function is actually loaded, which never happens for a hook nothing calls by name. Verified in a PTY test: the autoloaded handler never fires; the identical definition `source`d eagerly fires immediately. (The zellij module's `__fish_update_cwd_osc` works as an autoloaded event function only because it overrides a function fish itself loads.)
- Meanwhile the user confirmed the instant back-to-back toggle (`set -g fish_autosuggestion_enabled 0 && set -g fish_autosuggestion_enabled 1`) cures a lagging shell — so the handler body is right; only its registration was broken.
- Fix: the handler is now defined eagerly in `config.fish` via `programs.fish.interactiveShellInit` (lag-triage module), registered on **fish_postexec** (fires after every command — the moment TUIs exit) and **fish_cancel** (fires on Ctrl-C at the prompt), so a bare Ctrl-C is an instant no-command cure. Verified in an interactive PTY against the actual nix-generated snippet: registers at startup, fires on both events, still respects a deliberate manual disable.
- Coverage note: if a wedge forms with no command running (and no Ctrl-C), it heals at the next command; worst-case lag window is "until you run anything or press Ctrl-C".
## 2026-08-31 (later): automatic self-heal hook
- Confirmed by A/B in the live shell: after curing the lag with `set -g fish_autosuggestion_enabled 0`, re-enabling with `1` does **not** bring the lag back — the toggle resets the wedged autosuggestion state rather than merely masking it.
- Added `__autosuggestion_unwedge` (lag-triage module): a `fish_postexec` event handler that toggles `fish_autosuggestion_enabled` off/on after every command — i.e. at the exact moment a TUI has just exited, when the wedge forms. Builtins only, no visible output (verified in an interactive PTY test), and it skips the reset when the user has deliberately disabled autosuggestions.
- Honest caveat: the manual cure had keystrokes between the off and the on; whether the instant off/on inside an event handler resets the same reader-internal state is unproven. The flight recorder therefore STAYS ARMED (`~/.local/state/lag-triage/RECORD`) until the hook has survived normal use for a while. If lag recurs despite the hook: cure manually (`set … 0`, type a few chars, `set … 1`), and keep the flight log for that pid — then the hook needs the stronger form (disable at postexec, re-enable one prompt-cycle later, scoped to TUI commands).
- Limitations by design: the hook fires only in shells that run commands, so a wedge formed without any command executing in that shell (if that is possible — e.g. floating-pane TUIs never touch the pane shell) would not be healed until the next command runs there.
## 2026-08-31: culprit confirmed — fish's autosuggestion pipeline
- A/B test in a live lagging shell (pid 56089): `set -g fish_autosuggestion_enabled 0` (builtin only, nothing else) **instantly cured the lag**. The post-TUI typing lag is in fish 4.8.1's autosuggestion pipeline.
- Sampling that shell afterwards showed it had **only one thread** (the main thread): the poisoned state is main-thread-side bookkeeping, not a hung worker still sitting in the process. Source review (`src/threads/threads.rs`, `src/threads/debounce.rs`): `ThreadPool::perform` silently queues work with no spawn and no wake when it believes `total_threads == max_threads` — a leaked `total_threads` count (workers that died without decrementing, e.g. across a TUI's lifetime) would strand all future autosuggestion work forever; the Debounce then abandons its token every 500ms and re-enqueues per keystroke. The exact step that delays keystroke *echo* is still unproven — the flight recorder (armed via `~/.local/state/lag-triage/RECORD`) logs the reader's per-keystroke behavior and will capture it on the next occurrence in a recorded shell.
- Precedent: fish had a closely-related bug class before (#11841 — unread terminal query responses "causing noticeable lags"). No fish release newer than 4.8.1 exists, so no upstream fix to adopt; an upstream report with the flight-recorder capture is the path to a real fix.
- Practical interim cure (harmless, instant, in the lagging shell): `set -g fish_autosuggestion_enabled 0`, and re-enable with `1` — whether lag returns on re-enable is the next discriminating datum.
## 2026-08-30 (later): sampler attach CURES the lag — wedged-thread evidence + flight recorder
- Major new datum: in a lagging shell, running `mkdir` + `/usr/bin/sample $fish_pid … &` + `disown` **cured the lag instantly**, before any planned reset/toggle test could run. Plain external commands do NOT cure it (the 2026-08-29 triage ran many and the lag survived), so the distinguishing action is the sampler **attaching and suspending/resuming fish's threads**. Conclusion: a fish-internal thread/wait is wedged (missed wakeup or stuck blocking wait), and per-keystroke work at the main commandline stalls against it; suspension/resume kicks it loose. Consistent with: `read` prompts unaffected (no autosuggestion/highlight pipeline), subshells immune (fresh threads), raw input clean. The captured sample (`~/.local/state/lag-triage/fish-sample.txt`) shows only the post-cure state — sampling is a cure, not a capture.
- Therefore the observer must be running BEFORE the lag starts: the `fish-no-query-term` wrapper is now a **flight recorder**`touch ~/.local/state/lag-triage/RECORD`, then every newly spawned pane shell logs `FISH_DEBUG=reader,term-support,proc-termowner,iothread,fd-monitor,topic-monitor` to `~/.local/state/lag-triage/flight/fish-<ts>-<pid>.log` (3-day auto-cleanup; remove RECORD to disable, zero overhead when off). When lag next occurs, the log already contains what each keystroke did during the lag.
- `lag-sample` now takes a PID and should be run from a DIFFERENT pane (`echo $fish_pid` — a builtin — in the lagging shell to get it), since attaching from inside cures the lag.
- **Next-occurrence checklist (in order, least perturbing first):** (1) in the lagging shell, builtins only: `set -g fish_autosuggestion_enabled 0` → type at the real commandline; if cured, the autosuggestion/debounce path is implicated (a worker thread was seen in `HistorySearch::go_to_next_match`); (2) still laggy: `fish_default_key_bindings` → test (vi-mode path); (3) from another pane: `kill -WINCH <pid>` → test, then `kill -CONT <pid>` → test (discriminates reader-wakeup vs generic unwedge; if WINCH cures, a window resize would too); (4) from another pane: `lag-sample <pid>` while typing in the lagging pane; (5) immediately save the flight log for that pid.
## 2026-08-30
- **The post-TUI typing lag is NOT resolved** by the `fish-no-query-term` wrapper: lag recurred in a fresh zellij session after exiting Claude Code, in a shell verified (via `ps eww`) to have `fish_features=no-query-term` in its environment. The query-term reader-degradation bug proven on 2026-08-29 is real (and the wrapper stays as hardening against it), but it is not the mechanism behind this lag. Downgraded the entry below from "root cause" to "a root cause".
- Known constraints on the real mechanism: per-keystroke lag at the main fish commandline; fish `read` prompts unaffected; raw input reaches the pane practical as plain bytes; a subshell/`exec fish` cures it (process-local state). Note the 2026-08-29 triage's reset ladder short-circuited on a false "y" at stage A, so stages BG (mouse/keypad/altscreen/stty/DECSTR resets) were never actually tested against real lag.
- Added `lag-sample` (fish function): stack-samples the lagging fish process plus the zellij server/client via `/usr/bin/sample` for 8s while the user types at the commandline. This directly names where the time goes (fish reader? highlighting/autosuggestion threads? zellij render loop?) instead of inferring it. Next occurrence: run `lag-sample` in the lagging shell, type junk at the prompt until done, then inspect `~/.local/state/lag-triage/sample-*.txt`. Follow with `unlag` (full reset ladder, never yet truly tested), then A/B toggles: `set -g fish_autosuggestion_enabled 0`, `fish_default_key_bindings`.
## 2026-08-29 (a root cause found and fixed — but not THE lag)
- **Root-caused and fixed the recurring post-TUI typing lag** (fish + Zellij + Ghostty) using a `lag-triage` capture from a live lagging shell plus a deterministic PTY reproduction (`presets/programs/lag-triage/upstream_repro.py`): - **Root-caused and fixed the recurring post-TUI typing lag** (fish + Zellij + Ghostty) using a `lag-triage` capture from a live lagging shell plus a deterministic PTY reproduction (`presets/programs/lag-triage/upstream_repro.py`):
- **Root cause chain**: (1) fish latches feature flags from its **startup environment**, before `config.fish` runs — so the existing `set -gx fish_features no-query-term` in `shellInit` never applied to the shell that set it, only to its children. (2) Zellij spawns pane shells via `default_shell` with no `fish_features` in the environment, so every pane's fish latched `query-term` **on** (the fish 4.8.1 default; the triage log from the lagging shell confirmed `query-term on` while `$fish_features` was correctly set to `no-query-term`). (3) With query-term on, fish sends OSC 11 + CPR (`\e[6n`) + DA1 (`\e[0c`) after **every** command and waits for replies relayed by Zellij. (4) Reproduced on fish 4.8.1: if the terminal fails to reply during just **one** such cycle — answering everything before and after — that fish process's interactive reader is **permanently degraded** (keystroke echo >3s, never recovers; ~35ms before). In production Zellij drops/mangles a relay during TUI teardown or heavy output (cf. zellij-org/zellij#5158), e.g. after `nh home switch`, nvim, jjui, yazi. - **Root cause chain**: (1) fish latches feature flags from its **startup environment**, before `config.fish` runs — so the existing `set -gx fish_features no-query-term` in `shellInit` never applied to the shell that set it, only to its children. (2) Zellij spawns pane shells via `default_shell` with no `fish_features` in the environment, so every pane's fish latched `query-term` **on** (the fish 4.8.1 default; the triage log from the lagging shell confirmed `query-term on` while `$fish_features` was correctly set to `no-query-term`). (3) With query-term on, fish sends OSC 11 + CPR (`\e[6n`) + DA1 (`\e[0c`) after **every** command and waits for replies relayed by Zellij. (4) Reproduced on fish 4.8.1: if the terminal fails to reply during just **one** such cycle — answering everything before and after — that fish process's interactive reader is **permanently degraded** (keystroke echo >3s, never recovers; ~35ms before). In production Zellij drops/mangles a relay during TUI teardown or heavy output (cf. zellij-org/zellij#5158), e.g. after `nh home switch`, nvim, jjui, yazi.
+53 -25
View File
@@ -121,14 +121,38 @@ All client secrets should be encrypted with `agenix` under the respective servic
### 3. Nextcloud (`cloud.masu.rs`) ### 3. Nextcloud (`cloud.masu.rs`)
- **Pocket ID Redirect URI:** `https://cloud.masu.rs/apps/user_oidc/code` - **Pocket ID Redirect URI:** `https://cloud.masu.rs/apps/user_oidc/code`
- **Setup in `nextcloud/nextcloud.nix`:** - **Setup in `nextcloud/nextcloud.nix`:**
1. Add `user_oidc` to `services.nextcloud.extraApps`. ```nix
2. Configure the provider via `nextcloud-occ`: secrets.nextcloud-oidc-secret = {
```bash source = ./nextcloud-oidc-secret.age;
nextcloud-occ user_oidc:provider pocket-id \ dest = "${config.secretsDirectory}/nextcloud-oidc-secret";
--clientid="<client_id>" \ owner = "nextcloud";
--clientsecret="<client_secret>" \ group = "nextcloud";
--discoveryuri="https://auth.masu.rs/.well-known/openid-configuration" \ permissions = "0440";
--scope="openid profile email" };
systemd.services.nextcloud-oidc-secret-secret = {
requiredBy = [ "nextcloud-setup.service" ];
before = [ "nextcloud-setup.service" ];
};
services.nextcloud = {
settings.overwriteprotocol = "https";
extraApps = {
user_oidc = config.services.nextcloud.package.packages.apps.user_oidc;
};
};
systemd.services.nextcloud-setup = {
after = [ "nextcloud-oidc-secret-secret.service" ];
postStart = ''
${config.services.nextcloud.occ}/bin/nextcloud-occ user_oidc:provider pocket-id \
--clientid="c8a32c58-a781-4f14-9070-f498fdfda438" \
--clientsecret-file="${config.secrets.nextcloud-oidc-secret.dest}" \
--discoveryuri="https://${hostnames.auth}/.well-known/openid-configuration" \
--scope="openid profile email" \
--mapping-uid="preferred_username" \
--unique-uid=0
'';
};
``` ```
### 4. Grafana (`metrics.masu.rs`) ### 4. Grafana (`metrics.masu.rs`)
@@ -159,24 +183,28 @@ All client secrets should be encrypted with `agenix` under the respective servic
```nix ```nix
services.paperless.settings = { services.paperless.settings = {
PAPERLESS_APPS = "allauth.socialaccount.providers.openid_connect"; PAPERLESS_APPS = "allauth.socialaccount.providers.openid_connect";
PAPERLESS_SOCIALACCOUNT_PROVIDERS = builtins.toJSON { PAPERLESS_REDIRECT_LOGIN_TO_SSO = true;
openid_connect = {
SERVERS = [
{
id = "pocket-id";
name = "Pocket ID";
server_url = "https://auth.masu.rs";
token_auth_method = "client_secret_basic";
APP = {
client_id = "paperless";
secret = "..."; # or via environmentFile
};
}
];
};
};
PAPERLESS_REDIRECT_LOGIN_TO_SSO = "true"; # Optional: bypass local login
}; };
# Configured via environmentFile to pass the client secret, enable PKCE, and link by email:
# PAPERLESS_SOCIALACCOUNT_PROVIDERS = builtins.toJSON {
# openid_connect = {
# APPS = [
# {
# provider_id = "pocket-id";
# name = "Pocket ID";
# client_id = "e6ff7fce-8e67-4c66-8f32-5ec3db4740a9";
# secret = "...";
# settings = {
# server_url = "https://auth.masu.rs";
# token_auth_method = "client_secret_basic";
# oauth_pkce_enabled = true;
# email_authentication = true;
# verified_email = true;
# };
# }
# ];
# };
# };
``` ```
### 6. Mealie (`cooking.masu.rs`) ### 6. Mealie (`cooking.masu.rs`)
Generated
+85 -30
View File
@@ -1,10 +1,31 @@
{ {
"nodes": { "nodes": {
"api": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1789229011,
"narHash": "sha256-sbC3E9CAQwWto+ynyA0zXvEMMK5jboVnYPYoqTcxm0Y=",
"owner": "nmasur",
"repo": "api",
"rev": "28a522a274fe86da6d3390adcc026a6618911cb0",
"type": "github"
},
"original": {
"owner": "nmasur",
"repo": "api",
"type": "github"
}
},
"cl-nix-lite": { "cl-nix-lite": {
"inputs": { "inputs": {
"flake-parts": "flake-parts", "flake-parts": "flake-parts",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"systems": "systems", "systems": "systems_2",
"treefmt-nix": "treefmt-nix" "treefmt-nix": "treefmt-nix"
}, },
"locked": { "locked": {
@@ -151,6 +172,24 @@
} }
}, },
"flake-utils": { "flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": { "inputs": {
"systems": [ "systems": [
"mac-app-util", "mac-app-util",
@@ -170,9 +209,9 @@
"type": "indirect" "type": "indirect"
} }
}, },
"flake-utils_2": { "flake-utils_3": {
"inputs": { "inputs": {
"systems": "systems_3" "systems": "systems_4"
}, },
"locked": { "locked": {
"lastModified": 1731533236, "lastModified": 1731533236,
@@ -195,11 +234,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1788487777, "lastModified": 1789183968,
"narHash": "sha256-Ro/e1N4ZR8/XaFF+sF9SgfPK79HM5YNEppzFj8p+0Ak=", "narHash": "sha256-pEWnYdIF1pBMZwarp/rEPzl+Lknhm5hhjSfeLxH8nAA=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "693e8ce0fb240a73c116a03cfd7b19269c87af88", "rev": "cd1c9e552f41894aeb5cc5cb353d5a1d61550357",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -213,9 +252,9 @@
"inputs": { "inputs": {
"cl-nix-lite": "cl-nix-lite", "cl-nix-lite": "cl-nix-lite",
"flake-compat": "flake-compat", "flake-compat": "flake-compat",
"flake-utils": "flake-utils", "flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_3", "nixpkgs": "nixpkgs_3",
"systems": "systems_2", "systems": "systems_3",
"treefmt-nix": "treefmt-nix_2" "treefmt-nix": "treefmt-nix_2"
}, },
"locked": { "locked": {
@@ -259,11 +298,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1788080100, "lastModified": 1788680125,
"narHash": "sha256-n14luQo3F/ZLfCEHk1QieiI1nGnW92ZQZeZ0UIb0UMQ=", "narHash": "sha256-amGSoDobwmp4CFvCn841ws2iuitus+HUJdD/gKgsrJA=",
"owner": "nix-community", "owner": "nix-community",
"repo": "nix-index-database", "repo": "nix-index-database",
"rev": "dbb978fa87faf13398e90ccbc52c343d21c7dd6d", "rev": "116ad1c2adb642405ef8916f6a94c8626f971344",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -274,7 +313,7 @@
}, },
"nix2vim": { "nix2vim": {
"inputs": { "inputs": {
"flake-utils": "flake-utils_2", "flake-utils": "flake-utils_3",
"nixpkgs": [ "nixpkgs": [
"nixpkgs" "nixpkgs"
] ]
@@ -362,11 +401,11 @@
}, },
"nixpkgs-stable": { "nixpkgs-stable": {
"locked": { "locked": {
"lastModified": 1788405554, "lastModified": 1789114715,
"narHash": "sha256-r2f1oUwixlgq9zOdYLqJLfS/lWBT60/IITjhTKI59JU=", "narHash": "sha256-ugpsyk3NM2s87vXfUiIIiibbJ4Pp0JPS5p/3mfs+q+c=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "a5cc6f2c37bf518436dc8d1c288ccd0c43c2f4c4", "rev": "21a67dc470149f337cecafbe965d8d252a390518",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -442,11 +481,11 @@
}, },
"nixpkgs_6": { "nixpkgs_6": {
"locked": { "locked": {
"lastModified": 1788531059, "lastModified": 1789006805,
"narHash": "sha256-hLD4l3QOGBQhkVp3mQ2lJ/YbEi99qUgKapb40KovZ88=", "narHash": "sha256-xB8mKMOx1IA9vTDNLmJZ6n4wCMq/cuWBBOzGCRnqxrU=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "801bef6abd86b91e51083066b83fb354a11fc640", "rev": "8ce4ef6cb6f871616146b9fe26d2a5ae594e94fe",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -480,11 +519,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1788594396, "lastModified": 1789200175,
"narHash": "sha256-HLvxvUIMxBHl+vv/1OVTLPKrSLTinId97WBIxWWByJ0=", "narHash": "sha256-JriLm/izGMXB4uAHWXKj13kXa3nKZj2r/sR5xHdisgs=",
"owner": "nix-community", "owner": "nix-community",
"repo": "nur", "repo": "nur",
"rev": "82ca52a282cd5f90afabcf7c9acd27571b253523", "rev": "2c789298fc525713063e3b88359d02101651144f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -495,6 +534,7 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"api": "api",
"darwin": "darwin", "darwin": "darwin",
"disko": "disko", "disko": "disko",
"home-manager": "home-manager", "home-manager": "home-manager",
@@ -517,11 +557,11 @@
"nixpkgs": "nixpkgs_7" "nixpkgs": "nixpkgs_7"
}, },
"locked": { "locked": {
"lastModified": 1788591095, "lastModified": 1789196581,
"narHash": "sha256-Vh+BeLWfbTT9AecazIsQ/Tkg/RzJeX3lEduANf256WA=", "narHash": "sha256-yJr1Bt4fKkKIpPYbsKGqJ0VFdoDnURgRwuffmBQ2WzY=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "c361047d3a538f547f1617bb6b410411929ac9cc", "rev": "228ecefb6329d5a531b77b46b581a2f0c26ee056",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -546,6 +586,21 @@
} }
}, },
"systems_2": { "systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_3": {
"locked": { "locked": {
"lastModified": 1689347925, "lastModified": 1689347925,
"narHash": "sha256-ozenz5bFe1UUqOn7f60HRmgc01BgTGIKZ4Xl+HbocGQ=", "narHash": "sha256-ozenz5bFe1UUqOn7f60HRmgc01BgTGIKZ4Xl+HbocGQ=",
@@ -560,7 +615,7 @@
"type": "github" "type": "github"
} }
}, },
"systems_3": { "systems_4": {
"locked": { "locked": {
"lastModified": 1681028828, "lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
@@ -575,7 +630,7 @@
"type": "github" "type": "github"
} }
}, },
"systems_4": { "systems_5": {
"locked": { "locked": {
"lastModified": 1681028828, "lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
@@ -634,11 +689,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1784642409, "lastModified": 1789164534,
"narHash": "sha256-hcbDqFuySAJawljt5r0sKBCJKYnbtGD0T/ZIozH1Dq0=", "narHash": "sha256-DoYGPM6QpnYBLWj9gGw6ZwAzIX+HrAVov1BoT+8Jixo=",
"owner": "nix-community", "owner": "nix-community",
"repo": "NixOS-WSL", "repo": "NixOS-WSL",
"rev": "eaeb18da90024448a60eb1ec7132eafa4003404e", "rev": "72c92b11bb8289e6651c7fef29cc0a885fd6a255",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -655,7 +710,7 @@
"rust-overlay": [ "rust-overlay": [
"rust-overlay" "rust-overlay"
], ],
"systems": "systems_4" "systems": "systems_5"
}, },
"locked": { "locked": {
"lastModified": 1781873766, "lastModified": 1781873766,
+7
View File
@@ -86,6 +86,12 @@
inputs.rust-overlay.follows = "rust-overlay"; inputs.rust-overlay.follows = "rust-overlay";
}; };
# Generic self-hosted REST API service
api = {
url = "github:nmasur/api";
inputs.nixpkgs.follows = "nixpkgs";
};
# # Text editor # # Text editor
# helix = { # helix = {
# url = "github:helix-editor/helix"; # url = "github:helix-editor/helix";
@@ -127,6 +133,7 @@
baseName = "masu.rs"; baseName = "masu.rs";
in in
{ {
api = "api.${baseName}";
audiobooks = "read.${baseName}"; audiobooks = "read.${baseName}";
auth = "auth.${baseName}"; auth = "auth.${baseName}";
bookmarks = "keep.${baseName}"; bookmarks = "keep.${baseName}";
+4 -11
View File
@@ -66,6 +66,7 @@ lib
overlays = [ overlays = [
inputs.nur.overlays.default inputs.nur.overlays.default
inputs.nix2vim.overlay inputs.nix2vim.overlay
inputs.api.overlays.default
(final: prev: { (final: prev: {
zellij-switch = inputs.zellij-switch.packages.${prev.stdenv.hostPlatform.system}.default; zellij-switch = inputs.zellij-switch.packages.${prev.stdenv.hostPlatform.system}.default;
}) })
@@ -156,19 +157,10 @@ lib
inputs.home-manager.nixosModules.home-manager inputs.home-manager.nixosModules.home-manager
inputs.nix-index-database.nixosModules.default inputs.nix-index-database.nixosModules.default
inputs.disko.nixosModules.disko inputs.disko.nixosModules.disko
inputs.api.nixosModules.default
inputs.wsl.nixosModules.wsl inputs.wsl.nixosModules.wsl
{ imports = (nixFiles ../platforms/nixos); } { imports = (nixFiles ../platforms/nixos); }
module (builtins.removeAttrs module [ "home-manager" ])
# (builtins.removeAttrs module [ "home-manager" ])
{
home-manager = {
extraSpecialArgs = {
inherit colorscheme;
}
// specialArgs;
}
// homeModule.home-manager;
}
]; ];
}; };
@@ -229,6 +221,7 @@ lib
inputs.home-manager.nixosModules.home-manager inputs.home-manager.nixosModules.home-manager
inputs.nix-index-database.nixosModules.default inputs.nix-index-database.nixosModules.default
inputs.disko.nixosModules.disko inputs.disko.nixosModules.disko
inputs.api.nixosModules.default
inputs.wsl.nixosModules.wsl inputs.wsl.nixosModules.wsl
{ {
imports = (nixFiles ../platforms/nixos) ++ (nixFiles ../platforms/generators); imports = (nixFiles ../platforms/nixos) ++ (nixFiles ../platforms/generators);
@@ -23,6 +23,24 @@ in
home.packages = [ term-probe ]; home.packages = [ term-probe ];
# Ctrl-b: EXECUTE the proven manual cure as a real commandline. The cure
# is not the variable's end value (it starts and ends at 1) — it is the
# reader fully exiting readline and re-entering, which only command
# EXECUTION does. Setting the variable inline (a plain binding body, or the
# old fish_postexec/fish_cancel hook) never makes the reader exit/re-enter,
# so it never cured and, with extra repaints on a wedged reader, made it
# worse. `commandline -f execute` reproduces exactly what typing the cure
# and pressing Enter does — indistinguishable to fish from the manual cure.
# Note: this submits the current commandline, so it runs the cure in place
# of whatever is typed (fine for a rescue key hit at an empty prompt).
# Ctrl-b chosen because it is otherwise unbound (Ctrl-g is taken).
nmasur.presets.programs.fish.fish_user_key_bindings = # fish
''
for mode in insert default visual
bind -M $mode \cb heal-autosuggest
end
'';
programs.fish.functions = { programs.fish.functions = {
lag-triage = { lag-triage = {
description = "Diagnose post-TUI typing lag in the current shell"; description = "Diagnose post-TUI typing lag in the current shell";
@@ -32,6 +50,22 @@ in
description = "Reset terminal state left behind by a TUI"; description = "Reset terminal state left behind by a TUI";
body = builtins.readFile ./unlag.fish; body = builtins.readFile ./unlag.fish;
}; };
lag-sample = {
description = "Stack-sample fish and zellij while typing lag is happening";
body = builtins.readFile ./lag-sample.fish;
};
heal-autosuggest = {
description = "Heal post-TUI typing lag by executing the autosuggestion-toggle cure (bind to a key)";
# Replace the commandline with the exact cure the user runs by hand and
# execute it. Executing (not inline-setting) is what cures: it forces
# the reader to leave and re-enter readline. Runs in place of whatever
# is currently typed.
body = # fish
''
commandline -r 'set -g fish_autosuggestion_enabled 0; and set -g fish_autosuggestion_enabled 1'
commandline -f execute
'';
};
}; };
}; };
@@ -0,0 +1,53 @@
# Capture stack samples of this fish process, the zellij server, and the
# zellij client WHILE the typing lag is happening. This names the guilty
# component directly: if fish's main thread is busy/blocked per keystroke the
# stacks show exactly where; if fish is idle while typing feels laggy, the
# delay is in zellij's render path instead.
#
# CAUTION (learned 2026-08-30): attaching the sampler to a lagging fish CURES
# the lag (thread suspend/resume unwedges it), so run this from a DIFFERENT
# pane with the lagging shell's pid: `lag-sample <pid>` (get it in the lagging
# shell with the builtin-only `echo $fish_pid`). Have someone type in the
# lagging pane while sampling runs — the first samples may catch the wedge.
# With no argument it samples the current shell.
set -l target $fish_pid
if test (count $argv) -ge 1; and test -n "$argv[1]"
set target $argv[1]
end
set -l outdir ~/.local/state/lag-triage
mkdir -p $outdir
set -l ts (date +%Y%m%d-%H%M%S)
set -l dur 8
set -l fishfile $outdir/sample-$ts-fish-$target.txt
/usr/bin/sample $target $dur 1 -file $fishfile &>/dev/null &
disown
# this session's zellij server (socket path ends in the session name)
set -l serverpid (pgrep -f "zellij --server.*/$ZELLIJ_SESSION_NAME\$")
test -z "$serverpid"; and set serverpid (pgrep -f "zellij --server" | head -3)
for pid in $serverpid
/usr/bin/sample $pid $dur 1 -file $outdir/sample-$ts-zellij-server-$pid.txt &>/dev/null &
disown
end
# zellij clients (attached to ghostty): named zellij but without --server args
set -l allserver (pgrep -f "zellij --server")
set -l clientpid
for pid in (pgrep -x zellij)
contains $pid $allserver; or set -a clientpid $pid
end
for pid in $clientpid[1..3]
/usr/bin/sample $pid $dur 1 -file $outdir/sample-$ts-zellij-client-$pid.txt &>/dev/null &
disown
end
# notify when done, without occupying the commandline
fish -c "sleep (math $dur + 2); echo; echo '== lag-sample done: '$outdir'/sample-$ts-*.txt =='" &
disown
echo "Sampling fish (pid $target), zellij server(s) [$serverpid], client(s) [$clientpid] for $dur s."
echo ">>> TYPE CONTINUOUSLY IN THE LAGGING PANE NOW (junk text is fine) <<<"
echo "Files: $outdir/sample-$ts-*.txt"
@@ -20,8 +20,21 @@ let
# harness on fish 4.8.1 (see docs/CHANGELOG.md 2026-08-29 and # harness on fish 4.8.1 (see docs/CHANGELOG.md 2026-08-29 and
# presets/programs/lag-triage/upstream_repro.py). Spawning fish with the # presets/programs/lag-triage/upstream_repro.py). Spawning fish with the
# variable already exported makes every pane shell immune. # variable already exported makes every pane shell immune.
# Flight recorder for the still-unsolved post-TUI typing lag: sampling the
# process CURES the lag (a wedged thread gets kicked loose), so the only way
# to observe it is a recorder that is already running before the lag starts.
# Armed by `touch ~/.local/state/lag-triage/RECORD`; new panes then log
# fish's reader/thread internals to ~/.local/state/lag-triage/flight/.
# Remove the RECORD file to disable (zero overhead when off).
fish-no-query-term = pkgs.writeShellScriptBin "fish-no-query-term" '' fish-no-query-term = pkgs.writeShellScriptBin "fish-no-query-term" ''
export fish_features=no-query-term export fish_features=no-query-term
dir="$HOME/.local/state/lag-triage"
if [ -e "$dir/RECORD" ]; then
mkdir -p "$dir/flight"
find "$dir/flight" -type f -mtime +3 -delete 2>/dev/null
export FISH_DEBUG='reader,term-support,proc-termowner,iothread,fd-monitor,topic-monitor'
export FISH_DEBUG_OUTPUT="$dir/flight/fish-$(date +%Y%m%d-%H%M%S)-$$.log"
fi
exec ${lib.getExe pkgs.fish} "$@" exec ${lib.getExe pkgs.fish} "$@"
''; '';
@@ -0,0 +1,17 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IE1nSGFPdyBaUWNy
UGZPWkFhOS9nZ0N4bWY1S1FUc2pIVS95R2NLK2xLMWFmamNFNDBNCm5yQXhCNXM4
cUtqbHdjcVRmTWUzd2JoeUtMWDQ3ZEtwcFdsSU9MSE1pcDQKLT4gc3NoLWVkMjU1
MTkgWXlTVU1RIEtDMUo5cERoSHd0dm9jNCtMNDNWTkNXbTc2bjlPbUR4K0phSWdT
QVR2bUEKY3Y0b29MbzVCSVN0UWhkcEtzN2pBT3A4OWhpeitGQjAvLzg1OUVRdENQ
QQotPiBzc2gtZWQyNTUxOSBuanZYNUEgOWc0aXJtMWNKcXpUMEw4blJCSTZnVS9H
YmVyVE13TG1qaTcrdThMRk94WQovNHl5ZHJrYlhRNDVQUkc5TXRKd1Y2TXRUb0p0
NElrT1BzbWwwd2pFb1VvCi0+IHNzaC1lZDI1NTE5IENxSU9VQSB0RlpZVnAybFZk
R0E1TTVSR3lxVmpqVnNPOHo2VG5xckdONllVcUZWRzFJCk1RUnhoUzBPc1h5cmdn
MFdtSHRvU0xBRWllKy90dW9aV2NhVUVqaVpoVDgKLT4gc3NoLWVkMjU1MTkgejFP
Y1p3IFY2RmJMOHo1V0U1K3FNeVBhdDVYUE5lYnFHV2ZqZm5Uek1waDhNeGIwaDQK
UFJBcFdBb2hlOWxoTHIwdUtibjk2Q1E1emFJbVlxNDdLSzlqbWI1dm5XbwotLS0g
RDV3bWVQUG95c2h4S0lJQlJ4dlNBYkVNb2sxTlUxc0tlQ2pzb04yVmhPcwpv4CRQ
kRFkaF8/q1wY4JC7oD6VbJzZWpMsAUbQQWEs8QI9nI2HyqHB4yZ/dT8wSfoh7JPY
fHIVqN25xXGTu7uR2S/8Iic=
-----END AGE ENCRYPTED FILE-----
@@ -0,0 +1,17 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IE1nSGFPdyA5Y0JT
SjBqU1dEUVIwSXFBY3h3M3FDMFFpUTdSVmp6S3Y5QXYxK2Rjc2hrCll4UzN0RXVl
a2s0ZkxOdDIxd05kVkdJZWJHQmQ3ZDNjeGRpWnVFQ1Jzd3cKLT4gc3NoLWVkMjU1
MTkgWXlTVU1RIDJHS2JTM2R1NzNLSG12bC9yRURBYWx5K1N4d2VWUS8vU2NHamta
amsvUlUKRy8wOWc0cStGN3M1RnpGc2Z0aCt3Y3JiU3dZd3I0V0sxOWdSSE5OUEVy
QQotPiBzc2gtZWQyNTUxOSBuanZYNUEgSmJBbngwMVN3UHpvcHkxUjkvQ1FJaVdX
MzZrVnZtS1VRQVRGWVJocnB6WQpvb01VZ2tleGh6VW9FRTJjN0EwMncyVWZ4WHpi
WXRGZklZcHRkSDYyYmNZCi0+IHNzaC1lZDI1NTE5IENxSU9VQSBKTE0xWlMzRTh0
cEJjUU9NY0NiRWNGU1hvNzQ4clRQRTRJaU1VVzgrZDJZClluVXZiTHp6YUZxOWF3
U0UzWkRIM0xicmw3Qzc5QnhjeGNKOWFGMnptc28KLT4gc3NoLWVkMjU1MTkgejFP
Y1p3IFNkS01KbzhFcW9iS05OcVI2Szl3dXI5QTAvejdhRU1EbEhnQTcwRGo2MWcK
V01MTmhoYlE5YnBvL21qT0tubXRTOGtDYno2Wjd6M2hlY3Z4UHBjQU9WbwotLS0g
VWpndk84OHJSRVMwUFg4aUYwQmNVamIwN3BVL1dzcEV2LzZBODE5bmdoYwpg3LTS
kiMJpjLJ12CGFqmMCjwWwiJHjsC8FM47Ptf2uRxjD1XsOq6MWKHYSI9KxzrgJdyY
uxhU1m3Jfi5+7+oWX6BhfQA=
-----END AGE ENCRYPTED FILE-----
@@ -0,0 +1,18 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IE1nSGFPdyBlZEhP
b1ZUdTlROVAydktUNG9lRDVEejMxVmRtNjlhQ2V1NWMyblZFZHlJClczMHM4RXda
TzYzOUFhWmxxUTF0bjJXWVJoSS9HaWZ6TFJ1dndWQXF6WWcKLT4gc3NoLWVkMjU1
MTkgWXlTVU1RIGZuR28vNVFqLzUxUHJ4Nlc1c2h5ODNQQnh2bFZRU3hDS3VidERF
T3htUmsKRjd6ZVZuTE1lN2hrOTJaaWQxdE01djl0QlJBVUNnRHlEQ3JMWmMxNkEw
cwotPiBzc2gtZWQyNTUxOSBuanZYNUEgM3ZnOFd6REZNNkxXdW9NT2gzYkJ3ZUhm
UlRjb3kxRnppN2M2UERLNUFrSQp0YWRBL3JXZWs5Zyt0RjRBS2JjUkJockE3Ti9m
TzlNR1c4dFJYUzdRSzBNCi0+IHNzaC1lZDI1NTE5IENxSU9VQSA0SFRseGsxMnlI
TzhueGZGS2VzMzVxcmNDZVdPZDYwQ0c4SjNhbWp5OUVJCkpnSXJiVVZiTHY5SDg2
WnpmRDZQcHlOWnpySTZmUG9hTFdremp2MXEvRFUKLT4gc3NoLWVkMjU1MTkgejFP
Y1p3IHhHWFk4ckt5UCtGM3VGaVljOGl3aFV1UDhCVm5PV0lqRGJRcGg0RHJGejgK
NVR2cmFWTW9IYTRsT0V0VGs4VHBYY1BPM3A3bUY1enA2cVZhU0NTWXI2awotLS0g
a1Q4SnBSWXJxREpuaEJtS2VJaTZiNmVWdFp1QXhUOFJRSVVUUEUrVFA2SQqmH3al
3mHIyywuvc6U0lf0FheRGCp//BFORrAqo9bgJmUj/SsduvT1DNyJBGfi62k/8llH
/oQmcYx8rTIbZzCkDHu18DCxJxiZahaIVV/pilQAtSZVfdq/pPnzCBaB1QMMOnWY
cB/H5eQh
-----END AGE ENCRYPTED FILE-----
@@ -0,0 +1,104 @@
{
config,
pkgs,
lib,
...
}:
let
inherit (config.nmasur.settings) hostnames;
cfg = config.nmasur.presets.services.api;
in
{
options.nmasur.presets.services.api = {
enable = lib.mkEnableOption "API gateway and backend services";
};
config = lib.mkIf cfg.enable {
services.api = {
enable = true;
hostname = hostnames.api;
backends.actual = {
enable = true;
actualServerUrl = "http://127.0.0.1:${builtins.toString config.nmasur.presets.services.actualbudget.port}";
apiKeysFile = config.secrets.api-actual-keys.dest;
serverPasswordFile = config.secrets.actualbudget-password.dest;
budgets = {
budget1 = {
syncIdFile = config.secrets.api-actual-budget1-sync-id.dest;
};
budget2 = {
syncIdFile = config.secrets.api-actual-budget2-sync-id.dest;
};
};
};
};
secrets = {
api-actual-keys = {
source = ./api-actual-keys.age;
dest = "${config.secretsDirectory}/api-actual-keys";
owner = "api_actual";
group = "api_actual";
prefix = "API_KEYS=";
};
api-actual-budget1-sync-id = {
source = ./api-actual-budget1-sync-id.age;
dest = "${config.secretsDirectory}/api-actual-budget1-sync-id";
owner = "api_actual";
group = "api_actual";
prefix = "ACTUAL_SYNC_ID_BUDGET1=";
};
api-actual-budget2-sync-id = {
source = ./api-actual-budget2-sync-id.age;
dest = "${config.secretsDirectory}/api-actual-budget2-sync-id";
owner = "api_actual";
group = "api_actual";
prefix = "ACTUAL_SYNC_ID_BUDGET2=";
};
};
systemd.services.api-actual = {
after = [
"postgresql-setup.service"
"actual.service"
"api-actual-keys-secret.service"
"api-actual-budget1-sync-id-secret.service"
"api-actual-budget2-sync-id-secret.service"
"actualbudget-password-secret.service"
];
requires = [
"postgresql-setup.service"
"api-actual-keys-secret.service"
"api-actual-budget1-sync-id-secret.service"
"api-actual-budget2-sync-id-secret.service"
"actualbudget-password-secret.service"
];
};
# Postgres peer authentication for api_actual
services.postgresql.authentication = lib.mkAfter ''
local api_actual api_actual peer
'';
# Backup PostgreSQL database for api_actual
services.postgresqlBackup = {
enable = true;
databases = [ "api_actual" ];
};
# Allow web traffic to Caddy
nmasur.presets.services.caddy.routes = config.services.api.caddyRoutes;
# Configure Cloudflare DNS to point to this machine
services.cloudflare-dyndns.domains = [ hostnames.api ];
# Backups
services.restic.backups.default.paths = [ "/var/lib/api-actual" ];
};
}
@@ -44,9 +44,27 @@ in
{ {
match = [ { host = [ hostnames.stream ]; } ]; match = [ { host = [ hostnames.stream ]; } ];
handle = [ handle = [
# Resolve client IP: use Cloudflare's CF-Connecting-IP if present, otherwise remote host
{
handler = "map";
source = "{http.request.header.CF-Connecting-IP}";
destinations = [ "{client_ip}" ];
defaults = [ "{http.request.remote.host}" ];
mappings = [
{
input_regexp = "^(.+)$";
outputs = [ "\${1}" ];
}
];
}
{ {
handler = "reverse_proxy"; handler = "reverse_proxy";
upstreams = [ { dial = "localhost:8096"; } ]; upstreams = [ { dial = "localhost:8096"; } ];
headers.request.set = {
"X-Real-IP" = [ "{client_ip}" ];
"X-Forwarded-For" = [ "{client_ip}" ];
"X-Forwarded-Proto" = [ "{http.request.scheme}" ];
};
} }
]; ];
} }
@@ -0,0 +1,17 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IE1nSGFPdyAzMEJs
WW9mb1E5MW5vT0tYeDhKSnFSRWZFVFdrL0d6Vk45UUVBd3JubFNvClpIRDYzM1Zh
dEtMaFNSeEF4aHY2cWpZbUNNMUw0UW93SFV0NVBMZ2wvKzgKLT4gc3NoLWVkMjU1
MTkgWXlTVU1RIDlGUUQwdlFwd1JiOE9Sb0svWTM4cDVHbGNQeFVDcUJsa0xqZGp6
SGxkUlUKMHpPSFFSQXZSYjdwNUhnRnUxZzJVQWxLblB5MjBTZjRpM0d4NDJFSk1Z
RQotPiBzc2gtZWQyNTUxOSBuanZYNUEgK055eTE2NG9yN3FEZ1BSaTZKTGpWUmt0
YlFWU0hjSnZ3MVZSdE12Y0VVTQovNWpGM2Y0TUlNSHRVQmRJK3VCNTBHTFAzdlYx
MXNJOVNOdTJiNlZZdWkwCi0+IHNzaC1lZDI1NTE5IENxSU9VQSArblhobTFoN1BQ
ZUhuL04yVUtTRnF1V0tPR1BVK3lzWUVrMlhJQWxmZXhZClJkUnU0QUxuRnZ3Vjg2
TVFzdGs5dDJSbFZMQnFRamxObnFpZVBaTExFajQKLT4gc3NoLWVkMjU1MTkgejFP
Y1p3IFE1clkxRFFZbmVEV01LVWNXaVR6ZkdrcHJYUFNaSXI1STUrSmN5ejJnU2MK
STRhOWlXTFU2emVvVUkxMmxKc1FmbmJrQ1RFU3RLa2RBSUJlak5CRG1obwotLS0g
WFhjSmJxSmVMQlR0bjRKWVR1cllmQ1JjUkZZYWpTbEFzTjNQVzRvbHNJbwrrqVI1
yl6w97t0CDhjOml46f3wlob0kztDAdOFmUSiEVfiQU0eCrfNinQg7aohu5Y8MJpD
k2Du2m/AV6aIPR47LA==
-----END AGE ENCRYPTED FILE-----
@@ -33,6 +33,7 @@ in
# Allow access when hitting either of these hosts or IPs # Allow access when hitting either of these hosts or IPs
trusted_domains = [ hostnames.content ]; trusted_domains = [ hostnames.content ];
trusted_proxies = [ "127.0.0.1" ]; trusted_proxies = [ "127.0.0.1" ];
overwriteprotocol = "https";
maintenance_window_start = 4; # Run jobs at 4am UTC maintenance_window_start = 4; # Run jobs at 4am UTC
log_type = "file"; log_type = "file";
loglevel = 1; # Include all actions in the log loglevel = 1; # Include all actions in the log
@@ -41,6 +42,7 @@ in
extraApps = { extraApps = {
calendar = config.services.nextcloud.package.packages.apps.calendar; calendar = config.services.nextcloud.package.packages.apps.calendar;
contacts = config.services.nextcloud.package.packages.apps.contacts; contacts = config.services.nextcloud.package.packages.apps.contacts;
user_oidc = config.services.nextcloud.package.packages.apps.user_oidc;
# These apps are defined and pinned by overlay in flake. # These apps are defined and pinned by overlay in flake.
# news = pkgs.nextcloudApps.news; # news = pkgs.nextcloudApps.news;
# external = pkgs.nextcloudApps.external; # external = pkgs.nextcloudApps.external;
@@ -211,6 +213,32 @@ in
before = [ "nextcloud-setup.service" ]; before = [ "nextcloud-setup.service" ];
}; };
secrets.nextcloud-oidc-secret = {
source = ./nextcloud-oidc-secret.age;
dest = "${config.secretsDirectory}/nextcloud-oidc-secret";
owner = "nextcloud";
group = "nextcloud";
permissions = "0440";
};
systemd.services.nextcloud-oidc-secret-secret = {
requiredBy = [ "nextcloud-setup.service" ];
before = [ "nextcloud-setup.service" ];
};
# Configure Pocket ID OIDC provider after Nextcloud setup enables apps
systemd.services.nextcloud-setup = {
after = [ "nextcloud-oidc-secret-secret.service" ];
postStart = ''
${config.services.nextcloud.occ}/bin/nextcloud-occ user_oidc:provider pocket-id \
--clientid="c8a32c58-a781-4f14-9070-f498fdfda438" \
--clientsecret-file="${config.secrets.nextcloud-oidc-secret.dest}" \
--discoveryuri="https://${hostnames.auth}/.well-known/openid-configuration" \
--scope="openid profile email" \
--mapping-uid="preferred_username" \
--unique-uid=0
'';
};
# Grant user access to Nextcloud directories # Grant user access to Nextcloud directories
users.users.${username}.extraGroups = [ "nextcloud" ]; users.users.${username}.extraGroups = [ "nextcloud" ];
@@ -0,0 +1,17 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IE1nSGFPdyBCa2J1
QjFGRjdZa0RHcjd0V0JXL3lUNzRGczNHanIzb3BtaE93NlNvcGhnCnNOM3JBc0t3
VENnWVp1b09Sd1JwcDk1Q0pQM3BBMlJmWHh5TTJHakIvcGsKLT4gc3NoLWVkMjU1
MTkgWXlTVU1RIDljQUIrenlGZHMxSHlReG80VVhaSXhsbFZ0L2dtcCtWbTRvNE1i
WEdOeVkKUk9HK25Fci9TYWRSS1htSU9BNHlqbHpGT3c5bkI4b3RUL09QK1BYTE1Y
VQotPiBzc2gtZWQyNTUxOSBuanZYNUEgNXk5bzRhR1BoTjlMeHVRR2UyUEZJN0Y1
ZXAyU3BjeFJvRHhER0ptaVlsVQo0K3p6bXBwcTVIdGlTajBucWV5dzZKM2JyWWNB
R2s2UnhTMFVPRGwrVWhRCi0+IHNzaC1lZDI1NTE5IENxSU9VQSBaL3NTdHBjVFRX
ODNrcDVmZGFxNVRwbGkrcC9heFN5bmVCZ0xId1NESUhFCm5aQS9xZmhkSWRwMFl1
b3FPdUhPZU5YdVQvTEQ3eVdRODBhVzJSWjdyek0KLT4gc3NoLWVkMjU1MTkgejFP
Y1p3IHdDR3YwYTFyWlZQZWFDZ1cvWk00bjdRdW16c1M4eDlDaHl4Y3lkbVFGVFEK
dndhaG5pUTM3aU04WHZUSHJDdUdkZ0VWRXRaL2t6cUJoZTR1bTh6NlBFawotLS0g
NXRDeDFZL0RvaURVK2k3R0NVZGdKR0diZ2RwZjQxTFhuNk82SW9GMGVaRQqYDG6J
+/POhVn84NGh8ganDIzNUqQF7uBIxdUMiRoFa0kU4eJd9V4vmjsjtr/lbQ9O+Xfq
WJJ8Hjwyzwopx1Phrw==
-----END AGE ENCRYPTED FILE-----
@@ -1,6 +1,11 @@
# Paperless-ngx is a document scanning and management solution. # Paperless-ngx is a document scanning and management solution.
{ config, lib, ... }: {
config,
pkgs,
lib,
...
}:
let let
inherit (config.nmasur.settings) hostnames username; inherit (config.nmasur.settings) hostnames username;
@@ -17,12 +22,26 @@ in
enable = true; enable = true;
mediaDir = "/data/generic/paperless"; mediaDir = "/data/generic/paperless";
passwordFile = config.secrets.paperless.dest; passwordFile = config.secrets.paperless.dest;
environmentFile = "${config.secretsDirectory}/paperless-env";
configureTika = true; # Enable processing of emails configureTika = true; # Enable processing of emails
settings = { settings = {
PAPERLESS_OCR_USER_ARGS = builtins.toJSON { invalidate_digital_signatures = true; }; PAPERLESS_OCR_USER_ARGS = builtins.toJSON { invalidate_digital_signatures = true; };
PAPERLESS_URL = "https://${hostnames.paperless}"; PAPERLESS_URL = "https://${hostnames.paperless}";
PAPERLESS_DATE_ORDER = "MDY"; # Check document for US-formatted dates PAPERLESS_DATE_ORDER = "MDY"; # Check document for US-formatted dates
# OIDC Authentication with Pocket ID
PAPERLESS_APPS = "allauth.socialaccount.providers.openid_connect";
PAPERLESS_REDIRECT_LOGIN_TO_SSO = true;
PAPERLESS_USE_X_FORWARD_HOST = true;
PAPERLESS_PROXY_SSL_HEADER = [
"HTTP_X_FORWARDED_PROTO"
"https"
];
PAPERLESS_TRUSTED_PROXIES = [
"127.0.0.1"
"::1"
];
# Enable if changing the path name in Caddy # Enable if changing the path name in Caddy
# PAPERLESS_FORCE_SCRIPT_NAME = "/paperless"; # PAPERLESS_FORCE_SCRIPT_NAME = "/paperless";
# PAPERLESS_STATIC_URL = "/paperless/static/"; # PAPERLESS_STATIC_URL = "/paperless/static/";
@@ -61,8 +80,60 @@ in
permissions = "0440"; permissions = "0440";
}; };
systemd.services.paperless-secret = { systemd.services.paperless-secret = {
requiredBy = [ "paperless.service" ]; requiredBy = [ "paperless-scheduler.service" ];
before = [ "paperless.service" ]; before = [ "paperless-scheduler.service" ];
};
secrets.paperless-oidc-secret = {
source = ./paperless-oidc-secret.age;
dest = "${config.secretsDirectory}/paperless-oidc-secret";
owner = "paperless";
group = "paperless";
permissions = "0440";
};
systemd.services.paperless-oidc-secret-secret = {
requiredBy = [
"paperless-secret-key.service"
"paperless-scheduler.service"
"paperless-task-queue.service"
"paperless-consumer.service"
"paperless-web.service"
];
before = [
"paperless-secret-key.service"
"paperless-scheduler.service"
"paperless-task-queue.service"
"paperless-consumer.service"
"paperless-web.service"
];
postStart = ''
SECRET="$(tr -d '\r\n' < '${config.secrets.paperless-oidc-secret.dest}')"
JSON=$(${pkgs.jq}/bin/jq -nc \
--arg secret "$SECRET" \
--arg auth "https://${hostnames.auth}" \
'{
openid_connect: {
APPS: [
{
provider_id: "pocket-id",
name: "Pocket ID",
client_id: "e6ff7fce-8e67-4c66-8f32-5ec3db4740a9",
secret: $secret,
settings: {
server_url: $auth,
token_auth_method: "client_secret_basic",
oauth_pkce_enabled: true,
email_authentication: true,
verified_email: true
}
}
]
}
}')
echo "PAPERLESS_SOCIALACCOUNT_PROVIDERS='$JSON'" > '${config.secretsDirectory}/paperless-env'
chown paperless:paperless '${config.secretsDirectory}/paperless-env'
chmod 0440 '${config.secretsDirectory}/paperless-env'
'';
}; };
# Fix paperless shared permissions # Fix paperless shared permissions
@@ -37,6 +37,18 @@ in
} }
]; ];
}; };
# Refresh collation version if glibc was upgraded across system releases
systemd.services.postgresql-setup = {
preStart = ''
while ! ${config.services.postgresql.package}/bin/psql -d postgres -tAc 'SELECT 1' >/dev/null 2>&1; do
if ! systemctl is-active --quiet postgresql.service; then break; fi
sleep 0.1
done
${config.services.postgresql.package}/bin/psql -d postgres -tAc 'ALTER DATABASE template1 REFRESH COLLATION VERSION;' || true
${config.services.postgresql.package}/bin/psql -d postgres -tAc 'ALTER DATABASE postgres REFRESH COLLATION VERSION;' || true
'';
};
}; };
} }
@@ -29,7 +29,7 @@ in
}; };
}; };
programs.fish.enable = lib.mkDefault config.home-manager.users.${username}.programs.fish.enable; programs.fish.enable = lib.mkDefault (config.home-manager.users.${username}.programs.fish.enable or true);
# Allows us to declaritively set password # Allows us to declaritively set password
users.mutableUsers = lib.mkDefault false; users.mutableUsers = lib.mkDefault false;
@@ -62,7 +62,7 @@ in
# serviceConfig.TimeoutStartSec = lib.mkForce "45m"; # serviceConfig.TimeoutStartSec = lib.mkForce "45m";
# }; # };
allowUnfreePackages = config.home-manager.users.${username}.allowUnfreePackages; allowUnfreePackages = config.home-manager.users.${username}.allowUnfreePackages or [];
wsl.enable = lib.mkDefault false; wsl.enable = lib.mkDefault false;
@@ -22,7 +22,7 @@ in
services = { services = {
actualbudget.enable = lib.mkDefault true; actualbudget.enable = lib.mkDefault true;
actualtap = { actualtap = {
enable = lib.mkDefault true; enable = lib.mkDefault false;
instances = { instances = {
budget1 = { budget1 = {
port = 3031; port = 3031;
@@ -38,6 +38,7 @@ in
}; };
}; };
}; };
api.enable = lib.mkDefault true;
caddy.enable = lib.mkDefault true; caddy.enable = lib.mkDefault true;
cloudflare.enable = lib.mkDefault true; cloudflare.enable = lib.mkDefault true;
cloudflared.enable = lib.mkDefault true; cloudflared.enable = lib.mkDefault true;