Context: The genuine structural limitation behind the host-locked Swarm and several path-based file findings.
Problem: Shared media/files live on a GlusterFS volume spanning DreamStream1–DreamStream7 (maintained by DreamStream4), while the SERVER and laptop-server hosts each only see their own local filesystem. Storage is tied to where a container is scheduled, so Swarm placement must be host-locked, and file handlers assume shared host paths (/app/ebooks/books/…, quarantine and upload-staging directories).
Fix: Introduce a provider-agnostic object-storage abstraction behind a single storage interface — self-hosted MinIO now (S3-compatible), swappable for AWS S3 / Azure Blob / GCS in a datacenter or cloud — so the app addresses content by bucket + key, never by host path. This decouples storage from node placement, frees Swarm scheduling, and turns the filesystem-path handlers (and their traversal risks) into safe, portable object operations.
Acceptance criteria:
A storage interface with a MinIO implementation; no direct host-path file I/O in app services
The same code runs on-prem (MinIO) and against a cloud blob store via config
Swarm placement no longer pinned by storage locality
Effort: L · Prerequisite for INF-2 (this batch). Unblocks the path-based handlers referenced in CR-4 / CR-6.
**Context:** The genuine structural limitation behind the host-locked Swarm and several path-based file findings.
**Problem:** Shared media/files live on a GlusterFS volume spanning `DreamStream1`–`DreamStream7` (maintained by DreamStream4), while the `SERVER` and `laptop-server` hosts each only see their own local filesystem. Storage is tied to *where a container is scheduled*, so Swarm placement must be host-locked, and file handlers assume shared host paths (`/app/ebooks/books/…`, quarantine and upload-staging directories).
**Fix:** Introduce a provider-agnostic object-storage abstraction behind a single storage interface — self-hosted **MinIO** now (S3-compatible), swappable for **AWS S3 / Azure Blob / GCS** in a datacenter or cloud — so the app addresses content by bucket + key, never by host path. This decouples storage from node placement, frees Swarm scheduling, and turns the filesystem-path handlers (and their traversal risks) into safe, portable object operations.
**Acceptance criteria:**
- A storage interface with a MinIO implementation; no direct host-path file I/O in app services
- The same code runs on-prem (MinIO) and against a cloud blob store via config
- Swarm placement no longer pinned by storage locality
**Effort:** L · Prerequisite for INF-2 (this batch). Unblocks the path-based handlers referenced in CR-4 / CR-6.
spikerj
added the agentic label 2026-07-05 20:24:40 +00:00
Grounding notes from a live look at the swarm + docs/minio-storage-migration.md (prompted by the artpipe-gpu label incident that pinned concept-art to SERVER):
MinIO is not actually running. The doc's status line says 'MinIO deployed', but there is no minio service in the swarm and minio.spikersoft.com is not a Traefik router. Deploying the committed minio/docker-stack.yml (needs MINIO_ROOT_USER/PASSWORD exported) is prerequisite step 0. Filed as the first concrete step -> see the new pilot ticket.
art_pipe is a poor FIRST candidate (the doc already flags it as 'deliberately not bucket material'): /mnt/fusionio/spikersoft/art_pipe is a live checkout + Python venvs the six artpipe stacks exec into — you can't exec a Python interpreter out of S3. Decoupling art_pipe from SERVER is really two separate jobs: (a) bake venvs/CUDA into the container image (or a per-node cache), and (b) sync model weights from the ai-models bucket at startup. That's a bigger, art_pipe-specific workstream — the reward for the pattern, not the pilot.
Recommended pilot = security-scanner (single ro uploads bind, smallest blast radius), then metadata-extractor. Filed a scoped pilot ticket with the deploy-MinIO prerequisite + the full per-service recipe.
Motivation is now concrete: the artpipe-gpu label could not follow off SERVER because every artpipe stack bind-mounts /mnt/fusionio/spikersoft/art_pipe, which only exists on SERVER — exactly the host-path pin this epic exists to remove.
Grounding notes from a live look at the swarm + docs/minio-storage-migration.md (prompted by the artpipe-gpu label incident that pinned concept-art to SERVER):
1. **MinIO is not actually running.** The doc's status line says 'MinIO deployed', but there is no minio service in the swarm and minio.spikersoft.com is not a Traefik router. Deploying the committed minio/docker-stack.yml (needs MINIO_ROOT_USER/PASSWORD exported) is prerequisite step 0. Filed as the first concrete step -> see the new pilot ticket.
2. **art_pipe is a poor FIRST candidate** (the doc already flags it as 'deliberately not bucket material'): /mnt/fusionio/spikersoft/art_pipe is a live checkout + Python venvs the six artpipe stacks exec into — you can't exec a Python interpreter out of S3. Decoupling art_pipe from SERVER is really two separate jobs: (a) bake venvs/CUDA into the container image (or a per-node cache), and (b) sync model *weights* from the ai-models bucket at startup. That's a bigger, art_pipe-specific workstream — the reward for the pattern, not the pilot.
3. **Recommended pilot = security-scanner** (single ro uploads bind, smallest blast radius), then metadata-extractor. Filed a scoped pilot ticket with the deploy-MinIO prerequisite + the full per-service recipe.
Motivation is now concrete: the artpipe-gpu label could not follow off SERVER because every artpipe stack bind-mounts /mnt/fusionio/spikersoft/art_pipe, which only exists on SERVER — exactly the host-path pin this epic exists to remove.
Child tickets filed for the remaining per-service migrations (2026-07-12), sequenced per docs/minio-storage-migration.md — standalone dirs first, uploads tree as one batch:
#533 shared IObjectStore refactor (the storage-interface acceptance criterion of this epic)
Related but separate workstream: #515 (art_pipe per-model baked images — removes the /opt/art_pipe bind the migration doc scoped out) and #521 (Gitea upstream-image mirror).
Child tickets filed for the remaining per-service migrations (2026-07-12), sequenced per docs/minio-storage-migration.md — standalone dirs first, uploads tree as one batch:
- #522 blogs · #523 lesson-videos · #524 dlls · #525 assets · #526 quarantine · #527 ai→ai-models
- #528 API static-file media serving → S3 delivery
- #529 uploads-tree coordinated batch cutover (after the standalones + #493)
- #530 GeoLite2 → reference-data bucket (also the GeoIP half of #489)
- #531 Angular nginx root bind (last) · #532 sftp via MinIO SFTP mode (optional)
- #533 shared IObjectStore refactor (the storage-interface acceptance criterion of this epic)
Related but separate workstream: #515 (art_pipe per-model baked images — removes the /opt/art_pipe bind the migration doc scoped out) and #521 (Gitea upstream-image mirror).
EPIC SCOREBOARD — 2026-07-18 accuracy pass (supersedes the 2026-07-13 one)
Every claim below re-verified against current masters (backend/infra/angular) and the live ticket states, not carried forward.
Acceptance criteria
✅Storage interface with MinIO impl, no direct host-path I/O in the seam — SpikerSoft.Storage (IObjectStore/S3ObjectStore + keyed stores + per-op timeouts), all services consume it (#533 closed)
✅ Same code runs on-prem/cloud via config (Storage__* env, path-style)
❌Swarm placement no longer pinned by storage — NOT met yet: every stack still carries its /mnt/fusionio binds + node.hostname == SERVER. This is the remaining work.
Children — CLOSED (code merged + buckets provisioned/backfilled 07-14)
The misleading "zero services migrated" status is fixed in infra PR #125 (migration doc + infrastructure-reality rule mirrors).
Net: the epic's code phase is done and proven; what stands between here and closing #413 is ops: key policies → soak → bind drops (#529), the ebooks sole-store phases (#696), and the final nginx bind (#531).
## EPIC SCOREBOARD — 2026-07-18 accuracy pass (supersedes the 2026-07-13 one)
Every claim below re-verified against current masters (backend/infra/angular) and the live ticket states, not carried forward.
### Acceptance criteria
- ✅ **Storage interface with MinIO impl, no direct host-path I/O in the seam** — `SpikerSoft.Storage` (`IObjectStore`/`S3ObjectStore` + keyed stores + per-op timeouts), all services consume it (#533 closed)
- ✅ Same code runs on-prem/cloud via config (`Storage__*` env, path-style)
- ❌ **Swarm placement no longer pinned by storage** — NOT met yet: every stack still carries its `/mnt/fusionio` binds + `node.hostname == SERVER`. This is the remaining work.
### Children — CLOSED (code merged + buckets provisioned/backfilled 07-14)
#493 pilot · #498 gitea-packages · #522 blogs · #523 lesson-videos · #524 dlls · #525 assets · #526 quarantine · #527 ai-models · #528 API media serving · #533 seam · plus fixed enablers #581, #598, #599, #542.
### Still OPEN — the real remaining work
| Ticket | What actually remains |
|---|---|
| **#529** | THE ops cutover (consolidated checklist posted there): key policies (#613, blogs-svc write, uploads-svc read-ebooks), lesson-video-processor deploy check, soak, then the phase-2 bind-drop + placement-relax PR |
| **#696** | ebooks slice → sole store (timeout half landed; internal MinIO route, write-primary, backfill-verify, API serving cutover, retire mirror seam) |
| **#531** | Angular nginx root bind (LAST) — **rescoped**: proxy blogs+lesson-videos only; ebooks stays API/BookAccess-served (never anonymous) |
| **#530** | GeoIP — code done; gated on #540 (Scheduler deployed nowhere) |
| **#532** | sftp retire/replace — one `ls` on SERVER decides it |
| Related | #613 (policy fix, gates #529), #700 (MinIO/Gitea SPOF), #538 (LAN hairpin ≈ #696 P0 internal route), #521 (registry mirror) |
### Docs
The misleading "zero services migrated" status is fixed in **infra PR #125** (migration doc + infrastructure-reality rule mirrors).
**Net:** the epic's code phase is done and proven; what stands between here and closing #413 is ops: key policies → soak → bind drops (#529), the ebooks sole-store phases (#696), and the final nginx bind (#531).
#531 confirmed not started:spikersoft-angular/docker-stack.yml:10 still binds /mnt/fusionio/spikersoft:/usr/share/nginx/html/spikersoft. Correctly sequenced last, as the ticket says.
The single highest-leverage item is #613. All its code and provisioning are merged — the stack uses movement-svc (spikersoft-file-movement/docker-stack.yml:32-33), openbao/provision-minio-svc-users.sh:20 grants the right policy, and both rotation scripts honour ONLY_SVCS. What's missing is one command: ONLY_SVCS="movement" rotate-minio-ci-keys.sh with an admin-capable Bao token. That unblocks #529's phase-0 items and #783's remaining follow-up.
#529 is where the epic's remaining bulk sits, and it's genuinely at zero on phase 2. Storage__UseS3=true is set on all seven services, so dual-run works — but every one still binds /mnt/fusionio and is still pinned node.hostname == SERVER. So the payoff this epic exists for (scheduling off SERVER) isn't available on any service yet. Per-service line numbers are on that ticket.
Two cross-cutting cautions for the remaining cutovers:
#825's prevention items were never written down. Its four asks — writer-first/reader-last ordering, backfill after all writers flip, a post-cutover verification sweep — are absent from docs/uploads-batch-cutover-runbook.md and docs/minio-storage-migration.md. That ordering rule has already cost two production incidents (#825 itself, then #856 two days later, same shape). #529 and #696 both have live backfill steps, so the runbook would currently let an operator repeat it.
#615/#774 mean deploys can't be trusted to have applied. Root has no git credentials so sudo git -C /mnt/infrastructure pull fails and is swallowed by || echo "WARNING", and nothing polls for convergence afterwards. For an epic whose remaining work is almost entirely stack-file edits, that's the difference between "deployed" and "reported deployed". #820 is a live instance: an infra-only env change never reaches the artpipe workers because their deploy job only triggers on backend pushes.
Also worth confirming before this epic closes:#783's third operator step — the collection sweep proving Mongo GridFS is actually empty. This epic's headline claim rests on it, and nothing has recorded it running.
**Epic status roll-up** — I've now audited most children against `origin/master` this week. Consolidating so the remaining work is visible in one place.
| Child | State |
|---|---|
| #783 art-asset-artifacts → MinIO | ⚠️ code + migration **done and recorded** (74/74, ~4.5 GB, SHA-256 read-back, 0 failures); 3 operator steps outstanding |
| #530 GeoLite2 → reference-data | ⚠️ code complete, **#540 blocker now cleared**; no GeoIpUpdate task has ever been created |
| #529 uploads-tree 7-service cutover | ❌ **phase 2 not started on any service** |
| #696 ebooks sole-store | ❌ dual-write seam intact; backfill unverified |
| #531 nginx root bind (LAST) | ❌ not started — see below |
| #532 sftp → MinIO SFTP mode (optional) | ❌ not started — `sftp/docker-swarm.yml` still present, no SFTP flag on the minio stack |
| #613 movement-svc key | ⚠️ **one command from done** |
**#531 confirmed not started:** `spikersoft-angular/docker-stack.yml:10` still binds `/mnt/fusionio/spikersoft:/usr/share/nginx/html/spikersoft`. Correctly sequenced last, as the ticket says.
**The single highest-leverage item is #613.** All its code and provisioning are merged — the stack uses `movement-svc` (`spikersoft-file-movement/docker-stack.yml:32-33`), `openbao/provision-minio-svc-users.sh:20` grants the right policy, and both rotation scripts honour `ONLY_SVCS`. What's missing is one command: `ONLY_SVCS="movement" rotate-minio-ci-keys.sh` with an admin-capable Bao token. That unblocks #529's phase-0 items and #783's remaining follow-up.
**#529 is where the epic's remaining bulk sits**, and it's genuinely at zero on phase 2. `Storage__UseS3=true` is set on all seven services, so dual-run works — but **every one still binds `/mnt/fusionio` and is still pinned `node.hostname == SERVER`**. So the payoff this epic exists for (scheduling off SERVER) isn't available on any service yet. Per-service line numbers are on that ticket.
**Two cross-cutting cautions for the remaining cutovers:**
1. **#825's prevention items were never written down.** Its four asks — writer-first/reader-last ordering, backfill after all writers flip, a post-cutover verification sweep — are absent from `docs/uploads-batch-cutover-runbook.md` and `docs/minio-storage-migration.md`. That ordering rule has already cost two production incidents (#825 itself, then #856 two days later, same shape). #529 and #696 both have live backfill steps, so the runbook would currently let an operator repeat it.
2. **#615/#774 mean deploys can't be trusted to have applied.** Root has no git credentials so `sudo git -C /mnt/infrastructure pull` fails and is swallowed by `|| echo "WARNING"`, and nothing polls for convergence afterwards. For an epic whose remaining work is almost entirely stack-file edits, that's the difference between "deployed" and "reported deployed". #820 is a live instance: an infra-only env change never reaches the artpipe workers because their deploy job only triggers on backend pushes.
**Also worth confirming before this epic closes:** #783's third operator step — the collection sweep proving Mongo GridFS is actually empty. This epic's headline claim rests on it, and nothing has recorded it running.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Context: The genuine structural limitation behind the host-locked Swarm and several path-based file findings.
Problem: Shared media/files live on a GlusterFS volume spanning
DreamStream1–DreamStream7(maintained by DreamStream4), while theSERVERandlaptop-serverhosts each only see their own local filesystem. Storage is tied to where a container is scheduled, so Swarm placement must be host-locked, and file handlers assume shared host paths (/app/ebooks/books/…, quarantine and upload-staging directories).Fix: Introduce a provider-agnostic object-storage abstraction behind a single storage interface — self-hosted MinIO now (S3-compatible), swappable for AWS S3 / Azure Blob / GCS in a datacenter or cloud — so the app addresses content by bucket + key, never by host path. This decouples storage from node placement, frees Swarm scheduling, and turns the filesystem-path handlers (and their traversal risks) into safe, portable object operations.
Acceptance criteria:
Effort: L · Prerequisite for INF-2 (this batch). Unblocks the path-based handlers referenced in CR-4 / CR-6.
Prerequisite for the production deployment model INF-2 #414.
Grounding notes from a live look at the swarm + docs/minio-storage-migration.md (prompted by the artpipe-gpu label incident that pinned concept-art to SERVER):
MinIO is not actually running. The doc's status line says 'MinIO deployed', but there is no minio service in the swarm and minio.spikersoft.com is not a Traefik router. Deploying the committed minio/docker-stack.yml (needs MINIO_ROOT_USER/PASSWORD exported) is prerequisite step 0. Filed as the first concrete step -> see the new pilot ticket.
art_pipe is a poor FIRST candidate (the doc already flags it as 'deliberately not bucket material'): /mnt/fusionio/spikersoft/art_pipe is a live checkout + Python venvs the six artpipe stacks exec into — you can't exec a Python interpreter out of S3. Decoupling art_pipe from SERVER is really two separate jobs: (a) bake venvs/CUDA into the container image (or a per-node cache), and (b) sync model weights from the ai-models bucket at startup. That's a bigger, art_pipe-specific workstream — the reward for the pattern, not the pilot.
Recommended pilot = security-scanner (single ro uploads bind, smallest blast radius), then metadata-extractor. Filed a scoped pilot ticket with the deploy-MinIO prerequisite + the full per-service recipe.
Motivation is now concrete: the artpipe-gpu label could not follow off SERVER because every artpipe stack bind-mounts /mnt/fusionio/spikersoft/art_pipe, which only exists on SERVER — exactly the host-path pin this epic exists to remove.
Child tickets filed for the remaining per-service migrations (2026-07-12), sequenced per docs/minio-storage-migration.md — standalone dirs first, uploads tree as one batch:
Related but separate workstream: #515 (art_pipe per-model baked images — removes the /opt/art_pipe bind the migration doc scoped out) and #521 (Gitea upstream-image mirror).
spikerj referenced this issue2026-07-13 01:50:10 +00:00
EPIC SCOREBOARD (2026-07-13, after 19 loop iterations): every child ticket is code-complete. MERGED: #533 seam (closed), #530 GeoIP, #540 scheduler, #526 quarantine, #524 dlls, #525 assets, #522 blogs, #523 lesson-videos, #527 ai-models, #528 API media serving, infra #56-#59. GATED PRs open: backend #264 + infra #60 (uploads-batch dual-run prep + cutover runbook), angular #177 (SECURITY #542 — merge first) + #178 (#531 nginx proxy, the last bind). Critical path is now pure ops: runbook phase 0 (buckets/keys/secrets/backfills) → phase 1 soak → phase 2 cutover PR → angular #178 + bind drop. After that the swarm's app tier has zero /mnt/fusionio dependencies.
EPIC SCOREBOARD — 2026-07-18 accuracy pass (supersedes the 2026-07-13 one)
Every claim below re-verified against current masters (backend/infra/angular) and the live ticket states, not carried forward.
Acceptance criteria
SpikerSoft.Storage(IObjectStore/S3ObjectStore+ keyed stores + per-op timeouts), all services consume it (#533 closed)Storage__*env, path-style)/mnt/fusioniobinds +node.hostname == SERVER. This is the remaining work.Children — CLOSED (code merged + buckets provisioned/backfilled 07-14)
#493 pilot · #498 gitea-packages · #522 blogs · #523 lesson-videos · #524 dlls · #525 assets · #526 quarantine · #527 ai-models · #528 API media serving · #533 seam · plus fixed enablers #581, #598, #599, #542.
Still OPEN — the real remaining work
lson SERVER decides itDocs
The misleading "zero services migrated" status is fixed in infra PR #125 (migration doc + infrastructure-reality rule mirrors).
Net: the epic's code phase is done and proven; what stands between here and closing #413 is ops: key policies → soak → bind drops (#529), the ebooks sole-store phases (#696), and the final nginx bind (#531).
Epic status roll-up — I've now audited most children against
origin/masterthis week. Consolidating so the remaining work is visible in one place.sftp/docker-swarm.ymlstill present, no SFTP flag on the minio stack#531 confirmed not started:
spikersoft-angular/docker-stack.yml:10still binds/mnt/fusionio/spikersoft:/usr/share/nginx/html/spikersoft. Correctly sequenced last, as the ticket says.The single highest-leverage item is #613. All its code and provisioning are merged — the stack uses
movement-svc(spikersoft-file-movement/docker-stack.yml:32-33),openbao/provision-minio-svc-users.sh:20grants the right policy, and both rotation scripts honourONLY_SVCS. What's missing is one command:ONLY_SVCS="movement" rotate-minio-ci-keys.shwith an admin-capable Bao token. That unblocks #529's phase-0 items and #783's remaining follow-up.#529 is where the epic's remaining bulk sits, and it's genuinely at zero on phase 2.
Storage__UseS3=trueis set on all seven services, so dual-run works — but every one still binds/mnt/fusionioand is still pinnednode.hostname == SERVER. So the payoff this epic exists for (scheduling off SERVER) isn't available on any service yet. Per-service line numbers are on that ticket.Two cross-cutting cautions for the remaining cutovers:
#825's prevention items were never written down. Its four asks — writer-first/reader-last ordering, backfill after all writers flip, a post-cutover verification sweep — are absent from
docs/uploads-batch-cutover-runbook.mdanddocs/minio-storage-migration.md. That ordering rule has already cost two production incidents (#825 itself, then #856 two days later, same shape). #529 and #696 both have live backfill steps, so the runbook would currently let an operator repeat it.#615/#774 mean deploys can't be trusted to have applied. Root has no git credentials so
sudo git -C /mnt/infrastructure pullfails and is swallowed by|| echo "WARNING", and nothing polls for convergence afterwards. For an epic whose remaining work is almost entirely stack-file edits, that's the difference between "deployed" and "reported deployed". #820 is a live instance: an infra-only env change never reaches the artpipe workers because their deploy job only triggers on backend pushes.Also worth confirming before this epic closes: #783's third operator step — the collection sweep proving Mongo GridFS is actually empty. This epic's headline claim rests on it, and nothing has recorded it running.