diff --git a/99 Daily/2026-05-14.md b/99 Daily/2026-05-14.md index 2751b3b..4c4dd68 100644 --- a/99 Daily/2026-05-14.md +++ b/99 Daily/2026-05-14.md @@ -88,3 +88,4 @@ tags: [daily] - **Done:** Identified 5 fixes including removing duplicate compilation calls in flush.py, addressing uncompiled logs from 2026-05-11 to 05-14, and proposing Claude.md restructuring to separate core principles from infrastructure config. - 21:02 (<1min) — session ended | `memory-compiler` - 21:03 (<1min) — session ended | `memory-compiler` +- 21:08 (4min) — session ended | `memory-compiler` diff --git a/wiki/_master-index.md b/wiki/_master-index.md index 415d6d0..6fefab9 100644 --- a/wiki/_master-index.md +++ b/wiki/_master-index.md @@ -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, OMG API | 29 | | [[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, Cloud Run Jobs | 11 | | [[wiki/client-knowledge/_index\|client-knowledge/]] | Per-client notes for Ford, H&M, L'Oréal, Barclays, Ferrero, 3M, BAIC | 7 | -| [[wiki/concepts/_index\|concepts/]] | Atomic knowledge extracted from Claude Code sessions | 182 | +| [[wiki/concepts/_index\|concepts/]] | Atomic knowledge extracted from Claude Code sessions | 196 | | [[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, Celery prefork×faster_whisper memory stacking | 10 | | [[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 | 43 | diff --git a/wiki/client-knowledge/loreal.md b/wiki/client-knowledge/loreal.md index 0a2a4c8..f99f08c 100644 --- a/wiki/client-knowledge/loreal.md +++ b/wiki/client-knowledge/loreal.md @@ -52,6 +52,8 @@ Oliver Agency works with L'Oréal on campaign asset management (global → local - SLA calculator has two UX modes: full PM calculator vs simplified client estimator - L'Oréal uses eCom Content Factory workflow — their PM team are the primary users of the SLA tool - Timelog Viewer was deployed as a pre-built `dist/` without source — required `` workaround in `index.html` to fix asset 404s on subpath; see [[wiki/concepts/vite-prebuilt-subpath-workaround]] +- **Timelog Viewer deploy = `git pull` not rsync** — project is now in Git; after adding source, deploy with `git pull` on optical-dev, then rebuild `dist/` locally if needed. Do NOT use rsync to sync `dist/` manually. +- loreal-timelog-viewer lives on **optical-dev** at `/timelog/` Apache alias ## Related - [[wiki/tech-patterns/box-api-integration|box-api-integration]] — Box API patterns diff --git a/wiki/concepts/_index.md b/wiki/concepts/_index.md index 0a69ece..7177548 100644 --- a/wiki/concepts/_index.md +++ b/wiki/concepts/_index.md @@ -230,5 +230,16 @@ | [[wiki/concepts/figma-design-to-code-workflow\|Figma Design-to-Code — Workflow, Best Practices, Pixel-Perfect Positioning]] | ``` | raw/Structure your Figma file for better code.md | 2026-05-13 | | [[wiki/concepts/figma-mcp-tools-reference\|Figma MCP Server — Tool Reference]] | Complete list of the 18 tools exposed by the Figma MCP server. The primary 3 are bolded — they cover | raw/Tools and prompts Developer Docs.md | 2026-05-13 | | [[wiki/concepts/figma-skills-reference\|Figma Skills Reference — All Official Skills]] | Skills = pre-built workflow instructions bundled with the Figma plugin. They tell the AI which tools | raw/Figma skills for MCP.md | 2026-05-13 | +| [[wiki/concepts/css-clamp-negative-values]] | `clamp(MIN, val, MAX)` with negatives: MIN must be most-negative — reversed semantics, silent layout bug | daily/2026-05-13.md | 2026-05-13 | +| [[wiki/concepts/axios-401-interceptor-infinite-loop]] | Axios 401 interceptor must exclude `/auth/refresh` endpoint from retry — otherwise refresh 401 triggers infinite loop | daily/2026-05-13.md | 2026-05-13 | +| [[wiki/concepts/vue-router-registration-async-store]] | `app.use(router)` must come AFTER `await authStore.init()` — guards fire with wrong isAuthenticated if registered first | daily/2026-05-13.md | 2026-05-13 | +| [[wiki/concepts/gcs-cors-signed-url-get]] | GCS CORS rules must explicitly include GET method for browser fetch of signed URLs — omitting it causes preflight failure | daily/2026-05-13.md | 2026-05-13 | +| [[wiki/concepts/vtt-cue-settings-timestamp-parse]] | VTT cue settings (line:0%, position:50%) appear in the timestamp line — parser must strip suffix after first space | daily/2026-05-13.md | 2026-05-13 | +| [[wiki/concepts/payload-cms-seo-column-rename]] | Payload SEO plugin renamed `meta_meta_title` → `meta_title` between v3.33 and v3.84 — requires manual ALTER TABLE | daily/2026-05-13.md | 2026-05-13 | +| [[wiki/concepts/pnpm11-pnpmfile-requirement]] | pnpm 11.0.9+ requires `.pnpmfile.mjs` in project root even when no hooks are needed — install fails without it | daily/2026-05-13.md | 2026-05-13 | +| [[wiki/concepts/tsx-node22-esm-named-exports]] | tsx@4.21.0 + Node.js v22 with `--import tsx/esm` fails for named exports from CJS packages — use `npx tsx` or `--use-swc` | daily/2026-05-13.md | 2026-05-13 | +| [[wiki/concepts/docker-compose-force-recreate]] | `docker compose up -d` after build keeps old container running — `--force-recreate` required to apply new image | daily/2026-05-13.md | 2026-05-13 | +| [[wiki/concepts/next-app-router-favicon]] | Next.js App Router favicon requires both `public/favicon.ico` AND `src/app/icon.png` (512×512) — one file alone is insufficient | daily/2026-05-13.md | 2026-05-13 | +| [[wiki/concepts/pil-photo-compression-pipeline]] | PIL photo pipeline: `thumbnail((1200,1200), LANCZOS)` + JPEG 82 progressive; re-encoding already-compressed JPEG gives no size benefit | daily/2026-05-13.md | 2026-05-13 | diff --git a/wiki/concepts/axios-401-interceptor-infinite-loop.md b/wiki/concepts/axios-401-interceptor-infinite-loop.md new file mode 100644 index 0000000..b180d92 --- /dev/null +++ b/wiki/concepts/axios-401-interceptor-infinite-loop.md @@ -0,0 +1,73 @@ +--- +name: axios-401-interceptor-infinite-loop +description: Axios 401 response interceptor that calls the refresh endpoint must explicitly exclude that endpoint from retry — otherwise refresh failure triggers another refresh, infinite loop +type: concept +--- + +# Axios 401 Interceptor — Infinite Loop on Refresh Failure + +## The Bug + +A common pattern: intercept every 401 response → silently call `/api/auth/refresh` → retry original request. + +**Problem:** if `/api/auth/refresh` itself returns 401 (expired refresh token, logged-out user), the interceptor catches *that* 401 and tries to refresh again → infinite loop. + +``` +GET /api/data → 401 + → POST /api/auth/refresh → 401 ← interceptor catches this too! + → POST /api/auth/refresh → 401 ← infinite loop +``` + +## The Fix — Exclude the Refresh Endpoint + +```ts +import axios from 'axios' + +const api = axios.create({ baseURL: '/api' }) + +api.interceptors.response.use( + (response) => response, + async (error) => { + const originalRequest = error.config + + // CRITICAL: skip refresh if this IS the refresh request + if ( + error.response?.status === 401 && + !originalRequest._retry && + !originalRequest.url?.includes('/auth/refresh') // ← guard + ) { + originalRequest._retry = true + try { + await api.post('/auth/refresh') + return api(originalRequest) + } catch { + // Refresh failed — redirect to login + window.location.href = '/login' + return Promise.reject(error) + } + } + + return Promise.reject(error) + } +) +``` + +## Key Details + +| Guard | Purpose | +|-------|---------| +| `!originalRequest._retry` | Prevents retrying the same request twice | +| `!url.includes('/auth/refresh')` | Prevents the refresh call from re-triggering itself | + +## Vue Router Integration + +After failed refresh, use `router.push('/login')` instead of `window.location.href` to preserve SPA navigation: + +```ts +import router from '@/router' + +// inside the catch block: +await router.push('/login') +``` + +**Source:** daily/2026-05-13.md | 2026-05-13 diff --git a/wiki/concepts/css-clamp-negative-values.md b/wiki/concepts/css-clamp-negative-values.md new file mode 100644 index 0000000..1428b4d --- /dev/null +++ b/wiki/concepts/css-clamp-negative-values.md @@ -0,0 +1,61 @@ +--- +name: css-clamp-negative-values +description: CSS clamp() with negative values has reversed MIN/MAX semantics — the most-negative value is the constraint floor, not the ceiling +type: concept +--- + +# CSS `clamp()` with Negative Values — Reversed Semantics + +## The Gotcha + +`clamp(MIN, VALUE, MAX)` always returns a value between MIN and MAX. +With **negative values**, the "minimum" must be the most-negative number — which visually looks like the *largest negative magnitude*. + +```css +/* WRONG — clamp semantics violated: MIN > MAX */ +margin-top: clamp(-4px, -0.5vw, -1px); +/* ^ result is always -4px (MIN wins), intended range never applies */ + +/* CORRECT */ +margin-top: clamp(-4px, -0.5vw, -1px); +/* MIN=-4px (most negative = floor), MAX=-1px (least negative = ceiling) */ +``` + +Wait — that's the same example. The key rule: + +``` +For negative ranges: MIN must be the most negative, MAX must be the least negative. + +clamp(-40px, -2.78vw, -10px) ✓ (-40 ≤ result ≤ -10) +clamp(-10px, -2.78vw, -40px) ✗ (MIN > MAX, clamp always returns -10px) +``` + +## vw → px Conversion + +To hit an exact pixel value at a reference viewport width: + +``` +vw% = target_px / reference_width_px × 100 + +Example: -20px at 1440px wide +vw = -20 / 1440 × 100 = -1.389vw +``` + +## Practical Pattern + +Fluid negative margin/gap that scales between viewports: + +```css +.element { + /* -40px on narrow, scales to -10px at wide viewports */ + margin-top: clamp(-40px, -2.78vw, -10px); +} +``` + +## Why It's a Silent Bug + +- The browser doesn't warn when MIN > MAX in a negative clamp. +- The static fallback value (MIN) is used constantly — layout looks "fine" but never responds to viewport size. +- Detected only when resizing and noticing the value never changes. + +**Source:** daily/2026-05-13.md | 2026-05-13 diff --git a/wiki/concepts/docker-compose-force-recreate.md b/wiki/concepts/docker-compose-force-recreate.md new file mode 100644 index 0000000..9ee345f --- /dev/null +++ b/wiki/concepts/docker-compose-force-recreate.md @@ -0,0 +1,58 @@ +--- +name: docker-compose-force-recreate +description: docker compose up -d after build keeps the old container running unless --force-recreate is specified — new image is built but not used +type: concept +--- + +# Docker Compose — `--force-recreate` Required After Build + +## The Bug + +```bash +docker compose build web +docker compose up -d # ← old container still running +``` + +Even though the image was rebuilt, `up -d` without `--force-recreate` keeps the existing container if it's already running. Docker Compose detects "container is running and config hasn't changed" and skips recreation. + +**Result:** new code is in the image but the running container has the old code. + +## The Fix + +```bash +docker compose build web +docker compose up -d --force-recreate web +``` + +Or as a one-liner (rebuild + recreate): + +```bash +docker compose up -d --build --force-recreate web +``` + +## When Each Flag Is Needed + +| Command | Effect | +|---------|--------| +| `docker compose up -d` | Start stopped containers; skip running ones | +| `docker compose up -d --force-recreate` | Recreate ALL containers (new image, updated config) | +| `docker compose up -d --build` | Build image before starting, but may still skip recreation | +| `docker compose up -d --build --force-recreate` | Full rebuild + recreate — safest for code changes | +| `docker compose restart web` | Restart process inside existing container — **does NOT apply new image** | + +## Related + +- [[wiki/concepts/docker-compose-restart-no-code-reload]] — `restart` doesn't reload code (same issue, different flag) + +## Deploy Script Pattern + +```bash +#!/bin/bash +set -e +git pull +docker compose build web +docker compose up -d --force-recreate web +echo "Deployed." +``` + +**Source:** daily/2026-05-13.md | 2026-05-13 diff --git a/wiki/concepts/fastapi-orm-property-json-column.md b/wiki/concepts/fastapi-orm-property-json-column.md index 2f4d5b0..7f38d54 100644 --- a/wiki/concepts/fastapi-orm-property-json-column.md +++ b/wiki/concepts/fastapi-orm-property-json-column.md @@ -63,6 +63,37 @@ class TaskResponse(BaseModel): > [!info] `from_attributes=True` is required > Pydantic v2 `from_attributes=True` (previously `orm_mode = True` in v1) allows the schema to read Python object attributes, including `@property`, not just dict keys. +## Pydantic v2 Gotcha — `@property` Is NOT Serialized + +In **Pydantic v2**, a plain `@property` on a Pydantic model class is **not** included in `.model_dump()` or JSON serialization. The property must be declared as `@computed_field` to appear in the API response. + +```python +# WRONG in Pydantic v2 — property not serialized +class TaskResponse(BaseModel): + fields_json: dict = {} + + @property + def tags(self) -> list[str]: + return self.fields_json.get("tags", []) + # tags will NOT appear in .model_dump() or JSON response + +# CORRECT — use @computed_field +from pydantic import computed_field + +class TaskResponse(BaseModel): + fields_json: dict = {} + + @computed_field + @property + def tags(self) -> list[str]: + return self.fields_json.get("tags", []) + # tags WILL appear in .model_dump() and JSON response +``` + +**Note:** `@computed_field` is a Pydantic v2 feature — not available in Pydantic v1. + +For **SQLAlchemy ORM** models (not Pydantic models), `@property` works fine because Pydantic reads attributes via `from_attributes=True`. The `@computed_field` workaround is only needed when the property is on the Pydantic schema class itself. + ## When to Use | Scenario | Use `@property` | Use real column | diff --git a/wiki/concepts/gcs-cors-signed-url-get.md b/wiki/concepts/gcs-cors-signed-url-get.md new file mode 100644 index 0000000..d8a3383 --- /dev/null +++ b/wiki/concepts/gcs-cors-signed-url-get.md @@ -0,0 +1,70 @@ +--- +name: gcs-cors-signed-url-get +description: GCS CORS rules must explicitly list GET method for browser fetch of signed URLs — omitting it causes CORS preflight failure even though the signed URL is valid +type: concept +--- + +# GCS CORS — Signed URL Requires Explicit GET Method + +## The Problem + +A browser `fetch()` of a GCS signed URL fails with CORS error, but: +- The signed URL is valid (curl works) +- The bucket CORS config exists +- The bucket is not public + +Root cause: CORS config does not explicitly include `GET` in the allowed methods. + +## Why Signed URLs Still Need CORS + +Signed URLs authenticate the *request* via the URL signature, but CORS is a browser policy enforced separately. Even a perfectly signed URL triggers a CORS preflight (OPTIONS) when fetched from JavaScript — and the browser checks the CORS headers in the response, independent of the signature. + +## The Fix + +Set the GCS bucket CORS config to include `GET` (and `HEAD` for range requests): + +```json +[ + { + "origin": ["https://your-app.com", "http://localhost:5173"], + "method": ["GET", "HEAD"], + "responseHeader": ["Content-Type", "Content-Range", "Accept-Ranges"], + "maxAgeSeconds": 3600 + } +] +``` + +Apply via `gsutil`: + +```bash +gsutil cors set cors.json gs://your-bucket-name +gsutil cors get gs://your-bucket-name # verify +``` + +Or via Terraform: + +```hcl +resource "google_storage_bucket_iam_member" "..." { ... } + +resource "google_storage_bucket" "bucket" { + cors { + origin = ["https://your-app.com"] + method = ["GET", "HEAD"] + response_header = ["Content-Type"] + max_age_seconds = 3600 + } +} +``` + +## Checklist When Browser Fetch Fails + +1. Confirm signed URL works in curl → confirms auth is fine +2. Check bucket CORS with `gsutil cors get` → look for missing methods +3. Confirm `origin` in CORS includes the browser's origin exactly (no trailing slash) +4. Check `responseHeader` if the app reads custom response headers + +## Related + +- [[wiki/concepts/gcs-resumable-upload-pattern]] — browser→GCS via resumable session URI + +**Source:** daily/2026-05-13.md | 2026-05-13 diff --git a/wiki/concepts/next-app-router-favicon.md b/wiki/concepts/next-app-router-favicon.md new file mode 100644 index 0000000..9435645 --- /dev/null +++ b/wiki/concepts/next-app-router-favicon.md @@ -0,0 +1,81 @@ +--- +name: next-app-router-favicon +description: Next.js App Router requires both public/favicon.ico AND src/app/icon.png (512×512) — one file alone is not enough for all browsers and PWA +type: concept +--- + +# Next.js App Router — Favicon Requires Two Files + +## The Problem + +In Next.js 13+ App Router, placing only `public/favicon.ico` results in: +- Favicon works in most browsers (classic `.ico` fallback) +- But PWA manifest icon is missing +- Some browser address bars don't show the icon +- Lighthouse PWA audit fails + +Placing only `src/app/icon.png` results in: +- Modern browsers work +- Older browsers or bookmark imports may not find the icon + +## The Correct Setup + +Both files are required: + +``` +public/ + favicon.ico ← classic browsers, tab icon +src/ + app/ + icon.png ← 512×512 PNG, App Router metadata API + icon.tsx ← optional: generated icon via ImageResponse +``` + +### `src/app/icon.png` + +- Must be **exactly 512×512 pixels** (or at minimum 192×192) +- Next.js automatically serves it at `/_next/static/media/icon.png` and injects `` into `` +- Also used as the PWA app icon + +### `public/favicon.ico` + +- Classic `.ico` format (can contain multiple sizes: 16×16, 32×32, 48×48) +- Served at `/favicon.ico` — browsers always check this URL first + +## Metadata API Alternative + +Instead of a static `icon.png`, you can generate it dynamically: + +```tsx +// src/app/icon.tsx +import { ImageResponse } from 'next/og' + +export const size = { width: 512, height: 512 } +export const contentType = 'image/png' + +export default function Icon() { + return new ImageResponse( +
+ {/* Your icon JSX */} +
+ ) +} +``` + +## Manifest Icon + +If you have a `src/app/manifest.ts` or `public/manifest.json`, reference the icon: + +```ts +// src/app/manifest.ts +export default function manifest() { + return { + icons: [ + { src: '/icon.png', sizes: '512x512', type: 'image/png' }, + { src: '/favicon.ico', sizes: '16x16 32x32', type: 'image/x-icon' }, + ], + } +} +``` + +**Source:** daily/2026-05-13.md | 2026-05-13 diff --git a/wiki/concepts/payload-cms-push-dev-prod.md b/wiki/concepts/payload-cms-push-dev-prod.md index 717c646..f6b2d28 100644 --- a/wiki/concepts/payload-cms-push-dev-prod.md +++ b/wiki/concepts/payload-cms-push-dev-prod.md @@ -97,4 +97,28 @@ services: When generating a new Payload + Next.js project, the scaffold (as of Payload 3.x) may omit `push: false` in the production config template. Always check `payload.config.ts` before deploying to production. -**Source:** daily/2026-05-09.md | 2026-05-09 +## Production Gotcha — `push: true` Partial Schema Apply + +Even with `push: true`, Payload's `drizzle-kit push` in production may **silently skip some tables or columns**: + +- New tables/columns in small schema: applied correctly +- Complex schema changes (renamed columns, composite foreign keys, multi-table changes): partially applied +- No error in startup logs — admin panel works but some fields are missing + +**Symptoms:** +- New collection field exists in the admin form but returns `null` on GET +- `npx payload migrate:status` shows pending even after `push: true` startup + +**Fix:** +Run explicit migrations for production even if `push: true` worked in dev: + +```bash +# Verify what actually exists +psql $DATABASE_URI -c "\d+ collection_name" + +# If column missing: run the migration manually +npx payload migrate:create --name add-missing-column +npx payload migrate +``` + +**Source:** daily/2026-05-09.md, daily/2026-05-13.md | updated 2026-05-13 diff --git a/wiki/concepts/payload-cms-seo-column-rename.md b/wiki/concepts/payload-cms-seo-column-rename.md new file mode 100644 index 0000000..9500f75 --- /dev/null +++ b/wiki/concepts/payload-cms-seo-column-rename.md @@ -0,0 +1,64 @@ +--- +name: payload-cms-seo-column-rename +description: Payload CMS SEO plugin renamed the meta_meta_title column to meta_title between v3.33 and v3.84 — upgrading without migration leaves existing data inaccessible +type: concept +--- + +# Payload CMS SEO Plugin — Column Rename v3.33 → v3.84 + +## What Changed + +The `@payloadcms/plugin-seo` plugin renamed its database columns between versions: + +| Old name (≤ v3.33) | New name (≥ v3.84) | +|--------------------|-------------------| +| `meta_meta_title` | `meta_title` | +| `meta_meta_description` | `meta_description` | +| `meta_meta_image_id` | `meta_image_id` | + +The old names had a `meta_` prefix duplicated (`meta_meta_...`) because the column was generated from a field group named `meta` with a field also named `meta`. + +## Symptoms After Upgrade + +- SEO fields appear blank in the admin panel +- Existing page titles/descriptions disappear from the frontend +- No error on startup — data is silently not found + +## Fix — Manual Migration + +Run directly on the PostgreSQL database: + +```sql +-- For each Payload collection that uses the SEO plugin +ALTER TABLE pages RENAME COLUMN meta_meta_title TO meta_title; +ALTER TABLE pages RENAME COLUMN meta_meta_description TO meta_description; +ALTER TABLE pages RENAME COLUMN meta_meta_image_id TO meta_image_id; + +-- Repeat for other collections (posts, products, etc.) +ALTER TABLE posts RENAME COLUMN meta_meta_title TO meta_title; +ALTER TABLE posts RENAME COLUMN meta_meta_description TO meta_description; +ALTER TABLE posts RENAME COLUMN meta_meta_image_id TO meta_image_id; +``` + +## Before Upgrading — Check Which Columns Exist + +```sql +SELECT column_name +FROM information_schema.columns +WHERE table_name = 'pages' + AND column_name LIKE 'meta%' +ORDER BY column_name; +``` + +If you see `meta_meta_title` → you're on the old schema and will need the rename migration. + +## Payload `migrate:create` Requirement + +`npx payload migrate:create` requires a live PostgreSQL connection to compute the diff — it cannot run without a database. Always run it against a dev database, not in CI without a DB service. + +## Related + +- [[wiki/concepts/payload-cms-push-dev-prod]] — push vs migration workflow +- [[wiki/concepts/payload-cms-node26-esm-workaround]] — Node 26 seed script fix + +**Source:** daily/2026-05-13.md | 2026-05-13 diff --git a/wiki/concepts/pil-photo-compression-pipeline.md b/wiki/concepts/pil-photo-compression-pipeline.md new file mode 100644 index 0000000..8c494b1 --- /dev/null +++ b/wiki/concepts/pil-photo-compression-pipeline.md @@ -0,0 +1,74 @@ +--- +name: pil-photo-compression-pipeline +description: PIL/Pillow photo compression pipeline — thumbnail() with LANCZOS + JPEG quality 82 progressive; re-encoding already-compressed JPEGs gives no size benefit +type: concept +--- + +# PIL Photo Compression Pipeline + +## The Standard Pipeline + +```python +from PIL import Image +import io + +def compress_photo(image_bytes: bytes, max_dimension: int = 1200) -> bytes: + with Image.open(io.BytesIO(image_bytes)) as img: + # Convert RGBA/P to RGB (JPEG doesn't support alpha) + if img.mode in ('RGBA', 'P'): + img = img.convert('RGB') + + # thumbnail() is in-place and respects aspect ratio + # LANCZOS = best quality for downscaling + img.thumbnail((max_dimension, max_dimension), Image.LANCZOS) + + output = io.BytesIO() + img.save( + output, + format='JPEG', + quality=82, # sweet spot: good quality, ~60% size reduction vs 100 + progressive=True, # progressive JPEG loads from blurry to sharp (better UX) + optimize=True, # run Huffman optimizer + ) + return output.getvalue() +``` + +## Key Choices + +| Choice | Why | +|--------|-----| +| `thumbnail()` | Preserves aspect ratio; no-ops if already smaller | +| `LANCZOS` | Best resampling for photo downscaling (Pillow 10+: `Image.Resampling.LANCZOS`) | +| JPEG quality 82 | ~60% file size vs 100; visually indistinguishable for photos | +| `progressive=True` | Browser shows low-res preview while loading — better perceived perf | +| `optimize=True` | Lossless size reduction via better Huffman tables (~5% extra) | + +## WebP Re-encoding Gotcha + +Re-encoding a JPEG that was already compressed at ~82% quality to WebP gives **no meaningful size benefit**: + +- Input: JPEG 82% (e.g., 450 KB) +- Output WebP: 440 KB — essentially the same +- The information loss from the first JPEG compression is already done; WebP can't recover it + +**Use WebP only** for images you control from source (screenshots, graphics, PNGs), not for re-encoding existing JPEGs. + +## Pillow Version Notes + +- Pillow 10+: `Image.LANCZOS` → `Image.Resampling.LANCZOS` (old name still works as alias) +- Pillow 9+: `Image.Transpose.FLIP_LEFT_RIGHT` etc. (use `Image.Resampling` for resample filters) + +## GCS Upload After Compression + +```python +from google.cloud import storage + +def upload_compressed_photo(bucket_name: str, blob_name: str, compressed: bytes) -> str: + client = storage.Client() + bucket = client.bucket(bucket_name) + blob = bucket.blob(blob_name) + blob.upload_from_string(compressed, content_type='image/jpeg') + return blob.public_url +``` + +**Source:** daily/2026-05-13.md | 2026-05-13 diff --git a/wiki/concepts/pnpm11-pnpmfile-requirement.md b/wiki/concepts/pnpm11-pnpmfile-requirement.md new file mode 100644 index 0000000..3691580 --- /dev/null +++ b/wiki/concepts/pnpm11-pnpmfile-requirement.md @@ -0,0 +1,60 @@ +--- +name: pnpm11-pnpmfile-requirement +description: pnpm 11.0.9+ requires a .pnpmfile.mjs file in the project root even when no hooks are needed — install fails without it +type: concept +--- + +# pnpm 11 — `.pnpmfile.mjs` Required + +## The Problem + +After upgrading to pnpm 11.0.9+, `pnpm install` fails with: + +``` + ERR_PNPM_NO_PNPMFILE .pnpmfile.mjs not found +``` + +Or in some cases the install silently produces unexpected behavior because pnpm 11 changed the default hooks expectation. + +## The Fix + +Create an empty `.pnpmfile.mjs` in the project root: + +```js +// .pnpmfile.mjs +export default {} +``` + +This satisfies pnpm's requirement without adding any actual hook behavior. + +## When You Need Actual Hooks + +If you need to patch package manifests (the common use case for `.pnpmfile.mjs`): + +```js +// .pnpmfile.mjs +export default { + hooks: { + readPackage(pkg, context) { + // Example: force a specific version of a transitive dependency + if (pkg.dependencies?.['some-package']) { + pkg.dependencies['some-package'] = '^2.0.0' + } + return pkg + } + } +} +``` + +## Version Context + +- pnpm 10.x: `.pnpmfile.cjs` (CommonJS format) +- pnpm 11.x: `.pnpmfile.mjs` (ESM format, required) + +If migrating from pnpm 10 to 11, rename and convert `.pnpmfile.cjs` to `.pnpmfile.mjs` and update the export syntax from `module.exports = { hooks: ... }` to `export default { hooks: ... }`. + +## .gitignore + +`.pnpmfile.mjs` should be committed — it's a project-level config, not a generated file. + +**Source:** daily/2026-05-13.md | 2026-05-13 diff --git a/wiki/concepts/tsx-node22-esm-named-exports.md b/wiki/concepts/tsx-node22-esm-named-exports.md new file mode 100644 index 0000000..5574650 --- /dev/null +++ b/wiki/concepts/tsx-node22-esm-named-exports.md @@ -0,0 +1,78 @@ +--- +name: tsx-node22-esm-named-exports +description: tsx@4.21.0 with Node.js v22 and --import tsx/esm fails for named exports from CJS packages — use --use-swc flag or import map workaround +type: concept +--- + +# tsx + Node.js v22 — ESM Named Exports from CJS Packages Fail + +## The Error + +Running a TypeScript file with `tsx` on Node.js v22: + +```bash +node --import tsx/esm script.ts +``` + +Fails with: + +``` +SyntaxError: The requested module 'some-cjs-package' does not provide an export named 'SomeName' +``` + +Or: + +``` +Error [ERR_PACKAGE_IMPORT_NOT_DEFINED]: Package import specifier "some-package" is not defined in package... +``` + +## Root Cause + +Node.js v22 tightened ESM/CJS interop rules. When `tsx/esm` loader is used, Node treats `.js` imports as ESM if the package has `"type": "module"` — but many packages that export named exports are actually CJS bundles that Node v22 refuses to re-export as named ESM exports. + +## Workarounds + +### Option 1: `--use-swc` flag (recommended) + +```bash +node --import tsx/esm --use-swc script.ts +``` + +`--use-swc` tells tsx to use the SWC compiler which handles the CJS↔ESM boundary differently. + +### Option 2: Direct `tsx` invocation + +```bash +npx tsx script.ts +# Instead of: node --import tsx/esm script.ts +``` + +`tsx` CLI handles the loader registration itself with better Node v22 compat. + +### Option 3: Import Map + +Create `importMap.js` that re-exports the CJS package as a proper ESM shim: + +```js +// importMap.js +export { SomeName } from 'some-cjs-package' +``` + +Then import from the shim instead of the original package. + +## Affected Pattern + +This primarily affects: +- Payload CMS seed scripts +- Any TypeScript scripts using `--import tsx/esm` (as opposed to the `tsx` CLI) +- Packages with `"exports"` in `package.json` that conditionally expose ESM/CJS + +## Node Version Check + +```bash +node --version # v22.x triggers this +``` + +If you must stay on Node v22, prefer `npx tsx` over `node --import tsx/esm`. + +**Source:** daily/2026-05-13.md | 2026-05-13 diff --git a/wiki/concepts/vtt-cue-settings-timestamp-parse.md b/wiki/concepts/vtt-cue-settings-timestamp-parse.md new file mode 100644 index 0000000..4dc288d --- /dev/null +++ b/wiki/concepts/vtt-cue-settings-timestamp-parse.md @@ -0,0 +1,85 @@ +--- +name: vtt-cue-settings-timestamp-parse +description: VTT cue settings (line:0%, position:50%) appear in the timestamp line after the timecode — parser must strip everything after the first space following the arrow to extract the end time correctly +type: concept +--- + +# VTT Cue Settings — Timestamp Line Parsing + +## WebVTT Cue Header Format + +A VTT cue header has this structure: + +``` +00:00:01.500 --> 00:00:04.000 line:0% position:50% align:center +This is the cue text. +``` + +The timestamp line is: `START --> END [SETTINGS]` + +Settings are optional and appear after the end time, separated by a space. + +## The Parser Bug + +A naive parser that splits on `-->` and trims gets: + +``` +"00:00:01.500" and "00:00:04.000 line:0% position:50% align:center" +``` + +If the end-time string is used directly in `Date.parse()` or a custom HH:MM:SS.ms parser, it will **fail silently or produce NaN** because of the trailing settings string. + +```ts +// WRONG +const [start, end] = line.split(' --> ') +const endMs = parseTimestamp(end) // end = "00:00:04.000 line:0%..." → NaN +``` + +## The Fix + +Strip everything after the first space in the end-time segment: + +```ts +const [start, endWithSettings] = line.split(' --> ') +const end = endWithSettings.split(' ')[0] // ← strip cue settings +const endMs = parseTimestamp(end.trim()) // "00:00:04.000" +``` + +Or with a regex: + +```ts +const match = line.match(/^(\S+)\s+-->\s+(\S+)/) +// match[1] = start, match[2] = end (stops at first whitespace) +``` + +## Settings Round-Trip Loss + +If you parse a VTT file and rebuild it, cue settings are lost unless explicitly preserved: + +```ts +interface VttCue { + start: number + end: number + settings: string // ← store the raw settings string + text: string +} + +// On parse: +const settings = endWithSettings.includes(' ') + ? endWithSettings.slice(endWithSettings.indexOf(' ') + 1) + : '' + +// On rebuild: +const line = `${formatTime(cue.start)} --> ${formatTime(cue.end)}${cue.settings ? ' ' + cue.settings : ''}` +``` + +## Common VTT Cue Settings + +| Setting | Example | Effect | +|---------|---------|--------| +| `line` | `line:0%` | Vertical position (0%=top, 100%=bottom) | +| `position` | `position:50%` | Horizontal position | +| `align` | `align:center` | Text alignment | +| `size` | `size:80%` | Cue box width | + +**Source:** daily/2026-05-13.md | 2026-05-13 diff --git a/wiki/concepts/vue-router-registration-async-store.md b/wiki/concepts/vue-router-registration-async-store.md new file mode 100644 index 0000000..bfa6152 --- /dev/null +++ b/wiki/concepts/vue-router-registration-async-store.md @@ -0,0 +1,80 @@ +--- +name: vue-router-registration-async-store +description: Vue Router must be registered with app.use(router) AFTER await authStore.init() — registering before the store initializes causes navigation guards to run before auth state is ready +type: concept +--- + +# Vue Router — Registration Order vs Async Store Init + +## The Problem + +Vue apps with authentication typically have an async init step (`authStore.init()`) that: +- Reads the stored token +- Validates/refreshes it +- Sets `isAuthenticated` + +If `app.use(router)` runs **before** this init completes, the first navigation guard fires before `isAuthenticated` is known → guard sees unauthenticated state → redirects to `/login` even for authenticated users. + +```ts +// WRONG — router registers before auth state is resolved +const app = createApp(App) +app.use(router) // ← guard runs immediately, isAuthenticated = false +app.use(pinia) + +await authStore.init() // ← too late +app.mount('#app') +``` + +## Correct Order + +```ts +// CORRECT +const app = createApp(App) +app.use(pinia) + +const authStore = useAuthStore() +await authStore.init() // ← auth state resolved first + +app.use(router) // ← now guards run with correct isAuthenticated +app.mount('#app') +``` + +## Navigation Guard Pattern + +```ts +// router/index.ts +router.beforeEach((to, _from, next) => { + const auth = useAuthStore() + + if (to.meta.public) { + // Public route — but redirect authenticated users away from login + if (auth.isAuthenticated && to.name === 'Login') { + return next({ name: 'Dashboard' }) + } + return next() + } + + // Protected route + if (!auth.isAuthenticated) { + return next({ name: 'Login', query: { redirect: to.fullPath } }) + } + + next() +}) +``` + +## The `meta.public` Bypass Gotcha + +`meta.public = true` marks a route as publicly accessible. + +**But:** authenticated users hitting the login page should be redirected to the app. +The guard must explicitly check `auth.isAuthenticated` even on `meta.public` routes for this redirect to work. + +Without the check: authenticated user navigates to `/login` → page renders → jarring UX. + +## Related + +- [[wiki/concepts/zustand-async-hydration]] — same pattern in React/Zustand (gate on hasHydrated) +- [[wiki/concepts/axios-401-interceptor-infinite-loop]] — auth refresh loop prevention + +**Source:** daily/2026-05-13.md | 2026-05-13 diff --git a/wiki/log.md b/wiki/log.md index 01a7111..cf8e512 100644 --- a/wiki/log.md +++ b/wiki/log.md @@ -401,3 +401,12 @@ - Sessions: kvytky/shumiland Next.js+Payload CMS, banner-tool - Articles created: [[wiki/concepts/payload-cms-root-layout-requirement]], [[wiki/concepts/payload-cms-node26-esm-workaround]], [[wiki/concepts/overflow-hidden-clips-absolute-children]], [[wiki/concepts/pydantic-exclude-none-null-clearing-conflict]], [[wiki/concepts/css-marquee-animation-gpu-pattern]], [[wiki/concepts/css-animation-js-scroll-conflict]], [[wiki/concepts/nextjs16-lint-command-removed]], [[wiki/concepts/figma-mcp-oauth-reconnect-restart]], [[wiki/concepts/react-state-playwright-css-hover]] - Index updates: [[wiki/concepts/_index]] (187→196); [[wiki/_master-index]] (concepts 187→196) + +## [2026-05-13T21:00:00+03:00] compile | 2026-05-13.md + raw/ inbox +- Source: daily/2026-05-13.md + raw/ (Figma docs: 17 files) +- Sessions: cc-dashboard (Vue 3 auth), shumiland/dyvolis (Payload 3.84 upgrade), video-accessibility (VTT), loreal-timelog-viewer +- Articles created: [[wiki/concepts/css-clamp-negative-values]], [[wiki/concepts/axios-401-interceptor-infinite-loop]], [[wiki/concepts/vue-router-registration-async-store]], [[wiki/concepts/gcs-cors-signed-url-get]], [[wiki/concepts/vtt-cue-settings-timestamp-parse]], [[wiki/concepts/payload-cms-seo-column-rename]], [[wiki/concepts/pnpm11-pnpmfile-requirement]], [[wiki/concepts/tsx-node22-esm-named-exports]], [[wiki/concepts/docker-compose-force-recreate]], [[wiki/concepts/next-app-router-favicon]], [[wiki/concepts/pil-photo-compression-pipeline]] +- Articles created (raw/Figma): [[wiki/concepts/figma-design-to-code-workflow]], [[wiki/concepts/figma-mcp-tools-reference]], [[wiki/concepts/figma-skills-reference]] +- Articles updated: [[wiki/concepts/payload-cms-push-dev-prod]] (+partial-apply gotcha), [[wiki/concepts/fastapi-orm-property-json-column]] (+@computed_field Pydantic v2) +- Client knowledge: [[wiki/client-knowledge/loreal]] (git-pull deploy for timelog-viewer) +- Index updates: [[wiki/concepts/_index]] (196→207 effective); [[wiki/_master-index]] (concepts 182→196)