Skip to content

Troubleshooting

Austin Stanfield edited this page Oct 1, 2026 · 8 revisions

Troubleshooting

Things that have gone wrong before, and what fixed them.

Problem Fix
Photos not showing Must use /api/photos/[filename] path, not /photos/[filename]
Thumbnails show broken-image icons /api/photos/<file>/thumb is 404ing. Run npm run check:photos — it exercises thumbs, full-size, ?w= resize and the traversal guard against the route handler directly. In src/app/api/photos/[...filepath]/route.ts the trailing thumb segment must be stripped before the fs.existsSync() check, or it stats <file>/thumb and 404s everything
Can't log in Verify ADMIN_PASSWORD env var; try incognito
An admin page shows no data, or a save silently fails The API now returns 401 without the admin cookie. Check the browser has it (HttpOnly, SameSite=Lax) and that the session hasn't expired — the token lasts 2 hours. A page that fetches an admin endpoint from public code will 401 unless the endpoint is one of the three allowlisted reads in src/middleware.ts
A public page lost its content after a deploy It probably reads an /api/admin/* endpoint that is not allowlisted. The allowlist is PUBLIC_ADMIN_READS in src/middleware.ts; only add a read whose content is already public
The dev server loads but nothing works — buttons dead, forms submit as GETs Every chunk is 403ing: the origin you are browsing is not in allowedDevOrigins in next.config.ts. Next 16.3 enforces this where 16.0 only warned, and it bites the docker dev stack because it is reached by the server's LAN address
RSVP not found Guest name must match guest_list exactly (case-insensitive)
Registry fetch blank Amazon blocks scrapes — fill in manually after fetch attempt
WIP page showing Admin → WIP Control → toggle page to Live
Build fails Run rm -rf .next then rebuild — stale cache causes false errors
Demo instance is saving changes DEMO_MODE was refused. The flag needs the database in DATABASE_URL to be named demo; check the container's logs for the one-line refusal. A demo that is actually read-only always shows the banner, so banner + persisted writes is impossible
Demo shows the banner but no data The one-shot seed service failed. docker logs wedding-seed-demo — usually the database wasn't up yet, and docker compose up -d again re-runs it
Demo redeploy hits a port clash Something else is already on 3001 — most likely a copy of the stack under a different project name. docker ps --filter publish=3001, and bring the old one down. The demo compose file pins name: weddingdemo so it keeps the same volumes
A Portainer webhook 404s with "Unable to find the stack by webhook ID" The token was never saved. Portainer's UI can show you a UUID it did not persist — check the database before trusting it (see Deployment). Both servers pull their own images anyway, so a dead webhook costs nothing
Saving something returns 500 and the log says "violates not-null constraint" A blank was coerced to null for a column that is NOT NULL DEFAULT ''. The field needs blankAsEmpty: true in the resource map — regions.country and notes.body both hit this (v0.9.38); places.country and trip.focus_country already had it
A merge to main didn't publish an image Check the Wedding Planner run in Actions. A malformed CHANGELOG.md fails check:changelog before the image is built; the published version tag comes from its topmost heading
Map search says "lookup failed" or "the geocoder turned us away" on the server but works on your laptop Nominatim is refusing that host. Their usage policy needs a User-Agent that names the app and offers a way to reach you; a generic one gets 403 Access denied, and heavy use from one IP gets the same. Set GEOCODER_USER_AGENT on the stack to something with your email in it. Since v0.9.53 the portal falls back to Photon (keyless) automatically, so search keeps working either way — fallback rows are labelled "fuzzy match, check it" because it matches loosely, and a single fuzzy hit is never auto-applied. To see what the server actually gets: docker exec wedding-web-prod node -e 'fetch("https://nominatim.openstreetmap.org/search?q=test&format=jsonv2",{headers:{"User-Agent":"x"}}).then(r=>console.log(r.status))'
A geocoder hit lands on the wrong airport Both services match fuzzily on three-letter codes — asked for YBR airport, Photon offers YBL in British Columbia and a Don José in Argentina, both genuinely aerodromes. Ranking puts the mode's kind first and uses the typed code only to break ties, so the list is usually right, but check the row before applying it. A pasted Google Maps link, or right-clicking the pin there and copying the lat, lng, is always exact
"Fill in from schedule" on a travel leg says the API is not configured FLIGHT_API_KEY is unset or is not reaching the container. Before v0.9.51 the compose file passed only three variables through, so a key set on the stack never arrived — check with docker exec wedding-web-prod printenv FLIGHT_API_KEY. The key is a RapidAPI one subscribed to AeroDataBox; its free plan allows one request a second
"Fill in from schedule" fills in times from a different date Deliberate, and it says so. The flight does not operate on that leg's date, so the lookup fell back to that number's most recent actual schedule — same times, terminals, aircraft and zones, which is what you want for a flight two months out. Worth re-running closer to the trip
Map pins don't respond to clicks Fixed in v0.9.15, but the shape is worth knowing: Leaflet markers are focusable by default, and focusing one on a map that runs past the bottom of the window scrolls it out from under the cursor between mousedown and mouseup. Markers are created with keyboard: false

Backup

# Database
docker-compose exec db pg_dump -U wedding_user wedding_db > backup-$(date +%Y%m%d).sql

# Photos + config
docker cp <container>:/app/public/photos ./backup/photos
docker cp <container>:/app/public/config ./backup/config

"Unknown resource" when saving something

Symptom: an admin editor refuses to save and the error says Unknown resource. Seen on Finances → Settings, where changing the guest headcount did nothing (fixed in v0.9.100).

Cause: the finance and honeymoon APIs are each one generic route (/api/admin/finances/[resource], /api/admin/honeymoon/[resource]) with a whitelist of tables. Anything not in that whitelist answers 404 Unknown resource. A resource that is a singleton — one row, no id, no create, no delete — cannot be in the whitelist, so it is special-cased by name instead.

The trap: that special case has to exist in every verb that can reach it. settings had one in POST but not in PATCH, and every editor in the finance suite saves with a PATCH — so the endpoint looked healthy (a POST to the same path worked perfectly) while a whole tab silently could not save.

If you see it again:

  1. Open the network tab and read the method as well as the path. A 404 on PATCH /api/admin/<area>/<thing> where POST works is this exact shape.
  2. Find the handler for that verb and check whether the singleton branch is there, or whether it fell through to the whitelist lookup.
  3. Put the branch in a shared function called from both verbs rather than copying it, which is what let the two drift in the first place.

Covered by: npm run check:finance:db, which drives the real route handlers against a real Postgres and now asserts the verb the UI actually sends.


← All pages

The home page animation is skipped on a tablet or a sideways phone

Symptom: on a phone held portrait, and on a desktop, scrolling the home page plays the hero collapse. On a tablet — or the same phone turned on its side — scrolling skips it and lands on the collage immediately.

Cause: the hero draws two layouts, a narrow one and a wide one, and picks between them on width (max-width: 768px). Until v0.9.104 it used that same width to decide what to listen to, and the wide path listened only for a mouse wheel. A tablet, or a phone in landscape, is wider than the breakpoint and has no wheel, so nothing was listening at all: the finger scrolled the page normally, the snap-to-collapsed guard saw the page had moved, and the hero arrived at the collage with no animation in between.

The thing to remember: which layout to draw is a question about width; what someone is scrolling with is a question about their pointer. They are not the same question, and a breakpoint cannot answer both. The wide path now listens for touch as well as the wheel, and both inputs share the same collapse and expand so they cannot drift apart.

If it comes back: run npm run check:hero against a server whose site config has a hero photo. It drives the real page at five viewport-and-input pairings — phone portrait, phone landscape, tablet portrait, tablet landscape and desktop — and asserts the page is held still while the hero animates rather than jumping. Every case reporting "has hero photos to animate" as a failure means the server has no hero image configured, not that the animation is broken.

Clone this wiki locally