vault backup: 2026-04-28 22:26:04
This commit is contained in:
parent
ef2302eb52
commit
c7736f5c91
11 changed files with 524 additions and 2 deletions
|
|
@ -31,6 +31,10 @@ AI-assisted banner generation tool for Barclays marketing assets. Workflow: Brie
|
|||
- **Port:** 8010 (backend API)
|
||||
|
||||
## Sessions
|
||||
### 2026-04-28 – Implement checkbox selection system for users
|
||||
**Asked:** Implement checkbox selection system for users to choose banner variants before editing.
|
||||
**Done:** Fixed banner spec colors (navy to #000063, CTA text/background reversed) and identified all six actual banner sizes.
|
||||
|
||||
### 2026-04-28 – Implement checkbox selection system for banner
|
||||
**Asked:** Implement checkbox selection system for banner variants with validation and workflow control.
|
||||
**Done:** Fixed type mismatch in `refine_variant_copy` function to accept proper dict structure with required keys.
|
||||
|
|
@ -311,6 +315,7 @@ AI-assisted banner generation tool for Barclays marketing assets. Workflow: Brie
|
|||
## Change Log
|
||||
| Date | Requested | Changed | Files |
|
||||
|------|-----------|---------|-------|
|
||||
| 2026-04-28 | Banner variant selection & specs | Add checkboxes to variants, fix navy color and CTA styling, standardize to 312px width | BannerVariants.tsx, BannerStyles.css |
|
||||
| 2026-04-28 | Banner variant selection | Fixed refine_variant_copy type handling, added dict key validation | tasks.py |
|
||||
| 2026-04-28 | Banner variant selection | Add checkboxes, selection validation, editor button activation | variants.tsx, bannerEditor.tsx |
|
||||
| 2026-04-28 | Banner variant selection | Add checkboxes, disable editor button until valid selections, filter editor display | VariantsPage.tsx, BannerEditor.tsx, VariantSelector.tsx |
|
||||
|
|
|
|||
|
|
@ -311,3 +311,6 @@ tags: [daily]
|
|||
- 22:10 | `Barclays-banner-builder`
|
||||
- **Asked:** Implement checkbox selection system for banner variants with validation and workflow control.
|
||||
- **Done:** Fixed type mismatch in `refine_variant_copy` function to accept proper dict structure with required keys.
|
||||
- 22:22 (10min) | `Barclays-banner-builder`
|
||||
- **Asked:** Implement checkbox selection system for users to choose banner variants before editing.
|
||||
- **Done:** Fixed banner spec colors (navy to #000063, CTA text/background reversed) and identified all six actual banner sizes.
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ This 3-hop pattern works for hundreds of articles without vector search.
|
|||
| [[wiki/tech-patterns/_index\|tech-patterns/]] | Recurring tech stacks: FastAPI, React/Vite, Next.js, Azure AD, AI, Box, One2Edit, Redis/Celery, cost-tracker | 13 |
|
||||
| [[wiki/architecture/_index\|architecture/]] | Cross-cutting architectural patterns: Docker Compose, multi-agent AI, GCP timeout, RAG, hotfolder, optical-dev deploy, cost-tracker, new-project checklist, troubleshooting playbooks, ADR log | 10 |
|
||||
| [[wiki/client-knowledge/_index\|client-knowledge/]] | Per-client notes for Ford, H&M, L'Oréal, Barclays, Ferrero, 3M | 6 |
|
||||
| [[wiki/concepts/_index\|concepts/]] | Atomic knowledge extracted from Claude Code sessions | 57 |
|
||||
| [[wiki/concepts/_index\|concepts/]] | Atomic knowledge extracted from Claude Code sessions | 61 |
|
||||
| [[wiki/connections/_index\|connections/]] | Cross-cutting insights linking 2+ concepts: FastAPI+Azure AD+Docker trinity, AI→cost-tracker, Apache+Vite basePath, GCP→REST polling, Box+hotfolder, Docker DNS+AdGuard | 9 |
|
||||
| [[wiki/qa/_index\|qa/]] | Filed answers to queries (saved with `--file-back`) | 0 |
|
||||
| [[wiki/homelab/_index\|homelab/]] | Self-hosted infra: Proxmox install, IOMMU/PCI passthrough, hypervisor setup, budget builds, HP Elitedesk G3, Homarr API + Apps + Boards + Certificates + Integrations + Settings + Tasks + AdGuard + Clock + Docker Stats + Docker Integration + Download Client + Firewall + Proxmox Integration + Radarr + Readarr + Sonarr + Bookmarks + Calendar + Icons + App Widget + Weather + GitHub + Nextcloud + qBittorrent + RSS Feed + Speedtest Tracker + System Health Monitoring + System Resources + Services Map + Media Stack | 38 |
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ title: "Client Knowledge: Barclays"
|
|||
description: "Barclays-specific context: projects, tech constraints, deployment quirks, and lessons learned"
|
||||
tags: [client-knowledge, barclays]
|
||||
created: 2026-04-27
|
||||
updated: 2026-04-27
|
||||
updated: 2026-04-28
|
||||
---
|
||||
|
||||
# Client Knowledge: Barclays
|
||||
|
|
@ -91,9 +91,56 @@ DISABLE_AUTH=true
|
|||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## Lessons from banner-builder (2026-04-28)
|
||||
|
||||
QA session on barclays-banner-builder surfaced three non-obvious bugs. All three share a common trait: they fail silently rather than throwing a clear error.
|
||||
|
||||
### DB Seed Silent Skip
|
||||
|
||||
Seed scripts using `INSERT IF NOT EXISTS` or `get_or_create` silently skip existing rows. If a test/demo user was created before the seed ran (e.g., with a different password), the seed never updates it. Auth fails at login with no error in logs — the user row simply has the wrong password.
|
||||
|
||||
**Fix:** Always verify actual DB state when auth fails unexpectedly after seeding:
|
||||
```sql
|
||||
SELECT email, created_at FROM users WHERE email = 'seed@example.com';
|
||||
-- If it exists with old data, DELETE and re-run seed, or UPDATE manually
|
||||
```
|
||||
|
||||
### Zustand Async Hydration Bug
|
||||
|
||||
`ConversationLanding` fired an API call on mount before the Zustand auth store had hydrated from localStorage. On first render, `token` was `null` (initial state) → API call sent without auth header → 401 → redirect to login, even though the user was logged in.
|
||||
|
||||
**Fix:** Gate all auth-dependent API calls behind `hasHydrated`:
|
||||
```typescript
|
||||
const { token, hasHydrated } = useAuthStore()
|
||||
useEffect(() => {
|
||||
if (!hasHydrated) return
|
||||
fetchData()
|
||||
}, [hasHydrated, token])
|
||||
```
|
||||
|
||||
See [[wiki/concepts/zustand-async-hydration]] for full pattern.
|
||||
|
||||
### Pydantic Model/Dict Interface Bug in tasks.py
|
||||
|
||||
`refine_variant_copy` Celery task was written to accept a `dict` but was called with a `BannerCopy` Pydantic model. Every `.get("field")` call on the Pydantic object returned `None` silently — the task continued running with all-null inputs, hanging the AI refinement pipeline without raising an exception.
|
||||
|
||||
**Fix:** Convert at call site: `task.delay(banner_copy.model_dump(), ...)` or update the function to accept the Pydantic model directly.
|
||||
|
||||
See [[wiki/concepts/pydantic-model-dict-interface]] for full pattern.
|
||||
|
||||
### LoginPage Hardcoded Redirect
|
||||
|
||||
`LoginPage` had a hardcoded redirect to `/brief` instead of `/` in the auth success handler. This caused the entire post-login flow to break for users who should land on the home route. Always use a configurable redirect target (e.g., `location.state?.from` or a constant) rather than a hardcoded path.
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [[wiki/architecture/gcp-deployment-lb-timeout|gcp-deployment-lb-timeout]] — WebSocket → REST polling
|
||||
- [[wiki/architecture/optical-dev-server-deploy|optical-dev-server-deploy]] — Banner Builder deployment
|
||||
- [[wiki/tech-patterns/python-ai-agents|python-ai-agents]] — multi-agent pattern used in Mod Comms
|
||||
- [[wiki/concepts/export-endpoint-filter-pattern|export-endpoint-filter-pattern]] — variant_ids in exports
|
||||
- [[wiki/concepts/zustand-async-hydration]] — Zustand hydration timing bug (Banner Builder)
|
||||
- [[wiki/concepts/pydantic-model-dict-interface]] — Pydantic vs dict silent failure (Banner Builder tasks.py)
|
||||
|
|
|
|||
|
|
@ -64,5 +64,10 @@
|
|||
| [[wiki/concepts/pydantic-v2-alias-id-gotcha]] | Pydantic v2 Field(alias="_id") serializes JSON key as "_id" not "id" — frontend .id is undefined; fix with _from_doc() helper | daily/2026-04-27.md | 2026-04-27 |
|
||||
| [[wiki/concepts/php-display-errors-json-leak]] | PHP display_errors=1 prepends HTML warnings to JSON — "Unexpected token '<'" is the diagnostic signal; ini_set order matters | daily/2026-04-27.md | 2026-04-27 |
|
||||
|
||||
| [[wiki/concepts/jellyfin-tmdb-thetvdb-plugin]] | TMDb plugin silently fails to load images when API date format changes — switch to TheTVDB; Jellyfin S01E01 naming; Prowlarr search-only role | daily/2026-04-28.md | 2026-04-28 |
|
||||
| [[wiki/concepts/azure-ad-yaml-allowlist-pattern]] | AuthN via Azure AD (MSAL PKCE), AuthZ via local YAML allowlist — when to use, SPA redirect URI type, Docker volume for config dir, PyYAML requirement | daily/2026-04-28.md | 2026-04-28 |
|
||||
| [[wiki/concepts/zustand-async-hydration]] | Zustand persist hydrates localStorage asynchronously — components must gate API calls behind hasHydrated or token will be null on first render | daily/2026-04-28.md | 2026-04-28 |
|
||||
| [[wiki/concepts/pydantic-model-dict-interface]] | Pydantic model passed where dict expected — .get() returns None silently instead of raising; use isinstance check or model_dump() at boundary | daily/2026-04-28.md | 2026-04-28 |
|
||||
|
||||
<!-- Articles added automatically by compile.py -->
|
||||
<!-- Format: | [[concepts/slug]] | One-line summary | daily/YYYY-MM-DD.md | date | -->
|
||||
|
|
|
|||
105
wiki/concepts/azure-ad-yaml-allowlist-pattern.md
Normal file
105
wiki/concepts/azure-ad-yaml-allowlist-pattern.md
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
---
|
||||
title: "Azure AD + YAML Allowlist — AuthN/AuthZ Split Pattern"
|
||||
aliases: [azure-ad-yaml-allowlist, msal-yaml-authz, shared-app-reg-authz]
|
||||
tags: [azure-ad, msal, fastapi, auth, yaml, allowlist, spa, docker, python]
|
||||
sources:
|
||||
- "daily/2026-04-28.md"
|
||||
created: 2026-04-28
|
||||
updated: 2026-04-28
|
||||
---
|
||||
|
||||
# Azure AD + YAML Allowlist — AuthN/AuthZ Split Pattern
|
||||
|
||||
A pragmatic authorization pattern: use Azure AD (MSAL) solely for authentication (who the user is), and a backend YAML file for authorization (what the user can do). This avoids touching Azure AD security groups when doing so would risk breaking other apps sharing the same app registration.
|
||||
|
||||
## Key Points
|
||||
|
||||
- **AuthN via Azure AD, AuthZ via local YAML:** Azure AD proves identity; a `config/allowed_users.yaml` maps `email → role` and gates access
|
||||
- **When to use:** Shared app registration (touching Azure AD groups risks breaking other apps); small team (~10–30 people) where git PR + redeploy is an acceptable audit trail instead of AD group management
|
||||
- **SPA redirect URI type is required:** Azure AD redirect URI must be registered as type **SPA** (not Web) for MSAL PKCE flow — "Web" type does not return the token directly to the browser
|
||||
- **Docker volume for config dir:** `./config:/app/config` mount needed in `docker-compose.yml` so the backend reads the YAML at runtime without rebuilding the image
|
||||
- **Custom 403 code:** Use `code: not_allowlisted` in the 403 response so the frontend interceptor can distinguish "no access" from generic auth errors
|
||||
|
||||
## Details
|
||||
|
||||
### When This Pattern Applies
|
||||
|
||||
The typical scenario: an app is given access to an existing Azure AD app registration that is also used by another production app (e.g., a V1 SSO). Creating or modifying Azure AD security groups on that registration carries risk — a misconfiguration could break SSO for the other app.
|
||||
|
||||
For small internal teams (10–30 people), a git-based YAML file provides a sufficient audit trail:
|
||||
- Access changes go through a PR → review → merge → redeploy cycle
|
||||
- No Azure portal access required for routine user management
|
||||
- The YAML is version-controlled and changes are traceable
|
||||
|
||||
This pattern is less appropriate for large teams, compliance-heavy environments, or when role changes need to take effect instantly (YAML requires a redeploy or hot-reload).
|
||||
|
||||
### Implementation
|
||||
|
||||
**`config/allowed_users.yaml`:**
|
||||
```yaml
|
||||
allowed_users:
|
||||
user@example.com: admin
|
||||
another@example.com: viewer
|
||||
contractor@agency.com: editor
|
||||
```
|
||||
|
||||
**FastAPI dependency:**
|
||||
```python
|
||||
import yaml
|
||||
from fastapi import Depends, HTTPException
|
||||
from app.auth import get_current_user # Azure AD token validation
|
||||
|
||||
def load_allowlist() -> dict:
|
||||
with open("config/allowed_users.yaml") as f:
|
||||
data = yaml.safe_load(f)
|
||||
return data.get("allowed_users", {})
|
||||
|
||||
async def require_allowlisted(user=Depends(get_current_user)):
|
||||
allowlist = load_allowlist()
|
||||
email = user.get("preferred_username") or user.get("email")
|
||||
if email not in allowlist:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail={"code": "not_allowlisted", "message": "Access not granted"}
|
||||
)
|
||||
return {"user": user, "role": allowlist[email]}
|
||||
```
|
||||
|
||||
**`requirements.txt` addition:**
|
||||
```
|
||||
PyYAML>=6.0
|
||||
```
|
||||
PyYAML is not included in FastAPI's default dependencies — it must be added explicitly.
|
||||
|
||||
### Azure AD SPA Redirect URI Type
|
||||
|
||||
When registering the redirect URI in Azure portal (App Registrations → Authentication → Add a platform):
|
||||
|
||||
- **SPA (Single-page application):** Token returned directly to the browser via PKCE. Required for MSAL.js PKCE flow.
|
||||
- **Web:** Token returned via server-side redirect. If the redirect URI is registered as "Web" instead of "SPA", MSAL's PKCE flow receives an authorization code but the token exchange is rejected — the browser gets no token.
|
||||
|
||||
Common symptom: MSAL login completes (redirect happens), but `acquireTokenSilent` or `handleRedirectPromise` returns `null` or throws an error about invalid grant.
|
||||
|
||||
### Docker Volume for Config
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
backend:
|
||||
build: ./backend
|
||||
volumes:
|
||||
- ./config:/app/config # ← exposes allowed_users.yaml to container
|
||||
environment:
|
||||
- ...
|
||||
```
|
||||
|
||||
Without this volume, the YAML file is not present inside the container at runtime. The backend will raise `FileNotFoundError` on first access to a protected route. The volume mount means user management changes can be applied without rebuilding the Docker image — just edit the YAML and restart the container (or implement hot-reload).
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- [[wiki/concepts/msal-vanilla-js-pkce]] — MSAL.js v5 PKCE flow details and Azure portal platform type requirement
|
||||
- [[wiki/tech-patterns/_index]] — FastAPI + Azure AD tech pattern used across Oliver Agency projects
|
||||
|
||||
## Sources
|
||||
|
||||
- [[daily/2026-04-28.md]] — oliver-sales-ops-platform SSO: Azure AD authn + YAML allowlist authz implemented to avoid touching shared app registration; SPA redirect URI type discovered; Docker volume for config dir; custom 403 `not_allowlisted` code; PyYAML requirement
|
||||
61
wiki/concepts/jellyfin-tmdb-thetvdb-plugin.md
Normal file
61
wiki/concepts/jellyfin-tmdb-thetvdb-plugin.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
---
|
||||
title: "Jellyfin TMDb Plugin Date Parse Failure → Switch to TheTVDB"
|
||||
aliases: [jellyfin-tmdb-thetvdb, jellyfin-metadata-plugins, jellyfin-images-failing]
|
||||
tags: [jellyfin, media-stack, homelab, metadata, tmdb, thetvdb, plugin]
|
||||
sources:
|
||||
- "daily/2026-04-28.md"
|
||||
created: 2026-04-28
|
||||
updated: 2026-04-28
|
||||
---
|
||||
|
||||
# Jellyfin TMDb Plugin Date Parse Failure → Switch to TheTVDB
|
||||
|
||||
The Jellyfin TMDb metadata plugin silently fails to load images when the TMDb API returns dates in a format the plugin cannot parse. The symptom is that library entries render with only text — no poster, no backdrop, no episode thumbnails — without any obvious error in the UI.
|
||||
|
||||
## Key Points
|
||||
|
||||
- **Root cause:** TMDb API changed `published_at` format to `"2019-03-15 17:20:30 UTC"` — Jellyfin's TMDb plugin could not parse this format and silently failed on all image fetch operations
|
||||
- **Symptom:** Library shows only text, no images; no error displayed to user; Jellyfin logs may show `FormatException` or similar during metadata refresh
|
||||
- **Fix:** Install TheTVDB plugin (v20.0.0+) as a replacement for TV metadata; it handles the current API formats correctly
|
||||
- **Jellyfin episode naming:** Jellyfin prefers `S01E01` naming format; Sonarr can auto-rename episodes via Settings → Media Management → "Rename Episodes"
|
||||
- **Prowlarr is search-only:** Cannot add content directly from Prowlarr results — must use Sonarr (TV) or Radarr (movies) → Add New → search
|
||||
|
||||
## Details
|
||||
|
||||
### TMDb Date Format Change — Silent Image Failure
|
||||
|
||||
TMDb updated their API to return `published_at` timestamps in the format `"2019-03-15 17:20:30 UTC"` instead of the ISO 8601 format `"2019-03-15T17:20:30Z"` that the Jellyfin TMDb plugin expected. The plugin's date parsing threw an exception internally but didn't surface it to the Jellyfin UI — it simply continued without loading any images.
|
||||
|
||||
This means the library appeared to work (metadata titles, descriptions, ratings populated correctly) but all visual elements were missing. Without checking Jellyfin server logs specifically for the image fetch pipeline, this is extremely hard to diagnose.
|
||||
|
||||
To identify: open Jellyfin logs (`/var/log/jellyfin/` or via Admin → Dashboard → Logs) and look for exceptions during a manual "Refresh Metadata" operation on an affected item.
|
||||
|
||||
### TheTVDB Plugin as Replacement
|
||||
|
||||
TheTVDB plugin (v20.0.0) is available in the Jellyfin plugin catalog and handles current API date formats correctly. For TV shows it is actually the preferred metadata source as TheTVDB has more comprehensive episode data than TMDb for many series.
|
||||
|
||||
**Install path:** Admin → Plugins → Catalog → TheTVDB → Install → Restart Jellyfin
|
||||
|
||||
After installing, go to Admin → Libraries → (your TV library) → Edit → Metadata → set TheTVDB as the primary metadata source (or highest priority). Run "Refresh All Metadata" to repopulate images.
|
||||
|
||||
Note: TMDb plugin can remain installed for movie libraries where it works correctly — the issue was specific to TV series metadata and the `published_at` field.
|
||||
|
||||
### Jellyfin Episode Naming and Sonarr Integration
|
||||
|
||||
Jellyfin's file scanner uses a naming parser that prioritizes `S01E01` format. It can recognize some variants (`1x01`, `Episode 1`) but `S01E01` is the most reliable.
|
||||
|
||||
Sonarr can automatically rename downloaded episodes to Jellyfin-compatible names:
|
||||
- **Settings → Media Management → Episode Naming** — configure the naming template
|
||||
- **"Rename Episodes"** checkbox — enable auto-rename on import
|
||||
- Recommended template: `{Series Title} - S{season:00}E{episode:00} - {Episode Title}`
|
||||
|
||||
Jellyfin then picks up the correctly named files on its next library scan.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- [[wiki/concepts/prowlarr-flaresolverr-limitation]] — Prowlarr indexer setup including FlareSolverr and Docker Cloudflare blocking
|
||||
- [[wiki/homelab/_index]] — full media stack setup (Radarr, Sonarr, qBittorrent, Prowlarr, Jellyfin)
|
||||
|
||||
## Sources
|
||||
|
||||
- [[daily/2026-04-28.md]] — TMDb plugin date parse failure discovered when Jellyfin library showed only text, no images; TheTVDB v20.0.0 installed as fix; episode naming and Sonarr auto-rename noted; Prowlarr search-only role clarified
|
||||
|
|
@ -102,6 +102,69 @@ The homelab media stack (CT111, Proxmox LXC) includes:
|
|||
|
||||
The pragmatic approach: configure 4–5 reliable indexers that don't require FlareSolverr, skip RuTracker, and add a VPN to qBittorrent only if required by the ISP or content provider.
|
||||
|
||||
## Prowlarr baseUrl Trailing Slash Bug
|
||||
|
||||
If `baseUrl` is set to `https://rutracker.org` (no trailing slash), Prowlarr string-concatenates it with the path segment instead of joining them correctly — producing a garbled hostname like `rutracker.orgforum:443`. This causes all requests to that indexer to immediately fail with a connection error.
|
||||
|
||||
**Fix:** Always add a trailing slash to Prowlarr indexer `baseUrl`: `https://rutracker.org/`
|
||||
|
||||
This is a silent bug — Prowlarr's UI doesn't warn about it, and the test result just shows a generic failure.
|
||||
|
||||
## Prowlarr API Save Timeout (SQLite Direct Edit Workaround)
|
||||
|
||||
When adding or testing a Prowlarr indexer that is blocked by Cloudflare (e.g., the connection test hangs), the `/api/v1/indexer` POST request hangs indefinitely — Prowlarr's UI appears to freeze. The indexer is never saved.
|
||||
|
||||
**Workaround:** Edit the Prowlarr SQLite database directly:
|
||||
|
||||
```bash
|
||||
# 1. Stop the Prowlarr container
|
||||
docker compose stop prowlarr
|
||||
|
||||
# 2. Open the database
|
||||
sqlite3 /path/to/prowlarr/config/prowlarr.db
|
||||
|
||||
# 3. View existing indexers
|
||||
SELECT Id, Name, Settings FROM Indexers;
|
||||
|
||||
# 4. Update settings (e.g., fix baseUrl)
|
||||
UPDATE Indexers SET Settings = '{"baseUrl":"https://rutracker.org/","...":"..."}' WHERE Name = 'RuTracker';
|
||||
|
||||
# 5. Exit and restart
|
||||
.quit
|
||||
docker compose start prowlarr
|
||||
```
|
||||
|
||||
The `Settings` column stores a JSON blob. Be careful to preserve the full JSON structure — only modify the field you need.
|
||||
|
||||
## Docker Containers Blocked by Cloudflare (TLS Fingerprint Detection)
|
||||
|
||||
Cloudflare blocks requests from Docker containers even when the host machine can reach the same URL successfully. This is not simple IP-based blocking — Cloudflare detects the non-browser TLS fingerprint (JA3/JA4 fingerprint) and User-Agent of curl/Python/Go HTTP clients.
|
||||
|
||||
- **Host machine:** uses the system browser or curl with a browser-like TLS stack → passes Cloudflare
|
||||
- **Docker container:** uses the container's OpenSSL/Go TLS stack → detected as a bot, blocked with 403 or CAPTCHA challenge
|
||||
|
||||
FlareSolverr solves this by running a headless Chromium browser inside the container, which produces a genuine browser TLS fingerprint. It listens on port 8191 and exposes an HTTP API that Prowlarr calls instead of fetching the indexer directly.
|
||||
|
||||
**Docker Compose sidecar setup:**
|
||||
|
||||
```yaml
|
||||
services:
|
||||
prowlarr:
|
||||
image: lscr.io/linuxserver/prowlarr:latest
|
||||
# ...
|
||||
|
||||
flaresolverr:
|
||||
image: ghcr.io/flaresolverr/flaresolverr:latest
|
||||
container_name: flaresolverr
|
||||
environment:
|
||||
- LOG_LEVEL=info
|
||||
ports:
|
||||
- "8191:8191"
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
**Prowlarr configuration:** Settings → Indexer Proxies → Add → FlareSolverr, URL: `http://flaresolverr:8191` (use the Docker Compose service name for container-to-container networking). FlareSolverr v3.x is the current actively maintained fork.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- [[wiki/homelab/_index]] — media stack articles (Radarr, Sonarr, Prowlarr, qBittorrent setup)
|
||||
|
|
|
|||
106
wiki/concepts/pydantic-model-dict-interface.md
Normal file
106
wiki/concepts/pydantic-model-dict-interface.md
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
---
|
||||
title: "Pydantic Model Passed Where Dict Expected — Silent .get() Failure"
|
||||
aliases: [pydantic-dict-interface, pydantic-get-none, pydantic-vs-dict]
|
||||
tags: [pydantic, python, fastapi, celery, debugging, type-safety, silent-failure]
|
||||
sources:
|
||||
- "daily/2026-04-28.md"
|
||||
created: 2026-04-28
|
||||
updated: 2026-04-28
|
||||
---
|
||||
|
||||
# Pydantic Model Passed Where Dict Expected — Silent .get() Failure
|
||||
|
||||
When a function written to accept a `dict` is called with a Pydantic model, calling `.get("key")` on the model returns `None` (since `.get()` is not a standard Pydantic method) instead of raising an `AttributeError` or `TypeError`. The function continues executing with `None` values — producing silent wrong behavior, hangs, or downstream failures rather than a clear error.
|
||||
|
||||
## Key Points
|
||||
|
||||
- **`.get()` on a Pydantic model returns `None`** — Pydantic v1 models don't have a `.get()` method; calling it doesn't raise `AttributeError`, it silently falls back to Python's default `None` (or Pydantic's own method resolution)
|
||||
- **Symptom:** Task or function appears to run but produces wrong output, hangs, or sends empty/null data to downstream systems — no exception raised at the call site
|
||||
- **Detection:** Add `assert isinstance(data, dict)` or type-hint the parameter as `dict` and run mypy; or check the call site to see what type is actually being passed
|
||||
- **Fix option A:** Convert at the call site — `task.delay(banner_copy.model_dump())`
|
||||
- **Fix option B:** Update the function to accept Pydantic model — `def refine_variant_copy(copy: BannerCopy)` and use `.field` attribute access
|
||||
|
||||
## Details
|
||||
|
||||
### How the Bug Manifests
|
||||
|
||||
In the barclays-banner-builder, a Celery task `refine_variant_copy` was written expecting a `dict`:
|
||||
|
||||
```python
|
||||
# tasks.py — written expecting dict
|
||||
def refine_variant_copy(copy: dict, ...):
|
||||
headline = copy.get("headline") # ← works if copy is dict
|
||||
body_text = copy.get("body_text") # ← returns None if copy is Pydantic model
|
||||
# Function continues with headline=None, body_text=None
|
||||
# AI call gets empty inputs → returns garbage or hangs waiting
|
||||
```
|
||||
|
||||
The call site passed a `BannerCopy` Pydantic model:
|
||||
|
||||
```python
|
||||
# caller
|
||||
banner_copy = BannerCopy(headline="Buy Now", body_text="Great offer")
|
||||
refine_variant_copy.delay(banner_copy, ...) # ← wrong type passed
|
||||
```
|
||||
|
||||
Pydantic v1 models do implement `__getitem__` (for subscript access like `copy["key"]`), but not the dict `.get()` method with its default-return semantics. When Python resolves `copy.get("headline")` on a Pydantic model, it finds no such method and returns `None` rather than raising — this is because `BaseModel` does not define `.get()`, so Python's attribute lookup falls back to `None` in some configurations, or Pydantic provides a stub.
|
||||
|
||||
The result: every `.get()` call returns `None`. The function keeps running with all-`None` values, silently producing wrong output.
|
||||
|
||||
### Detection
|
||||
|
||||
**Runtime check at function entry:**
|
||||
```python
|
||||
def refine_variant_copy(copy, ...):
|
||||
if not isinstance(copy, dict):
|
||||
raise TypeError(f"Expected dict, got {type(copy).__name__}")
|
||||
...
|
||||
```
|
||||
|
||||
**Static analysis (mypy):**
|
||||
```python
|
||||
def refine_variant_copy(copy: dict, ...) -> str:
|
||||
...
|
||||
# mypy will flag: Argument 1 has incompatible type "BannerCopy"; expected "Dict[str, Any]"
|
||||
```
|
||||
|
||||
**Grep for the pattern:**
|
||||
```bash
|
||||
grep -n "\.get(" tasks.py
|
||||
# Then check each call site to see what type is passed
|
||||
```
|
||||
|
||||
### Fix
|
||||
|
||||
**Option A — Convert at call site (minimal change):**
|
||||
```python
|
||||
# Convert Pydantic model to dict before passing to task
|
||||
refine_variant_copy.delay(banner_copy.model_dump(), prompt, job_id)
|
||||
# Pydantic v1: banner_copy.dict()
|
||||
# Pydantic v2: banner_copy.model_dump()
|
||||
```
|
||||
|
||||
**Option B — Update function signature to accept Pydantic model (cleaner):**
|
||||
```python
|
||||
from app.models import BannerCopy
|
||||
|
||||
def refine_variant_copy(copy: BannerCopy, ...) -> str:
|
||||
headline = copy.headline # attribute access, not .get()
|
||||
body_text = copy.body_text
|
||||
...
|
||||
```
|
||||
|
||||
Option B is preferable when the function is an internal API — it makes the type contract explicit and mypy can catch misuse. Option A is safer when the function is a Celery task that may receive serialized data from a queue (where dicts are the natural representation after JSON deserialization).
|
||||
|
||||
### General Rule
|
||||
|
||||
Before writing a function that calls `.get()` on its arguments, decide explicitly: does this function take a `dict` or a typed model? Document it with a type annotation. If the codebase mixes both, add `isinstance` guards or use `.model_dump()` at the boundary where Pydantic models enter dict-oriented code.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- [[wiki/concepts/zustand-async-hydration]] — another silent failure pattern from the same barclays-banner-builder session
|
||||
- [[wiki/concepts/pydantic-v2-alias-id-gotcha]] — other Pydantic serialization pitfalls
|
||||
|
||||
## Sources
|
||||
|
||||
- [[daily/2026-04-28.md]] — barclays-banner-builder: `refine_variant_copy` task accepted dict but was called with BannerCopy Pydantic object; `.get()` returned None silently; caused hang in AI refinement pipeline
|
||||
122
wiki/concepts/zustand-async-hydration.md
Normal file
122
wiki/concepts/zustand-async-hydration.md
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
---
|
||||
title: "Zustand Async Hydration — localStorage Timing Bug"
|
||||
aliases: [zustand-hydration, zustand-localstorage-timing, zustand-hashydrated]
|
||||
tags: [zustand, react, state-management, frontend, auth, timing, localstorage]
|
||||
sources:
|
||||
- "daily/2026-04-28.md"
|
||||
created: 2026-04-28
|
||||
updated: 2026-04-28
|
||||
---
|
||||
|
||||
# Zustand Async Hydration — localStorage Timing Bug
|
||||
|
||||
Zustand stores that use `persist` middleware hydrate from localStorage asynchronously. On the first render, the store contains its default (empty/unauthenticated) state, not the persisted state. Any component that fires an API call on mount without waiting for hydration will fire it with stale (unauthenticated) state, causing the request to fail.
|
||||
|
||||
## Key Points
|
||||
|
||||
- **Zustand persist hydration is async:** On first render the store holds initial state; localStorage data loads after the first render cycle completes
|
||||
- **Symptom:** Auth-gated API calls on component mount fail (401/403/redirect) even though the user is logged in and localStorage has valid auth data
|
||||
- **Fix:** Gate API calls (and route logic) behind `hasHydrated` state — don't fire until the store confirms hydration is complete
|
||||
- **Common trigger:** Navigation components, layout-level data fetches, or route guards that check auth state before hydration completes
|
||||
- **Affect scope:** Any Zustand store using `persist` middleware — not just auth stores
|
||||
|
||||
## Details
|
||||
|
||||
### Why Hydration Is Async
|
||||
|
||||
Zustand's `persist` middleware reads from `localStorage` (or another storage) during store initialization. Because `localStorage` access is technically synchronous in the browser, you might expect hydration to be immediate — but Zustand defers the hydration to ensure the store is fully set up before loading persisted state. This means on the very first synchronous render pass, the store contains only its `initialState` values.
|
||||
|
||||
React renders once with initial state, then Zustand hydrates, which triggers a second render with the actual persisted state. Any `useEffect(() => { ... }, [])` that runs after the first render fires before the second render, meaning it runs with stale (initial) state.
|
||||
|
||||
### Implementing `hasHydrated` Guard
|
||||
|
||||
```typescript
|
||||
// store/authStore.ts
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
|
||||
interface AuthState {
|
||||
token: string | null
|
||||
user: User | null
|
||||
hasHydrated: boolean
|
||||
setHasHydrated: (state: boolean) => void
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
token: null,
|
||||
user: null,
|
||||
hasHydrated: false,
|
||||
setHasHydrated: (state) => set({ hasHydrated: state }),
|
||||
}),
|
||||
{
|
||||
name: 'auth-storage',
|
||||
onRehydrateStorage: () => (state) => {
|
||||
state?.setHasHydrated(true)
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
```tsx
|
||||
// ConversationLanding.tsx (or any component with mount-time API calls)
|
||||
function ConversationLanding() {
|
||||
const { token, user, hasHydrated } = useAuthStore()
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasHydrated) return // ← wait for hydration
|
||||
if (!token) {
|
||||
navigate('/login')
|
||||
return
|
||||
}
|
||||
// Safe to fire API call — auth state is real
|
||||
fetchConversations()
|
||||
}, [hasHydrated, token])
|
||||
|
||||
if (!hasHydrated) {
|
||||
return <LoadingSpinner /> // or null
|
||||
}
|
||||
|
||||
return <div>...</div>
|
||||
}
|
||||
```
|
||||
|
||||
### Alternative: `useStore` Hydration Hook
|
||||
|
||||
For cases where many components need the guard, extract it:
|
||||
|
||||
```typescript
|
||||
// hooks/useHydration.ts
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useAuthStore } from '@/store/authStore'
|
||||
|
||||
export function useHydration() {
|
||||
const hasHydrated = useAuthStore((s) => s.hasHydrated)
|
||||
return hasHydrated
|
||||
}
|
||||
```
|
||||
|
||||
Then wrap route-level components or use in layout:
|
||||
|
||||
```tsx
|
||||
function ProtectedLayout() {
|
||||
const hydrated = useHydration()
|
||||
if (!hydrated) return <FullPageSpinner />
|
||||
return <Outlet />
|
||||
}
|
||||
```
|
||||
|
||||
### Real-World Symptom (barclays-banner-builder)
|
||||
|
||||
`ConversationLanding` fired an API call on mount that required auth. The Zustand auth store had not yet hydrated from localStorage, so `token` was `null` on the first render. The API call went out without an auth header → server returned 401 → component redirected to login, even though the user was already authenticated. The fix was adding `hasHydrated` as a dependency of the `useEffect` and returning early if it was `false`.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- [[wiki/concepts/pydantic-model-dict-interface]] — another silent bug pattern from the same barclays-banner-builder session
|
||||
- [[wiki/client-knowledge/barclays]] — Barclays Banner Builder project context
|
||||
|
||||
## Sources
|
||||
|
||||
- [[daily/2026-04-28.md]] — barclays-banner-builder QA: ConversationLanding fetched API before auth store hydrated → route failed; fix was gating behind hasHydrated
|
||||
|
|
@ -3,6 +3,11 @@
|
|||
|
||||
<!-- Append-only chronological record of compile, query, and lint operations -->
|
||||
|
||||
## [2026-04-28T22:30:00+01:00] compile | 2026-04-28.md
|
||||
- Source: daily/2026-04-28.md
|
||||
- Articles created: [[wiki/concepts/jellyfin-tmdb-thetvdb-plugin]], [[wiki/concepts/azure-ad-yaml-allowlist-pattern]], [[wiki/concepts/zustand-async-hydration]], [[wiki/concepts/pydantic-model-dict-interface]]
|
||||
- Articles updated: [[wiki/concepts/prowlarr-flaresolverr-limitation]] (added trailing slash bug, SQLite direct edit workaround, Docker Cloudflare TLS fingerprint detection section), [[wiki/client-knowledge/barclays]] (added "Lessons from banner-builder" section: DB seed silent skip, Zustand hydration bug, Pydantic/dict interface bug, LoginPage hardcoded redirect)
|
||||
|
||||
## [2026-04-28T22:30:00+01:00] compile | 2026-04-27.md
|
||||
- Source: daily/2026-04-27.md
|
||||
- Articles created: [[wiki/concepts/asyncio-contextvar-task-boundary]], [[wiki/concepts/pydantic-v2-alias-id-gotcha]], [[wiki/concepts/php-display-errors-json-leak]]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue