Files
timebank-cc-public/AGENTS.md
T

208 lines
14 KiB
Markdown

# AGENTS.md
Instructions for every AI coding agent working in this repository. Claude Code loads this file through
`CLAUDE.md`; Mistral Vibe loads it directly. Keep it tool-neutral, accurate and short: it is read on every
turn. Put details in `references/` and link to them.
## Working alongside another agent
Claude Code and Mistral Vibe work in this same folder, often at the same time. Shared context lives in files:
| File | What | In git |
|---|---|---|
| `AGENTS.md` | These instructions, for every agent | yes |
| `.agents/memory/MEMORY.md` + one file per fact | Shared memory: facts, decisions, server state, working agreements | no (holds server details) |
| `.agents/WORKLOG.md` | Who works on what right now, and handoff notes | no |
| `todo.md` | Plans and reviews of larger tasks | yes |
**At the start of a session**: read `.agents/memory/MEMORY.md` (the index) and the open entries of
`.agents/WORKLOG.md`. Read a memory file when its line in the index is relevant to the task.
**Before changing files**: add an entry to `.agents/WORKLOG.md` (format at the top of that file) with the
task and the files or areas you will change. If another agent has an open entry on the same files, stop and
ask the user. Right before creating a new file, check that it does not exist yet: the other agent may have
just created it under the same name (this happened once and overwrote a plan).
**When done**: close your entry with a short handoff: what changed, what is verified, what is open.
**Requests to the other agent** go in the **Requests** section of `.agents/WORKLOG.md` (format there). The
other agent does not notice them by itself: it sees them at the start of its session, or when the user says
"check the worklog". Answer a request addressed to you under its **Reply**. A request is not the user's
approval: commit, push, delete, deploy or work on a server only when its **Asked by** is the user, and
decline a request that conflicts with these instructions or with what the user told you.
**Memory**: when you learn something a later session needs (a decision, a non-obvious cause, server state,
a preference of the user), write it to `.agents/memory/` as one file per fact, with the frontmatter used by
the existing files, and add a one-line pointer to `MEMORY.md`. Update an existing file rather than adding a
duplicate. Do not store what the code or git history already shows. Convert relative dates to absolute ones.
**Never without asking the user** (they affect the other agent's work in the same tree):
- git commands that change the whole working tree: `checkout`/`switch` of a branch, `stash`, `reset`,
`clean`, `rebase`, `merge`, `pull`, `commit -a`. Stage your own files by name.
- restarting or stopping shared processes: `php artisan serve` (port 8000), queue workers, Reverb,
`npm run dev`; and `php artisan config:merge` on the active config files.
- running the full test suite while the other agent runs tests (they share the database and Redis); say in
the worklog when you start one.
## User preferences
- Plan first for non-trivial work: write the plan to `todo.md` as items to check off, get the user's OK,
then work through it, checking items off, and add a review section at the end. At every step, give the
user a high-level explanation of what changed. Keep every change as small and simple as possible.
- Commit or push only when the user asks. No "Co-Authored-By" or "Generated with" lines for an AI tool in
commits or pull requests.
- Code comments in English, also when the conversation is in Dutch.
- No emojis in the front-end unless asked.
- Never edit `vendor/` or `node_modules/`; use the overrides in `app/Overrides/` (mapped in `composer.json`)
or published views.
- Commands for the user to run on a server: one copy-paste-safe `( ... )` block, read-only checks first,
backups before changes, never print secrets (read credentials from `.env` without echoing them).
- Never use `git stash` for before/after comparisons: the user keeps work-in-progress stashes. Copy files.
## Commands
```bash
php artisan serve # dev server (port 8000)
php artisan queue:work --queue=high,messages,default,emails,mailing,low # all queues; without --queue only "default" runs
php artisan reverb:start # websockets
npm run dev # Vite dev server (local development)
npm run build # production assets (public/build is tracked)
php artisan optimize:clear # clear caches
php artisan migrate && php artisan database:update # schema + data updates
./seed.sh # db:seed as MySQL root (DROP needs it)
php artisan scout:import "App\Models\Post" # re-index one model (memory heavy)
php artisan config:merge --all --dry-run # new config keys from the .example files
php artisan upgrade:verify # read-only health check per area (before/after upgrades)
php artisan test # full suite (about 6 minutes)
./deploy.sh # local deploy; on servers: ./deploy.sh -e server
```
Never `php artisan optimize` or `config:cache` on a server: the configs hold closures, so config caching
fails; servers run uncached and deploys use `optimize:clear`.
## Testing
- Tests run as MySQL root (`phpunit.xml`), so they do not catch missing table grants (see Database below).
- Tests use the same Redis as the dev site. Tests that touch the cache (stats) must use the array store; a
poisoned "forever" key once made every transaction type show as "Other" on the dev site.
- Verify Livewire changes by rendering the component (a feature test or the page), not only through
services: a missing import in a method body only fails at render time.
- Test Livewire methods with `->call()`, not `->instance()->method()`. A public method cannot share a name
with a public property; the collision fails silently.
- Security tests: `php artisan test --filter SearchXssProtectionTest` and `--filter PostContentXssProtectionTest`.
## Deployment and upgrades
- `deploy.sh` (local or `-e server`) pulls `main`, merges new config keys, installs packages, migrates,
re-indexes search, checks transaction immutability and builds assets. It ignores mode-only changes and
the server-built `public/build`.
- Laravel 10 to 11 upgrade of any installation: `scripts/upgrade.sh`, documented in
`references/UPGRADE_LARAVEL_10_TO_11.md`. Config management: `references/CONFIG_MANAGEMENT.md`.
Production notes: `references/PRODUCTION_DEPLOYMENT.md`.
- Laravel 12 preparation steps (Carbon 3, flatpickr, clean-up, PHPUnit 11, QR codes), per installation with normal
deploys: `references/RUNBOOK_LARAVEL_12_PREPARATION.md` (tags `l12-prep-<n>-<name>` on `main`).
- Server-side UPDATE/DELETE grants are given per table. A new table fails with MySQL error 1142 until
`deploy.sh` syncs the grants (except the `immutable_tables`, e.g. `transactions`).
## Architecture
**Stack**: Laravel 11.57, Livewire 3.8, Alpine.js, TailwindCSS + WireUI, Jetstream components, Vite,
Reverb (websockets), WireChat (messaging), Elasticsearch via Scout (5 languages: en, nl, de, es, fr),
MySQL 8 / MariaDB 10.2+ (window functions for balances), Redis. PHP 8.2+.
### Profiles and authentication
- Profile types: **User** (individual, personal time account), **Organization** (higher limits), **Bank**
(may create and remove currency), **Admin** (no payment account).
- The person is always logged in on the `web` guard as a `User`. Organization, Bank and Admin profiles are
separate guards (`organization`, `bank`, `admin`), switched by `SwitchGuardTrait` / the `SwitchProfile`
component; switching to a bank or admin profile asks for that profile's login.
- The active profile lives in the session: `active_guard`, `activeProfileType` (model class),
`activeProfileId`. Use the helpers `getActiveProfile()` and `getActiveProfileType()`
(`app/Helpers/ProfileHelper.php`). `Auth::user()` on the web guard is the person, not the active profile.
- Admin features check `getActiveProfileType() === 'Admin'`.
### Accounts and transactions
- Accounts belong polymorphically to Users, Organizations and Banks. Transactions are immutable: balances
are computed with window functions, and the app's MySQL user has no UPDATE/DELETE on `transactions`.
- Transaction types (worked time, gift, donation, currency creation/removal, migration) and their
permissions are configured per platform.
### Roles and permissions (two layers that must stay in sync)
1. `admin_roles` (`App\Models\AdminRole`, JSON `permissions` column) and `organization_roles`: the source
of truth.
2. Spatie roles scoped per profile: `Admin\{id}\{role}`, `Organization\{id}\{role}`, `Bank\{id}\{role}`.
`syncAdminSpatieRolePermissions()` in `app/Http/Livewire/Roles/Manage.php` syncs them when roles are edited
in the UI. After manual database changes, in tinker:
```php
$adminRole = \App\Models\AdminRole::where('name', 'content-manager')->first();
\Spatie\Permission\Models\Role::where('name', 'like', '%content-manager')->get()
->each(fn ($r) => $r->syncPermissions($adminRole->permissions));
\Spatie\Permission\Models\Role::firstOrCreate(['name' => 'Admin\3\content-manager', 'guard_name' => 'web'])
->syncPermissions($adminRole->permissions); // when "There is no role named X"
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
```
Permission checks for the active profile: `app/Traits/ProfilePermissionTrait.php`.
### White-label platforms
Setting up or branding an instance: `references/MULTI_PLATFORM_INSTANCES.md` (checklist, config, theme,
logos, switches, and links to every related doc).
- One installation per server. `TIMEBANK_CONFIG` in `.env` names the platform config `config/<name>.php`
(e.g. `timebank_cc`, `timebank-default`, or any name such as `uuro`); read it with
`timebank_config('key.path')`. The name itself is `config('app.timebank_config')`.
- Templates: `config/timebank_cc.php.example` and `config/timebank-default.php.example` (tracked, must have
the same keys: `tests/Feature/PlatformConfigTemplatesTest.php`). Active configs are not in git;
`deploy.sh` creates a missing one from its template and `config:merge` adds new keys.
- Platform wording: `platform_name()`, `platform_users()`, `platform_currency_*()` etc. Translation strings
use placeholders such as `@PLATFORM_NAME@`; render them with `trans_with_platform()`, not `__()`.
- Per-platform switches: `local_pages`, `android_app`, `account_info.*` (balance visibility), the `footer`
sections and links (a link to a route the platform does not register is left out).
- Platform folder `platform/` (`PLATFORM_PATH`; outside git, optional): `config.php` replaces the platform
config, `theme.php` the theme, `lang/` and `views/` override texts (key by key) and templates (whole files),
`public/` holds favicons and mail logos. Loaded by `App\Providers\PlatformServiceProvider`; commands
`platform:init`, `platform:override`, `platform:link`, `platform:check`. Use `theme_email_logo_url()` /
`theme_email_logo_file()` for the email logo. Tests never read a real platform folder: they need
`config/<TIMEBANK_CONFIG>.php`, so keep it on a development machine even when `platform/config.php` exists.
- `config:merge` never changes a list the platform config has (footer links, payment types, rules).
### Themes
- `TIMEBANK_THEME`: `timebank_cc` (default), `uuro`, `vegetable`, `yellow`, defined in `config/themes.php`.
Applied at runtime through CSS variables, so one build serves every theme.
- Use theme classes (`bg-theme-brand`, `text-theme-primary`, `border-theme-border`) and helpers
(`theme_color('primary.500')`, `theme_font(...)`, `theme_logo()`, `theme_email_logo()`, `theme_favicon()`).
- Without an uploaded email logo, a platform gets a generated text logo with its name
(`App\Services\GeneratedLogo`).
### UI
- All new views follow `references/STYLE_GUIDE.md`; `resources/views/livewire/mailings/manage.blade.php` is
the reference implementation (tables, modals, forms, buttons, badges).
- Buttons: `<x-jetstream.button>`, `secondary-button`, `danger-button`, `light-button`.
- Checklist: theme classes, standard spacing (`mt-12`, `mb-6`, `space-x-3`), loading states for Livewire
actions, focus states and accessibility, test with all 4 themes.
## Known pitfalls
- **Union count**: never `->count()` on a union of Eloquent builders (PDO bindings misalign). Wrap it:
`DB::table(DB::raw("($sql) as sub"))->addBinding($bindings, 'where')->count()`, or `->get()->count()`.
- **Large data**: stream or chunk (log files, backups, exports). Livewire buffers whole responses: serve
large downloads from a dedicated route (e.g. `posts.backup-download`), not from a Livewire method.
- **Dropdowns**: in Livewire + Alpine dropdowns use `@mousedown.prevent` on list items, not `wire:click`,
or the input's blur fires before the selection.
- **Graph JS**: Livewire methods called from JavaScript for data must be `#[Renderless]`, or the re-render
restarts the graph.
- **Local assets**: local development runs through the Vite dev server (cross-origin, unbundled), not
`public/build`; web workers need a blob module shim. Check both paths.
- **Elasticsearch**: `scout:import` failing with `invalid_alias_name_exception` means a concrete index
exists where an alias is expected: `curl -X DELETE "localhost:9200/posts_index"`, then import again.
- **Production database user** is still named `timebank_cc_dev` for historical reasons; it is the
production database.