- Android → Google Play, UTM-Werte als Play-referrer (Play Console)
- iPhone → App Store, sobald APP_STORE_URL gesetzt ist (D5); bis dahin Web
- sonst → Web-Startseite; ?ref=<Code> führt auf die Einladungsseite /r/<Code>
- nur sichere UTM-Werte werden übernommen, nie gecacht (302, no-store)
- Zählung pro Tag/Quelle/Gerät in Redis, Anzeige im Admin unter
Empfehlungen → Übersicht (30 Tage)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Pet.isFoundingMember: in pets.create per bewachter SQL-Anweisung für die
ersten 250 Tiere gesetzt, nie entzogen; Abzeichen (Spross) in Feed und
Profil, Web + App, unabhängig vom Empfehlungsprogramm
- Kurator "PawFeed Team": folgt jedem neuen Tier und schickt eine
Begrüßungs-Direktnachricht (DE/EN nach Sprache beim Anlegen), im
Hintergrund — blockiert das Onboarding nicht
- CuratorSettings + Admin-Seite /p/[secret]/curator: Schalter, Texte mit
Pflicht-Platzhalter {name} und Vorschau, Zähler der Gründungsmitglieder
- prisma/backfill_founding_members.sql, prisma/enable_rls_curator.sql
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- stories.listActive: der Owner bekommt seine Storys der letzten 7 Tage mit
isExpired, alle anderen weiterhin nur die aktiven 24 Stunden
- stories.hasActiveStory: optional includeOwnRecent → grauer Ring
(expiredOnly) nur in der Story-Leiste, nicht an Profilbildern im Feed
- Viewer: Hinweis "Abgelaufen – nur für dich sichtbar", Kommentare bei
abgelaufenen Storys nur lesbar, Pfote nur als Zähler; Löschen und
Zuschauerliste bleiben
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- /api/cron/cleanup-stories (CRON_SECRET): löscht Storys älter als 7 Tage
samt Aufrufen, Pfoten, Kommentaren, Supabase-Bild und Mux-Asset; max. 200
pro Lauf, Fehler einzelner Storys brechen den Lauf nicht ab
- gemeinsame Funktion deleteStoryWithMedia (src/lib/story-cleanup.ts), auch
von stories.delete genutzt
- Datenschutzerklärung Abschnitt 6: Storys spätestens nach 7 Tagen gelöscht
- Datenexport: Story-Block heißt "stories" statt "activeStories"
- CLAUDE.md: Cron-Jobs laufen über die NAS-Crontab + docker/run-cron.sh
(der alte Hinweis "nichts ruft trim-feeds auf" war veraltet)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- stories.delete: nur Owner; Aufrufe, Story (Pfoten/Kommentare per Cascade),
danach Bild in Supabase Storage bzw. Mux-Asset best-effort
- Web: Papierkorb neben der Zuschauerzahl + Rückfrage, Timer pausiert,
gelöscht wird die beim Antippen gemerkte Story
- App: gleicher Papierkorb mit nativer Rückfrage, Timer pausiert
- found.md: Nutzer-Feedback "Story löschbar" erledigt, 7-Tage-Wunsch mit
Befund als offener Punkt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- App-Viewer: Pfote, Kommentar-Sheet (schreiben, eigene löschen) und für die
eigene Story die Zuschauerliste; Timer pausiert, solange ein Sheet offen ist
- stories.listActive: optionales viewerPetId → reactedByViewer, damit die
Pfote gefüllt startet (abwärtskompatibel für alte App-Builds)
- Web-StoryPawButton nutzt reactedByViewer ebenfalls und wird pro Story
neu initialisiert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Die Leiste zeigte nur Storys gefolgter Tiere — das eigene aktive Tier war
nie enthalten, man konnte die eigene Story nach dem Posten nicht ansehen.
Neuer Ring direkt nach der "+"-Kachel, solange stories.hasActiveStory für
das aktive Tier true ist.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- App: post-new.tsx sortierte HEIC/HEIF still aus, obwohl resizeForUpload()
ohnehin in JPEG umwandelt — jetzt erlaubt, plus Hinweis bei übersprungenen
Fotos
- Web: neues src/lib/heic-to-jpeg.ts wandelt HEIC vor dem Upload im Browser
in JPEG um (Safari kann HEIC nativ dekodieren); eingebaut in Beiträge,
Storys, Meilensteine und Tierbild, accept um HEIC ergänzt. Browser ohne
HEIC-Dekodierung bekommen eine klare Meldung statt "falsches Format".
- Server-Whitelist (JPEG/PNG/WebP) bleibt unverändert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Die Avatar-Variante "lg" setzt per data-Attribut fest size-10 (40 px) und
überstimmte die eigene Größe — jetzt Standardvariante mit size-28 / sm:size-36
und dezentem Ring in Markenfarbe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Datenexport (Art. 15): neuer Block referralProgram mit eigenem Code,
Herkunftsangabe, "eingeladen von", gemachten Einladungen, erreichten Rängen,
aktuellen Vorteilen und den früheren Einmal-Codes. Andere Personen nur über
den Tiernamen — invitedById (Clerk-ID eines Dritten) fliegt aus dem Export.
- Kontolöschung: invitedById bei Eingeladenen und usedById bei Beta-Codes
werden geleert, damit keine Clerk-ID der gelöschten Person zurückbleibt.
Gezählte Einladungen bleiben gezählt.
- Einlösen sperrt jetzt auch über invitedAt, damit nach dem Leeren von
invitedById kein zweiter Code eingelöst werden kann.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Proxy-Sperre für /sign-up, INVITE_REQUIRED, set-invite/consume-invite,
invites-Router, Admin-Invite-Seite und -Prozeduren, Startcodes in
pets.create entfernt — Registrierung ist offen
- /join?code=… leitet auf /r/<code> um, /invite auf /einladen; alte geteilte
Links laufen nicht ins Leere
- InviteCode-Tabelle bleibt nur als Historie (Kontolöschung räumt sie weiter auf)
- Datenschutzerklärung 4b (Empfehlungsprogramm, Herkunftsangabe, pf_ref-Cookie)
und Nutzungsbedingungen 3b (Programmbedingungen, Bestandsschutz, Missbrauch)
- CLAUDE.md: Abschnitt "Referral program" statt "Invite system"
Build, Typecheck und alle Tests grün.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Bildschirm "Freunde einladen" (/invite, aus den Einstellungen erreichbar):
eigener Code mit nativem Teilen-Dialog, Rangleiter mit Fortschritt,
eingeladene Tiere, nachträgliche Code-Eingabe im Einlösefenster
- Onboarding-Schritt "Wie hast du von PawFeed erfahren?" mit Code-Eingabe,
einmal pro Owner (referrals.onboardingState)
- Rang-Abzeichen in PetIdentityBadges (Feed und Profil), Profilring in
Rangfarbe, "Eingeladen von …" im Tierprofil
- Benachrichtigungen REFERRAL_JOINED / REFERRAL_RANK_UP
- Keine neuen nativen Module (React Native Share) — per OTA auslieferbar
Typecheck grün; nicht auf einem Gerät getestet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Persönliche, unbegrenzt nutzbare Empfehlungscodes (z. B. BUBI-7K3M), die erst
nach der Registrierung im Onboarding eingelöst werden — keine Kopplung an den
Clerk-Sign-up-Ablauf mehr. Registrierung ohne Code bleibt möglich.
- Schema (rein additiv): Owner-Felder für Code, Einladungsstatus, Zähler und
Rang-Cache, ReferralRank / ReferralRankAchievement / ReferralSettings,
AcquisitionSource, NotificationType REFERRAL_JOINED / REFERRAL_RANK_UP
- Kernlogik src/lib/referrals.ts: Einlösen (7-Tage-Fenster, kein eigener Code,
keine Kreis-Einladungen, gesperrte Einladende ungültig), gegenseitiges
Folgen, Qualifizierung erst nach Tier + erstem Beitrag (Foto oder fertiges
Video), Tageslimit mit Prüf-Markierung, Ränge mit Bestandsschutz
- Web: /einladen, öffentliche Seite /r/[code], Onboarding-Schritt
"Wie hast du von PawFeed erfahren?", Rang-Abzeichen, Profilring,
"Eingeladen von …", neue Benachrichtigungen
- Admin /p/[secret]/referrals: Ränge anlegen/bearbeiten/deaktivieren mit
Live-Vorschau, Einstellungen, Übersicht mit Prüfung und Aberkennung;
bevorzugte Verifizierung als Vorteil
- prisma/scripts/referral-setup.ts: Start-Ränge, Settings, Code-Backfill,
optional --credit-existing für Alt-Einladungen
Noch offen: Mobile-App, Abbau des alten InviteCode-Ablaufs, Rechtstexte,
db push + Deploy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Build 2 was rejected in TestFlight beta review (missing demo credentials)
and cannot be resubmitted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Also includes the user's own pending found.md edits (new entries, iOS
section header, trailing-whitespace cleanup).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The middle toe beans reach y = -9 in the 0–100 viewBox. Web draws the SVG
with overflow: visible; react-native-svg clips at the viewBox, cutting the
paw off. Extend the viewBox upward and grow the box to match; the "add
story" tile uses the same box so it lines up with the rings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Android is edge-to-edge, and stack screens (no tab bar below) had no
bottom inset on their scroll containers — the first post on a pet profile
sat in the gesture area and couldn't be tapped (found.md). New
useBottomSafePadding hook adds insets.bottom to the contentContainerStyle
of pet profile, followers, following, health, milestones, edit pet,
settings, search, messages list and delete account.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Block (Guideline 1.2, UGC): useBlockPet hook (native confirm Alert,
blocks.create). PostCard "..." on other pets' posts now offers
Report / Block; pet profile gets a header "..." with Report pet / Block
(hidden for any of the owner's own pets), navigating back after a block.
Feed, comments and DMs already enforce blocks server-side; explore and
search don't yet (server follow-up).
- Delete account (Guideline 5.1.1(v)): new delete-account screen reached
from a quiet link below Log out in Settings. Mirrors web's
DeleteAccountDialog (warning list + type a pet name) and calls the same
POST /api/account/delete with the Clerk bearer token, then clears the
query cache and signs out.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ArachnophobiaOptIn, UploadConsent, OnboardingGuidelines and AvatarStep
still used the static light-palette tokens in module-level
StyleSheet.create, so titles and checkbox labels rendered dark-on-dark.
Switched them to useTheme() + makeStyles(colors) like the other screens.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
iOS labels the back button with the previous screen's title, which was
the untitled (tabs) route group. Use headerBackButtonDisplayMode
"minimal" (chevron only) for all stack screens.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- eas.json: new `preview-simulator` profile (extends preview, ios.simulator)
for EAS cloud simulator builds without an Apple Developer account
- app.json: iOS deployment target 17.0 via expo-build-properties
(@clerk/expo's ClerkKit SPM package requires iOS 17 — pod install failed
on the default 16.4), iPhone-only (supportsTablet: false),
usesAppleSignIn, ITSAppUsesNonExemptEncryption: false
- AppleSignInButton: native Apple sheet via Clerk's useSignInWithApple
(App Store Guideline 4.8, since Google login is offered); same
legal-consent handling as GoogleSignInButton; iOS-only
- Not yet testable end-to-end: needs Apple Developer account, the
Sign in with Apple capability on the App ID and Apple enabled in Clerk
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Replace next/font/google with next/font/local + vendored Inter variable
woff2 (latin, @fontsource-variable/inter@5.3.0), so neither visitors nor
the Docker build contact Google's servers.
- Add PlayStoreBadge (official Google Play artwork, DE/EN by locale, served
locally from /public/badges) to the hero, a new "PawFeed in your pocket"
section, and the shared landing footer (also covers /ueber-uns).
- Centralize the Play Store URL in src/lib/app-links.ts (derived from the
android.package id org.pawfeed.app).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Multiple users reported "Unmatched Route pawfeed://sso-callback" after
Google sign-in. Root cause: Android's custom tab delivers the OAuth
redirect intent to the app twice — once caught correctly by
expo-web-browser's openAuthSessionAsync() session, but also as a plain
deep link seen by expo-router's own Linking listener. With no matching
route, expo-router briefly showed its built-in Unmatched Route error
screen instead of letting the real sign-in flow (GoogleSignInButton ->
setActive -> router.replace) take over.
Adds app/sso-callback.tsx as a themed loading spinner so expo-router has
a valid screen for that window; no auth logic needed there since
useProtectedRoute() in app/_layout.tsx already redirects based on
Clerk's session state. JS-only change, OTA-deployable, no native rebuild
needed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The story upload input's accept attribute listed explicit image+video
MIME subtypes (image/jpeg,...,video/mp4,...). Several mobile
browsers'/OS's native 'Photos & Videos' pickers only reliably offer
both media kinds when given the broad image/*,video/* wildcard
categories - the explicit mixed list was silently narrowing the picker
to photos only, matching the user's report ('nur Zugriff auf Fotos,
Videos werden gar nicht angezeigt').
The actual accept/reject decision is unchanged (still enforced in
handleFile() against the specific ALLOWED_IMAGE_TYPES/
ALLOWED_VIDEO_TYPES lists) - this only relaxes what the OS offers in
the picker dialog.
Code-reviewed before commit (WORKFLOW.md step 6) - fixed the one
finding that became newly reachable by this fix: videos were
previously never selectable at all, so a parameterized/codec-suffixed
MIME string (e.g. from some Android camera-roll
pickers) could never surface as a rejection; now strips codec
parameters before the ALLOWED_VIDEO_TYPES comparison. Documented two
other findings as accepted, not-fixed-here trade-offs in code comments
(iOS HEIC auto-transcode interaction, broader picker surfacing formats
that still get rejected post-selection) rather than silently ignoring
them.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two independent root causes behind 'admin music upload fails':
1. Supabase bucket name mismatch — code expects 'music-tracks' (plural,
MUSIC_STORAGE_BUCKET), but the bucket was created as 'music-track'
(singular). Every upload failed with a bucket-not-found error from
Supabase Storage, surfaced only as a generic toast. Fixed by creating
the correctly-named bucket (public, 5MB limit, audio/mpeg|mp4|aac
allowed_mime_types matching the app's own constants) via the Supabase
Storage API; verified end-to-end with a signed-upload-url + PUT probe.
The old empty 'music-track' bucket is unused but still exists -
deleting it needs the user's own action (blocked here by the
auto-mode classifier's cloud-storage-delete guard).
2. Client-side validation trusted the browser-reported File.type, which
is sniffed from the OS's MIME registry and is inconsistent for
.m4a/.aac (e.g. Windows Chrome reports .m4a as audio/x-m4a, not
audio/mp4) - a perfectly valid file could get rejected as 'wrong
type'. Now derives the content-type from the file extension instead.
Code-reviewed before commit (WORKFLOW.md step 6) - fixed two of four
findings: the extension lookup now uses a Map instead of a plain object
(prototype-shadowing via a file named e.g. 'x.constructor' previously
bypassed the rejection), and requires an actual '.' before the
extension (a bare filename like 'mp3' no longer matches). Extracted the
allowed-types/size constants into a new client-safe src/lib/music-
content-types.ts (supabase-storage.ts re-exports them) so the client
form and server-side Zod schema share one source of truth instead of
two independently maintained copies. Left the trust-the-extension
trade-off as-is (documented in code): this panel sits behind
ADMIN_SECRET + a role check, not the public upload surface, and the
old File.type check was never real content-based validation either.
Full test suite green (one unrelated legal.test.ts timeout in the
full-suite run was confirmed flaky - passes cleanly in isolation).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Google SSO broke live with 'redirect url ... does not match an
authorized redirect URI ... pawfeed://sso-callback', despite the SSO
code (99575dd) being unchanged. Root cause was outside the repo
entirely: Clerk's per-instance authorized-redirect-URL allowlist is
server-side config (CLERK_SECRET_KEY-authenticated Backend API), not
tracked in git, and only had a stale org.pawfeed.app2 entry - the
current pawfeed:// scheme's callback was never registered.
Fixed live via POST https://api.clerk.com/v1/redirect_urls (no app
deploy needed), verified through the Google consent screen on an
emulator and end-to-end by the user on a real device.
Documented in both mobile/found.md (full incident) and README.md's
Known Issues table (the reusable gotcha: check Clerk's redirect-URL
allowlist before debugging app code when SSO breaks unexpectedly).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds the merge commit (30c57b6), emulator end-to-end verification, and
OTA update group reference to the Settings-header-icon found.md entry.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Closes Gitea #36. Plan: ~/.claude/plans/mobile-settings-tab-extraction.md.
Extracts the 5 footer sections pets.tsx had grown (Darstellung, Sprache,
Tier-Vorschlaege opt-out, Rechtliches+Version/OTA, Abmelden) into their
own files under src/components/settings/, and a new app/(app)/settings.tsx
screen reached via a gear icon in every tab's header, next to Messages -
not a 5th bottom tab. Chosen over a tab (see the two mockups in the plan
folder) because Settings is a low-frequency destination for a typical
user; the header icon still reaches it in one tap from anywhere without
spending permanent bottom-nav space on it.
settings.tsx lives outside (tabs)/, registered as a plain Stack.Screen
(app/(app)/_layout.tsx) matching search.tsx's pattern - normal back-arrow
push, no presentation:modal, no tab bar visible while it's open.
SettingsHeaderButton is wired into (tabs)/_layout.tsx's global headerRight
default plus the 3 tabs (index/explore/pets) that override it.
pets.tsx is pet-list-only again. FooterErrorBoundary now wraps the whole
Settings screen instead of only the logout button, closing a gap a prior
code review found (AppearanceSection/LegalSection were rendering
unprotected).
Code-reviewed before commit (WORKFLOW.md step 6) - fixed two of three
findings: extracted the section/sectionTitle style duplicated across
AppearanceSection/LanguageSection/PetSuggestionsSection into a shared
sectionTitleStyle.ts, and reduced every header icon's hitSlop 12->8 (rows
can now hold 3 icons with gap:16; 12+12=24 overlapped the gap, 8+8=16
exactly meets it - see the comment on headerButtons in (tabs)/_layout.tsx).
Left the pets.* i18n namespace as-is on the moved strings (documented in
settings.tsx's doc comment) - pets.language* is shared with the pre-login
LanguageSwitcher, so a partial rename would misdescribe those call sites.
Typecheck + lint clean across all touched/new files, i18n key parity
verified (de/en).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Meine Tiere tab footer now shows 'Version X.X.X - OTA <short-id>' (or
'Build (kein OTA)' when running the embedded/non-OTA bundle) via
expo-updates' Updates.isEmbeddedLaunch/updateId. Directly answers the
support question 'did the update actually arrive on this device?' by
comparing the shown short id against the 'Android update ID' an
'eas update' publish prints - the exact ambiguity that cost a multi-day
debugging session earlier (see .claude/handoffs/2026-09-12-ota-update-
mystery-expo-dev-client-root-cause.md).
Code-reviewed before commit (per WORKFLOW.md step 6): fixed two of four
findings - wrapped the expo-updates reads in a try/catch (LegalSection
sits outside FooterErrorBoundary, so an unguarded throw here would have
taken down the whole footer) and moved the hardcoded 'OTA ' prefix into
i18n (pets.otaId). The web-shim-always-unknown finding is a documented
dead branch (this app ships no web build - eas update only ever runs
--platform android). No test coverage added - no RN test infrastructure
exists in mobile/ yet (pre-existing gap, tracked in WORKFLOW.md).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds WORKFLOW.md (the living, discussion-oriented version) and a
contributor-facing summary in README.md, reflecting four automation
decisions made 2026-09-19:
- OTA publish and web rolling-deploy run automatically once
typecheck+lint+code-review are clean, no per-change confirmation
- a code-review pass (code-review skill - the code-reviewer agent named
in .claude/rules/ecc isn't installed in this environment) runs on
every diff before commit
- twin instances of a found bug pattern get fixed in the same pass,
not just reported
- large mobile features go through a planning step (Plan agent /
EnterPlanMode - the installed equivalent of the referenced planner
agent) and their own feature branch, merging to main (and only then
triggering the automatic deploy) once complete
Also adds a Mermaid system architecture diagram to README.md covering
both clients (web + mobile) against the shared Next.js/tRPC backend,
data/media services, and the two deploy paths (NAS rolling-deploy vs.
EAS Build/Update) - validated by rendering it through mermaid-cli.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same root cause as post-new.tsx/story-new.tsx: search.tsx imported the
static light-only colors/typography instead of useTheme(). Bigger
refactor than the other two since TabButton/PetRow/Loading/EmptyState
are separate top-level components, not closures inside the screen -
they now receive styles/colors as props from the theme-aware
makeStyles(colors) built in SearchScreen, rather than importing the
static tokens themselves.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
post-new.tsx (and story-new.tsx, same latent bug) still imported the
static, light-only colors/typography from theme/tokens.ts instead of
useTheme() - container background stayed permanently light while the
shared TextField correctly pulled dark-mode text color from the theme,
producing white text on a white background in the caption input.
Migrated both to the useTheme() + makeStyles(colors) pattern already
used by milestone-new.tsx. search.tsx has the same underlying issue but
needs a larger refactor (several sub-components reference the static
tokens directly) - left for a separate pass, noted in found.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two Known-Issues entries from tonight's OTA-publish session:
- eas update needs --platform android explicitly (no web target/
react-native-web in this project, --platform all fails bundling)
- mobile/node_modules can end up with silently missing nested files after
a bad install, invisible to npm install itself (seen across @expo/cli,
@sentry/browser, @sentry-internal/browser-utils in the same session) -
full rm -rf node_modules && npm install is the fix, not a targeted one
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
docker/restore-db.sh - emergency-only, not wired into any crontab. Lists
available backups when run without args; restores via pg_restore
(--clean --if-exists --no-owner --no-privileges -j 4) against DIRECT_URL,
same throwaway postgres:17-alpine container pattern as backup-db.sh.
Requires typing RESTORE to confirm (FORCE=1 skips it for scripted use).
README.md gets a new 'Database backups & disaster recovery' section
(setup step 9) documenting all three scripts, the DIRECT_URL vs
DATABASE_URL reasoning, and the storage-metadata restore caveat. No
admin-panel UI for this by design - restore is SSH-only, deliberately
higher-friction than a browser button.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same gap as run-cron.sh: the crontab's 03:00 backup-db.sh entry pointed
at a script that never actually existed, so there have been no automated
DB backups since that entry was added.
pg_dump via a throwaway postgres:17-alpine container (matches Supabase's
server version; no postgresql-client on the NAS host), against DIRECT_URL
(non-pooled) rather than the pgbouncer-fronted DATABASE_URL, custom
format (-Fc). Writes to /srv/dev-disk-by-uuid-ad295d7f-a870-4f70-9de2-dfded1fabf7f/BACKUPALL/pawfeed/
with 30-day retention, logs to docker/backup.log for visibility (no local
mail delivery configured on this NAS).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both existing crontab entries (trim-feeds, anniversaries) and backup-db.sh
referenced a run-cron.sh that was never actually created/committed - cron
has been executing it daily (04:00/08:00) with exit 127 since at least
today, silently, because no local mail delivery is configured for the
cron user's failure notifications.
Recreates it per README's documented curl pattern (bearer token via
CRON_SECRET, --resolve for the origin TLS cert quirk) and adds logging
to docker/cron.log so future failures are visible without relying on
cron's mail notification.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Catches up several native builds' worth of work that was already tested
live and shipped to the Play Store (per mobile/found.md TODO Neu4-Neu6)
but never made it into a git commit:
- Google SSO via expo-auth-session/expo-web-browser (GoogleSignInButton,
useWarmUpBrowser) on sign-in/sign-up.
- Upload-time image downscaling (resize-image.ts) to fix slow first
content loads — capped at 1600px, re-encoded JPEG @0.8.
- @sentry/react-native (GlitchTip-compatible) crash reporting, plus a
10s auth-loading timeout screen.
- Dark-mode splash screen background, breed/birthday editing in
PetCreateForm, and the OptionPicker Modal rewrite that fixed the
Android nested-scroll dropdown bug across Explore/onboarding/edit.
- expo-dev-client removed from the shared plugin list (was silently
blocking OTA updates in production), app.json bumped to 1.0.6/
versionCode 7, several expo-* packages patched via `expo install --fix`.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three pieces of work from this session:
- Pet-suggestion notifications: a weekly cron (species/breed affinity
heuristic) suggests pets to follow, with an owner opt-out and web/mobile
notification UI wiring.
- Story music overlay: admin-curated royalty-free track catalog
(/p/[secret]/music), attachable to stories on web (StoryForm/StoryViewer),
with mute-original support for video stories. Mobile UI is deferred to
the next native build (needs an audio-playback module) — tracked in
mobile/PENDING-NATIVE-BUILD.md.
- Auth UX fixes: sign-in/sign-up now use KeyboardAvoidingView + ScrollView
and a shared PasswordField with a show/hide toggle (mobile). Web's
ActivePetContext gained clearGactivePetId(), wired to sign-out via a new
SignOutCleanup component, fixing a false "you do not have permission to
access this pet" error after switching accounts on the same browser.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>