Follow-up to #359 (closed; epic #346). #359 delivered the Art Studio flow on the Mac: scripts/mac-dev-up.sh brings up compose infra + native API + native ArtPipeProcessor (MPS). This ticket extends that to the entire site flow, so quiz generation can be tested locally alongside Art Studio. All work is in spikersoft-backend unless noted.
Why each piece is needed (traced, not speculative)
SpikerSoft.EventHandlers.QuizGeneration reads pre-extracted page text from Mongo (QuizGenerationWorkerService — "no PDF re-parsing needed"). Without the upload pipeline having run, there is nothing to generate quizzes from. The producer chain is: upload-coordinator → security-scanner (ClamAV) → metadata-extractor → book-management / file-movement, with SpikerSoft.EventHandlers.Embeddings storing pages + vectors (BookPageStorageService).
AI:EnableRAG defaults to true and queries RediSearch KNN (RedisVectorSearchService), which needs the embeddings worker to have populated the index. The mac compose override's single-node Redis Stack already loads the redisearch module.
Both quiz + embeddings csprojs hard-reference LLamaSharp.Backend.Cuda12, which cannot load on macOS.
Task 1 — LLamaSharp Metal backend (only real code change)
In SpikerSoft.EventHandlers.QuizGeneration/SpikerSoft.EventHandlers.QuizGeneration.csproj and SpikerSoft.EventHandlers.Embeddings/SpikerSoft.EventHandlers.Embeddings.csproj, make the backend package conditional:
Backend.Cpu ships Metal-enabled llama.cpp binaries for osx-arm64. Keep the existing comment about the PrivateAssets defense-in-depth. Verify Metal engages at runtime (llama.cpp logs ggml_metal / offloaded layers with GpuLayerCount=99). Note: this changes the Linux/CUDA build not at all — Docker images build on Linux and keep Cuda12.
Task 2 — GGUF models on the host
Add scripts/mac-dev-models.sh (idempotent, curl/hf download into ~/ai-models/gguf):
Appsettings paths are container paths (/app/ai/...), so the native workers must be launched with AI__Models__Fast__Path, AI__Models__Balanced__Path, AI__Models__Quality__Path, and AI__EmbeddingModelPath overrides pointing at the host files.
Task 3 — Upload pipeline containers in the mac compose override
Add to docker-compose.mac.yml and to COMPOSE_SERVICES in scripts/mac-dev-common.sh: upload-coordinator, security-scanner, metadata-extractor, book-management, file-movement, clamav.
clamav: add platform: linux/amd64 (no arm64 image; runs under Rosetta — this was the documented workaround in the mac.yml header, now wired in for real). First boot downloads signatures (start_period 300s already in base file).
The base file's volume mounts are Windows paths (C:\temp\spikersoft-api-local\...). Override them in the mac file to a shared host dir, e.g. ~/spikersoft-local/{uploads,ebooks,blogs,dlls,code,assets,quarantine}.
The host-side API launched by mac-dev-up.sh must write uploads into that same shared dir — add the corresponding path config overrides to the api_env block (find the upload/asset root keys in SpikerSoft.Api appsettings).
Task 4 — Native quiz + embeddings workers in mac-dev-up.sh
New flag --full (implies existing behavior plus):
Build + launch SpikerSoft.EventHandlers.QuizGeneration and SpikerSoft.EventHandlers.Embeddings host-side, same nohup/pid-file/log pattern as the API and worker (.mac-dev/quiz.pid, .mac-dev/embeddings.pid, logs in .mac-dev/logs/).
Env for both: ASPNETCORE_ENVIRONMENT=Development, Mongo__ConnectionString=mongodb://localhost:27117/..., RabbitMQ__HostName=localhost, Redis__ConnectionString=localhost:6380,... (embeddings), Seq__ServerUrl=http://localhost:5341, Jaeger__EndPoint=http://localhost:4317, GpuScheduling__Mode=dedicated (single-user laptop; no coordinator arbitration needed), model path overrides from Task 2, distinct Kestrel ports for healthz (avoid 8080 — the art worker owns it; suggest 8180/8181).
Preflight: warn when GGUFs are missing (mirror the venv warning pattern) and point at scripts/mac-dev-models.sh.
scripts/mac-dev-down.sh stops both.
Task 5 — End-to-end smoke
Add scripts/mac-dev-smoke-quiz.sh (or extend mac-dev-smoke.sh): upload a small PDF via the API (HTTPS :5291, multipart), poll Mongo until pre-extracted pages exist for the book, trigger quiz generation via the API, poll until the quiz document appears; print timings. If the RAG/RediSearch path misbehaves on single-node Redis Stack, fall back to AI__EnableRAG=false for the quiz worker and note it in the doc.
Task 6 — Docs
Update docs/mac-local-stack.md: full-flow recipe (mac-dev-models.sh once → mac-dev-up.sh --full), download sizes, shared-dir layout, ClamAV/Rosetta caveat, healthz port map.
Out of scope
image-description (Qwen3-VL) on MPS — possible natively via transformers, not needed for quiz or art flows; separate ticket if wanted.
CUDA-only art lanes (TripoSG, Hunyuan3DPaint) — no Mac path exists; production hardware track covers these.
Acceptance
On the M5 with venvs + Blender already set up per #359: scripts/mac-dev-models.sh && scripts/mac-dev-up.sh --full brings everything up; uploading a PDF through the local site produces an available book; requesting a quiz on it returns generated questions using Metal-accelerated gemma (verify offload in the quiz log); the Art Studio smoke from #359 still passes; mac-dev-down.sh --all tears down clean.
Follow-up to #359 (closed; epic #346). #359 delivered the Art Studio flow on the Mac: `scripts/mac-dev-up.sh` brings up compose infra + native API + native ArtPipeProcessor (MPS). This ticket extends that to the **entire site flow**, so quiz generation can be tested locally alongside Art Studio. All work is in `spikersoft-backend` unless noted.
## Why each piece is needed (traced, not speculative)
- `SpikerSoft.EventHandlers.QuizGeneration` reads **pre-extracted page text from Mongo** (`QuizGenerationWorkerService` — "no PDF re-parsing needed"). Without the upload pipeline having run, there is nothing to generate quizzes from. The producer chain is: upload-coordinator → security-scanner (ClamAV) → metadata-extractor → book-management / file-movement, with `SpikerSoft.EventHandlers.Embeddings` storing pages + vectors (`BookPageStorageService`).
- `AI:EnableRAG` defaults to `true` and queries RediSearch KNN (`RedisVectorSearchService`), which needs the embeddings worker to have populated the index. The mac compose override's single-node Redis Stack already loads the redisearch module.
- Both quiz + embeddings csprojs hard-reference `LLamaSharp.Backend.Cuda12`, which cannot load on macOS.
## Task 1 — LLamaSharp Metal backend (only real code change)
In `SpikerSoft.EventHandlers.QuizGeneration/SpikerSoft.EventHandlers.QuizGeneration.csproj` and `SpikerSoft.EventHandlers.Embeddings/SpikerSoft.EventHandlers.Embeddings.csproj`, make the backend package conditional:
```xml
<PackageReference Include="LLamaSharp.Backend.Cuda12" Version="0.27.0" Condition="!$([MSBuild]::IsOsPlatform('OSX'))" />
<PackageReference Include="LLamaSharp.Backend.Cpu" Version="0.27.0" Condition="$([MSBuild]::IsOsPlatform('OSX'))" />
```
`Backend.Cpu` ships Metal-enabled llama.cpp binaries for osx-arm64. Keep the existing comment about the PrivateAssets defense-in-depth. Verify Metal engages at runtime (llama.cpp logs `ggml_metal` / offloaded layers with `GpuLayerCount=99`). Note: this changes the Linux/CUDA build not at all — Docker images build on Linux and keep Cuda12.
## Task 2 — GGUF models on the host
Add `scripts/mac-dev-models.sh` (idempotent, curl/hf download into `~/ai-models/gguf`):
- `gemma-3n-E2B-it-Q8_0.gguf` (Fast preset, ~5 GB)
- `gemma-3-12b-it-Q4_K_M.gguf` (Balanced preset — default, ~9 GB)
- `nomic-embed-text-v2-moe.f16.gguf` (embeddings)
- `Phi-4-reasoning-plus-Q4_K_M.gguf` (Quality preset) optional / `--all`
Appsettings paths are container paths (`/app/ai/...`), so the native workers must be launched with `AI__Models__Fast__Path`, `AI__Models__Balanced__Path`, `AI__Models__Quality__Path`, and `AI__EmbeddingModelPath` overrides pointing at the host files.
## Task 3 — Upload pipeline containers in the mac compose override
Add to `docker-compose.mac.yml` and to `COMPOSE_SERVICES` in `scripts/mac-dev-common.sh`: `upload-coordinator`, `security-scanner`, `metadata-extractor`, `book-management`, `file-movement`, `clamav`.
- `clamav`: add `platform: linux/amd64` (no arm64 image; runs under Rosetta — this was the documented workaround in the mac.yml header, now wired in for real). First boot downloads signatures (start_period 300s already in base file).
- The base file's volume mounts are Windows paths (`C:\temp\spikersoft-api-local\...`). Override them in the mac file to a shared host dir, e.g. `~/spikersoft-local/{uploads,ebooks,blogs,dlls,code,assets,quarantine}`.
- The host-side API launched by mac-dev-up.sh must write uploads into that same shared dir — add the corresponding path config overrides to the `api_env` block (find the upload/asset root keys in `SpikerSoft.Api` appsettings).
## Task 4 — Native quiz + embeddings workers in mac-dev-up.sh
New flag `--full` (implies existing behavior plus):
- Build + launch `SpikerSoft.EventHandlers.QuizGeneration` and `SpikerSoft.EventHandlers.Embeddings` host-side, same nohup/pid-file/log pattern as the API and worker (`.mac-dev/quiz.pid`, `.mac-dev/embeddings.pid`, logs in `.mac-dev/logs/`).
- Env for both: `ASPNETCORE_ENVIRONMENT=Development`, `Mongo__ConnectionString=mongodb://localhost:27117/...`, `RabbitMQ__HostName=localhost`, `Redis__ConnectionString=localhost:6380,...` (embeddings), `Seq__ServerUrl=http://localhost:5341`, `Jaeger__EndPoint=http://localhost:4317`, `GpuScheduling__Mode=dedicated` (single-user laptop; no coordinator arbitration needed), model path overrides from Task 2, distinct Kestrel ports for healthz (avoid 8080 — the art worker owns it; suggest 8180/8181).
- Preflight: warn when GGUFs are missing (mirror the venv warning pattern) and point at `scripts/mac-dev-models.sh`.
- `scripts/mac-dev-down.sh` stops both.
## Task 5 — End-to-end smoke
Add `scripts/mac-dev-smoke-quiz.sh` (or extend mac-dev-smoke.sh): upload a small PDF via the API (HTTPS :5291, multipart), poll Mongo until pre-extracted pages exist for the book, trigger quiz generation via the API, poll until the quiz document appears; print timings. If the RAG/RediSearch path misbehaves on single-node Redis Stack, fall back to `AI__EnableRAG=false` for the quiz worker and note it in the doc.
## Task 6 — Docs
Update `docs/mac-local-stack.md`: full-flow recipe (`mac-dev-models.sh` once → `mac-dev-up.sh --full`), download sizes, shared-dir layout, ClamAV/Rosetta caveat, healthz port map.
## Out of scope
- image-description (Qwen3-VL) on MPS — possible natively via transformers, not needed for quiz or art flows; separate ticket if wanted.
- CUDA-only art lanes (TripoSG, Hunyuan3DPaint) — no Mac path exists; production hardware track covers these.
## Acceptance
On the M5 with venvs + Blender already set up per #359: `scripts/mac-dev-models.sh && scripts/mac-dev-up.sh --full` brings everything up; uploading a PDF through the local site produces an available book; requesting a quiz on it returns generated questions using Metal-accelerated gemma (verify offload in the quiz log); the Art Studio smoke from #359 still passes; `mac-dev-down.sh --all` tears down clean.
Status 2026-07-08: implementation complete, committed locally on spikersoft-backend branch feat/464-mac-full-site-flow (add055b). Paused before the full acceptance run — home internet is currently unusable (~200 KB/s, flaky), which blocks the GGUF downloads and the in-Docker NuGet restores.
Done & verified on the M5
Task 1 — LLamaSharp.Backend.Cuda12 is now Condition="!IsOsPlatform('OSX')" with Backend.Cpu on OSX in both csprojs. Both workers restore/build on macOS and runtimes/osx-arm64/native/libggml-metal.dylib is in the output (Metal binaries confirmed). Linux/Docker builds unchanged.
Task 2 — scripts/mac-dev-models.sh (idempotent, curl resume, --all for Phi-4). Note: the README's google//bartowski/ HF URLs are gated or 404 today; switched to the ungated unsloth/ + nomic-ai/ mirrors (exact filename matches verified via HF API; README updated).
Task 3 — pipeline services in docker-compose.mac.yml (clamav platform: linux/amd64), Windows mounts replaced with $MAC_SHARED_DIR (~/spikersoft-local). Design note: the pipeline messages carry absolute StagingPaths and metadata-extractor hardcodes /app/ebooks in code, so each container mounts the shared dir at both /app/<subdir> and the identical absolute host path (mirror mount). Merged compose config validates. Kept the pipeline services out of the base COMPOSE_SERVICES (art-only bring-up shouldn't build 5 images / run Rosetta ClamAV) — they're a separate UPLOAD_PIPELINE_SERVICES list appended by --full.
Task 4 — mac-dev-up.sh --full builds+launches both native workers (pids/logs in .mac-dev/), GGUF preflight warning, health waits on :8180/:8181; mac-dev-down.sh stops them (exercised). Extra code change the ticket needed: WithKestrel now honors a Kestrel:Port config override — it calls UseUrls, which env vars can't override, so 8180/8181 was impossible without it. Containers don't set the key; deployed behavior unchanged. Also added MAC_DEV_ENABLE_RAG=false as the RediSearch fallback knob.
Task 5 — scripts/mac-dev-smoke-quiz.sh: generates a text-only PDF via cupsfilter (books with images wait ~30 min on the out-of-scope image-description worker — documented), uploads as test user, approves as the seeded e2e.staff account, follows the workflow in Mongo (status → book-pages → EmbeddingsComplete → quiz doc), prints timings + sample question. Second extra code change:ApproveUpload requires the staff role, which the role-less test user lacks — under UseTestAuthentication=true the real Keycloak JWT bearer scheme now stays registered alongside the test scheme. Verified live on the M5: token-less → 403 on GET /api/book/pending (art-studio behavior from #359 unchanged), real e2e.staff token (password grant, spikersoft-backend client) → 200. Keycloak only contacted when a Bearer token is present, so the stack stays offline-capable. Bonus: a locally-served Angular app can now hit the local API with real logins.
Task 6 — docs/mac-local-stack.md §7: recipe, model table with sizes/sources, healthz port map (API 5290/5291, art 8080, quiz 8180, embeddings 8181), shared-dir layout, ClamAV/Rosetta + GET /api/book/{id}/file caveats.
Blocked on connectivity (resume when internet is good)
scripts/mac-dev-models.sh — resumes the three .part files already in ~/ai-models/gguf (downloads smallest-first: nomic → gemma-3n → gemma-12b).
docker compose -f docker-compose.yml -f docker-compose.mac.yml -f docker-compose.redisport.yml build upload-coordinator security-scanner metadata-extractor book-management file-movement (first attempt died in in-container dotnet restore) — or just let mac-dev-up.sh --full build them.
scripts/mac-dev-up.sh --full → scripts/mac-dev-smoke-quiz.sh (needs spikersoft-angular/.env.e2e staff creds — already present on the M5), verify ggml_metal offload in .mac-dev/logs/quiz.log, re-run the #359 art smoke, mac-dev-down.sh --all.
Push branch + open the PR once the smoke passes.
**Status 2026-07-08: implementation complete, committed locally on `spikersoft-backend` branch `feat/464-mac-full-site-flow` (add055b). Paused before the full acceptance run — home internet is currently unusable (~200 KB/s, flaky), which blocks the GGUF downloads and the in-Docker NuGet restores.**
### Done & verified on the M5
- **Task 1** — `LLamaSharp.Backend.Cuda12` is now `Condition="!IsOsPlatform('OSX')"` with `Backend.Cpu` on OSX in both csprojs. Both workers restore/build on macOS and `runtimes/osx-arm64/native/libggml-metal.dylib` is in the output (Metal binaries confirmed). Linux/Docker builds unchanged.
- **Task 2** — `scripts/mac-dev-models.sh` (idempotent, curl resume, `--all` for Phi-4). Note: the README's `google/`/`bartowski/` HF URLs are gated or 404 today; switched to the ungated `unsloth/` + `nomic-ai/` mirrors (exact filename matches verified via HF API; README updated).
- **Task 3** — pipeline services in `docker-compose.mac.yml` (clamav `platform: linux/amd64`), Windows mounts replaced with `$MAC_SHARED_DIR` (~/spikersoft-local). Design note: the pipeline messages carry **absolute** StagingPaths and metadata-extractor hardcodes `/app/ebooks` in code, so each container mounts the shared dir at both `/app/<subdir>` and the identical absolute host path (mirror mount). Merged compose config validates. Kept the pipeline services out of the base `COMPOSE_SERVICES` (art-only bring-up shouldn't build 5 images / run Rosetta ClamAV) — they're a separate `UPLOAD_PIPELINE_SERVICES` list appended by `--full`.
- **Task 4** — `mac-dev-up.sh --full` builds+launches both native workers (pids/logs in `.mac-dev/`), GGUF preflight warning, health waits on :8180/:8181; `mac-dev-down.sh` stops them (exercised). Extra code change the ticket needed: `WithKestrel` now honors a `Kestrel:Port` config override — it calls `UseUrls`, which env vars can't override, so 8180/8181 was impossible without it. Containers don't set the key; deployed behavior unchanged. Also added `MAC_DEV_ENABLE_RAG=false` as the RediSearch fallback knob.
- **Task 5** — `scripts/mac-dev-smoke-quiz.sh`: generates a **text-only** PDF via cupsfilter (books with images wait ~30 min on the out-of-scope image-description worker — documented), uploads as test user, approves as the seeded `e2e.staff` account, follows the workflow in Mongo (status → book-pages → EmbeddingsComplete → quiz doc), prints timings + sample question. **Second extra code change:** `ApproveUpload` requires the staff role, which the role-less test user lacks — under `UseTestAuthentication=true` the real Keycloak JWT bearer scheme now stays registered alongside the test scheme. Verified live on the M5: token-less → 403 on `GET /api/book/pending` (art-studio behavior from #359 unchanged), real `e2e.staff` token (password grant, `spikersoft-backend` client) → 200. Keycloak only contacted when a Bearer token is present, so the stack stays offline-capable. Bonus: a locally-served Angular app can now hit the local API with real logins.
- **Task 6** — `docs/mac-local-stack.md` §7: recipe, model table with sizes/sources, healthz port map (API 5290/5291, art 8080, quiz 8180, embeddings 8181), shared-dir layout, ClamAV/Rosetta + `GET /api/book/{id}/file` caveats.
- `SpikerSoft.Tests.Unit`: 11437 passed, 4 failed — the identical 4 fail on unmodified master (environmental: GitService per-user repo ×2, Python curriculum ×2). shellcheck clean.
### Blocked on connectivity (resume when internet is good)
1. `scripts/mac-dev-models.sh` — resumes the three `.part` files already in `~/ai-models/gguf` (downloads smallest-first: nomic → gemma-3n → gemma-12b).
2. `docker compose -f docker-compose.yml -f docker-compose.mac.yml -f docker-compose.redisport.yml build upload-coordinator security-scanner metadata-extractor book-management file-movement` (first attempt died in in-container `dotnet restore`) — or just let `mac-dev-up.sh --full` build them.
3. `scripts/mac-dev-up.sh --full` → `scripts/mac-dev-smoke-quiz.sh` (needs `spikersoft-angular/.env.e2e` staff creds — already present on the M5), verify `ggml_metal` offload in `.mac-dev/logs/quiz.log`, re-run the #359 art smoke, `mac-dev-down.sh --all`.
4. Push branch + open the PR once the smoke passes.
Task 1 done — merged to master in spikersoft-backend PR #188 (commit d0e65d8).
LLamaSharp.Backend.Cuda12 is now OS-conditional (!IsOsPlatform('OSX')) with LLamaSharp.Backend.Cpu added on OSX, in both SpikerSoft.EventHandlers.QuizGeneration.csproj and SpikerSoft.EventHandlers.Embeddings.csproj. Backend.Cpu ships Metal-enabled llama.cpp binaries for osx-arm64. The Linux/CUDA production build is unchanged (negated condition + Docker builds on Linux). Verified locally on macOS arm64: QuizGeneration, Embeddings, and SpikerSoft.Tests.Unit all build 0 errors.
Leaving this ticket open — Tasks 2–6 (scripts/mac-dev-models.sh, the mac compose upload-pipeline services, the --full native quiz+embeddings launch in mac-dev-up.sh, the E2E smoke script, and docs) remain, and are in progress on branch feat/464-mac-full-site-flow. Runtime Metal-offload verification depends on the Task 2 GGUFs.
**Task 1 done** — merged to `master` in spikersoft-backend PR #188 (commit `d0e65d8`).
`LLamaSharp.Backend.Cuda12` is now OS-conditional (`!IsOsPlatform('OSX')`) with `LLamaSharp.Backend.Cpu` added on OSX, in both `SpikerSoft.EventHandlers.QuizGeneration.csproj` and `SpikerSoft.EventHandlers.Embeddings.csproj`. Backend.Cpu ships Metal-enabled llama.cpp binaries for osx-arm64. The Linux/CUDA production build is unchanged (negated condition + Docker builds on Linux). Verified locally on macOS arm64: QuizGeneration, Embeddings, and `SpikerSoft.Tests.Unit` all build 0 errors.
Leaving this ticket **open** — Tasks 2–6 (`scripts/mac-dev-models.sh`, the mac compose upload-pipeline services, the `--full` native quiz+embeddings launch in `mac-dev-up.sh`, the E2E smoke script, and docs) remain, and are in progress on branch `feat/464-mac-full-site-flow`. Runtime Metal-offload verification depends on the Task 2 GGUFs.
Acceptance run complete (M5, overnight 2026-07-09) — everything passes. Branch feat/464-mac-full-site-flow (00f672d) pushed to spikersoft-backend; PR not yet opened (waiting for Joey's go-ahead).
mac-dev-models.sh → 3 GGUFs, 12.2 GB (resumed the .part files once the connection recovered; ~1 MB/s overnight)
mac-dev-up.sh --full → infra + 6 pipeline containers + API + art/quiz/embeddings workers healthy in ~21 s (after one-time image builds)
mac-dev-smoke-quiz.sh → PASSED: upload → e2e.staff approval (real Keycloak token against the test-auth API) → ClamAV scan t+4 s → 8 pre-extracted pages in book-pages t+8 s → embeddings + BookPageChunk:* RediSearch vectors t+18 s → 9 RAG-grounded questions t+114 s (sample: "According to the provided text, what is a primary function of message brokers like RabbitMQ?")
Metal offload confirmed: every gemma layer assigned to device MTL0 in quiz.log; ggml_metal_device_init: GPU family MTLGPUFamilyApple10
#359 art smoke still passes: Prop plan, 5 stages, 47 s, all artifacts in GridFS
mac-dev-down.sh --all tears down clean (no orphan processes/containers); restart-after-teardown verified — stack left running
One extra fix found during the run: the base compose ClamAV healthcheck (/usr/local/bin/clamd --ping) doesn't exist in current clamav images, so the container reported unhealthy forever — the mac override now probes with clamdcheck.sh (fix included in the branch)
Remaining: open the PR from feat/464-mac-full-site-flow → master, merge after CI, then comment+close this ticket per the tracker workflow.
**Acceptance run complete (M5, overnight 2026-07-09) — everything passes. Branch `feat/464-mac-full-site-flow` (00f672d) pushed to `spikersoft-backend`; PR not yet opened (waiting for Joey's go-ahead).**
- `mac-dev-models.sh` → 3 GGUFs, 12.2 GB (resumed the `.part` files once the connection recovered; ~1 MB/s overnight)
- `mac-dev-up.sh --full` → infra + 6 pipeline containers + API + art/quiz/embeddings workers healthy in ~21 s (after one-time image builds)
- `mac-dev-smoke-quiz.sh` → **PASSED**: upload → `e2e.staff` approval (real Keycloak token against the test-auth API) → ClamAV scan t+4 s → 8 pre-extracted pages in `book-pages` t+8 s → embeddings + `BookPageChunk:*` RediSearch vectors t+18 s → **9 RAG-grounded questions t+114 s** (sample: "According to the provided text, what is a primary function of message brokers like RabbitMQ?")
- Metal offload confirmed: every gemma layer `assigned to device MTL0` in quiz.log; `ggml_metal_device_init: GPU family MTLGPUFamilyApple10`
- #359 art smoke still passes: Prop plan, 5 stages, 47 s, all artifacts in GridFS
- `mac-dev-down.sh --all` tears down clean (no orphan processes/containers); restart-after-teardown verified — stack left running
- One extra fix found during the run: the base compose ClamAV healthcheck (`/usr/local/bin/clamd --ping`) doesn't exist in current clamav images, so the container reported unhealthy forever — the mac override now probes with `clamdcheck.sh` (fix included in the branch)
Remaining: open the PR from `feat/464-mac-full-site-flow` → master, merge after CI, then comment+close this ticket per the tracker workflow.
Resolved in spikersoft-backend PR #198 (merged; Task 1 in PR #188): mac-dev-up.sh --full brings up the entire site flow on the M5 — quiz generation + embeddings + book upload pipeline — with OS-conditional LLamaSharp Metal backend. Acceptance-verified 2026-07-09. Closing.
Resolved in spikersoft-backend PR #198 (merged; Task 1 in PR #188): mac-dev-up.sh --full brings up the entire site flow on the M5 — quiz generation + embeddings + book upload pipeline — with OS-conditional LLamaSharp Metal backend. Acceptance-verified 2026-07-09. Closing.
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.
Follow-up to #359 (closed; epic #346). #359 delivered the Art Studio flow on the Mac:
scripts/mac-dev-up.shbrings up compose infra + native API + native ArtPipeProcessor (MPS). This ticket extends that to the entire site flow, so quiz generation can be tested locally alongside Art Studio. All work is inspikersoft-backendunless noted.Why each piece is needed (traced, not speculative)
SpikerSoft.EventHandlers.QuizGenerationreads pre-extracted page text from Mongo (QuizGenerationWorkerService— "no PDF re-parsing needed"). Without the upload pipeline having run, there is nothing to generate quizzes from. The producer chain is: upload-coordinator → security-scanner (ClamAV) → metadata-extractor → book-management / file-movement, withSpikerSoft.EventHandlers.Embeddingsstoring pages + vectors (BookPageStorageService).AI:EnableRAGdefaults totrueand queries RediSearch KNN (RedisVectorSearchService), which needs the embeddings worker to have populated the index. The mac compose override's single-node Redis Stack already loads the redisearch module.LLamaSharp.Backend.Cuda12, which cannot load on macOS.Task 1 — LLamaSharp Metal backend (only real code change)
In
SpikerSoft.EventHandlers.QuizGeneration/SpikerSoft.EventHandlers.QuizGeneration.csprojandSpikerSoft.EventHandlers.Embeddings/SpikerSoft.EventHandlers.Embeddings.csproj, make the backend package conditional:Backend.Cpuships Metal-enabled llama.cpp binaries for osx-arm64. Keep the existing comment about the PrivateAssets defense-in-depth. Verify Metal engages at runtime (llama.cpp logsggml_metal/ offloaded layers withGpuLayerCount=99). Note: this changes the Linux/CUDA build not at all — Docker images build on Linux and keep Cuda12.Task 2 — GGUF models on the host
Add
scripts/mac-dev-models.sh(idempotent, curl/hf download into~/ai-models/gguf):gemma-3n-E2B-it-Q8_0.gguf(Fast preset, ~5 GB)gemma-3-12b-it-Q4_K_M.gguf(Balanced preset — default, ~9 GB)nomic-embed-text-v2-moe.f16.gguf(embeddings)Phi-4-reasoning-plus-Q4_K_M.gguf(Quality preset) optional /--allAppsettings paths are container paths (
/app/ai/...), so the native workers must be launched withAI__Models__Fast__Path,AI__Models__Balanced__Path,AI__Models__Quality__Path, andAI__EmbeddingModelPathoverrides pointing at the host files.Task 3 — Upload pipeline containers in the mac compose override
Add to
docker-compose.mac.ymland toCOMPOSE_SERVICESinscripts/mac-dev-common.sh:upload-coordinator,security-scanner,metadata-extractor,book-management,file-movement,clamav.clamav: addplatform: linux/amd64(no arm64 image; runs under Rosetta — this was the documented workaround in the mac.yml header, now wired in for real). First boot downloads signatures (start_period 300s already in base file).C:\temp\spikersoft-api-local\...). Override them in the mac file to a shared host dir, e.g.~/spikersoft-local/{uploads,ebooks,blogs,dlls,code,assets,quarantine}.api_envblock (find the upload/asset root keys inSpikerSoft.Apiappsettings).Task 4 — Native quiz + embeddings workers in mac-dev-up.sh
New flag
--full(implies existing behavior plus):SpikerSoft.EventHandlers.QuizGenerationandSpikerSoft.EventHandlers.Embeddingshost-side, same nohup/pid-file/log pattern as the API and worker (.mac-dev/quiz.pid,.mac-dev/embeddings.pid, logs in.mac-dev/logs/).ASPNETCORE_ENVIRONMENT=Development,Mongo__ConnectionString=mongodb://localhost:27117/...,RabbitMQ__HostName=localhost,Redis__ConnectionString=localhost:6380,...(embeddings),Seq__ServerUrl=http://localhost:5341,Jaeger__EndPoint=http://localhost:4317,GpuScheduling__Mode=dedicated(single-user laptop; no coordinator arbitration needed), model path overrides from Task 2, distinct Kestrel ports for healthz (avoid 8080 — the art worker owns it; suggest 8180/8181).scripts/mac-dev-models.sh.scripts/mac-dev-down.shstops both.Task 5 — End-to-end smoke
Add
scripts/mac-dev-smoke-quiz.sh(or extend mac-dev-smoke.sh): upload a small PDF via the API (HTTPS :5291, multipart), poll Mongo until pre-extracted pages exist for the book, trigger quiz generation via the API, poll until the quiz document appears; print timings. If the RAG/RediSearch path misbehaves on single-node Redis Stack, fall back toAI__EnableRAG=falsefor the quiz worker and note it in the doc.Task 6 — Docs
Update
docs/mac-local-stack.md: full-flow recipe (mac-dev-models.shonce →mac-dev-up.sh --full), download sizes, shared-dir layout, ClamAV/Rosetta caveat, healthz port map.Out of scope
Acceptance
On the M5 with venvs + Blender already set up per #359:
scripts/mac-dev-models.sh && scripts/mac-dev-up.sh --fullbrings everything up; uploading a PDF through the local site produces an available book; requesting a quiz on it returns generated questions using Metal-accelerated gemma (verify offload in the quiz log); the Art Studio smoke from #359 still passes;mac-dev-down.sh --alltears down clean.Status 2026-07-08: implementation complete, committed locally on
spikersoft-backendbranchfeat/464-mac-full-site-flow(add055b). Paused before the full acceptance run — home internet is currently unusable (~200 KB/s, flaky), which blocks the GGUF downloads and the in-Docker NuGet restores.Done & verified on the M5
LLamaSharp.Backend.Cuda12is nowCondition="!IsOsPlatform('OSX')"withBackend.Cpuon OSX in both csprojs. Both workers restore/build on macOS andruntimes/osx-arm64/native/libggml-metal.dylibis in the output (Metal binaries confirmed). Linux/Docker builds unchanged.scripts/mac-dev-models.sh(idempotent, curl resume,--allfor Phi-4). Note: the README'sgoogle//bartowski/HF URLs are gated or 404 today; switched to the ungatedunsloth/+nomic-ai/mirrors (exact filename matches verified via HF API; README updated).docker-compose.mac.yml(clamavplatform: linux/amd64), Windows mounts replaced with$MAC_SHARED_DIR(~/spikersoft-local). Design note: the pipeline messages carry absolute StagingPaths and metadata-extractor hardcodes/app/ebooksin code, so each container mounts the shared dir at both/app/<subdir>and the identical absolute host path (mirror mount). Merged compose config validates. Kept the pipeline services out of the baseCOMPOSE_SERVICES(art-only bring-up shouldn't build 5 images / run Rosetta ClamAV) — they're a separateUPLOAD_PIPELINE_SERVICESlist appended by--full.mac-dev-up.sh --fullbuilds+launches both native workers (pids/logs in.mac-dev/), GGUF preflight warning, health waits on :8180/:8181;mac-dev-down.shstops them (exercised). Extra code change the ticket needed:WithKestrelnow honors aKestrel:Portconfig override — it callsUseUrls, which env vars can't override, so 8180/8181 was impossible without it. Containers don't set the key; deployed behavior unchanged. Also addedMAC_DEV_ENABLE_RAG=falseas the RediSearch fallback knob.scripts/mac-dev-smoke-quiz.sh: generates a text-only PDF via cupsfilter (books with images wait ~30 min on the out-of-scope image-description worker — documented), uploads as test user, approves as the seedede2e.staffaccount, follows the workflow in Mongo (status → book-pages → EmbeddingsComplete → quiz doc), prints timings + sample question. Second extra code change:ApproveUploadrequires the staff role, which the role-less test user lacks — underUseTestAuthentication=truethe real Keycloak JWT bearer scheme now stays registered alongside the test scheme. Verified live on the M5: token-less → 403 onGET /api/book/pending(art-studio behavior from #359 unchanged), reale2e.stafftoken (password grant,spikersoft-backendclient) → 200. Keycloak only contacted when a Bearer token is present, so the stack stays offline-capable. Bonus: a locally-served Angular app can now hit the local API with real logins.docs/mac-local-stack.md§7: recipe, model table with sizes/sources, healthz port map (API 5290/5291, art 8080, quiz 8180, embeddings 8181), shared-dir layout, ClamAV/Rosetta +GET /api/book/{id}/filecaveats.SpikerSoft.Tests.Unit: 11437 passed, 4 failed — the identical 4 fail on unmodified master (environmental: GitService per-user repo ×2, Python curriculum ×2). shellcheck clean.Blocked on connectivity (resume when internet is good)
scripts/mac-dev-models.sh— resumes the three.partfiles already in~/ai-models/gguf(downloads smallest-first: nomic → gemma-3n → gemma-12b).docker compose -f docker-compose.yml -f docker-compose.mac.yml -f docker-compose.redisport.yml build upload-coordinator security-scanner metadata-extractor book-management file-movement(first attempt died in in-containerdotnet restore) — or just letmac-dev-up.sh --fullbuild them.scripts/mac-dev-up.sh --full→scripts/mac-dev-smoke-quiz.sh(needsspikersoft-angular/.env.e2estaff creds — already present on the M5), verifyggml_metaloffload in.mac-dev/logs/quiz.log, re-run the #359 art smoke,mac-dev-down.sh --all.Task 1 done — merged to
masterin spikersoft-backend PR #188 (commitd0e65d8).LLamaSharp.Backend.Cuda12is now OS-conditional (!IsOsPlatform('OSX')) withLLamaSharp.Backend.Cpuadded on OSX, in bothSpikerSoft.EventHandlers.QuizGeneration.csprojandSpikerSoft.EventHandlers.Embeddings.csproj. Backend.Cpu ships Metal-enabled llama.cpp binaries for osx-arm64. The Linux/CUDA production build is unchanged (negated condition + Docker builds on Linux). Verified locally on macOS arm64: QuizGeneration, Embeddings, andSpikerSoft.Tests.Unitall build 0 errors.Leaving this ticket open — Tasks 2–6 (
scripts/mac-dev-models.sh, the mac compose upload-pipeline services, the--fullnative quiz+embeddings launch inmac-dev-up.sh, the E2E smoke script, and docs) remain, and are in progress on branchfeat/464-mac-full-site-flow. Runtime Metal-offload verification depends on the Task 2 GGUFs.Acceptance run complete (M5, overnight 2026-07-09) — everything passes. Branch
feat/464-mac-full-site-flow(00f672d) pushed tospikersoft-backend; PR not yet opened (waiting for Joey's go-ahead).mac-dev-models.sh→ 3 GGUFs, 12.2 GB (resumed the.partfiles once the connection recovered; ~1 MB/s overnight)mac-dev-up.sh --full→ infra + 6 pipeline containers + API + art/quiz/embeddings workers healthy in ~21 s (after one-time image builds)mac-dev-smoke-quiz.sh→ PASSED: upload →e2e.staffapproval (real Keycloak token against the test-auth API) → ClamAV scan t+4 s → 8 pre-extracted pages inbook-pagest+8 s → embeddings +BookPageChunk:*RediSearch vectors t+18 s → 9 RAG-grounded questions t+114 s (sample: "According to the provided text, what is a primary function of message brokers like RabbitMQ?")assigned to device MTL0in quiz.log;ggml_metal_device_init: GPU family MTLGPUFamilyApple10mac-dev-down.sh --alltears down clean (no orphan processes/containers); restart-after-teardown verified — stack left running/usr/local/bin/clamd --ping) doesn't exist in current clamav images, so the container reported unhealthy forever — the mac override now probes withclamdcheck.sh(fix included in the branch)Remaining: open the PR from
feat/464-mac-full-site-flow→ master, merge after CI, then comment+close this ticket per the tracker workflow.Resolved in spikersoft-backend PR #198 (merged; Task 1 in PR #188): mac-dev-up.sh --full brings up the entire site flow on the M5 — quiz generation + embeddings + book upload pipeline — with OS-conditional LLamaSharp Metal backend. Acceptance-verified 2026-07-09. Closing.