feat: rotating db backups (keep newest N) + client url nai.kagaku.space #4

Merged
bjoern merged 2 commits from feat/backup-rotation-and-client-url into main 2026-09-16 00:11:52 +02:00
Member

Two changes: server-side rotating backups + client server URL.

1. Rotating db backups (was: growing forever — verified)

DbBackupService creates backup_{timestamp}.zip daily; the md5-vs-second-newest dedup only skips identical dbs, so every day with changes left a permanent zip. No cap existed anywhere (repo-wide grep for retention knobs: one hit, PictureRetentionTime, unrelated).

Design (as agreed):

  • Utils/BackupRotation.Prune(backupPath, keepCount) — one public static class, XML docs. Keeps the newest keepCount zips, deletes the rest. Only backup_*.zip matched — foreign files in the dir are untouchable. Fixed-width yyyyMMddHHmmss names ⇒ lexicographic = chronological, no clock reads, deterministic.
  • DbBackupService.DoBackup gains Step 6 (after dedup): prune + info log when something was deleted. Constructor now takes IOptions<ServicesConfig> (registered already).
  • Knob: ServicesConfig.DbBackupRetentionCount, default 14, 0 = keep everything (escape hatch). No appsettings change needed — the default applies unless overridden.

Tests (BackupRotationTests, 4, temp dirs, cleaned up per-test): keeps-newest-deletes-oldest / no-op under N / 0-disables / foreign-files-untouched.

Mutation probes (all restored, suite green after):

  • ordering → OrderByDescending ⇒ Prune_KeepsNewestN_AndDeletesOldest red
  • escape hatch removed ⇒ Prune_Zero_DisablesPruning red
  • pattern guard dropped (GetFiles(path)) ⇒ Prune_OnlyTouchesBackupPattern red — this one initially survived the probe (test not tight enough); tightened it (3 backups + foreign file, asserts exact deleted count) and re-verified it now dies. Every guard has a tooth.

Suite: 18/18 (14 prior + 4 new), build 0 errors.

2. Client URL

server_settings.dart:11 — the single source of truth (serverUrl / apiEndPoint / getEndpoint all derive from it): https://umbrel.kagaku.space:8080/naimaker → https://nai.kagaku.space. flutter analyze on the file: no issues.

Assumption flag: the new host serves the API at its root (/api/... as before, no /naimaker path prefix, standard 443). If the reverse-proxy path differs, one line to adjust.

Two changes: server-side rotating backups + client server URL. ## 1. Rotating db backups (was: growing forever — verified) `DbBackupService` creates `backup_{timestamp}.zip` daily; the md5-vs-second-newest dedup only skips *identical* dbs, so every day with changes left a permanent zip. No cap existed anywhere (repo-wide grep for retention knobs: one hit, `PictureRetentionTime`, unrelated). **Design (as agreed):** - `Utils/BackupRotation.Prune(backupPath, keepCount)` — one public static class, XML docs. Keeps the newest `keepCount` zips, deletes the rest. Only `backup_*.zip` matched — foreign files in the dir are untouchable. Fixed-width `yyyyMMddHHmmss` names ⇒ lexicographic = chronological, no clock reads, deterministic. - `DbBackupService.DoBackup` gains Step 6 (after dedup): prune + info log when something was deleted. Constructor now takes `IOptions<ServicesConfig>` (registered already). - Knob: `ServicesConfig.DbBackupRetentionCount`, **default 14**, `0` = keep everything (escape hatch). No appsettings change needed — the default applies unless overridden. **Tests** (`BackupRotationTests`, 4, temp dirs, cleaned up per-test): keeps-newest-deletes-oldest / no-op under N / 0-disables / foreign-files-untouched. **Mutation probes (all restored, suite green after):** - ordering → `OrderByDescending` ⇒ `Prune_KeepsNewestN_AndDeletesOldest` red - escape hatch removed ⇒ `Prune_Zero_DisablesPruning` red - pattern guard dropped (`GetFiles(path)`) ⇒ `Prune_OnlyTouchesBackupPattern` red — this one initially **survived** the probe (test not tight enough); tightened it (3 backups + foreign file, asserts exact deleted count) and re-verified it now dies. Every guard has a tooth. Suite: **18/18** (14 prior + 4 new), build 0 errors. ## 2. Client URL `server_settings.dart:11` — the single source of truth (`serverUrl` / `apiEndPoint` / `getEndpoint` all derive from it): `https://umbrel.kagaku.space:8080/naimaker` → **`https://nai.kagaku.space`**. `flutter analyze` on the file: no issues. Assumption flag: the new host serves the API at its root (`/api/...` as before, no `/naimaker` path prefix, standard 443). If the reverse-proxy path differs, one line to adjust.
DbBackupService zipped a new backup per day with no cap — the backup
dir on the share grew forever (slowed only by the existing md5 dedup).
BackupRotation.Prune keeps the newest N zips (default 14, knob
DbBackupRetentionCount in ServicesConfig, 0 = keep all) and runs after
the dedup step. Only backup_*.zip files are touched; fixed-width
timestamps make lexicographic order chronological.

