docs(13): map phase patterns
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
# Phase 13: Płytarium API (wishlist, notifications, CSV, credentials, public) - Pattern Map
|
||||
|
||||
**Mapped:** 2026-10-02
|
||||
**Files analyzed:** 18 (groups)
|
||||
**Analogs found:** 16 / 18
|
||||
|
||||
Paths: `APP` = `/media/nvme/dev/golem15/summercms.io/summercms/fonoteka.go`, `FW` = `/media/nvme/dev/golem15/summercms.io/summercms/summercms.go`, `PLG` = `APP/plugins/golem15/fonoteka`. Every analog listed here is tracked source.
|
||||
|
||||
## File Classification
|
||||
|
||||
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|
||||
|---|---|---|---|---|
|
||||
| `PLG/controllers/api/notifications_controller.go` (new) | controller | CRUD | `PLG/controllers/api/invitations_controller.go` | exact |
|
||||
| `PLG/controllers/api/credentials_controller.go` (new; Winter 500 pages) | controller | CRUD | `invitations_controller.go` + `http_errors.go` | exact |
|
||||
| `PLG/controllers/api/onboarding_controller.go`, `invitation_inspect_controller.go` (new) | controller | request-response | `invitations_controller.go` (`InvitationAccept`) | exact |
|
||||
| `PLG/controllers/api/wishlist_*_controller.go` (new, split by share/settings/household/items/subscriptions/reservations) | controller | CRUD | `collection_share_controller.go`, `albums_controller.go` | exact |
|
||||
| `PLG/controllers/api/csv_import_controller.go`, `csv_export_controller.go` (new) | controller | file-I/O + batch | `collection_media_controller.go` (multipart upload) + `albums_controller.go` | role-match |
|
||||
| `PLG/controllers/api/public_share_controller.go` (new) | controller | request-response (anonymous) | `collection_share_controller.go` + `album_search_controller.go` | role-match |
|
||||
| `PLG/classes/notification_service.go` (modify: purchase/reveal/digest, wishlist branch of `albumAddedCallback`) | service | event-driven | itself, lines 90-340 | exact |
|
||||
| `PLG/classes/share_service.go` (add `ResolvePublic`) | service | CRUD | itself | exact |
|
||||
| `PLG/classes/album_search.go` (public mode, `TEXT_FIELDS_PUBLIC`) | service | transform | itself lines 22-30, 317-320 | exact |
|
||||
| `PLG/classes/credential_write_service.go` (expand) | service | CRUD | `classes/collection_write_service.go` | role-match |
|
||||
| `PLG/classes/wishlist_*.go`, `csv_*` parser package (new) | service/utility | transform | `classes/tracklist_text_parser.go`, `collection_write_service.go` | role-match |
|
||||
| `PLG/jobs.go` + `PLG/mail.go` (purchase mail job + templates; digest/csv Dispatch with no worker) | job/config | event-driven | `PLG/jobs.go` invitation mail | exact |
|
||||
| `PLG/routes.go` (fill onboarding/public_invitation/public groups; D-14 per-route buckets) | route | config | `PLG/routes.go` lines 150-200 | exact |
|
||||
| `PLG/plugin.go` (register-event listener D-10) | provider | event-driven | `PLG/plugin.go` lines 72-86 (`GetApiArrayEvent` listener) | exact |
|
||||
| `PLG/middleware/public_share_headers.go` (`no-store, private`) | middleware | request-response | itself | exact |
|
||||
| `APP/plugins/golem15/user/classes/events.go` + register path (additive exports D-09/D-11) | model/event | event-driven | itself (`RegisterEvent`, line 19) | exact |
|
||||
| `PLG/routes_table_phase13_test.go`, extend `write_endpoints_fuzz_test.go` | test | - | `routes_table_phase12_test.go`, `write_endpoints_fuzz_test.go` | exact |
|
||||
| `FW/scripts/check-phase13.sh` | config/gate | batch | `FW/scripts/check-phase12.sh` | exact |
|
||||
| FW `surf` overlap dispatch, `conga` unregistered-kind insert, `lagoon` `prohibited`, tide masks | framework | - | (search in module; no single analog mapped) | none |
|
||||
| `APP/parity/manifest.yaml`, `capture-rules.yaml`, `php_parity.sh` | config | - | existing entries (capture-rules.yaml:108-121 collection share) | exact |
|
||||
|
||||
Note: `check-phase12.sh` lives in `FW/scripts/`, not `APP/scripts/`. `check-phase13.sh` goes there too.
|
||||
|
||||
## Pattern Assignments
|
||||
|
||||
### Controllers (all new `controllers/api/*.go`)
|
||||
|
||||
**Analog:** `PLG/controllers/api/collection_share_controller.go`
|
||||
|
||||
Imports (lines 1-12):
|
||||
```go
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"git.golem15.com/golem15/fonoteka/plugins/golem15/fonoteka/classes"
|
||||
"git.golem15.com/golem15/fonoteka/plugins/golem15/fonoteka/models"
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/lagoon"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
```
|
||||
|
||||
Rule table + handler factory (lines 14-68):
|
||||
```go
|
||||
var collectionShareRules = []lagoon.RequestRule{
|
||||
{Field: "enabled", Rules: lagoon.ParseRules("sometimes|boolean")},
|
||||
{Field: "name", Rules: lagoon.ParseRules("sometimes|required|string|min:1|max:255")},
|
||||
}
|
||||
|
||||
func CollectionShareUpdate(app *backpack.App) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
gdb, c, ok := ownedShareCollection(w, r, app)
|
||||
if !ok { return }
|
||||
input, ok := readInput(w, r)
|
||||
if !ok { return }
|
||||
if !validateInput(w, r, app, gdb, input, collectionShareRules) { return }
|
||||
...
|
||||
if err := classes.RenameShare(ctx, gdb, c, name); err != nil {
|
||||
writeOpaque500(w)
|
||||
return
|
||||
}
|
||||
```
|
||||
Rules: one exported `XxxYyy(app *backpack.App) http.HandlerFunc` per route; only validated keys read from `input` (forged owner/token fields never reach writers); PHP bool via `phpBoolCast` / `laravelBoolean` (`request.go:149`).
|
||||
|
||||
Auth/scope (`request.go:172-190`) — use for every JWT+token route:
|
||||
```go
|
||||
gdb, user, token, ok := requestScope(w, r, app) // token nil on JWT group
|
||||
```
|
||||
Owner checks: `ownerCollection(...)` (`invitations_controller.go:132`), resolve errors via `writeResolveError` (`:153`).
|
||||
|
||||
Error helpers (`http_errors.go`): `writeWinterHTTPError(w, app, status)` (line 48, Winter HTML page, `Cache-Control: no-cache, private`; credentials 500 pages use this), `writeValidationFailed(w, errs, order)` (line 78, `{"error":"Validation failed","errors":{...}}` in Laravel order), `writeOpaque500(w)`. Add new Winter pages as `winter_<status>.html` next to the existing ones (loaded in `init`, line 26).
|
||||
|
||||
### Uploads / CSV / private bucket
|
||||
Copy multipart handling from `controllers/api/collection_media_controller.go` and `uploadBucket(app)` (`request.go:192-201`); add a parallel private-bucket lookup rather than reusing the public bucket.
|
||||
|
||||
### `classes/notification_service.go` (modify)
|
||||
Reuse `WriteNotification(ctx, tx, svc, userID, typ, payload)` (line 90) and the constants `NotificationTypeWishlistItemAdded/Purchased` (lines 26-27). Wishlist branch sits in `albumAddedCallback` (lines 291-331), which today returns early for wishlists; it runs in-transaction and fails the insert via `db.AddError(err)`. Registration pattern (lines 334-339):
|
||||
```go
|
||||
func init() {
|
||||
RegisterHook(func(gdb *gorm.DB) error {
|
||||
return gdb.Callback().Create().After("gorm:after_create").Before("gorm:commit_or_rollback_transaction").
|
||||
Register("fonoteka:album_added_notification", albumAddedCallback)
|
||||
})
|
||||
}
|
||||
```
|
||||
If the digest River insert must happen after commit (D-08), do it in an explicit write-service call or an after-commit hook, not inside this callback.
|
||||
|
||||
### `jobs.go` / `mail.go` (purchase mail + workerless Dispatch)
|
||||
Analog `PLG/jobs.go` lines 28-58:
|
||||
```go
|
||||
func (p *Plugin) Jobs() []pact.Job {
|
||||
return []pact.Job{
|
||||
conga.Job(p.sendInvitationMail,
|
||||
conga.OnQueue(classes.InvitationMailQueue),
|
||||
conga.MaxAttempts(classes.InvitationMailAttempts)),
|
||||
}
|
||||
}
|
||||
func (p *Plugin) sendInvitationMail(ctx context.Context, args InvitationMailArgs) error {
|
||||
gdb, ok := p.app.Lookup[*gorm.DB]() ...
|
||||
mailer, ok := p.app.Lookup[postcard.Mailer]() ...
|
||||
return deliverInvitationMail(ctx, gdb, mailer, appURL, log, args)
|
||||
}
|
||||
```
|
||||
Delivery fn pattern (lines 67-121): skip with `log.Info` + `return nil` when row is gone; locale picks template (`-en` suffix); `mailer.Send(ctx, postcard.Message{Template, To, Vars})`; never log args/tokens. Template constants + `MailTemplates()` list in `mail.go` lines 16-29 — add `wishlist_item_purchased` / `-en` (vars `albumName`, `wishlistName`; layout `plytarium`). Digest and CSV jobs: only `Dispatch` on queues `fonoteka.wishlist.digest`, `fonoteka.csv.import`, `fonoteka.csv.match`; do not register workers or touch `config/queue.yaml`.
|
||||
|
||||
### `routes.go`
|
||||
Handler values built once at the top of `Routes` (lines 13-36) and shared between JWT group (line 37, `surf.Use("jwt.auth","locale.from-principal","inv.must-change-password")`) and token group (line 150, `surf.Use("inv_token","throttle:fonoteka-api-token")`). Token routes carry exactly one `"inv.scope:read|write"`; each `{id}` followed by `g.Where("id","[0-9]+")`. Inline throttles as trailing middleware string (`"throttle:10,1"`). Empty groups to fill: lines ~196-202 (onboarding, public_invitation, public). D-14: drop group-level `throttle:fonoteka-public-token`/`-ip`; put them on albums index/show routes only, `throttle:10,1` on the `resolve` routes. Bucket definitions: `plugin.go` lines 307-320.
|
||||
|
||||
### `plugin.go` register listener (D-10)
|
||||
Copy the event listener shape (lines 74-86):
|
||||
```go
|
||||
app.Events.Listen[*userclasses.GetApiArrayEvent]("golem15.fonoteka", func(_ context.Context, e *userclasses.GetApiArrayEvent) error { ... })
|
||||
```
|
||||
with `*userclasses.RegisterEvent` (`user/classes/events.go:19-22`, currently `{User *models.User}` with no listener). Additive user-plugin fields (e.g. invitation token) go on that struct; commit in the submodule via `ssu` first, then pointer bump.
|
||||
|
||||
### `middleware/public_share_headers.go`
|
||||
Change line `dst.Set("Cache-Control", "private, no-store")` to the PHP value per research; keep the 429 rewrite.
|
||||
|
||||
## Shared Patterns
|
||||
|
||||
### Route-table test
|
||||
**Source:** `PLG/routes_table_phase12_test.go` lines 1-80: `phase12Route{method, path, group, scope, throttle, line}` with routes.php line numbers, `jwtPrefix`/`tokenPrefix` constants (reuse, do not redeclare in the same package). New `phase13Route`/`phase13Routes` table + `TestRouteTablePhase13`, also asserting the 4 Phase 14 routes are absent.
|
||||
|
||||
### Write-endpoint fuzz
|
||||
**Source:** `PLG/write_endpoints_fuzz_test.go`: `writeSpec` (lines 196-205: `path`, `base`, `carol`, `upload`, `change`, `when`, `create`, `remove`), table-name constants (207-225; `tNotes` exists, add wishlist/csv/credential tables), `seedFuzzWorld` (101-179), `fuzzWriteSpecs` map keyed `"METHOD /path"` (line 250). Add one entry per Phase 13 write route.
|
||||
|
||||
### Phase gate
|
||||
**Source:** `FW/scripts/check-phase12.sh`: env-overridable `ROOT/APP/PHASE_DIR` (lines 19-21), `EXPECTED_PORTED=99` → 157, `COVERAGE_FLOOR=80`, modes `--self-test --go --parity --named --removal --coverage --evidence --all`, `phase12_detect` go-test-json detector, `PHASE12_REQUIRE` named tests (line 253). Rename prefixes to `PHASE13_`, require new flows (`nuxt-wishlist`, `nuxt-csv`, `public-anonymous`, `onboarding`).
|
||||
|
||||
### Parity recording
|
||||
`APP/parity/capture-rules.yaml` lines 108-121 (collection share token capture) → copy for `share:wishlist` on `PUT/POST wishlist/share*`. `php_parity.sh:41` `export QUEUE_CONNECTION=sync` → `"${QUEUE_CONNECTION:-sync}"`.
|
||||
|
||||
## No Analog Found
|
||||
|
||||
| File | Role | Reason |
|
||||
|---|---|---|
|
||||
| FW `surf` constraint-aware overlap dispatch | framework router | New capability; read `FW/modules/surf` router internals directly |
|
||||
| `conga` insert of unregistered job kind | framework queue | No existing workerless-dispatch path; follow RESEARCH.md |
|
||||
| CSV `fputcsv` port / parser truth tables | utility | No CSV code yet; use RESEARCH.md PHP truth tables (closest style: `classes/tracklist_text_parser.go`) |
|
||||
|
||||
## Metadata
|
||||
**Analog search scope:** `PLG/{controllers/api,classes,middleware,models}`, `PLG/*.go`, `APP/plugins/golem15/user/classes`, `FW/scripts`
|
||||
**Pattern extraction date:** 2026-10-02
|
||||
Reference in New Issue
Block a user