mirror of
https://github.com/nmasur/dotfiles
synced 2026-09-14 17:38:11 +00:00
Compare commits
30
Commits
4ba1ed807c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ac07e2874 | ||
|
|
985ab2b433 | ||
|
|
d6209c039b | ||
|
|
3e246645a2 | ||
|
|
ed8be9dee6 | ||
|
|
ab98a202da | ||
|
|
e00560c42c | ||
|
|
d4e56dd190 | ||
|
|
db49e746b8 | ||
|
|
7f8400ff58 | ||
|
|
d3914768c8 | ||
|
|
fd9cae9c40 | ||
|
|
f593fdb81f | ||
|
|
01ee98155a | ||
|
|
2a3cbfb5f5 | ||
|
|
ca1343f593 | ||
|
|
a053a9ab0f | ||
|
|
90dc80f7b4 | ||
|
|
aff596aa64 | ||
|
|
6e0f4e2000 | ||
|
|
72c4f4823b | ||
|
|
a93fa75779 | ||
|
|
a50d2c01a9 | ||
|
|
a72e81fcce | ||
|
|
fe2843ead6 | ||
|
|
68320577ae | ||
|
|
38ba019c3d | ||
|
|
e339a4969f | ||
|
|
44f2ca803e | ||
|
|
239b56c1b1 |
@@ -1,5 +1,156 @@
|
||||
# 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
|
||||
|
||||
- **Configured OpenID Connect (OIDC) authentication for Immich**:
|
||||
- Configured `services.immich.settings.oauth` with OIDC settings pointing to Pocket ID (`auth.masu.rs`), using client ID `1f4e0f8d-6cee-4d67-8d53-74bf6c18ae09`.
|
||||
- Added secret management for `immich-oidc-secret.age` via `secrets.immich-oidc-secret` with owner `immich` and group `shared` (0440).
|
||||
- Wired client secret substitution using NixOS's native `clientSecret._secret = config.secrets.immich-oidc-secret.dest`, leveraging `utils.genJqSecretsReplacement` with systemd `LoadCredential`.
|
||||
- Configured `systemd.services.immich-server` to order after `immich-oidc-secret-secret.service`.
|
||||
- Updated `docs/oidc-services.md` with the verified configuration and redirect URIs.
|
||||
|
||||
- **Fixed OpenSSH authorized principals certificate authentication for Cloudflare Tunnel**:
|
||||
- Replaced manual `environment.etc."ssh/authorized_principals/${username}"` symlink and `Match User` config with NixOS native `users.users.<name>.openssh.authorizedPrincipals`.
|
||||
- Root cause: `environment.etc` without an explicit `mode` creates symlinks pointing into `/nix/store`, which has group-writable mode `0775` (`nixbld` group). Under `StrictModes yes`, sshd refused authentication with `bad ownership or modes for directory /nix/store`, causing certificate principal matching to fail with `Certificate does not contain an authorized principal`.
|
||||
- Setting `users.users.<name>.openssh.authorizedPrincipals` causes NixOS to generate `/etc/ssh/authorized_principals.d/<name>` with `mode = "0444"`, copying the file instead of symlinking into the store, and automatically configuring `services.openssh.settings.AuthorizedPrincipalsFile = "/etc/ssh/authorized_principals.d/%u"`.
|
||||
- Also added `mode = "0444"` to `/etc/ssh/ca.pub` and moved `TrustedUserCAKeys` into `services.openssh.settings`.
|
||||
|
||||
- **Configured OpenID Connect (OIDC) authentication for Mealie**:
|
||||
- Configured `services.mealie.settings` with OIDC settings pointing to Pocket ID (`auth.masu.rs`), using client ID `040925ed-b39e-4442-b8e8-369c948c0cd2`.
|
||||
- Added secret management for `mealie-oidc-secret.age` via `secrets.mealie-oidc-secret`, using `prefix = "OIDC_CLIENT_SECRET="` to generate an environment file.
|
||||
- Configured `services.mealie.credentialsFile` to load the client secret via systemd's `EnvironmentFile` without exposing it in the world-readable Nix store or systemd unit file.
|
||||
- Configured `systemd.services.mealie` to order after `mealie-oidc-secret-secret.service`.
|
||||
- Updated `docs/oidc-services.md` with the verified configuration and callback URI (`https://cooking.masu.rs/login`).
|
||||
|
||||
- **Configured OpenID Connect (OIDC) authentication for Actual Budget**:
|
||||
- Configured `services.actual.settings` with `loginMethod = "openid"` and `openId` settings pointing to Pocket ID (`auth.masu.rs`), using client ID `92afe9f8-7ef6-42ab-8a06-701df3c7179d`.
|
||||
- Added secret management for `actualbudget-oidc-secret.age` via `secrets.actualbudget-oidc-secret`, set with owner `actualbudget` and group `shared` (0440).
|
||||
- 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.
|
||||
|
||||
## 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 B–G (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 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.
|
||||
- **Why every previous observation finally makes sense**: subshells and `exec fish` were never "resetting" anything — they *inherited* the exported `fish_features=no-query-term` from config.fish, latched query-term off at startup, and were therefore **immune**. The parent zellij-spawned shell never had the variable at startup and stayed vulnerable. Raw keystroke capture in the lagging pane showed instant plain bytes (input path fine) and no stuck terminal modes — the damage was inside the fish process, exactly as the repro shows.
|
||||
- **Fix**: `zellij.nix` now spawns panes through a `fish-no-query-term` wrapper (`export fish_features=no-query-term; exec fish`), so the feature is latched off in every pane shell. Verified: interactive fish through the built wrapper with the real config reports `query-term off`; the PTY repro with `no-query-term` in the environment shows ~35ms echo through all failure phases.
|
||||
- **Correction** to the earlier 2026-08-29 entry: `query-term` does **not** default to off in fish 4.8.1 — it defaults on; it only *appeared* off in non-interactive checks because the user config's `set -gx` takes effect for `fish -c` (no reader latch) but not for interactive shells.
|
||||
- Upstream: fish-shell should bound the reader's wait for query replies instead of degrading permanently (repro script kept at `presets/programs/lag-triage/upstream_repro.py` for filing); Zellij's reply relaying is the trigger (zellij-org/zellij#5158).
|
||||
- `lag-triage` now checks `status features` and calls out `query-term on` as the known root cause, and warns that its `read`-prompt typing tests may not exhibit main-commandline lag (which produced a false "fixed by stage A" in the first capture).
|
||||
|
||||
## 2026-08-29 (later)
|
||||
|
||||
- Added a diagnostic toolkit (`lag-triage` / `unlag` fish functions + `term-probe` binary, `presets/programs/lag-triage/`) for the still-recurring post-TUI typing lag in fish + Zellij + Ghostty, instead of another blind fix. Findings that motivated it:
|
||||
- All three prior fixes were either no-ops or insufficient: `fish_features = no-query-term` is a **no-op** because `query-term` already defaults to *off* in fish 4.8.1 (verified with `status features`); disabling Ghostty's fish integration inside Zellij and setting `support_kitty_keyboard_protocol = false` did not stop recurrence.
|
||||
- PTY captures of fish 4.8.1 (`TERM=xterm-256color`, with and without `$ZELLIJ`) show fish never writes Kitty keyboard sequences to the wire — it uses modifyOtherKeys (`\e[>4;1m`), application keypad (`\e=`), bracketed paste (`?2004`), and color-theme reporting (`?2031`), enabling them at every prompt and disabling them before every external command. Crucially, a fresh subshell's startup bytes are identical to the parent's post-command re-enable bytes, so "a subshell fixes the lag" cannot be explained by a simple terminal-state reset — leaving two competing hypotheses that only live capture can separate: (1) fish-internal reader state poisoned by stray/partial escape bytes (e.g. leaked from a closing floating pane), cleared only by a new fish process; (2) Zellij/Ghostty-level stuck state (Zellij 0.45's `StdinAnsiParser` is already a proven source of input delays — see the Alt-Shift-P fix below).
|
||||
- Also note: the floating-pane TUIs (jjui via Alt-Shift-J, yazi via Alt-Shift-Y, scrollback editor) run in their own panes and never pass through the shell's fish process at all, while `nvim` runs inside the shell pane — the triage log records which path preceded the lag.
|
||||
- **Next occurrence: run `lag-triage` in the lagging shell BEFORE starting a new shell.** It snapshots the environment, queries pane terminal state (kitty flags, modifyOtherKeys, DEC modes, DA1 round-trip latency), captures raw keystroke bytes+timing bypassing fish, then applies staged resets (kitty pop/clear, modifyOtherKeys off, keypad/cursor, mouse/focus/sync, altscreen, stty, DECSTR) — the stage that cures it names the stuck layer. Logs to `~/.local/state/lag-triage/` for an upstream issue. `unlag` is the one-shot convenience version (if `unlag` never helps but `exec fish` does, the bug is fish-internal).
|
||||
|
||||
## 2026-08-29
|
||||
|
||||
- Fixed 1.5-second latency when pressing `Alt-Shift-P` to trigger `zellij-session` in Zellij 0.45.0 + Ghostty:
|
||||
- **Root Cause**: Zellij 0.45.0 introduced `StdinAnsiParser` (`zellij-client/src/stdin_ansi_parser.rs`) using `termwiz::InputParser` to parse ANSI control strings (OSCs, CSIs, DCSs) arriving on stdin. When pressing `Alt-Shift-P` (Option-Shift-P) with `support_kitty_keyboard_protocol = false`, Ghostty sent `\x1bP` (`ESC` + uppercase `P`). In ECMA-48 / VT100 standards, `ESC P` is the 7-bit ASCII representation of `DCS` (Device Control String). `StdinAnsiParser` buffered `\x1bP` waiting for a DCS string payload and string terminator (`ST` / `\x1b\`), hitting a ~1.5-second escape timeout before flushing `\x1bP` as residue to the keyboard handler.
|
||||
- **Fix**: Added `alt+shift+p=text:\x1b[112;4u` and `super+shift+p=text:\x1b[112;4u` in `ghostty.nix` to send the explicit CSI-u sequence for `Alt+Shift+p` (`'p'` with modifier 4 = `ALT | SHIFT`). `StdinAnsiParser` immediately recognizes `\x1b[112;4u` as non-DCS input and passes it straight to the keyboard handler with 0ms latency.
|
||||
|
||||
## 2026-08-26
|
||||
|
||||
- Fixed macOS shortcuts (`Cmd+T`, `Ctrl+Tab`, `Cmd+Shift+]`, `Cmd+Shift+[`, `Cmd+K`, `Cmd+Shift+E`) in Zellij + Ghostty after disabling the Kitty keyboard protocol:
|
||||
- Mapped Ghostty keybindings (`super+t`, `super+shift+]`, `super+shift+[`, `ctrl+tab`, `ctrl+shift+tab`, `super+k`, `super+shift+e`) to send standard `Alt` (`ESC`-prefix) text sequences (`\x1bt`, `\x1b}`, `\x1b{`, `\x1bK`, `\x1bE`).
|
||||
- Added matching `Alt` keybindings (`Alt t`, `Alt ]`, `Alt }`, `Alt [`, `Alt {`, `Alt Shift k`, `Alt Shift e`) in `zellij.nix` for tab creation, tab navigation, scroll mode, and scrollback editing. Symbols like `]` and `}` are parsed by Zellij's termwiz input engine as distinct character codes (`'}'` vs `']'`), so binding both `Alt }` and `Alt Shift ]` ensures `\x1b}` triggers tab navigation correctly.
|
||||
- Keeps Kitty keyboard protocol disabled in Zellij (`support_kitty_keyboard_protocol = false`) so no CSI-u flags leak into Fish shell, guaranteeing zero post-TUI typing lag while restoring all shortcuts.
|
||||
|
||||
- Fixed persistent Fish typing lag after long TUI sessions (Neovim, jjui, Yazi) inside Zellij + Ghostty, which the `no-query-term` / Ghostty-integration fixes from 2026-08-25 did not resolve:
|
||||
- Verified on Fish 4.8.1 that the `query-term` feature already defaults to `off`, so exporting `fish_features = no-query-term` is a no-op on this Fish version — it isn't the cause of (or fix for) this class of lag.
|
||||
- Set `support_kitty_keyboard_protocol = false` in `zellij.nix`. Zellij and Ghostty have several open upstream bugs (zellij-org/zellij#3887, #3723, #4178) where the Kitty keyboard protocol's enhancement-flag stack is left in an elevated state after a full-screen TUI exits without properly popping it. Every subsequent keystroke then arrives as a CSI-u sequence that Fish must wait out an escape-disambiguation timeout to parse, which reads as typing lag that worsens the longer the TUI session ran, and persists until the pane's protocol state resets (e.g. a fresh shell/pane). Disabling the protocol support in Zellij avoids the whole bug class; trades off precise modifier reporting (e.g. distinguishing Ctrl+Shift+key) for TUIs running inside Zellij panes, which this setup doesn't otherwise depend on (Shift+Enter is handled via a literal Ghostty `text:` keybind, not the Kitty protocol).
|
||||
|
||||
## 2026-08-25
|
||||
|
||||
- Fixed Nix evaluation warnings for `stdenv` deprecation and `gemini-cli`:
|
||||
@@ -67,6 +218,11 @@
|
||||
to bind, the failure took down the entire terminal instead of erroring
|
||||
gracefully.
|
||||
|
||||
## 2026-09-06
|
||||
|
||||
- Configured Grafana OIDC authentication via Pocket ID in `platforms/nixos/modules/nmasur/presets/services/grafana/grafana.nix`.
|
||||
- Enabled `auth.oauth_allow_insecure_email_lookup = true` in Grafana settings to allow linking an incoming OAuth login to an existing Grafana user account with the same email.
|
||||
|
||||
## 2026-07-25
|
||||
|
||||
- Added jjui config generation to `jujutsu.nix` in Home Manager to include custom GitHub ruleset bypass commands (`ctrl+b` and `ctrl+shift+b`).
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
# OIDC Service Analysis & Setup Requirements
|
||||
|
||||
This document analyzes the services defined in this repository (specifically within `platforms/nixos/modules/nmasur/presets/services/` and related server profiles like `flame` and `swan`), evaluates their OpenID Connect (OIDC) compatibility, and details the requirements for configuring OIDC logins.
|
||||
|
||||
---
|
||||
|
||||
## Identity Provider Context: Pocket ID
|
||||
|
||||
The configuration already includes a central self-hosted identity provider: **Pocket ID** (`platforms/nixos/modules/nmasur/presets/services/pocket-id/pocket-id.nix`), hosted on the communications server (`flame`) behind Caddy at **`https://auth.masu.rs`**.
|
||||
|
||||
Pocket ID is an OpenID Connect (OIDC) provider with WebAuthn/Passkey support. It exposes the following standard OIDC endpoints:
|
||||
- **Issuer URL:** `https://auth.masu.rs`
|
||||
- **Discovery Endpoint:** `https://auth.masu.rs/.well-known/openid-configuration`
|
||||
- **Authorization Endpoint:** `https://auth.masu.rs/authorize`
|
||||
- **Token Endpoint:** `https://auth.masu.rs/api/oidc/token`
|
||||
- **Userinfo Endpoint:** `https://auth.masu.rs/api/oidc/userinfo`
|
||||
- **JWKS Endpoint:** `https://auth.masu.rs/.well-known/jwks.json`
|
||||
|
||||
---
|
||||
|
||||
## 1. Service Compatibility Breakdown
|
||||
|
||||
### Tier 1: First-Class / Native OIDC Support
|
||||
|
||||
These services support OpenID Connect natively without requiring external authentication proxies or custom code:
|
||||
|
||||
| Service | Hostname | OIDC Support Level | Configuration Method |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Immich** | `photos.masu.rs` | Native core feature | NixOS config (`services.immich.settings.oauth`) |
|
||||
| **Gitea** | `git.masu.rs` | Native core feature | NixOS config / CLI or Web UI |
|
||||
| **Nextcloud** | `cloud.masu.rs` | Native (official `user_oidc` app) | Nextcloud app + `nextcloud-occ user_oidc:provider` |
|
||||
| **Grafana** | `metrics.masu.rs` | Native (Generic OAuth) | NixOS config (`services.grafana.settings."auth.generic_oauth"`) |
|
||||
| **Paperless-ngx** | `paper.masu.rs` | Native (`django-allauth`) | NixOS env vars (`PAPERLESS_SOCIALACCOUNT_PROVIDERS`) |
|
||||
| **Mealie** | `cooking.masu.rs` | Native core feature | NixOS config (`services.mealie.settings` + `credentialsFile`) |
|
||||
| **Karakeep / Hoarder** | `keep.masu.rs` | Native (NextAuth OIDC) | NixOS env vars (`OAUTH_WELLKNOWN_URL`, etc.) |
|
||||
| **Audiobookshelf** | `read.masu.rs` | Native core feature (v2.3+) | Web UI (Settings → Authentication) |
|
||||
| **Actual Budget** | `money.masu.rs` | Native core feature (v24.3+) | NixOS config (`services.actual.settings.openId`) |
|
||||
|
||||
---
|
||||
|
||||
### Tier 2: OIDC via Plugins or Reverse Proxy Header Auth
|
||||
|
||||
These services do not have generic OIDC in their core web UI, but can support single sign-on through plugins or reverse proxy headers (`Remote-User` / `X-Forwarded-User`):
|
||||
|
||||
| Service | Hostname | Strategy | Notes |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Jellyfin** | `stream.masu.rs` | `jellyfin-plugin-sso` | Web clients work well; TV and native apps typically rely on Quick Connect. |
|
||||
| **Calibre-Web** | `books.masu.rs` | Reverse proxy header auth | Set `services.calibre-web.options.reverseProxyAuth.enable = true` behind an authenticating reverse proxy. |
|
||||
| **File Browser** | `files.masu.rs` | Reverse proxy header auth | Set `auth.method = "proxy"` and `auth.header = "X-Forwarded-User"` behind an authenticating reverse proxy. |
|
||||
| **Navidrome** | `music.masu.rs` | Reverse proxy header auth | Supports `ReverseProxyUserHeader` for web UI; Subsonic API clients (Feishin, etc.) still require native user passwords. |
|
||||
| **Stalwart** | `contacts.masu.rs` | OIDC Directory / SASL OAuth | Stalwart supports OIDC directories, but CardDAV/CalDAV clients usually require HTTP Basic Auth or application passwords. |
|
||||
|
||||
---
|
||||
|
||||
### Tier 3: Incompatible or No Native OIDC Support
|
||||
|
||||
- **Vaultwarden (`vault.masu.rs`):** Incompatible for vault decryption. Vaultwarden uses client-side zero-knowledge encryption where vault keys are derived from the user's master password. Bitwarden Enterprise SSO relies on a proprietary Key Connector that Vaultwarden does not implement.
|
||||
- **n8n (`n8n.masu.rs`):** SAML/OIDC SSO is an **Enterprise / commercial-only feature**; it is disabled in the free self-hosted Community edition.
|
||||
- **The Arr Stack (`download.masu.rs`):** Radarr, Sonarr, Lidarr, Readarr, Prowlarr, Bazarr, and Sabnzbd only support API keys, Basic Auth, or Forms.
|
||||
- **Transmission (`transmission.masu.rs`):** BitTorrent daemon; supports only HTTP Basic Auth / RPC whitelist.
|
||||
- **Uptime Kuma (`status.masu.rs`), ntfy (`ntfy.masu.rs`), The Lounge (`irc.masu.rs`), Pgweb (`pg.masu.rs`), Mathesar (`mathesar.masu.rs`), Hister (`hister.masu.rs`):** No native generic OIDC login mechanism.
|
||||
- **Infrastructure / Daemon Services:** PostgreSQL, InfluxDB, bind, avahi, cloudflared, wireguard, litestream (no user web UI).
|
||||
|
||||
---
|
||||
|
||||
## 2. General Setup Requirements
|
||||
|
||||
Configuring OIDC requires setup across four layers:
|
||||
|
||||
### A. Pocket ID Setup
|
||||
For each service, an OIDC client application must be registered in the Pocket ID admin interface:
|
||||
1. **Client ID:** A unique identifier slug (e.g. `gitea`, `immich`, `paperless`).
|
||||
2. **Client Secret:** A cryptographically secure random token.
|
||||
3. **Redirect URIs / Callback URLs:** The exact target URLs the service exposes for authorization code callbacks.
|
||||
4. **Scopes:** Usually `openid`, `profile`, and `email`.
|
||||
|
||||
### B. Secrets Management (agenix)
|
||||
All client secrets should be encrypted with `agenix` under the respective service directory:
|
||||
- Example: `platforms/nixos/modules/nmasur/presets/services/<service>/<service>-oidc.age`
|
||||
- Defined in NixOS under `config.secrets.<service>-oidc` with appropriate owner/group permissions.
|
||||
|
||||
### C. Network & Reverse Proxy (Caddy)
|
||||
1. **Back-Channel Connectivity:** When a user logs in, the service backend makes a server-to-server HTTPS call to `https://auth.masu.rs/api/oidc/token` to exchange the authorization code for tokens. Services hosted on `swan` (NAS) must be able to resolve and reach `auth.masu.rs` over HTTPS.
|
||||
2. **Proxy Headers:** Caddy's `reverse_proxy` handles `X-Forwarded-Proto`, `X-Forwarded-Host`, and `X-Forwarded-For` by default. Services should have reverse proxy trust enabled (e.g., `trusted_proxies = ["127.0.0.1"]`) so generated redirect URIs preserve the `https://` scheme.
|
||||
|
||||
---
|
||||
|
||||
## 3. Detailed Setup Requirements for Tier 1 Services
|
||||
|
||||
### 1. Immich (`photos.masu.rs`)
|
||||
- **Pocket ID Redirect URIs:**
|
||||
- Web: `https://photos.masu.rs/auth/login`
|
||||
- Mobile: `app.immich:///oauth-callback`
|
||||
- **Setup in `immich/immich.nix`:**
|
||||
```nix
|
||||
services.immich.settings.oauth = {
|
||||
enabled = true;
|
||||
issuerUrl = "https://${hostnames.auth}";
|
||||
clientId = "1f4e0f8d-6cee-4d67-8d53-74bf6c18ae09";
|
||||
clientSecret._secret = config.secrets.immich-oidc-secret.dest;
|
||||
scope = "openid profile email";
|
||||
autoRegister = true;
|
||||
buttonText = "Login with Pocket ID";
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Gitea (`git.masu.rs`)
|
||||
- **Pocket ID Redirect URI:** `https://git.masu.rs/user/oauth2/pocket-id/callback`
|
||||
- **Setup in Gitea:**
|
||||
Can be configured in the Web UI under **Site Administration → Authentication Sources** or via CLI:
|
||||
```bash
|
||||
gitea admin auth add-oauth \
|
||||
--name "Pocket ID" \
|
||||
--provider openidConnect \
|
||||
--key "<client_id>" \
|
||||
--secret "<client_secret>" \
|
||||
--auto-discover-url "https://auth.masu.rs/.well-known/openid-configuration"
|
||||
```
|
||||
- Optional: Enable automatic account linking by matching email.
|
||||
|
||||
### 3. Nextcloud (`cloud.masu.rs`)
|
||||
- **Pocket ID Redirect URI:** `https://cloud.masu.rs/apps/user_oidc/code`
|
||||
- **Setup in `nextcloud/nextcloud.nix`:**
|
||||
```nix
|
||||
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" ];
|
||||
};
|
||||
|
||||
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`)
|
||||
- **Pocket ID Redirect URI:** `https://metrics.masu.rs/login/generic_oauth`
|
||||
- **Setup in `grafana/grafana.nix`:**
|
||||
```nix
|
||||
services.grafana.settings = {
|
||||
auth.oauth_allow_insecure_email_lookup = true;
|
||||
"auth.generic_oauth" = {
|
||||
enabled = true;
|
||||
name = "Pocket ID";
|
||||
allow_sign_up = true;
|
||||
client_id = "85d879ed-1a86-4984-b33d-43806500ef98";
|
||||
client_secret = "$__file{${config.secrets.grafana-oidc-secret.dest}}";
|
||||
scopes = "openid profile email";
|
||||
auth_url = "https://${hostnames.auth}/authorize";
|
||||
token_url = "https://${hostnames.auth}/api/oidc/token";
|
||||
api_url = "https://${hostnames.auth}/api/oidc/userinfo";
|
||||
login_attribute_path = "preferred_username";
|
||||
skip_org_role_sync = true;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### 5. Paperless-ngx (`paper.masu.rs`)
|
||||
- **Pocket ID Redirect URI:** `https://paper.masu.rs/accounts/oidc/pocket-id/login/callback/`
|
||||
- **Setup in `paperless/paperless.nix`:**
|
||||
```nix
|
||||
services.paperless.settings = {
|
||||
PAPERLESS_APPS = "allauth.socialaccount.providers.openid_connect";
|
||||
PAPERLESS_REDIRECT_LOGIN_TO_SSO = true;
|
||||
};
|
||||
# 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`)
|
||||
- **Pocket ID Redirect URI:** `https://cooking.masu.rs/login`
|
||||
- **Setup in `mealie/mealie.nix`:**
|
||||
```nix
|
||||
services.mealie = {
|
||||
credentialsFile = config.secrets.mealie-oidc-secret.dest;
|
||||
settings = {
|
||||
OIDC_AUTH_ENABLED = "true";
|
||||
OIDC_SIGNUP_ENABLED = "true";
|
||||
OIDC_CONFIGURATION_URL = "https://${hostnames.auth}/.well-known/openid-configuration";
|
||||
OIDC_CLIENT_ID = "040925ed-b39e-4442-b8e8-369c948c0cd2";
|
||||
OIDC_PROVIDER_NAME = "Pocket ID";
|
||||
OIDC_USER_CLAIM = "email";
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### 7. Karakeep / Hoarder (`keep.masu.rs`)
|
||||
- **Pocket ID Redirect URI:** `https://keep.masu.rs/api/auth/callback/custom`
|
||||
- **Setup in `karakeep.nix`:**
|
||||
```nix
|
||||
services.karakeep.extraEnvironment = {
|
||||
OAUTH_WELLKNOWN_URL = "https://auth.masu.rs/.well-known/openid-configuration";
|
||||
OAUTH_CLIENT_ID = "hoarder";
|
||||
OAUTH_CLIENT_SECRET = "...";
|
||||
OAUTH_PROVIDER_NAME = "Pocket ID";
|
||||
OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING = "true";
|
||||
};
|
||||
```
|
||||
|
||||
### 8. Audiobookshelf (`read.masu.rs`)
|
||||
- **Pocket ID Redirect URIs:**
|
||||
- Web: `https://read.masu.rs/auth/openid/callback`
|
||||
- Mobile: `audiobookshelf://oauth`
|
||||
- **Setup in Audiobookshelf:**
|
||||
Configured in the Web UI (**Settings → Authentication → OpenID Connect**):
|
||||
- Issuer URL: `https://auth.masu.rs`
|
||||
- Client ID & Client Secret
|
||||
- Match user by email or username
|
||||
|
||||
### 9. Actual Budget (`money.masu.rs`)
|
||||
- **Pocket ID Redirect URI:** `https://money.masu.rs/openid/callback`
|
||||
- **Setup in `actualbudget/actualbudget.nix`:**
|
||||
```nix
|
||||
services.actual.settings = {
|
||||
loginMethod = "openid";
|
||||
openId = {
|
||||
discoveryURL = "https://${hostnames.auth}/.well-known/openid-configuration";
|
||||
client_id = "92afe9f8-7ef6-42ab-8a06-701df3c7179d";
|
||||
client_secret._secret = config.secrets.actualbudget-oidc-secret.dest;
|
||||
server_hostname = "https://${hostnames.budget}";
|
||||
authMethod = "openid";
|
||||
};
|
||||
};
|
||||
```
|
||||
*Note:* The optional end-to-end budget encryption password remains separate from the server authentication.
|
||||
|
||||
---
|
||||
|
||||
## 4. Forward Auth Architecture for Remaining Services
|
||||
|
||||
For services without native OIDC support (such as Calibre-Web, File Browser, Uptime Kuma, and the Arr stack), you can implement **Forward Auth via Caddy**:
|
||||
|
||||
1. Deploy an authenticating proxy (such as **OAuth2-Proxy** or **Authelia**) configured with Pocket ID as its OIDC provider.
|
||||
2. Configure Caddy routes using the `forward_auth` directive to verify user sessions with the proxy before forwarding requests to the target service.
|
||||
3. For services supporting reverse proxy authentication (Calibre-Web and File Browser), Caddy injects identity headers (e.g. `Remote-User: noah` or `X-Forwarded-User: noah`), enabling seamless single sign-on without requiring separate logins.
|
||||
Generated
+85
-30
@@ -1,10 +1,31 @@
|
||||
{
|
||||
"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": {
|
||||
"inputs": {
|
||||
"flake-parts": "flake-parts",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"systems": "systems",
|
||||
"systems": "systems_2",
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
},
|
||||
"locked": {
|
||||
@@ -151,6 +172,24 @@
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"systems": [
|
||||
"mac-app-util",
|
||||
@@ -170,9 +209,9 @@
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"flake-utils_3": {
|
||||
"inputs": {
|
||||
"systems": "systems_3"
|
||||
"systems": "systems_4"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
@@ -195,11 +234,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1787176219,
|
||||
"narHash": "sha256-djoRr6jBpe35q/0JwAvXpFXg1Ktf+X54NU2RLR7wnHw=",
|
||||
"lastModified": 1789183968,
|
||||
"narHash": "sha256-pEWnYdIF1pBMZwarp/rEPzl+Lknhm5hhjSfeLxH8nAA=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "c53d643b3737e2fcd04e6cb3b3580ef50b2087a0",
|
||||
"rev": "cd1c9e552f41894aeb5cc5cb353d5a1d61550357",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -213,9 +252,9 @@
|
||||
"inputs": {
|
||||
"cl-nix-lite": "cl-nix-lite",
|
||||
"flake-compat": "flake-compat",
|
||||
"flake-utils": "flake-utils",
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nixpkgs": "nixpkgs_3",
|
||||
"systems": "systems_2",
|
||||
"systems": "systems_3",
|
||||
"treefmt-nix": "treefmt-nix_2"
|
||||
},
|
||||
"locked": {
|
||||
@@ -259,11 +298,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1786852476,
|
||||
"narHash": "sha256-IM5CYtf86W4w8eUPpKcY/LpdHElmVBtJhaKnoTKxZEA=",
|
||||
"lastModified": 1788680125,
|
||||
"narHash": "sha256-amGSoDobwmp4CFvCn841ws2iuitus+HUJdD/gKgsrJA=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nix-index-database",
|
||||
"rev": "c7962dc97b45129df8d751bedaf37beb5a17706e",
|
||||
"rev": "116ad1c2adb642405ef8916f6a94c8626f971344",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -274,7 +313,7 @@
|
||||
},
|
||||
"nix2vim": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"flake-utils": "flake-utils_3",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
@@ -362,11 +401,11 @@
|
||||
},
|
||||
"nixpkgs-stable": {
|
||||
"locked": {
|
||||
"lastModified": 1787204541,
|
||||
"narHash": "sha256-OURZPknrTjQrlNyxPdqzyqmU/81Wes1CUP/Ft1Rv/YI=",
|
||||
"lastModified": 1789114715,
|
||||
"narHash": "sha256-ugpsyk3NM2s87vXfUiIIiibbJ4Pp0JPS5p/3mfs+q+c=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "5880666fd9eb563038431edb35c2d0aa595884e6",
|
||||
"rev": "21a67dc470149f337cecafbe965d8d252a390518",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -442,11 +481,11 @@
|
||||
},
|
||||
"nixpkgs_6": {
|
||||
"locked": {
|
||||
"lastModified": 1787135253,
|
||||
"narHash": "sha256-RD2kNWCG+Bjo6h+JVjWVNntZs2GtRoeY2xHjts/FNkA=",
|
||||
"lastModified": 1789006805,
|
||||
"narHash": "sha256-xB8mKMOx1IA9vTDNLmJZ6n4wCMq/cuWBBOzGCRnqxrU=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "ffb3c9b700e759be2ef13237c9d8f953b32a1e46",
|
||||
"rev": "8ce4ef6cb6f871616146b9fe26d2a5ae594e94fe",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -480,11 +519,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1787371589,
|
||||
"narHash": "sha256-5Z6kwy0f8qIW3QvEr7SYhJ5JK8RIsBKyMzB795362kM=",
|
||||
"lastModified": 1789200175,
|
||||
"narHash": "sha256-JriLm/izGMXB4uAHWXKj13kXa3nKZj2r/sR5xHdisgs=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nur",
|
||||
"rev": "aca4ff4662f393ef7f3ca88c8805c082cfc11b08",
|
||||
"rev": "2c789298fc525713063e3b88359d02101651144f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -495,6 +534,7 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"api": "api",
|
||||
"darwin": "darwin",
|
||||
"disko": "disko",
|
||||
"home-manager": "home-manager",
|
||||
@@ -517,11 +557,11 @@
|
||||
"nixpkgs": "nixpkgs_7"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1787367552,
|
||||
"narHash": "sha256-YT4Fs2k7bi+7YzuLt93EtIRgjpwHK5ZfsQEIh5dEQSk=",
|
||||
"lastModified": 1789196581,
|
||||
"narHash": "sha256-yJr1Bt4fKkKIpPYbsKGqJ0VFdoDnURgRwuffmBQ2WzY=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "fd2ebb9cc4323d0c5a1336138dab5c3c5a5d8bd9",
|
||||
"rev": "228ecefb6329d5a531b77b46b581a2f0c26ee056",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -546,6 +586,21 @@
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"lastModified": 1689347925,
|
||||
"narHash": "sha256-ozenz5bFe1UUqOn7f60HRmgc01BgTGIKZ4Xl+HbocGQ=",
|
||||
@@ -560,7 +615,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_3": {
|
||||
"systems_4": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
@@ -575,7 +630,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_4": {
|
||||
"systems_5": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
@@ -634,11 +689,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1784642409,
|
||||
"narHash": "sha256-hcbDqFuySAJawljt5r0sKBCJKYnbtGD0T/ZIozH1Dq0=",
|
||||
"lastModified": 1789164534,
|
||||
"narHash": "sha256-DoYGPM6QpnYBLWj9gGw6ZwAzIX+HrAVov1BoT+8Jixo=",
|
||||
"owner": "nix-community",
|
||||
"repo": "NixOS-WSL",
|
||||
"rev": "eaeb18da90024448a60eb1ec7132eafa4003404e",
|
||||
"rev": "72c92b11bb8289e6651c7fef29cc0a885fd6a255",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -655,7 +710,7 @@
|
||||
"rust-overlay": [
|
||||
"rust-overlay"
|
||||
],
|
||||
"systems": "systems_4"
|
||||
"systems": "systems_5"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1781873766,
|
||||
|
||||
@@ -86,6 +86,12 @@
|
||||
inputs.rust-overlay.follows = "rust-overlay";
|
||||
};
|
||||
|
||||
# Generic self-hosted REST API service
|
||||
api = {
|
||||
url = "github:nmasur/api";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
# # Text editor
|
||||
# helix = {
|
||||
# url = "github:helix-editor/helix";
|
||||
@@ -127,7 +133,9 @@
|
||||
baseName = "masu.rs";
|
||||
in
|
||||
{
|
||||
api = "api.${baseName}";
|
||||
audiobooks = "read.${baseName}";
|
||||
auth = "auth.${baseName}";
|
||||
bookmarks = "keep.${baseName}";
|
||||
books = "books.${baseName}";
|
||||
budget = "money.${baseName}";
|
||||
@@ -136,6 +144,7 @@
|
||||
download = "download.${baseName}";
|
||||
files = "files.${baseName}";
|
||||
git = "git.${baseName}";
|
||||
hister = "hister.${baseName}";
|
||||
imap = "imap.purelymail.com";
|
||||
influxdb = "influxdb.${baseName}";
|
||||
irc = "irc.${baseName}";
|
||||
|
||||
+5
-11
@@ -66,6 +66,7 @@ lib
|
||||
overlays = [
|
||||
inputs.nur.overlays.default
|
||||
inputs.nix2vim.overlay
|
||||
inputs.api.overlays.default
|
||||
(final: prev: {
|
||||
zellij-switch = inputs.zellij-switch.packages.${prev.stdenv.hostPlatform.system}.default;
|
||||
})
|
||||
@@ -105,6 +106,7 @@ lib
|
||||
# Build-time-only dep of karakeep's frontend; CVEs don't reach
|
||||
# the runtime closure. Remove once nixpkgs bumps it.
|
||||
"pnpm-9.15.9"
|
||||
"keybase-gui-6.5.1"
|
||||
];
|
||||
config.allowUnfree = true;
|
||||
}
|
||||
@@ -155,19 +157,10 @@ lib
|
||||
inputs.home-manager.nixosModules.home-manager
|
||||
inputs.nix-index-database.nixosModules.default
|
||||
inputs.disko.nixosModules.disko
|
||||
inputs.api.nixosModules.default
|
||||
inputs.wsl.nixosModules.wsl
|
||||
{ imports = (nixFiles ../platforms/nixos); }
|
||||
module
|
||||
# (builtins.removeAttrs module [ "home-manager" ])
|
||||
{
|
||||
home-manager = {
|
||||
extraSpecialArgs = {
|
||||
inherit colorscheme;
|
||||
}
|
||||
// specialArgs;
|
||||
}
|
||||
// homeModule.home-manager;
|
||||
}
|
||||
(builtins.removeAttrs module [ "home-manager" ])
|
||||
];
|
||||
};
|
||||
|
||||
@@ -228,6 +221,7 @@ lib
|
||||
inputs.home-manager.nixosModules.home-manager
|
||||
inputs.nix-index-database.nixosModules.default
|
||||
inputs.disko.nixosModules.disko
|
||||
inputs.api.nixosModules.default
|
||||
inputs.wsl.nixosModules.wsl
|
||||
{
|
||||
imports = (nixFiles ../platforms/nixos) ++ (nixFiles ../platforms/generators);
|
||||
|
||||
@@ -36,15 +36,18 @@ in
|
||||
quit-after-last-window-closed = lib.mkIf pkgs.stdenv.hostPlatform.isDarwin true;
|
||||
fullscreen = if pkgs.stdenv.hostPlatform.isDarwin then true else false;
|
||||
keybind = [
|
||||
"super+t=unbind" # Pass super-t to underlying tool (e.g. zellij tabs)
|
||||
"super+shift+]=unbind"
|
||||
"super+shift+[=unbind"
|
||||
"ctrl+tab=unbind"
|
||||
"ctrl+shift+tab=unbind"
|
||||
"ctrl+tab=text:\\x1b[9;5u"
|
||||
"ctrl+shift+tab=text:\\x1b[9;6u"
|
||||
"super+k=unbind"
|
||||
"super+shift+e=unbind"
|
||||
# Translate Mac Super & Ctrl combinations into Alt (ESC prefix) sequences
|
||||
# so Zellij receives them without needing Kitty keyboard protocol
|
||||
"super+t=text:\\x1bt"
|
||||
"super+shift+]=text:\\x1b}"
|
||||
"super+shift+[=text:\\x1b{"
|
||||
"ctrl+tab=text:\\x1b}"
|
||||
"ctrl+shift+tab=text:\\x1b{"
|
||||
"super+k=text:\\x1bK"
|
||||
"super+shift+e=text:\\x1bE"
|
||||
# Send CSI-u sequence for Alt+Shift+P / Super+Shift+P to bypass Zellij 0.45's DCS (ESC P) parser timeout (~1.5s delay)
|
||||
"alt+shift+p=text:\\x1b[112;4u"
|
||||
"super+shift+p=text:\\x1b[112;4u"
|
||||
];
|
||||
};
|
||||
themes."gruvbox" = {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.nmasur.presets.programs.lag-triage;
|
||||
|
||||
term-probe = pkgs.writeScriptBin "term-probe" ''
|
||||
#!${lib.getExe pkgs.python3}
|
||||
${builtins.readFile ./term_probe.py}
|
||||
'';
|
||||
in
|
||||
|
||||
{
|
||||
|
||||
options.nmasur.presets.programs.lag-triage.enable =
|
||||
lib.mkEnableOption "Terminal input-lag triage tools";
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
|
||||
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 = {
|
||||
lag-triage = {
|
||||
description = "Diagnose post-TUI typing lag in the current shell";
|
||||
body = builtins.readFile ./lag-triage.fish;
|
||||
};
|
||||
unlag = {
|
||||
description = "Reset terminal state left behind by a TUI";
|
||||
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"
|
||||
@@ -0,0 +1,182 @@
|
||||
# Guided diagnosis for the post-TUI typing-lag problem (Ghostty + Zellij + fish).
|
||||
# Run this IN THE LAGGING SHELL the moment you notice the lag, BEFORE starting
|
||||
# a new shell. It captures evidence, then applies targeted resets one at a time
|
||||
# so the stage that cures the lag identifies the layer holding stuck state.
|
||||
# Everything is logged for filing an upstream issue.
|
||||
|
||||
set -l logdir ~/.local/state/lag-triage
|
||||
mkdir -p $logdir
|
||||
set -l logfile $logdir/(date +%Y%m%d-%H%M%S).log
|
||||
|
||||
function _lt --inherit-variable logfile
|
||||
echo $argv | tee -a $logfile
|
||||
end
|
||||
|
||||
function _lt_ask --inherit-variable logfile
|
||||
# usage: _lt_ask VARNAME prompt... -> sets global $VARNAME (default: skip)
|
||||
set -l __name $argv[1]
|
||||
read -g -P "$argv[2..] " $__name
|
||||
or set -g $__name skip
|
||||
test -z "$$__name"; and set -g $__name skip
|
||||
echo "ANSWER $__name: $$__name" >>$logfile
|
||||
end
|
||||
|
||||
_lt "== lag-triage "(date)" =="
|
||||
_lt "Log: $logfile"
|
||||
_lt "Answer y / n, or press Enter to skip a question."
|
||||
_lt ""
|
||||
|
||||
# ---- 1. Context -------------------------------------------------------------
|
||||
_lt_ask ans_tui "Which TUI did you just exit (nvim/jjui/yazi/other)?"
|
||||
_lt_ask ans_launch "Launched via (f)loating-pane keybind or (c)ommand typed in this shell?"
|
||||
|
||||
# ---- 2. Snapshot ------------------------------------------------------------
|
||||
begin
|
||||
echo "-- snapshot --"
|
||||
fish --version
|
||||
echo "fish pid: $fish_pid, started: "(ps -o lstart= -p $fish_pid 2>/dev/null)
|
||||
zellij --version 2>/dev/null
|
||||
echo "escape delay: '$fish_escape_delay_ms' sequence delay: '$fish_sequence_key_delay_ms'"
|
||||
env | grep -iE '^(TERM|ZELLIJ|GHOSTTY|COLORTERM)' | sort
|
||||
echo "-- status features --"
|
||||
status features
|
||||
echo "-- stty -a --"
|
||||
stty -a
|
||||
end >>$logfile 2>&1
|
||||
_lt "Captured shell + environment snapshot."
|
||||
|
||||
# Proven root cause of the 2026-08 lag (see docs/CHANGELOG.md 2026-08-29):
|
||||
# fish latches feature flags from its startup env before config.fish runs, so
|
||||
# a shell with query-term ON sends terminal queries after every command; one
|
||||
# reply zellij fails to relay permanently degrades this process's reader.
|
||||
if status features | string match -qr '^query-term\s+on'
|
||||
_lt ""
|
||||
_lt "!! query-term is ON in this shell: fish did NOT get fish_features="
|
||||
_lt "!! no-query-term in its STARTUP environment (config.fish is too late)."
|
||||
_lt "!! This is the proven root cause of the post-TUI lag — a query reply"
|
||||
_lt "!! lost by zellij permanently degrades this fish process's reader."
|
||||
_lt "!! Fix: spawn fish with the variable exported (zellij default_shell"
|
||||
_lt "!! wrapper fish-no-query-term). Subshells are immune because they"
|
||||
_lt "!! inherit the exported variable — that's why a new shell 'fixes' it."
|
||||
else
|
||||
_lt "query-term is off in this shell (good — the known root cause is ruled out)."
|
||||
end
|
||||
|
||||
# ---- 3. Terminal state below the shell --------------------------------------
|
||||
_lt ""
|
||||
_lt "Querying terminal state (takes a few seconds)..."
|
||||
term-probe report 2>&1 | tee -a $logfile
|
||||
_lt ""
|
||||
_lt " ^ Things to look for: kitty flags with a reply > 0, modifyOtherKeys > 1,"
|
||||
_lt " any mouse/alternate-screen mode SET while at a shell prompt, or a slow"
|
||||
_lt " DA1 round-trip (> 100 ms means the input path itself is delayed)."
|
||||
|
||||
# ---- 4. Raw keystroke capture (bypasses fish entirely) ----------------------
|
||||
_lt ""
|
||||
_lt "Raw input capture: type ~10 characters at a steady pace, including one"
|
||||
_lt "ESC press and one arrow key. This shows the exact bytes this pane delivers"
|
||||
_lt "and their timing, with fish's input handling out of the picture."
|
||||
term-probe keylog 2>&1 | tee -a $logfile
|
||||
_lt_ask ans_keylog_instant "Did each keypress appear INSTANTLY in the capture? (y/n)"
|
||||
_lt_ask ans_keylog_plain "Were plain letters single plain bytes like b'a' (not escape sequences)? (y/n)"
|
||||
|
||||
# ---- 5. Scope ---------------------------------------------------------------
|
||||
_lt ""
|
||||
_lt_ask ans_scope_pane "Optional: open a NEW zellij pane/tab and type — laggy there too? (y/n)"
|
||||
_lt_ask ans_scope_window "Optional: type in a separate Ghostty window (outside this zellij session) — laggy? (y/n)"
|
||||
|
||||
# ---- 6. Staged resets -------------------------------------------------------
|
||||
# Each stage resets one category of state a TUI could have left behind.
|
||||
# The first stage that cures the lag names the culprit.
|
||||
set -l fixed none
|
||||
|
||||
_lt ""
|
||||
_lt "Now applying resets one at a time. After each, type into the test prompt"
|
||||
_lt "to judge whether the lag is gone."
|
||||
_lt "CAVEAT: fish's read prompt may NOT exhibit lag even when the main"
|
||||
_lt "commandline does. If typing at these test prompts never feels laggy at"
|
||||
_lt "all, answer 'u' (unsure) instead of 'y' — a 'y' here is only meaningful"
|
||||
_lt "if you could feel the lag at the test prompts before the reset."
|
||||
|
||||
if test $fixed = none
|
||||
_lt ""
|
||||
_lt "Stage A - kitty keyboard protocol: pop stack + clear all flags"
|
||||
printf '\e[<9u\e[=0;1u'
|
||||
_lt_ask ans_stage_a " Test typing here, then Enter — lag gone? (y/n)"
|
||||
test "$ans_stage_a" = y; and set fixed "A (kitty keyboard state)"
|
||||
end
|
||||
|
||||
if test $fixed = none
|
||||
_lt "Stage B - modifyOtherKeys off"
|
||||
printf '\e[>4;0m'
|
||||
_lt_ask ans_stage_b " Test typing here, then Enter — lag gone? (y/n)"
|
||||
test "$ans_stage_b" = y; and set fixed "B (modifyOtherKeys)"
|
||||
end
|
||||
|
||||
if test $fixed = none
|
||||
_lt "Stage C - normal keypad + normal cursor keys"
|
||||
printf '\e>\e[?1l'
|
||||
_lt_ask ans_stage_c " Test typing here, then Enter — lag gone? (y/n)"
|
||||
test "$ans_stage_c" = y; and set fixed "C (application keypad/cursor mode)"
|
||||
end
|
||||
|
||||
if test $fixed = none
|
||||
_lt "Stage D - disable mouse, focus reporting, synchronized output"
|
||||
printf '\e[?1000l\e[?1001l\e[?1002l\e[?1003l\e[?1005l\e[?1006l\e[?1015l\e[?1016l\e[?1004l\e[?2026l'
|
||||
_lt_ask ans_stage_d " Test typing here, then Enter — lag gone? (y/n)"
|
||||
test "$ans_stage_d" = y; and set fixed "D (mouse/focus/sync modes)"
|
||||
end
|
||||
|
||||
if test $fixed = none
|
||||
_lt "Stage E - leave alternate screen"
|
||||
printf '\e[?1049l'
|
||||
_lt_ask ans_stage_e " Test typing here, then Enter — lag gone? (y/n)"
|
||||
test "$ans_stage_e" = y; and set fixed "E (alternate screen)"
|
||||
end
|
||||
|
||||
if test $fixed = none
|
||||
_lt "Stage F - stty sane (line-discipline reset)"
|
||||
stty sane
|
||||
_lt_ask ans_stage_f " Test typing here, then Enter — lag gone? (y/n)"
|
||||
test "$ans_stage_f" = y; and set fixed "F (termios/line discipline)"
|
||||
end
|
||||
|
||||
if test $fixed = none
|
||||
_lt "Stage G - DECSTR soft terminal reset"
|
||||
printf '\e[!p'
|
||||
_lt_ask ans_stage_g " Test typing here, then Enter — lag gone? (y/n)"
|
||||
test "$ans_stage_g" = y; and set fixed "G (DECSTR-resettable mode)"
|
||||
end
|
||||
|
||||
# ---- 7. Verdict --------------------------------------------------------------
|
||||
_lt ""
|
||||
_lt "== Verdict =="
|
||||
if test $fixed != none
|
||||
_lt "Lag cleared by stage $fixed."
|
||||
_lt "That state was stuck BELOW fish — in the Zellij pane or relayed to"
|
||||
_lt "Ghostty — and the TUI you exited ($ans_tui) failed to restore it, or"
|
||||
_lt "Zellij failed to restore it when the pane closed."
|
||||
_lt "Re-probing terminal state after the fix for comparison:"
|
||||
term-probe report 2>&1 | tee -a $logfile
|
||||
_lt ""
|
||||
_lt "-> File this log against zellij (or ghostty, if a separate window also"
|
||||
_lt " lagged). The before/after probe diff pinpoints the exact stuck mode."
|
||||
else if test "$ans_keylog_instant" = y; and test "$ans_keylog_plain" = y
|
||||
_lt "Raw input reaches this pane instantly as plain bytes, and no terminal"
|
||||
_lt "state reset helps: the lag lives INSIDE this fish process (reader state)."
|
||||
_lt "Confirm now: run 'exec fish' — if that cures it, it is fish-internal."
|
||||
_lt ""
|
||||
_lt "-> To catch it in the act, run your next long-lived shell as:"
|
||||
_lt " FISH_DEBUG='reader,term-support' FISH_DEBUG_OUTPUT=$logdir/fish-debug.log fish"
|
||||
_lt " then re-run lag-triage when it recurs and file both logs to fish-shell."
|
||||
else
|
||||
_lt "Keystrokes were delayed or arrived as escape sequences BEFORE fish saw"
|
||||
_lt "them: the problem is in Zellij (client stdin parser / server) or Ghostty."
|
||||
_lt " new pane also laggy: $ans_scope_pane (y -> session-wide, not this pane)"
|
||||
_lt " separate window laggy: $ans_scope_window (y -> Ghostty itself)"
|
||||
_lt "-> File this log against zellij; include the keylog byte capture."
|
||||
end
|
||||
_lt ""
|
||||
_lt "Full log: $logfile"
|
||||
|
||||
functions -e _lt _lt_ask
|
||||
@@ -0,0 +1,165 @@
|
||||
"""Probe the terminal state of the current pane, below the shell.
|
||||
|
||||
Modes:
|
||||
report - query kitty-keyboard flags, modifyOtherKeys, and DEC private
|
||||
modes directly on /dev/tty, reporting each reply (or lack of
|
||||
one) and its round-trip latency. Answers may come from Zellij
|
||||
(pane state) or be relayed from Ghostty (window state).
|
||||
keylog - raw-mode keystroke capture: prints the exact bytes and
|
||||
inter-key latency for every keypress, bypassing the shell's
|
||||
input machinery entirely. Press q to finish.
|
||||
|
||||
Used by the `lag-triage` fish function to pin down which layer
|
||||
(fish / zellij / ghostty) is holding stuck state when typing lags
|
||||
after a TUI exits.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import select
|
||||
import sys
|
||||
import termios
|
||||
import time
|
||||
import tty
|
||||
|
||||
# DECRQM reply values
|
||||
DECRQM_VALUES = {
|
||||
"0": "not recognized",
|
||||
"1": "SET",
|
||||
"2": "reset",
|
||||
"3": "permanently set",
|
||||
"4": "permanently reset",
|
||||
}
|
||||
|
||||
DEC_MODES = [
|
||||
(1, "application cursor keys (DECCKM)"),
|
||||
(25, "cursor visible"),
|
||||
(1000, "mouse click reporting"),
|
||||
(1002, "mouse drag reporting"),
|
||||
(1003, "mouse all-motion reporting"),
|
||||
(1004, "focus reporting"),
|
||||
(1006, "SGR mouse encoding"),
|
||||
(1049, "alternate screen"),
|
||||
(2004, "bracketed paste"),
|
||||
(2026, "synchronized output"),
|
||||
(2031, "color theme reporting"),
|
||||
]
|
||||
|
||||
QUERIES = [
|
||||
("kitty keyboard flags (\\e[?u)", b"\x1b[?u", rb"\x1b\[\?(\d+)u", None),
|
||||
("modifyOtherKeys (XTQMODKEYS)", b"\x1b[?4m", rb"\x1b\[>4;(\d+)m", None),
|
||||
("background color (OSC 11)", b"\x1b]11;?\x1b\\", rb"\x1b\]11;([^\x07\x1b]+)", None),
|
||||
] + [
|
||||
(
|
||||
f"DEC mode {num} — {desc}",
|
||||
b"\x1b[?%d$p" % num,
|
||||
rb"\x1b\[\?%d;(\d+)\$y" % num,
|
||||
DECRQM_VALUES,
|
||||
)
|
||||
for num, desc in DEC_MODES
|
||||
]
|
||||
|
||||
|
||||
def read_for(fd, seconds):
|
||||
buf = b""
|
||||
end = time.monotonic() + seconds
|
||||
while True:
|
||||
remaining = end - time.monotonic()
|
||||
if remaining <= 0:
|
||||
break
|
||||
r, _, _ = select.select([fd], [], [], remaining)
|
||||
if not r:
|
||||
break
|
||||
buf += os.read(fd, 4096)
|
||||
return buf
|
||||
|
||||
|
||||
def report(fd):
|
||||
lines = []
|
||||
raw_dump = b""
|
||||
for label, query, pattern, value_names in QUERIES:
|
||||
raw_dump += read_for(fd, 0.02) # drain stragglers
|
||||
start = time.monotonic()
|
||||
os.write(fd, query)
|
||||
buf = b""
|
||||
match = None
|
||||
deadline = time.monotonic() + 0.35
|
||||
while time.monotonic() < deadline:
|
||||
buf += read_for(fd, 0.05)
|
||||
match = re.search(pattern, buf)
|
||||
if match:
|
||||
break
|
||||
raw_dump += buf
|
||||
if match:
|
||||
latency = (time.monotonic() - start) * 1000
|
||||
value = match.group(1).decode("ascii", "replace")
|
||||
if value_names:
|
||||
value = f"{value} ({value_names.get(value, '?')})"
|
||||
lines.append(f" {label:45s} = {value:24s} [{latency:6.1f} ms]")
|
||||
else:
|
||||
lines.append(f" {label:45s} = (no reply)")
|
||||
|
||||
# DA1 as a fence: every terminal answers it, so its round-trip time
|
||||
# measures the whole input path (ghostty -> zellij -> pane -> here).
|
||||
start = time.monotonic()
|
||||
os.write(fd, b"\x1b[c")
|
||||
buf = b""
|
||||
match = None
|
||||
deadline = time.monotonic() + 2.0
|
||||
while time.monotonic() < deadline:
|
||||
buf += read_for(fd, 0.05)
|
||||
match = re.search(rb"\x1b\[\?([0-9;]*)c", buf)
|
||||
if match:
|
||||
break
|
||||
raw_dump += buf
|
||||
if match:
|
||||
latency = (time.monotonic() - start) * 1000
|
||||
lines.append(
|
||||
f" {'device attributes (DA1) round-trip':45s} = "
|
||||
f"{match.group(1).decode():24s} [{latency:6.1f} ms]"
|
||||
)
|
||||
else:
|
||||
lines.append(f" {'device attributes (DA1) round-trip':45s} = (NO REPLY in 2s!)")
|
||||
lines.append(f" raw bytes received: {raw_dump!r}")
|
||||
return lines
|
||||
|
||||
|
||||
def keylog(fd):
|
||||
sys.stdout.write("keylog: capturing raw bytes from the tty. Press q to finish.\r\n")
|
||||
sys.stdout.flush()
|
||||
last = time.monotonic()
|
||||
while True:
|
||||
select.select([fd], [], [], None)
|
||||
data = os.read(fd, 4096)
|
||||
now = time.monotonic()
|
||||
delta_ms = (now - last) * 1000
|
||||
last = now
|
||||
sys.stdout.write(f" +{delta_ms:8.1f} ms {data!r} hex={data.hex(' ')}\r\n")
|
||||
sys.stdout.flush()
|
||||
if data in (b"q", b"\x03", b"\x04"):
|
||||
break
|
||||
|
||||
|
||||
def main():
|
||||
mode = sys.argv[1] if len(sys.argv) > 1 else "report"
|
||||
fd = os.open("/dev/tty", os.O_RDWR)
|
||||
old = termios.tcgetattr(fd)
|
||||
lines = None
|
||||
try:
|
||||
tty.setraw(fd)
|
||||
if mode == "report":
|
||||
lines = report(fd)
|
||||
elif mode == "keylog":
|
||||
keylog(fd)
|
||||
else:
|
||||
raise SystemExit(f"unknown mode: {mode}")
|
||||
finally:
|
||||
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
||||
os.close(fd)
|
||||
if lines:
|
||||
print("terminal state as seen from this pane:")
|
||||
print("\n".join(lines))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,15 @@
|
||||
# One-shot reset of terminal state a TUI may have left behind (kitty keyboard
|
||||
# flags, modifyOtherKeys, application keypad/cursor, mouse/focus reporting,
|
||||
# alternate screen, termios). fish re-enables the modes it wants at the next
|
||||
# prompt, so this is safe to run any time.
|
||||
#
|
||||
# Diagnostic value: if this cures the lag, the stuck state was below fish
|
||||
# (run lag-triage next time to find which mode). If only `exec fish` cures
|
||||
# it, the lag is inside the fish process itself.
|
||||
printf '\e[<9u\e[=0;1u'
|
||||
printf '\e[>4;0m'
|
||||
printf '\e>\e[?1l'
|
||||
printf '\e[?1000l\e[?1001l\e[?1002l\e[?1003l\e[?1005l\e[?1006l\e[?1015l\e[?1016l\e[?1004l\e[?2026l'
|
||||
printf '\e[?1049l'
|
||||
stty sane
|
||||
echo "terminal state reset — if typing still lags, run lag-triage (before exec fish!)"
|
||||
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Deterministic repro of permanent fish reader degradation (fish 4.8.1).
|
||||
|
||||
This is evidence for an upstream fish-shell report, and the proof behind the
|
||||
fish-no-query-term wrapper in presets/programs/zellij.nix. Not installed by
|
||||
the nix module; run directly: python3 upstream_repro.py [queryterm|noqueryterm]
|
||||
|
||||
Finding: with the query-term feature enabled (latched from the startup env,
|
||||
which is fish's default), fish sends OSC 11 + CPR (\e[6n) + DA1 (\e[0c)
|
||||
after every external command and waits for the replies. If the terminal
|
||||
fails to reply during ONE such cycle -- even though it answered every query
|
||||
before and answers every query after -- that fish process's interactive
|
||||
reader is PERMANENTLY degraded: keystrokes are no longer echoed (>3s each,
|
||||
never recovers). In production this happens when zellij drops/mis-relays a
|
||||
reply during TUI teardown or heavy output (cf. zellij-org/zellij#5158), and
|
||||
it presents as permanent typing lag cured only by replacing the process.
|
||||
With fish_features=no-query-term in the startup environment, the same
|
||||
sequence has zero effect (~35ms echo throughout).
|
||||
|
||||
Phases:
|
||||
A. terminal answers all queries -> echo ~35ms (both variants)
|
||||
B. replies dropped for one command -> queryterm: echo dead, permanently
|
||||
C. replies restored, another command -> queryterm: still dead
|
||||
"""
|
||||
import os, pty, re, select, subprocess, sys, time, fcntl, termios
|
||||
|
||||
VARIANT = sys.argv[1] if len(sys.argv) > 1 else "queryterm"
|
||||
|
||||
env = dict(os.environ)
|
||||
env["TERM"] = "xterm-ghostty"
|
||||
env["ZELLIJ"] = "0"
|
||||
env["ZELLIJ_SESSION_NAME"] = "repro"
|
||||
env["FISH_DEBUG"] = "term-support"
|
||||
env["FISH_DEBUG_OUTPUT"] = f"/tmp/fish-lagrepro-{VARIANT}.log"
|
||||
env.pop("fish_features", None)
|
||||
if VARIANT == "noqueryterm":
|
||||
env["fish_features"] = "no-query-term"
|
||||
|
||||
master, slave = pty.openpty()
|
||||
# give it a size
|
||||
fcntl.ioctl(master, termios.TIOCSWINSZ, b"\x00\x28\x00\x78\x00\x00\x00\x00")
|
||||
proc = subprocess.Popen(
|
||||
["fish", "-i", "--no-config"],
|
||||
stdin=slave, stdout=slave, stderr=slave, env=env,
|
||||
preexec_fn=lambda: (os.setsid(), fcntl.ioctl(0, termios.TIOCSCTTY, 0)),
|
||||
close_fds=True,
|
||||
)
|
||||
os.close(slave)
|
||||
|
||||
RESPOND = True
|
||||
transcript = []
|
||||
|
||||
def respond(data):
|
||||
"""Answer terminal queries the way a well-behaved terminal would."""
|
||||
out = b""
|
||||
for m in re.finditer(rb"\x1b\[6n", data):
|
||||
out += b"\x1b[40;1R" # CPR
|
||||
for m in re.finditer(rb"\x1b\[0?c", data):
|
||||
out += b"\x1b[?62;22c" # DA1
|
||||
for m in re.finditer(rb"\x1b\[\?u", data):
|
||||
out += b"\x1b[?0u" # kitty flags
|
||||
for m in re.finditer(rb"\x1b\]11;\?", data):
|
||||
out += b"\x1b]11;rgb:2828/2828/2828\x1b\\" # OSC 11
|
||||
for m in re.finditer(rb"\x1b\[>0?q", data):
|
||||
out += b"\x1bP>|ghostty 1.3.1\x1b\\" # XTVERSION
|
||||
for m in re.finditer(rb"\x1bP\+q[0-9a-fA-F;]+\x1b\\", data):
|
||||
out += b"\x1bP0+r\x1b\\" # XTGETTCAP: not found
|
||||
for m in re.finditer(rb"\x1b\[\?(\d+)\$p", data):
|
||||
out += b"\x1b[?%s;2$y" % m.group(1) # DECRQM: reset
|
||||
return out
|
||||
|
||||
def pump(timeout):
|
||||
"""Read fish output for `timeout` seconds, answering queries if RESPOND."""
|
||||
buf = b""
|
||||
end = time.monotonic() + timeout
|
||||
while time.monotonic() < end:
|
||||
r, _, _ = select.select([master], [], [], 0.03)
|
||||
if r:
|
||||
try:
|
||||
data = os.read(master, 65536)
|
||||
except OSError:
|
||||
return buf
|
||||
buf += data
|
||||
transcript.append(data)
|
||||
if RESPOND:
|
||||
reply = respond(data)
|
||||
if reply:
|
||||
os.write(master, reply)
|
||||
return buf
|
||||
|
||||
def send(s):
|
||||
os.write(master, s if isinstance(s, bytes) else s.encode())
|
||||
|
||||
def measure_echo(chars, settle=0.1):
|
||||
"""Send chars one at a time; measure time until each is echoed."""
|
||||
results = []
|
||||
for ch in chars:
|
||||
pump(settle)
|
||||
t0 = time.monotonic()
|
||||
send(ch)
|
||||
deadline = time.monotonic() + 3.0
|
||||
latency = None
|
||||
buf = b""
|
||||
while time.monotonic() < deadline:
|
||||
buf += pump(0.02)
|
||||
if ch.encode() in buf:
|
||||
latency = (time.monotonic() - t0) * 1000
|
||||
break
|
||||
results.append((ch, latency))
|
||||
return results
|
||||
|
||||
print(f"=== variant: {VARIANT} ===")
|
||||
pump(1.2) # startup, queries answered
|
||||
|
||||
send("echo warmup\r")
|
||||
pump(0.8)
|
||||
|
||||
print("phase A: terminal responsive, echo latency per key:")
|
||||
for ch, ms in measure_echo("abcde"):
|
||||
print(f" {ch}: {ms:.0f} ms" if ms else f" {ch}: NO ECHO in 3s")
|
||||
send("\x15") # ctrl-u clear line
|
||||
pump(0.3)
|
||||
|
||||
# Run external command, then STOP answering queries (simulate lost relay)
|
||||
send("sh -c true\r")
|
||||
time.sleep(0.05)
|
||||
RESPOND = False
|
||||
pump(1.0)
|
||||
|
||||
print("phase B: after external command with query replies DROPPED:")
|
||||
for ch, ms in measure_echo("fghij"):
|
||||
print(f" {ch}: {ms:.0f} ms" if ms else f" {ch}: NO ECHO in 3s")
|
||||
send("\x15")
|
||||
pump(0.3)
|
||||
|
||||
# Does it persist across further commands, with responses restored?
|
||||
RESPOND = True
|
||||
send("sh -c true\r")
|
||||
pump(1.0)
|
||||
print("phase C: responses restored, after another external command:")
|
||||
for ch, ms in measure_echo("klmno"):
|
||||
print(f" {ch}: {ms:.0f} ms" if ms else f" {ch}: NO ECHO in 3s")
|
||||
|
||||
send("\x15")
|
||||
pump(0.2)
|
||||
send("exit\r")
|
||||
pump(0.5)
|
||||
try:
|
||||
proc.wait(timeout=3)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
@@ -9,6 +9,35 @@ let
|
||||
inherit (config.nmasur.settings) username;
|
||||
cfg = config.nmasur.presets.programs.zellij;
|
||||
|
||||
# fish latches feature flags from its startup ENVIRONMENT before config.fish
|
||||
# runs, so the `set -gx fish_features no-query-term` in config.fish only
|
||||
# protects CHILD fish processes — which is why subshells/exec fish were always
|
||||
# immune to the post-TUI typing lag while zellij-spawned pane shells were not.
|
||||
# With query-term latched on, fish sends OSC 11 + CPR + DA1 queries after
|
||||
# every command and waits for replies; if zellij fails to relay even one
|
||||
# reply (a race during TUI teardown or heavy output), that fish process's
|
||||
# reader is PERMANENTLY degraded — reproduced deterministically in a PTY
|
||||
# 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
|
||||
# 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" ''
|
||||
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} "$@"
|
||||
'';
|
||||
|
||||
zellij-switch-to-last = pkgs.writeShellScriptBin "zellij-switch-to-last" ''
|
||||
TARGET_SESSION=$(cat ~/.local/state/zellij-last-session)
|
||||
if [ -z "$TARGET_SESSION" ]; then
|
||||
@@ -25,8 +54,6 @@ in
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
|
||||
home.packages = [ pkgs.zellij-switch ];
|
||||
|
||||
programs.fish = {
|
||||
shellAbbrs.z = "zellij";
|
||||
functions = {
|
||||
@@ -130,10 +157,22 @@ in
|
||||
# Spawn fish directly instead of trusting $SHELL, which inherits the
|
||||
# macOS login shell. On darwin that login shell is no longer managed by
|
||||
# nix-darwin, so $SHELL can point at a stale /run/current-system path.
|
||||
default_shell = lib.getExe pkgs.fish;
|
||||
# Wrapped to export fish_features=no-query-term BEFORE fish starts —
|
||||
# see the fish-no-query-term comment above for why this must happen in
|
||||
# the environment rather than in config.fish.
|
||||
default_shell = lib.getExe fish-no-query-term;
|
||||
# default_layout = "compact-top";
|
||||
# Remove border
|
||||
pane_frames = false;
|
||||
# Ghostty + Zellij have several open upstream bugs where the Kitty
|
||||
# keyboard protocol's "enhancement" flags get left in a stuck/elevated
|
||||
# state after a full-screen TUI exits (e.g. zellij-org/zellij#3887,
|
||||
# #3723, #4178), causing every subsequent keystroke to be sent as a
|
||||
# CSI-u sequence that fish has to wait out an escape-timeout to
|
||||
# disambiguate. This shows up as typing lag that builds up the longer
|
||||
# you were inside the TUI, until the pane's protocol state resets.
|
||||
# Disabling it entirely avoids the whole bug class.
|
||||
support_kitty_keyboard_protocol = false;
|
||||
# Scrollback
|
||||
scrollback_editor = config.home.sessionVariables.EDITOR;
|
||||
|
||||
@@ -142,19 +181,23 @@ in
|
||||
keybinds = {
|
||||
locked = lib.mkIf (pkgs.stdenv.hostPlatform.isLinux && !config.programs.ghostty.enable) {
|
||||
# For servers that I SSH into, use c-a-G to unlock
|
||||
"bind \"Ctrl Alt G\"" = lib.mkIf (pkgs.stdenv.hostPlatform.isLinux && !config.programs.ghostty.enable) {
|
||||
SwitchToMode = {
|
||||
_args = [ "normal" ];
|
||||
};
|
||||
};
|
||||
"bind \"Ctrl Alt G\"" =
|
||||
lib.mkIf (pkgs.stdenv.hostPlatform.isLinux && !config.programs.ghostty.enable)
|
||||
{
|
||||
SwitchToMode = {
|
||||
_args = [ "normal" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
normal = lib.mkIf (pkgs.stdenv.hostPlatform.isLinux && !config.programs.ghostty.enable) {
|
||||
# For servers that I SSH into, use c-a-G to lock
|
||||
"bind \"Ctrl Alt G\"" = lib.mkIf (pkgs.stdenv.hostPlatform.isLinux && !config.programs.ghostty.enable) {
|
||||
SwitchToMode = {
|
||||
_args = [ "locked" ];
|
||||
};
|
||||
};
|
||||
"bind \"Ctrl Alt G\"" =
|
||||
lib.mkIf (pkgs.stdenv.hostPlatform.isLinux && !config.programs.ghostty.enable)
|
||||
{
|
||||
SwitchToMode = {
|
||||
_args = [ "locked" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
session = {
|
||||
"bind \"w\"" = {
|
||||
@@ -286,9 +329,33 @@ in
|
||||
"bind \"Super Shift ]\"" = {
|
||||
GoToNextTab = { };
|
||||
};
|
||||
"bind \"Alt ]\"" = {
|
||||
GoToNextTab = { };
|
||||
};
|
||||
"bind \"Alt }\"" = {
|
||||
GoToNextTab = { };
|
||||
};
|
||||
"bind \"Alt Shift ]\"" = {
|
||||
GoToNextTab = { };
|
||||
};
|
||||
"bind \"Alt Shift }\"" = {
|
||||
GoToNextTab = { };
|
||||
};
|
||||
"bind \"Super Shift [\"" = {
|
||||
GoToPreviousTab = { };
|
||||
};
|
||||
"bind \"Alt [\"" = {
|
||||
GoToPreviousTab = { };
|
||||
};
|
||||
"bind \"Alt {\"" = {
|
||||
GoToPreviousTab = { };
|
||||
};
|
||||
"bind \"Alt Shift [\"" = {
|
||||
GoToPreviousTab = { };
|
||||
};
|
||||
"bind \"Alt Shift {\"" = {
|
||||
GoToPreviousTab = { };
|
||||
};
|
||||
"bind \"Ctrl Tab\"" = {
|
||||
GoToNextTab = { };
|
||||
};
|
||||
@@ -298,7 +365,7 @@ in
|
||||
"bind \"Super t\"" = lib.mkIf pkgs.stdenv.hostPlatform.isDarwin {
|
||||
NewTab = { };
|
||||
};
|
||||
"bind \"Alt t\"" = lib.mkIf pkgs.stdenv.hostPlatform.isLinux {
|
||||
"bind \"Alt t\"" = {
|
||||
NewTab = { };
|
||||
};
|
||||
"bind \"Super k\"" = lib.mkIf pkgs.stdenv.hostPlatform.isDarwin {
|
||||
@@ -306,13 +373,18 @@ in
|
||||
_args = [ "scroll" ];
|
||||
};
|
||||
};
|
||||
"bind \"Alt Shift k\"" = {
|
||||
SwitchToMode = {
|
||||
_args = [ "scroll" ];
|
||||
};
|
||||
};
|
||||
"bind \"Super Shift e\"" = lib.mkIf pkgs.stdenv.hostPlatform.isDarwin {
|
||||
EditScrollback = { };
|
||||
SwitchToMode = {
|
||||
_args = [ "locked" ];
|
||||
};
|
||||
};
|
||||
"bind \"Alt Shift e\"" = lib.mkIf pkgs.stdenv.hostPlatform.isLinux {
|
||||
"bind \"Alt Shift e\"" = {
|
||||
EditScrollback = { };
|
||||
SwitchToMode = {
|
||||
_args = [ "locked" ];
|
||||
|
||||
@@ -63,6 +63,7 @@ in
|
||||
git.enable = lib.mkDefault true;
|
||||
helix.enable = lib.mkDefault true;
|
||||
jujutsu.enable = lib.mkDefault true;
|
||||
lag-triage.enable = lib.mkDefault true;
|
||||
lazygit.enable = lib.mkDefault true;
|
||||
# neovim.enable = lib.mkDefault true;
|
||||
nix-index.enable = lib.mkDefault true;
|
||||
|
||||
@@ -47,6 +47,7 @@ in
|
||||
host = cfg.host;
|
||||
passwordeval = "${pkgs.age}/bin/age --decrypt --identity ${config.identityFile} ${pkgs.writeText "mailpass-system.age" (builtins.readFile cfg.passwordFile)}";
|
||||
user = cfg.user;
|
||||
from = cfg.user;
|
||||
from_full_name = "${config.networking.hostName} System";
|
||||
port = 465;
|
||||
tls = true;
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IE1nSGFPdyBycCtF
|
||||
S1ErMkNoME1ZNzYramZ6cFJGL1VLbU1sSnVBaTA2Y3B2M1FUYTM0Cnl1a0dmdEpt
|
||||
U3puRS9RUEUyVVBDelRRRGlGQXdWQzc4cHprZGNRb2RuYmcKLT4gc3NoLWVkMjU1
|
||||
MTkgWXlTVU1RIDJ1M1dmTnB4Vzg3WDZ3QzZDZ1phYTU4YUk0U1VmV2M5cHZ2bENE
|
||||
eEE5aDQKODFrOWJBWjE0WFJKRTVrb2dlTmVHdEVHeVc4MU9GRHhTY1l5Mzh1YnZo
|
||||
dwotPiBzc2gtZWQyNTUxOSBuanZYNUEgM2JNTmVuTCtiSnNOS2UybE5qQXptcW92
|
||||
QngyaGh5Y0VqOGZEaWpkazYzawpjZmExWmF5MjV0NE05ZXVzenhYVXpRaGg4R25x
|
||||
QkpFa080S3haTXpTUi80Ci0+IHNzaC1lZDI1NTE5IENxSU9VQSBQcjgrV0c3S21I
|
||||
RXY5WnlSdWRML1g4ZmtvNWVmTGNwb3owZUFWN0s0ckhZCm1PbGtKY1hOanBGU0V6
|
||||
MzRrd3lPSUIrUVY3ZW5VTzFrWnJjblNvZmVIYk0KLT4gc3NoLWVkMjU1MTkgejFP
|
||||
Y1p3IGdoRHVhbG52OE5wUEFwWjlvb0xZSUpOS0loNm1QOWZ0N2RGZ2xMcW5RQXcK
|
||||
UUpNdDh1aUN0cHM2N0M3bGMvcjIwT2YrV01sdjdBQThkM3BxQ1hXY1A3bwotLS0g
|
||||
K21sZndObVJLdkNBR3IvWHZ3c0RjSEJQajFUV2tFUmNXWDQ3RU1DNzJKcwon7wF2
|
||||
i2V9/fXS9QBfTvUJGP9MQ1gCe+UxstI3IPv+ODudHLeCGT0JoxkN6DZtayxKYYJn
|
||||
iKghS8tUNdzshOBqpA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
@@ -32,6 +32,22 @@ in
|
||||
enable = true;
|
||||
settings = {
|
||||
port = cfg.port;
|
||||
loginMethod = "openid";
|
||||
openId = {
|
||||
discoveryURL = "https://${hostnames.auth}/.well-known/openid-configuration";
|
||||
client_id = "92afe9f8-7ef6-42ab-8a06-701df3c7179d";
|
||||
client_secret._secret = config.secrets.actualbudget-oidc-secret.dest;
|
||||
server_hostname = "https://${hostnames.budget}";
|
||||
authMethod = "openid";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.actual = {
|
||||
after = [ "actualbudget-oidc-secret-secret.service" ];
|
||||
serviceConfig = {
|
||||
PrivateUsers = lib.mkForce false;
|
||||
SupplementaryGroups = [ "shared" ];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -107,6 +123,17 @@ in
|
||||
owner = builtins.toString config.users.users.actualbudget.uid;
|
||||
group = builtins.toString config.users.users.actualbudget.uid;
|
||||
};
|
||||
secrets.actualbudget-oidc-secret = {
|
||||
source = ./actualbudget-oidc-secret.age;
|
||||
dest = "${config.secretsDirectory}/actualbudget-oidc-secret";
|
||||
owner = config.users.users.actualbudget.name;
|
||||
group = config.users.groups.shared.name;
|
||||
permissions = "0440";
|
||||
};
|
||||
systemd.services.actualbudget-oidc-secret-secret = {
|
||||
requiredBy = [ "actual.service" ];
|
||||
before = [ "actual.service" ];
|
||||
};
|
||||
|
||||
# Allow web traffic to Caddy
|
||||
nmasur.presets.services.caddy.routes = lib.mkAfter [
|
||||
|
||||
@@ -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" ];
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
@@ -25,7 +25,7 @@
|
||||
# Set ca = "<public key>"
|
||||
|
||||
let
|
||||
inherit (config.nmasur.settings) username;
|
||||
inherit (config.nmasur.settings) username hostnames;
|
||||
cfg = config.nmasur.presets.services.cloudflared;
|
||||
in
|
||||
|
||||
@@ -68,27 +68,24 @@ in
|
||||
|
||||
# Grant Cloudflare access to SSH into this server
|
||||
environment.etc = {
|
||||
"ssh/ca.pub".text = ''
|
||||
${cfg.tunnel.ca}
|
||||
'';
|
||||
|
||||
# Must match the username portion of the email address in Cloudflare
|
||||
# Access
|
||||
"ssh/authorized_principals".text = ''
|
||||
${username}
|
||||
'';
|
||||
"ssh/ca.pub" = {
|
||||
text = ''
|
||||
${cfg.tunnel.ca}
|
||||
'';
|
||||
mode = "0444";
|
||||
};
|
||||
};
|
||||
|
||||
users.users.${username}.openssh.authorizedPrincipals = [
|
||||
username
|
||||
"${username}@${hostnames.mail}"
|
||||
];
|
||||
|
||||
# Adjust SSH config to allow access from Cloudflare's certificate
|
||||
services.openssh.extraConfig = ''
|
||||
PubkeyAuthentication yes
|
||||
TrustedUserCAKeys /etc/ssh/ca.pub
|
||||
Match User '${username}'
|
||||
AuthorizedPrincipalsFile /etc/ssh/authorized_principals
|
||||
# if there is no existing AuthenticationMethods
|
||||
AuthenticationMethods publickey
|
||||
'';
|
||||
services.openssh.settings.Macs = [ "hmac-sha2-512" ]; # Fix for failure to find matching mac
|
||||
services.openssh.settings = {
|
||||
TrustedUserCAKeys = "/etc/ssh/ca.pub";
|
||||
Macs = [ "hmac-sha2-512" ]; # Fix for failure to find matching mac
|
||||
};
|
||||
|
||||
# Create credentials file for Cloudflare
|
||||
secrets.cloudflared = {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IE1nSGFPdyBmRTZ2
|
||||
Q0pxZ2ZOZ21HcG9EWHJOR0FZbU5UeWFTQXZwRCtKK2hXWDZsQVJzCjh1MURnU0dr
|
||||
MERUWGVETHFZZjhiZnJsMVlqTDdSNnZwbDNQNFRCRDZuQ3MKLT4gc3NoLWVkMjU1
|
||||
MTkgWXlTVU1RIFl0V3Jac3IydVJDOVI3c3lscWZiQzU4TG1tYlNZSWZHbmtSakRl
|
||||
bGhVbk0KNzNFZDFXM3FhWkNWbUliRG00dGN3WjNxZktCT2RPd2FUbi9nS1NobEtQ
|
||||
cwotPiBzc2gtZWQyNTUxOSBuanZYNUEga29CZkplcC9yN25JU0lNakQxSW92UFBz
|
||||
bCtHQUNPd0FBdzVMaEhRVjJoWQpmV2xsWFcwU0o0SlljbjEzY2dPT0VlazVhZ3h3
|
||||
bDEwNzRGUWdJM0Y3UDhvCi0+IHNzaC1lZDI1NTE5IENxSU9VQSBzVWhPT01tSmZh
|
||||
UlpDb0ltZGdiRXRkVTFEQW45Tk5PMDVqdFczRTBlQ0hVCm1OOEJDNEN4K1lFZU1r
|
||||
cU1ENVRqM3BLV3lVTzRpOUhjbVFCU0t4Q0owR28KLT4gc3NoLWVkMjU1MTkgejFP
|
||||
Y1p3IFRTU1FBa2d2ZGRCc0JuanRpbEE2OU53M1htOGhJT1hoM0s5R2Rqbk1FMXcK
|
||||
SE1qbjFid1ZnUmljUDBlMU4yTlU0bWlOenhFN3Bkd3BSS3NIYStQNHFHYwotLS0g
|
||||
c2ZwZjNGaTQxVVBKUC9keTZhOGtxRHA0MkpmRkNwUGFjMlFXVnM0Y0VTWQpi8R/5
|
||||
BRFSxYiEYeB54XjyVXRcyct94jH4wentMOYUFcZP+kd3dwKnXdtrqZDlCk3eM9sy
|
||||
xzOoHpd8BhmmRK9hgw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
@@ -29,13 +29,25 @@ in
|
||||
};
|
||||
|
||||
secrets.grafana-secret-key = {
|
||||
source = ./grafana-secret-key.age;
|
||||
source = ./grafana-secret-key.age;
|
||||
dest = "${config.secretsDirectory}/grafana-secret-key";
|
||||
owner = "grafana";
|
||||
group = "grafana";
|
||||
permissions = "0440";
|
||||
};
|
||||
systemd.services.grafana-secret-key-secret = {
|
||||
systemd.services.grafana-secret-key-secret = {
|
||||
requiredBy = [ "grafana.service" ];
|
||||
before = [ "grafana.service" ];
|
||||
};
|
||||
|
||||
secrets.grafana-oidc-secret = {
|
||||
source = ./grafana-oidc-secret.age;
|
||||
dest = "${config.secretsDirectory}/grafana-oidc-secret";
|
||||
owner = "grafana";
|
||||
group = "grafana";
|
||||
permissions = "0440";
|
||||
};
|
||||
systemd.services.grafana-oidc-secret-secret = {
|
||||
requiredBy = [ "grafana.service" ];
|
||||
before = [ "grafana.service" ];
|
||||
};
|
||||
@@ -44,11 +56,15 @@ in
|
||||
enable = true;
|
||||
settings = {
|
||||
security.secret_key = "$__file{${config.secrets.grafana-secret-key.dest}}";
|
||||
auth = {
|
||||
oauth_allow_insecure_email_lookup = true;
|
||||
};
|
||||
server = {
|
||||
domain = hostnames.metrics;
|
||||
http_addr = "127.0.0.1";
|
||||
http_port = 3000;
|
||||
protocol = "http";
|
||||
root_url = "https://${hostnames.metrics}/";
|
||||
};
|
||||
smtp = rec {
|
||||
enabled = true;
|
||||
@@ -58,6 +74,19 @@ in
|
||||
from_name = "Grafana";
|
||||
from_address = user;
|
||||
};
|
||||
"auth.generic_oauth" = {
|
||||
enabled = true;
|
||||
name = "Pocket ID";
|
||||
allow_sign_up = true;
|
||||
client_id = "85d879ed-1a86-4984-b33d-43806500ef98";
|
||||
client_secret = "$__file{${config.secrets.grafana-oidc-secret.dest}}";
|
||||
scopes = "openid profile email";
|
||||
auth_url = "https://${hostnames.auth}/authorize";
|
||||
token_url = "https://${hostnames.auth}/api/oidc/token";
|
||||
api_url = "https://${hostnames.auth}/api/oidc/userinfo";
|
||||
login_attribute_path = "preferred_username";
|
||||
skip_org_role_sync = true;
|
||||
};
|
||||
};
|
||||
provision = {
|
||||
enable = true;
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
inherit (config.nmasur.settings) hostnames;
|
||||
cfg = config.nmasur.presets.services.hister;
|
||||
in
|
||||
|
||||
{
|
||||
|
||||
options.nmasur.presets.services.hister = {
|
||||
enable = lib.mkEnableOption "Hister web history service";
|
||||
port = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
description = "Port to use for the localhost";
|
||||
default = 4433;
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
|
||||
services.hister = {
|
||||
enable = true;
|
||||
port = cfg.port;
|
||||
settings = {
|
||||
app = {
|
||||
user_handling = true;
|
||||
};
|
||||
server = {
|
||||
base_url = "https://${hostnames.hister}";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Adding new users:
|
||||
# sudo -u hister hister --config /run/hister/config.yml create-user <username> --admin
|
||||
|
||||
# Allow web traffic to Caddy
|
||||
nmasur.presets.services.caddy.routes = [
|
||||
{
|
||||
match = [ { host = [ hostnames.hister ]; } ];
|
||||
handle = [
|
||||
{
|
||||
handler = "reverse_proxy";
|
||||
upstreams = [ { dial = "localhost:${builtins.toString cfg.port}"; } ];
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
|
||||
# Configure Cloudflare DNS to point to this machine
|
||||
services.cloudflare-dyndns.domains = [ hostnames.hister ];
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IE1nSGFPdyBOUXFn
|
||||
MmFqRWIxOUltQ3lPd1czZ3prbElXaURRV01rSUJGczhka0FOZkNrCkVFaXp2cnd0
|
||||
eTFhQUVlY001WnhRUVowSEdORS9MQWdTajhiUWtBVnpaZjAKLT4gc3NoLWVkMjU1
|
||||
MTkgWXlTVU1RIHg2ZGVSVDRSRVBUenBSTWhVbTFTRzFESXRyTEl6OEdoUDJmV2FZ
|
||||
T0tPMXMKZ281MmhJd1EyV3VsNTVlNHYxcWE5cGdmTW5mVkNiM1J5ejNWQlBVQ3hM
|
||||
UQotPiBzc2gtZWQyNTUxOSBuanZYNUEgUDhObFRyMCsxWkhGK1lKU25WenllZmRj
|
||||
NnB5RHJyQjlscGE4NW1uVEJuQQpUQTFXNklzdEcrZjlOSUM3ZzV5OGlvREdJRjlu
|
||||
MERGUkdjcXR6REtNUU1ZCi0+IHNzaC1lZDI1NTE5IENxSU9VQSBob2ZLQnNYVFpx
|
||||
aVBEb2ZVOWVpUGZtU3FTUmNoS214cUxGeVZhM1NFZkNZCk5IV2VDMnc3L2Y4OEEx
|
||||
b1ZSVys0dHpDUlo5UFA2akNKSHFNTi9JTUlnNEEKLT4gc3NoLWVkMjU1MTkgejFP
|
||||
Y1p3IDZBd3k2ZE85Nm53MGdhNnJkUWFWVXEzTFRWU1lMSWp2L1FaYkdZRy9zQ2cK
|
||||
TENkNFhsUi9VY2xPSFMwVmRNeXBxS1dFR1VtRDZsQ0xRSTdJQkJxODhiSQotLS0g
|
||||
bnpxUGRtVVYvc1R5VUk5VDE4Mmx0WEVNMDZNc0tSSktCTzJlcjJmL3Y0bwqr2hdI
|
||||
U3VNIk0qy4sWUdRCPqdyGK+F4qmcbD5MrPBQYNrc2aQ/zr5E/OkZn7k6vCvGMC9k
|
||||
zrjpVlAxLB2cRsBmVw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
+28
-1
@@ -10,6 +10,18 @@ in
|
||||
options.nmasur.presets.services.immich.enable = lib.mkEnableOption "Immich photo manager";
|
||||
config = lib.mkIf cfg.enable {
|
||||
|
||||
secrets.immich-oidc-secret = {
|
||||
source = ./immich-oidc-secret.age;
|
||||
dest = "${config.secretsDirectory}/immich-oidc-secret";
|
||||
owner = config.services.immich.user;
|
||||
group = config.services.immich.group;
|
||||
permissions = "0440";
|
||||
};
|
||||
systemd.services.immich-oidc-secret-secret = {
|
||||
requiredBy = [ "immich-server.service" ];
|
||||
before = [ "immich-server.service" ];
|
||||
};
|
||||
|
||||
services.immich = {
|
||||
enable = true;
|
||||
port = 2283;
|
||||
@@ -19,7 +31,18 @@ in
|
||||
machine-learning.environment = { };
|
||||
mediaLocation = "/data/images";
|
||||
secretsFile = null;
|
||||
settings.server.externalDomain = "https://${hostnames.photos}";
|
||||
settings = {
|
||||
server.externalDomain = "https://${hostnames.photos}";
|
||||
oauth = {
|
||||
enabled = true;
|
||||
issuerUrl = "https://${hostnames.auth}";
|
||||
clientId = "1f4e0f8d-6cee-4d67-8d53-74bf6c18ae09";
|
||||
clientSecret._secret = config.secrets.immich-oidc-secret.dest;
|
||||
scope = "openid profile email";
|
||||
autoRegister = true;
|
||||
buttonText = "Login with Pocket ID";
|
||||
};
|
||||
};
|
||||
environment = {
|
||||
IMMICH_ENV = "production";
|
||||
IMMICH_LOG_LEVEL = "log";
|
||||
@@ -28,6 +51,10 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.immich-server = {
|
||||
after = [ "immich-oidc-secret-secret.service" ];
|
||||
};
|
||||
|
||||
nmasur.presets.services.caddy.routes = [
|
||||
{
|
||||
match = [ { host = [ hostnames.photos ]; } ];
|
||||
@@ -44,9 +44,27 @@ in
|
||||
{
|
||||
match = [ { host = [ hostnames.stream ]; } ];
|
||||
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";
|
||||
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+IHNzaC1lZDI1NTE5IE1nSGFPdyBhZWw0
|
||||
UTJSYitUUTY0b3A3RDZZTjhnWjh3QWdTaFBzZVRpRWs2UW43R3k0Cmg1ODRWWnI5
|
||||
TFdSbmtlTU9zdzhyTHpzMW1hNmk3Tk1sRjZlbFYwZURML1kKLT4gc3NoLWVkMjU1
|
||||
MTkgWXlTVU1RIHVvaGZjeVh3Zm8yWUcyMEZnb1VITHpveGY4aWxTOVhUTzVpbnI0
|
||||
c2lYQ0EKNHI4ZEVpUWFYRjVxTTUrTElwdTliWVc2RjdJT21wTTFtSi9ydUo1MVF3
|
||||
UQotPiBzc2gtZWQyNTUxOSBuanZYNUEgODhzWTIwVjJvZzZKWGJKNkNHRXFSOWI2
|
||||
bDZab1hLUFBOUmNBZXhEQUtoNAozRXBXNUF6THN4Ry9OcG9mb0pTNGZrL01zTzhr
|
||||
QWZpRHg1dUNtdnFJTnhNCi0+IHNzaC1lZDI1NTE5IENxSU9VQSA2NWYza2ZDWmda
|
||||
N3Eycll2cXNPM0Q3UVk5OHlta1p5OTU3QlQwY3dxNm1NCktLaUxVTkYwMFpZV1px
|
||||
SUFUU1JYZ0pyUU9oRURKWVNVck5VSXdVT3N3b0UKLT4gc3NoLWVkMjU1MTkgejFP
|
||||
Y1p3IGZNV3NRWVg5N3FQeUdKYjhwQ2xydzBsaUZDVzl5Y1BrTlhZNCtOOWduZ28K
|
||||
dnozT0lFa2NsYW9hbGpTL2pNK1YwcjM2QkVoWHJUUVdjT09XMlJGNUx0RQotLS0g
|
||||
My9aVWJuVlBQbTNYZWd2bXUzUGp0NzZXcmFicXNob2ROeWY1cnViWXNQRQpYZ/zy
|
||||
L37ulzvRl/Wt2+tIz9fgfOtK2KHuagnPD27T9e8bC9oVinJngwrzfFsV4t7/GCCs
|
||||
iI+gfp86+RPSbIwrCA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
+21
@@ -10,16 +10,37 @@ in
|
||||
options.nmasur.presets.services.mealie.enable = lib.mkEnableOption "mealie recipe manager";
|
||||
config = lib.mkIf cfg.enable {
|
||||
|
||||
secrets.mealie-oidc-secret = {
|
||||
source = ./mealie-oidc-secret.age;
|
||||
dest = "${config.secretsDirectory}/mealie-oidc-secret";
|
||||
prefix = "OIDC_CLIENT_SECRET=";
|
||||
};
|
||||
systemd.services.mealie-oidc-secret-secret = {
|
||||
requiredBy = [ "mealie.service" ];
|
||||
before = [ "mealie.service" ];
|
||||
};
|
||||
|
||||
services.mealie = {
|
||||
enable = true;
|
||||
port = 9099;
|
||||
database.createLocally = true;
|
||||
listenAddress = "127.0.0.1";
|
||||
credentialsFile = config.secrets.mealie-oidc-secret.dest;
|
||||
settings = {
|
||||
TOKEN_TIME = 7200; # Hours for login to last (300 days)
|
||||
OIDC_AUTH_ENABLED = "true";
|
||||
OIDC_SIGNUP_ENABLED = "true";
|
||||
OIDC_CONFIGURATION_URL = "https://${hostnames.auth}/.well-known/openid-configuration";
|
||||
OIDC_CLIENT_ID = "040925ed-b39e-4442-b8e8-369c948c0cd2";
|
||||
OIDC_PROVIDER_NAME = "Pocket ID";
|
||||
OIDC_USER_CLAIM = "email";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.mealie = {
|
||||
after = [ "mealie-oidc-secret-secret.service" ];
|
||||
};
|
||||
|
||||
# Fix BASE_URL for downloading backups
|
||||
systemd.services.mealie.environment.BASE_URL = lib.mkForce "https://${hostnames.recipes}";
|
||||
|
||||
@@ -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
|
||||
trusted_domains = [ hostnames.content ];
|
||||
trusted_proxies = [ "127.0.0.1" ];
|
||||
overwriteprotocol = "https";
|
||||
maintenance_window_start = 4; # Run jobs at 4am UTC
|
||||
log_type = "file";
|
||||
loglevel = 1; # Include all actions in the log
|
||||
@@ -41,6 +42,7 @@ in
|
||||
extraApps = {
|
||||
calendar = config.services.nextcloud.package.packages.apps.calendar;
|
||||
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.
|
||||
# news = pkgs.nextcloudApps.news;
|
||||
# external = pkgs.nextcloudApps.external;
|
||||
@@ -211,6 +213,32 @@ in
|
||||
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
|
||||
users.users.${username}.extraGroups = [ "nextcloud" ];
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
}:
|
||||
|
||||
let
|
||||
inherit (config.nmasur.settings) username;
|
||||
cfg = config.nmasur.presets.services.nix-autoupgrade;
|
||||
in
|
||||
|
||||
@@ -27,7 +26,7 @@ in
|
||||
system.autoUpgrade = {
|
||||
enable = true;
|
||||
dates = "09:33";
|
||||
flake = "git+${cfg.repo}";
|
||||
flake = "git+ssh://${cfg.repo}";
|
||||
randomizedDelaySec = "25min";
|
||||
operation = "switch";
|
||||
allowReboot = true;
|
||||
@@ -55,8 +54,7 @@ in
|
||||
systemctl status $SERVICE_ID >> $TEMPFILE
|
||||
set -e
|
||||
${lib.getExe pkgs.msmtp} \
|
||||
--file=${config.home-manager.users.${username}.xdg.configHome}/msmtp/config \
|
||||
--account=system \
|
||||
--account=default \
|
||||
${address} < $TEMPFILE
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
{ config, lib, ... }:
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
inherit (config.nmasur.settings) hostnames username;
|
||||
@@ -17,12 +22,26 @@ in
|
||||
enable = true;
|
||||
mediaDir = "/data/generic/paperless";
|
||||
passwordFile = config.secrets.paperless.dest;
|
||||
environmentFile = "${config.secretsDirectory}/paperless-env";
|
||||
configureTika = true; # Enable processing of emails
|
||||
settings = {
|
||||
PAPERLESS_OCR_USER_ARGS = builtins.toJSON { invalidate_digital_signatures = true; };
|
||||
PAPERLESS_URL = "https://${hostnames.paperless}";
|
||||
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
|
||||
# PAPERLESS_FORCE_SCRIPT_NAME = "/paperless";
|
||||
# PAPERLESS_STATIC_URL = "/paperless/static/";
|
||||
@@ -61,8 +80,60 @@ in
|
||||
permissions = "0440";
|
||||
};
|
||||
systemd.services.paperless-secret = {
|
||||
requiredBy = [ "paperless.service" ];
|
||||
before = [ "paperless.service" ];
|
||||
requiredBy = [ "paperless-scheduler.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
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IE1nSGFPdyB3ajlu
|
||||
V0RkeHB5WE8zWGhrYnFpaU9LQ2l0dHd6bWNydll6Y3F4ampaT0JnCi9QTS81aldn
|
||||
NVhhSm00RWJDaHl0V0ludEJleW9HaitKNlZwQlFRcWdVZUUKLT4gc3NoLWVkMjU1
|
||||
MTkgWXlTVU1RIG8waldTNzdJNmJKQjdPS2ZpV1ZpcTN3RGVjSHR5UmlyQzBsM3Va
|
||||
SFdLM2sKYVhhczRxS1BNdElSN3ppa2ZPNjZhajFhVlVkNngydFlibHhTYVZFWS9C
|
||||
cwotPiBzc2gtZWQyNTUxOSBuanZYNUEgeVBqak4yelZnUXBZbFMrR3AzU05hNldy
|
||||
NDl4SkpTY1BBS2xHQ3J5TkZ3TQpYRGJKSlFzMFB5c3lWMGZKOTl1TWRURnk3U3hi
|
||||
dkMrK0FDNVVKL3RzTXJBCi0+IHNzaC1lZDI1NTE5IENxSU9VQSBEQXlSRFpNYVRN
|
||||
bTdFejNXMEcvaGZkbDF1NnFjZFdXdHpFV2ZjMGhMWWtNCkpvTFJVUUF0eEl1U2N0
|
||||
Z1hScXRXQndxU3kzL0pPYjNQL01XUzUwbTFwRUEKLT4gc3NoLWVkMjU1MTkgejFP
|
||||
Y1p3IHpNVnUwWURYSHFHMUxVMmhIZkMrdHBEYVdEQTZ0R3plcU1Na21IQkZoSHMK
|
||||
THBiRlg2NWhFcSs0SkUveTRMdXdxajl1emFoV0NvTmRSZ3JTZTltZFovQQotLS0g
|
||||
aUg4aEIwWHhkZFFNVnQ5ZVJpWjlFQis3Tjk5NHBNMWVzTTYxdEdhSlJ6VQrk/zq+
|
||||
MS3OPv0vbS4yDmLFFrJUdUPj0WuJ/yPp32Elz3+FW7QFDqNKChZnC3rTTVjc/MKL
|
||||
IVlGYXpoYuwve4ABi40QAcN0Qs8XqeIt2A==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
@@ -0,0 +1,58 @@
|
||||
{ config, lib, ... }:
|
||||
|
||||
let
|
||||
inherit (config.nmasur.settings) hostnames;
|
||||
cfg = config.nmasur.presets.services.pocket-id;
|
||||
in
|
||||
|
||||
{
|
||||
|
||||
options.nmasur.presets.services.pocket-id.enable = lib.mkEnableOption "Pocket ID OIDC provider";
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
|
||||
secrets.pocket-id = {
|
||||
source = ./pocket-id.age;
|
||||
dest = "${config.secretsDirectory}/pocket-id";
|
||||
owner = "pocket-id";
|
||||
group = "pocket-id";
|
||||
permissions = "0440";
|
||||
};
|
||||
systemd.services.pocket-id-secret = {
|
||||
requiredBy = [ "pocket-id.service" ];
|
||||
before = [ "pocket-id.service" ];
|
||||
};
|
||||
|
||||
services.pocket-id = {
|
||||
enable = true;
|
||||
settings = {
|
||||
APP_URL = "https://${hostnames.auth}";
|
||||
TRUST_PROXY = true;
|
||||
PORT = 3034;
|
||||
};
|
||||
credentials = {
|
||||
ENCRYPTION_KEY = config.secrets.pocket-id.dest;
|
||||
};
|
||||
};
|
||||
|
||||
# Allow web traffic to Caddy
|
||||
nmasur.presets.services.caddy.routes = [
|
||||
{
|
||||
match = [ { host = [ hostnames.auth ]; } ];
|
||||
handle = [
|
||||
{
|
||||
handler = "reverse_proxy";
|
||||
upstreams = [
|
||||
{ dial = "localhost:${builtins.toString config.services.pocket-id.settings.PORT}"; }
|
||||
];
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
|
||||
# Configure Cloudflare DNS to point to this machine
|
||||
services.cloudflare-dyndns.domains = [ hostnames.auth ];
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
@@ -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
|
||||
users.mutableUsers = lib.mkDefault false;
|
||||
@@ -62,7 +62,7 @@ in
|
||||
# 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;
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ in
|
||||
services = {
|
||||
actualbudget.enable = lib.mkDefault true;
|
||||
actualtap = {
|
||||
enable = lib.mkDefault true;
|
||||
enable = lib.mkDefault false;
|
||||
instances = {
|
||||
budget1 = {
|
||||
port = 3031;
|
||||
@@ -38,11 +38,13 @@ in
|
||||
};
|
||||
};
|
||||
};
|
||||
api.enable = lib.mkDefault true;
|
||||
caddy.enable = lib.mkDefault true;
|
||||
cloudflare.enable = lib.mkDefault true;
|
||||
cloudflared.enable = lib.mkDefault true;
|
||||
gitea.enable = lib.mkDefault true;
|
||||
grafana.enable = lib.mkDefault true;
|
||||
hister.enable = lib.mkDefault true;
|
||||
influxdb2.enable = lib.mkDefault true;
|
||||
karakeep.enable = lib.mkDefault true;
|
||||
litestream.enable = lib.mkDefault true;
|
||||
@@ -54,6 +56,7 @@ in
|
||||
nix-autoupgrade.enable = lib.mkDefault true; # On by default for communications
|
||||
ntfy-sh.enable = lib.mkDefault true;
|
||||
pgweb.enable = lib.mkDefault true;
|
||||
pocket-id.enable = lib.mkDefault true;
|
||||
postgresql.enable = lib.mkDefault true;
|
||||
stalwart.enable = lib.mkDefault true;
|
||||
thelounge.enable = lib.mkDefault true;
|
||||
|
||||
Reference in New Issue
Block a user