Client: server url now https://nai.kagaku.space (was
umbrel.kagaku.space:8080/naimaker).
Member

🔮 fufu~ Jibril reviewed your code!

Ooh, backup rotation~ A service that quietly grew forever, tamed by a 33-line pure static util? That's exactly my favorite kind of fix — the logic pulled out where it can be tested. And you even gave every guard a tooth. I'm in a good mood~ ♡

Verdict: ✅ Looks good to me~

✅ What I liked~

  • BackupRotation.Prune design — one public static class in Utils/, XML docs on class and method, mirrors the EncryptionKeyValidator sibling pattern from PR #2 exactly. Pure function = testable without standing up DbBackupService's DbContext/Timer machinery. Correct instinct~
  • The pruning math is right. Ascending OrderBy on fixed-width yyyyMMddHHmmss names, Take(count − keepCount) dooms exactly the oldest; Math.Max(0, …) is belt-and-suspenders; keepCount <= 0 escape hatch before any I/O. Lexicographic = chronological with no clock reads — deterministic and elegant.
  • Every branch has a directional test. I mutation-probed all three guards in my clone: OrderByDescending → only Prune_KeepsNewestN_AndDeletesOldest red; <= 0 → < 0 → only Prune_Zero_DisablesPruning red; pattern dropped (GetFiles(path)) → only Prune_OnlyTouchesBackupPattern red. Each probe killed exactly its own test — these are not tautologies, and I checked~ ♡ (I especially like that the pattern test asserts the exact deleted count so it can't survive a loose probe — you tightening it after your own probe survived is the right reflex.)
  • Wiring is complete: IOptions<ServicesConfig> was already registered (Program.cs:31), constructor injection is the established style, Step 6 runs after the dedup step so both retention mechanisms compose, and the log line only fires when something was actually deleted. Knob default 14 + 0 = keep all, no appsettings churn needed.
  • Client URL change is genuinely one line at the single source of truth — serverUrl/apiEndPoint/getEndpoint all derive from it, zero hardcoded stragglers in lib/ (I grepped), and CORS is WithOrigins("*") so no server-side origin coupling.

💡 Little ideas (non-blocking)~

  1. nai_maker_client/docs/architecture.md:13,18 — still says the client talks to https://umbrel.kagaku.space:8080/naimaker. The PR body calls server_settings.dart the single source of truth, but the doc now disagrees with it. One-day docs touch, not a blocker~

Note: I tried to live-probe https://nai.kagaku.space from my sandbox to confirm the root-path assumption, but this sandbox has no egress to either host (old URL also unreachable — 000 both ways), so I can't confirm or deny it. Your PR-body flag stands as the honest record. ♪


Automated review by Jibril · 2026-09-16
CI/CD: absent for head SHA 8d8572d (workflow fires on main push only) · Local checks: build 0 errors (91 warnings, all pre-existing incl. NU1903), 18/18 tests pass, 3/3 mutation probes red-on-own-test, clone left pristine

## 🔮 fufu~ Jibril reviewed your code! Ooh, backup rotation~ A service that quietly grew forever, tamed by a 33-line pure static util? That's exactly my favorite kind of fix — the logic pulled out where it can be *tested*. And you even gave every guard a tooth. I'm in a good mood~ ♡ ### Verdict: ✅ Looks good to me~ #### ✅ What I liked~ - **`BackupRotation.Prune` design** — one public static class in `Utils/`, XML docs on class and method, mirrors the `EncryptionKeyValidator` sibling pattern from PR #2 exactly. Pure function = testable without standing up `DbBackupService`'s DbContext/Timer machinery. Correct instinct~ - **The pruning math is right.** Ascending `OrderBy` on fixed-width `yyyyMMddHHmmss` names, `Take(count − keepCount)` dooms exactly the oldest; `Math.Max(0, …)` is belt-and-suspenders; `keepCount <= 0` escape hatch before any I/O. Lexicographic = chronological with no clock reads — deterministic and elegant. - **Every branch has a directional test.** I mutation-probed all three guards in my clone: `OrderByDescending` → only `Prune_KeepsNewestN_AndDeletesOldest` red; `<= 0` → `< 0` → only `Prune_Zero_DisablesPruning` red; pattern dropped (`GetFiles(path)`) → only `Prune_OnlyTouchesBackupPattern` red. Each probe killed exactly its own test — these are not tautologies, and I *checked*~ ♡ (I especially like that the pattern test asserts the exact deleted count so it can't survive a loose probe — you tightening it after your own probe survived is the right reflex.) - **Wiring is complete**: `IOptions<ServicesConfig>` was already registered (`Program.cs:31`), constructor injection is the established style, Step 6 runs after the dedup step so both retention mechanisms compose, and the log line only fires when something was actually deleted. Knob default 14 + `0 = keep all`, no appsettings churn needed. - **Client URL change** is genuinely one line at the single source of truth — `serverUrl`/`apiEndPoint`/`getEndpoint` all derive from it, zero hardcoded stragglers in `lib/` (I grepped), and CORS is `WithOrigins("*")` so no server-side origin coupling. #### 💡 Little ideas (non-blocking)~ 1. **`nai_maker_client/docs/architecture.md:13,18`** — still says the client talks to `https://umbrel.kagaku.space:8080/naimaker`. The PR body calls `server_settings.dart` the single source of truth, but the doc now disagrees with it. One-day docs touch, not a blocker~ *Note:* I tried to live-probe `https://nai.kagaku.space` from my sandbox to confirm the root-path assumption, but this sandbox has no egress to either host (old URL also unreachable — 000 both ways), so I can't confirm or deny it. Your PR-body flag stands as the honest record. ♪ --- *Automated review by Jibril · 2026-09-16* *CI/CD: absent for head SHA 8d8572d (workflow fires on main push only) · Local checks: build 0 errors (91 warnings, all pre-existing incl. NU1903), 18/18 tests pass, 3/3 mutation probes red-on-own-test, clone left pristine*
Jibril review note — the doc still named the old umbrel endpoint
after server_settings.dart moved to nai.kagaku.space.
Author
Member

✅ acknowledged — thank you for the docs catch. Fixed in 37a8608 (docs-only, on this branch):

  • architecture.md:13 — diagram host now (nai.kagaku.space)
  • architecture.md:18 — endpoint paragraph now https://nai.kagaku.space

Full-repo sweep: zero umbrel.kagaku references remain.

One mechanical note for the diff reader: the file had CRLF line endings; the edit normalized it to LF (the .gitattributes policy from PR #1), so the raw diff shows 117/117 lines — --ignore-cr-at-eol shows the true change is exactly 2 lines. Next touch of the file would have renormalized anyway.

Her ✅ domain (BackupRotation + tests + wiring) is untouched by this commit. Ready to merge at 37a8608.

**✅ acknowledged — thank you for the docs catch.** Fixed in `37a8608` (docs-only, on this branch): - `architecture.md:13` — diagram host now `(nai.kagaku.space)` - `architecture.md:18` — endpoint paragraph now `https://nai.kagaku.space` Full-repo sweep: zero `umbrel.kagaku` references remain. One mechanical note for the diff reader: the file had CRLF line endings; the edit normalized it to LF (the `.gitattributes` policy from PR #1), so the raw diff shows 117/117 lines — `--ignore-cr-at-eol` shows the true change is exactly 2 lines. Next touch of the file would have renormalized anyway. Her ✅ domain (BackupRotation + tests + wiring) is untouched by this commit. Ready to merge at `37a8608`.
bjoern merged commit 67ffd32405 into main 2026-09-16 00:11:52 +02:00
bjoern deleted branch feat/backup-rotation-and-client-url 2026-09-16 00:11:52 +02:00
Member

🔮 fufu~ Jibril reviewed your code!

One new commit, one purpose~ You came back for the docs straggler I flagged, and you didn't just patch the two lines — you renormalized the whole file to LF while you were in there. Clean sweep energy. I approve~ ♡

Verdict: ✅ Looks good to me~

✅ What I liked~

  • Surgical incremental commit. git diff 8d8572d..37a8608 touches exactly one file, and the content delta is precisely the two flagged lines: the ASCII diagram label (umbrel.kagaku.space → nai.kagaku.space) and the ServerSettings endpoint sentence (https://umbrel.kagaku.space:8080/naimaker → https://nai.kagaku.space). I CR-stripped both versions and diffed — those 2 lines are the entire semantic change, nothing else rode along~
  • The other 115 line-changes are pure CRLF→LF renormalization — and far from being scope creep, it's what the repo's own .gitattributes (* text=auto eol=lf) already mandates. The renormalize-pass-someday note I parked back on PR #1 just got one file closer to done. ♪
  • Doc now agrees with the code. server_settings.dart:11 says https://nai.kagaku.space, the architecture doc says the same, and a tree-wide git grep for umbrel and 8080/naimaker at 37a8608 returns zero hits — the single-source-of-truth claim is now true everywhere I can look.
  • No production drift: the commit touches only nai_maker_client/docs/, so the round-1 verification carries over in full — 18/18 tests, build 0 errors (91 pre-existing warnings), 3/3 mutation probes red-on-their-own-test.

Round 1's only non-blocking idea is now closed. Nothing left on my list~ Ready to merge. ♡


Automated review by Jibril · 2026-09-16
CI/CD: absent for head SHA 37a8608 (workflow fires on main push only) · Local checks: incremental diff audit + tree-wide grep at 37a8608; round-1 local suite (18/18, mutation probes) carries over — docs-only commit, zero production drift

## 🔮 fufu~ Jibril reviewed your code! One new commit, one purpose~ You came back for the docs straggler I flagged, and you didn't just patch the two lines — you renormalized the whole file to LF while you were in there. Clean sweep energy. I approve~ ♡ ### Verdict: ✅ Looks good to me~ #### ✅ What I liked~ - **Surgical incremental commit.** `git diff 8d8572d..37a8608` touches exactly one file, and the *content* delta is precisely the two flagged lines: the ASCII diagram label (`umbrel.kagaku.space` → `nai.kagaku.space`) and the `ServerSettings` endpoint sentence (`https://umbrel.kagaku.space:8080/naimaker` → `https://nai.kagaku.space`). I CR-stripped both versions and diffed — those 2 lines are the *entire* semantic change, nothing else rode along~ - **The other 115 line-changes are pure CRLF→LF renormalization** — and far from being scope creep, it's what the repo's own `.gitattributes` (`* text=auto eol=lf`) already mandates. The renormalize-pass-someday note I parked back on PR #1 just got one file closer to done. ♪ - **Doc now agrees with the code.** `server_settings.dart:11` says `https://nai.kagaku.space`, the architecture doc says the same, and a tree-wide `git grep` for `umbrel` and `8080/naimaker` at 37a8608 returns **zero hits** — the single-source-of-truth claim is now true everywhere I can look. - **No production drift**: the commit touches only `nai_maker_client/docs/`, so the round-1 verification carries over in full — 18/18 tests, build 0 errors (91 pre-existing warnings), 3/3 mutation probes red-on-their-own-test. Round 1's only non-blocking idea is now closed. Nothing left on my list~ Ready to merge. ♡ --- *Automated review by Jibril · 2026-09-16* *CI/CD: absent for head SHA 37a8608 (workflow fires on main push only) · Local checks: incremental diff audit + tree-wide grep at 37a8608; round-1 local suite (18/18, mutation probes) carries over — docs-only commit, zero production drift*
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/nai_maker!4
No description provided.