vault backup: 2026-05-14 21:09:52
This commit is contained in:
parent
c5e42c2c52
commit
4442916df4
18 changed files with 864 additions and 2 deletions
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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 `<base href="/timelog/">` 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
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
<!-- Articles added automatically by compile.py -->
|
||||
<!-- Format: | [[concepts/slug]] | One-line summary | daily/YYYY-MM-DD.md | date | -->
|
||||
|
|
|
|||
73
wiki/concepts/axios-401-interceptor-infinite-loop.md
Normal file
73
wiki/concepts/axios-401-interceptor-infinite-loop.md
Normal file
|
|
@ -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
|
||||
61
wiki/concepts/css-clamp-negative-values.md
Normal file
61
wiki/concepts/css-clamp-negative-values.md
Normal file
|
|
@ -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
|
||||
58
wiki/concepts/docker-compose-force-recreate.md
Normal file
58
wiki/concepts/docker-compose-force-recreate.md
Normal file
|
|
@ -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
|
||||
|
|
@ -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 |
|
||||
|
|
|
|||
70
wiki/concepts/gcs-cors-signed-url-get.md
Normal file
70
wiki/concepts/gcs-cors-signed-url-get.md
Normal file
|
|
@ -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
|
||||
81
wiki/concepts/next-app-router-favicon.md
Normal file
81
wiki/concepts/next-app-router-favicon.md
Normal file
|
|
@ -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 `<link rel="icon">` into `<head>`
|
||||
- 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(
|
||||
<div style={{ background: '#000', width: '100%', height: '100%' }}>
|
||||
{/* Your icon JSX */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
64
wiki/concepts/payload-cms-seo-column-rename.md
Normal file
64
wiki/concepts/payload-cms-seo-column-rename.md
Normal file
|
|
@ -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
|
||||
74
wiki/concepts/pil-photo-compression-pipeline.md
Normal file
74
wiki/concepts/pil-photo-compression-pipeline.md
Normal file
|
|
@ -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
|
||||
60
wiki/concepts/pnpm11-pnpmfile-requirement.md
Normal file
60
wiki/concepts/pnpm11-pnpmfile-requirement.md
Normal file
|
|
@ -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
|
||||
78
wiki/concepts/tsx-node22-esm-named-exports.md
Normal file
78
wiki/concepts/tsx-node22-esm-named-exports.md
Normal file
|
|
@ -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
|
||||
85
wiki/concepts/vtt-cue-settings-timestamp-parse.md
Normal file
85
wiki/concepts/vtt-cue-settings-timestamp-parse.md
Normal file
|
|
@ -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
|
||||
80
wiki/concepts/vue-router-registration-async-store.md
Normal file
80
wiki/concepts/vue-router-registration-async-store.md
Normal file
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue