# Pins — agent operating file

> Loaded automatically every session, so **everything here is believed**. Keep it short, concrete
> and *true*. See `PLAYBOOK.md` §0.3 for the rules on maintaining this file.
> **Never put secrets here.**

## Rules

- Follow **`PLAYBOOK.md`** (vendored copy at the repo root) without being asked.
- Product decisions defer to **`VALUES.md`** — flag anything that conflicts with it, including the
  owner's own ideas.
- Ship with the release checklist (`PLAYBOOK.md` §15).
- **Is the playbook stale?** At the start of substantial work and before a release, compare the
  version in `PLAYBOOK.md`'s header to the canonical one, and **offer** to sync if it's behind
  (`PLAYBOOK.md` §16.4). Never sync silently or mid-release.
  ```sh
  gh api repos/Maaggel/Playbook/contents/VERSION -H "Accept: application/vnd.github.raw"
  ```

## What this is

A personal keep-anything tool — text, links and images, captured in seconds, tagged, searchable.
See `README.md`; product intent lives in `VALUES.md`; **how it fits together and why is in
`ARCHITECTURE.md` — read it before touching sync, the data model, or the share target.**

## Status

Kickoff interview done 2026-07-15/16 (`VALUES.md`, `README.md`); design in `ARCHITECTURE.md`.
**Frontend and backend scaffolded; no product code in either.** No migrations written yet — the
schema is designed in `ARCHITECTURE.md` but there's no local database to verify it against, and it
hasn't been reviewed by the owner. Both are prerequisites, in that order.

## Layout

```
app/        React + TS + Vite, wrapped by Capacitor → web (PWA) and Android
app/android/  the native Android project. COMMITTED — the share-target intent-filter lives here.
server/     Laravel + MySQL              (not scaffolded yet)
wear/       Kotlin + Compose companion   (later)
scripts/    one command each, no tribal knowledge
```

## Commands

Copy-pasteable. Add each one in the same commit that makes it real.

```sh
cd app && npm run dev            # web dev server
cd app && npm run build          # type-check + build → app/dist
cd app && npx cap sync android   # push web build + plugins into the native project

# Android APK. JAVA_HOME is NOT optional — see Gotchas.
cd app/android && JAVA_HOME="D:/Programs/Android Studio/jbr" ./gradlew assembleDebug
#   → app/build/outputs/apk/debug/app-debug.apk

cd server && php artisan --version    # Laravel 13.20. Anything touching the DB fails — see Gotchas.

# Deploy: git-based, through a restricted SSH gate (one whitelisted command per connection).
ssh pins-deploy commands               # self-documenting allow-list — read this, don't guess
ssh pins-deploy 'git pull --ff-only'
ssh pins-deploy 'composer install --no-dev --optimize-autoloader'
ssh pins-deploy 'php artisan migrate --force'
ssh pins-deploy 'php artisan optimize'   # rebuild config/route caches after deploy

# Playbook freshness (PLAYBOOK.md §16.4)
gh api repos/Maaggel/Playbook/contents/VERSION -H "Accept: application/vnd.github.raw"
```

Not yet real, deliberately absent: test, lint, e2e, ship, deploy. Add them when they exist.

## Environment

- **Node 25.9**, npm 11.12 · **Capacitor 8.4.2** · Vite + React 19 + TS.
- **JDK 21 required** (Capacitor 8). The `java` on PATH is **JDK 17** — Android Studio's bundled JBR
  at `D:/Programs/Android Studio/jbr` is 21.0.5 and is what works. See Gotchas.
- Android SDK: `ANDROID_HOME=D:\Programs\android_studio_sdk` (already set).
- **PHP 8.3.1 comes from MAMP** (`C:\MAMP\bin\php\php8.3.1`), not a standalone install — that's where
  its `php.ini` and extensions live. Composer 2.5.8. **The Pi also runs PHP 8.3** (confirmed by the
  owner, 2026-07-16).
- **Laravel 13.20**, which requires PHP `^8.3` — the Pi is *exactly* at the floor, with no headroom.
  A future Laravel major needing 8.4 would mean upgrading the Pi first, not just bumping a version.
- **No working local database.** `pdo_sqlite` isn't enabled and MySQL isn't running — see Gotchas.
- Required env vars: *(none yet — Laravel `.env` arrives with the backend; mirror every key into a
  committed `.env.example` with no values.)*

## Stack

Decided at kickoff (2026-07-16), with reasons — see `ARCHITECTURE.md` when it exists.

- **Web + Android: one React + TypeScript codebase, wrapped in Capacitor.** Chosen over native
  Kotlin because the owner is fluent in React and this is evening work: one maintainable codebase
  beats two, and that serves longevity (`VALUES.md` #1) more than native feel does.
- **Ionic's UI kit is *not* in use** — Capacitor (the native bridge) only.
- **Windows: the web app installed as a PWA.** No Electron. Revisit only for a real need (global
  capture hotkey, filesystem access), never by default.
- **Backend: Laravel + MySQL on Apache**, self-hosted on the owner's Raspberry Pi.
- **Wear OS companion (later, voice notes): native Kotlin + Compose** — Capacitor cannot target Wear
  OS, so this is the one place a second codebase is accepted. Keep it small: capture and hand off,
  nothing else. It talks **directly to the server over HTTP** with its own auth token, and queues
  locally when offline — deliberately *not* via the Wear Data Layer API, which would make the watch
  depend on the phone app being awake.

## Deploy

Git-based, through a **restricted SSH gate** — `bmx-pins@admin.blommemix.dk`, aliased `pins-deploy`
in `~/.ssh/config`. One whitelisted command per connection, no shell, no chaining. The allow-list is
self-documenting (`ssh pins-deploy commands`) and destructive verbs (`migrate:fresh`, `db:wipe`,
`tinker`, rollbacks) are hard-blocked. So the deploy flow is: push to the git remote → `git pull
--ff-only` on the server → `composer install` → `migrate --force` → `optimize`.

**Not yet wired, prerequisites (owner does the one-time parts — they're not in the allow-list):**
1. The repo needs a **git remote** the server can pull from (none set yet — repo is local-only).
2. The server's `public_html` needs an **initial `git clone`** (only `git pull` is whitelisted).
3. A **`server/.env`** must be created on the server by hand (it's gitignored, so it never arrives
   via pull) — DB creds + `APP_KEY`.
4. **Web-root exposure must be checked** — see Gotchas. This is a leak risk, not a nicety.

## Hosts & targets

- **Backend / DB:** self-hosted Raspberry Pi, Apache + MySQL, booting from USB SSD. Full root
  access. Migrates to off-site hosting if Pins ever goes to an app store.
- **Web:** `pins.blommemix.dk` (provisional name).
- **Android:** sideloaded APK by hand for v1. Later: self-hosted APK server + in-app update checks
  (`PLAYBOOK.md` §9.1 is the worked example — follow it).
- **Backup target:** owner's NAS (nightly cron). Offsite: parents' NAS (planned) or Gigahost.dk
  (interim). **Not yet built** — see Gotchas.
- **Environments:** **prod only** for now. Recorded deviation, see below.
- **Primary device:** OnePlus 13 (Android 15). No legacy Android constraint — take Capacitor's
  default `minSdk` rather than inventing one.

## Secrets

**Names only — never values.** These must never be committed:

- `.env` — Laravel app key, DB credentials. Lives on the Pi only. A committed `.env.example` holds
  the keys with no values.
- **Android release keystore** — never in the repo. Password manager + NAS backup. **Losing it means
  the app can never be updated again**, on any channel. It won't exist until the first release
  build; flag it loudly at that moment.
- Wear OS device token, and any future AI API key — **server-side only, never in a client.** An API
  key shipped in an APK is a leaked key.

## Recorded deviations

Per `PLAYBOOK.md` §16.2 — deliberate, with reasons. Not drift.

- **Prod only, no dev environment** (§10.5). Owner's call, 2026-07-16: start with what becomes the
  prod server, add dev once it's live. Accepted because single-user — **conditional on backups
  working**, since every migration runs live against the only copy of the data. If it starts to
  hurt, a second DB on the same Pi is a cheap dev environment.
- **Backups will ship untested** (§10.1 wants a proven restore). Owner's call, 2026-07-16: get data
  flowing offsite now, test the restore after returning home (~2026-07-30). Accepted — an untested
  backup beats no backup, and the alternative was waiting two weeks with nothing. **This does not
  make the data protected.** The real hazard is the false confidence: until one restore has actually
  run, treat the Pi as unprotected and keep destructive operations off it. Don't re-litigate this
  (it's been raised twice and decided) — but do ask whether the restore has happened yet.

## How the owner likes to work

Preferences worth knowing *before* the first suggestion — the next session has no memory of being
told. Add one whenever you learn it (`PLAYBOOK.md` §16.6).

- **Delegates technical calls freely** ("this is your call", "it's up to you") — that is an
  invitation to *decide and justify*, not to survey options. Make the call, give the reason, make it
  easy to overrule.
- **Engages well with pushback and changes his mind on a good argument.** Disagree in plain words
  when you think something's wrong; it lands as intended.
- **Danish; writes English.** Based in Denmark, self-hosts his own infrastructure, comfortable at
  the systems level.
- **The conversation is part of the point, not overhead.** He builds these projects for enjoyment
  and likes talking the design through. Don't rush to code to seem productive — thinking out loud
  with him is doing the work, and a long design conversation is a good outcome, not a delay.
- **Never comment on his time, pace, or holiday. No "don't stress about this", no "that'll keep till
  you're home", no "go to sleep".** He thinks about these projects while tucking his kids in and
  sitting by the pool, *because he wants to* — and if he doesn't want to, he puts the phone down. He
  does not need help managing that, and being told otherwise is patronising.
  **This rule was already written here and then broken three times in the same session** — the polite
  version ("offer the work, let him pick the moment") didn't fire, because the impulse doesn't feel
  like scheduling him, it feels like being considerate. It isn't. Answer the question; skip the
  caveat about when he should do it. If a thing genuinely blocks on being home (physical hardware),
  say only that.
- **He runs several projects with sibling sessions** — Iacta (live for friends and family), Memory
  Lane (pre-alpha), Blommemix Admin (infrastructure), and this. They share the one Raspberry Pi, so
  a change to that box is never only about Pins.

## Conventions

Only what isn't obvious from reading the code.

- **Tags, not folders.** A pin has zero or more tags. No hierarchy, no single "home", no folders.
  Tagging is optional at capture, permanently (`VALUES.md`). Don't reintroduce folders without
  revisiting this — the decision is about capture friction, not taste.
- **Local-first.** Every client writes locally and confirms to the user *immediately*, then syncs in
  the background. **Capture must never block on the server** — that's the tiebreak between the two
  top values and it applies everywhere the two meet.
- **Localized from the first component** (`PLAYBOOK.md` §7). English source strings, Danish
  translation. No hardcoded user-facing text, ever — retrofitting i18n is a slog.

## Gotchas

The traps that cost an hour. Add one whenever you learn it — learning it twice is a bug.

- **Assume the Pi's data is unprotected until told otherwise.** No confirmed backup covers Pins as of
  2026-07-16. No destructive migrations; don't put anything on the Pi that would hurt to lose. An
  untested backup is a rumour (`PLAYBOOK.md` §10.1). **Ask; don't assume it landed.**
- **Backups are being built in the Blommemix Admin project** (a sibling session), not here — and much
  of it already existed: cron scheduling, gzip/tar, retention, SFTP/SMB upload, backup history. What
  was missing is file/folder selection, multiple destinations, plain FTP and keep-X-days.
  **The Pins-specific catch: the existing backup is DATABASE-ONLY.** Pins keeps image blobs on the
  filesystem by design (`ARCHITECTURE.md`), so a DB-only backup restores pins that point at photos
  which no longer exist — a backup that looks complete and isn't. **Pins is not backed up until file
  backup covers `storage/app/blobs/`.** Check this before trusting any "backups are done" report.
- **`scripts/sync-playbook` is POSIX `sh`.** The owner is on Windows — run it from Git Bash, not
  PowerShell.
- **Gradle needs JDK 21; the `java` on PATH is 17.** Verified 2026-07-16: without `JAVA_HOME` pointed
  at `D:/Programs/Android Studio/jbr` (21.0.5), the build dies with `invalid source release: 21` —
  an error that names the version but not the cause, and sends you hunting through `build.gradle`.
  Capacitor 8 requires 21. Android Studio handles this silently; the command line does not.
- **The Pi's PHP version is the ceiling** (`PLAYBOOK.md` §4.2) — it's 8.3, matching local, which is
  exactly Laravel 13's minimum. Syntax-lint before every deploy; don't reach for 8.4 syntax.
- **`php artisan migrate` and anything else touching a database fails locally.** MAMP's PHP ships
  `php_pdo_sqlite.dll` but doesn't enable it, and MAMP's MySQL isn't running (nothing on :3306). So
  `could not find driver` is a *local environment* message, not a broken app. Fix by starting MAMP's
  MySQL and pointing `.env` at it. **Prefer MySQL over enabling sqlite:** prod is MySQL, and the
  schema leans on `BINARY(16)`, `ENUM`, `DATETIME(3)` and `SELECT ... FOR UPDATE` — the last one
  being the sync design's sharpest edge (`ARCHITECTURE.md`). SQLite would let those migrations pass
  while proving nothing about the things that can actually break.
- **The Android `.gitignore` template ships with the keystore lines commented out**, i.e. it will
  happily commit your signing key. Uncommented in `app/android/.gitignore` — but **`npx cap add
  android` regenerates that file and silently reverts the fix** (confirmed 2026-07-17, twelve hours
  after writing this warning). The **root `.gitignore` is the real protection** and survives
  regeneration; the native one is defence in depth. Re-check after any `cap add`.
- **`appId` is permanent** — `com.inspireme.pins`. Once published it is the app's identity forever
  and cannot be changed. Changing it means regenerating `app/android/` (Capacitor bakes it into
  Gradle and the Java package), which also resets the `.gitignore` above.
- **Web root leak risk — CONFIRMED LIVE, must be fixed before any deploy (§6).** Verified 2026-07-16:
  the `pins.blommemix.dk` docroot has **Apache directory listing (autoindex) ON** and points at an
  empty folder (`Index of /`). Empty today, so nothing leaks yet — but a monorepo git-checkout landing
  there turns `https://pins.blommemix.dk/` into a browsable index of `app/`, `server/`, `.git/` and
  `.env` (DB creds + `APP_KEY`). Two fixes, both owner-side (root/Apache config): **(1) docroot must
  point at Laravel's `server/public/`, not the repo root; (2) autoindex OFF.** After the first deploy,
  verify: `/.env`, `/.git/config`, `/server/.env` must all return 403/404, and `/` must not list files.
- **Reaching the server from this dev box:** the domain resolves to the **public** IP (86.52.116.116)
  and times out on the LAN (hairpin NAT; pihole loopback not yet propagated). Use the **LAN IP
  `192.168.0.51`** with a Host header, or `curl --resolve pins.blommemix.dk:443:192.168.0.51` to get
  correct SNI/cert. TLS is enforced (HTTP 301→HTTPS). The SSH gate is a different host,
  `admin.blommemix.dk`, and reachable directly.
- **The Android share target needs native glue** — an `intent-filter` in `AndroidManifest.xml` plus a
  Capacitor plugin to receive the intent. It's the feature the entire habit depends on, so stand it
  up *early*; discovering it's awkward in week six is the bad outcome.
- **Verify on the real OnePlus, not the browser** (`PLAYBOOK.md` §8.2). Share sheets, permissions and
  notifications routinely no-op in a browser or emulator while looking fine.
