vault backup: 2026-05-14 21:09:52

This commit is contained in:
Vadym Samoilenko 2026-05-14 21:09:52 +01:00
parent c5e42c2c52
commit 4442916df4
18 changed files with 864 additions and 2 deletions

View file

@ -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`

View file

@ -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 |

View file

@ -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

View file

@ -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 | -->

View 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

View 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

View 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

View file

@ -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 |

View 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

View 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

View file

@ -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

View 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

View 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

View 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

View 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

View 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

View 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

View file

@ -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)