Plan checker iteration 1 flagged unresolved research questions and a missing decision note for golang.org/x/net/html. D-18 approves x/net/html for the partial sanitizer; D-19 fixes the Discogs widget fill to [year, format]. STATE marks the phase ready to execute.
265 lines
34 KiB
Markdown
265 lines
34 KiB
Markdown
---
|
|
phase: 10.1-runtime-admin-extension-point
|
|
plan: 03
|
|
type: execute
|
|
wave: 3
|
|
depends_on: [10.1-01, 10.1-02]
|
|
files_modified:
|
|
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/albums_admin_controller.go
|
|
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/albums/_stats.htm
|
|
- ../fonoteka.go/plugins/golem15/fonoteka/controllers/albums/config_list.yaml
|
|
- ../fonoteka.go/plugins/golem15/fonoteka/models/album/fields.yaml
|
|
- ../fonoteka.go/plugins/golem15/fonoteka/assets/js/discogs-lookup.js
|
|
- ../fonoteka.go/plugins/golem15/fonoteka/assets/css/albums.css
|
|
- ../fonoteka.go/plugins/golem15/fonoteka/admin.go
|
|
- ../fonoteka.go/plugins/golem15/fonoteka/lang/en/lang.yaml
|
|
- ../fonoteka.go/plugins/golem15/fonoteka/lang/pl/lang.yaml
|
|
- ../fonoteka.go/plugins/golem15/fonoteka/admin_albums_test.go
|
|
- ../fonoteka.go/plugins/golem15/fonoteka/admin_phase10_controllers_test.go
|
|
- ../fonoteka.go/plugins/golem15/fonoteka/admin_phase10_copy_test.go
|
|
- ../fonoteka.go/plugins/golem15/fonoteka/admin_phase101_smoke_test.go
|
|
autonomous: true
|
|
requirements: [ADMIN-07]
|
|
estimate:
|
|
tokens: 80000
|
|
raw_tokens: 80000
|
|
tasks: 3
|
|
confidence: low
|
|
must_haves:
|
|
truths:
|
|
- "Per D-01, D-03 and D-11, the Albums list declares `headerPartial: stats` in config_list.yaml and GET /plytadmin/api/v1/golem15/fonoteka/albums/partials/stats returns a statistics strip for the admin's own collection: the total, one item per stored format with count above zero in getFormatOptions order, and a no-shelf item only when that count is above zero."
|
|
- "Per D-10, every stats count runs through albumsAdminController.scopeAlbums, and the view model is a curated struct of labels and integers, never an Album model; an admin bound to one collection never sees another collection's counts."
|
|
- "Per D-01, D-02, D-04, D-06, D-07 and D-19, the Albums form declares a `year` number field after `shelf` and a last, full-width `discogs` widget (widget golem15-fonoteka-discogs-lookup, action discogsLookup, fill [year, format]) that renders on create and update; its stub action answers the message key golem15.fonoteka::lang.discogs.stub_filled with fill {year: 1977, format: \"LP\"} and makes no outbound call; Phase 14 replaces the stub."
|
|
- "Per D-12, the Albums toolbar declares buttons [create, delete, discogsSync]; the discogsSync stub answers golem15.fonoteka::lang.discogs.stub_not_implemented and changes nothing; both Discogs actions require the existing Winter permission golem15.fonoteka.access_albums, so a limited admin gets 403."
|
|
- "Per D-13 to D-16, the controller declares assets/js/discogs-lookup.js and assets/css/albums.css through AdminJS/AdminCSS, both embedded in the plugin AdminFS and served under /plytadmin/assets/golem15/fonoteka/ with a JavaScript or CSS content type; the element is plain JS with no import, no user-facing string, no network request and no cookie or storage access."
|
|
- "UI consideration (populated S1 Albums stats strip): one .summer-stats card shows the total, then formats with count above zero in option order, then No shelf when above zero, each a 13px muted label under a 20px/600 value."
|
|
- "UI consideration (zero-one-many S1): with 0 albums the strip shows All albums 0 and no format items; counts are plain integers and labels are not pluralised, so 0, 1 and many share one layout."
|
|
- "UI consideration (overflow S1 stats items): the strip uses only the partial style kit classes, so items wrap inside the card with the kit's gaps and never scroll horizontally."
|
|
- statement: "UI consideration (long-text S1 stat labels): labels wrap without truncation inside min-width 0 items; a visual check with the longest pl labels at 768px width is required."
|
|
verification: backstop
|
|
- statement: "UI consideration (long-text S3 widget button and S4 toolbar button): labels are nowrap, the toolbar cluster wraps and the widget button grows past its 160px min-width; a visual check at 768px with the pl labels is required."
|
|
verification: backstop
|
|
- "go vet and the fonoteka plugin tests stay green after every task; the Phase 10 tests that pinned the Albums shape (field count, toolbarButtons, built-in field types) are updated in the same commit as the YAML change."
|
|
artifacts:
|
|
- path: "../fonoteka.go/plugins/golem15/fonoteka/controllers/albums/_stats.htm"
|
|
provides: "Albums statistics strip template using the partial style kit"
|
|
- path: "../fonoteka.go/plugins/golem15/fonoteka/assets/js/discogs-lookup.js"
|
|
provides: "golem15-fonoteka-discogs-lookup custom element"
|
|
- path: "../fonoteka.go/plugins/golem15/fonoteka/assets/css/albums.css"
|
|
provides: "Widget button styles reading only public --c-* variables"
|
|
- path: "../fonoteka.go/plugins/golem15/fonoteka/controllers/albums_admin_controller.go"
|
|
provides: "PartialData stats view model, AdminJS/AdminCSS, discogsLookup and discogsSync stub actions"
|
|
- path: "../fonoteka.go/plugins/golem15/fonoteka/admin_phase101_smoke_test.go"
|
|
provides: "TestPhase101AlbumsSmoke through the assembled router"
|
|
key_links:
|
|
- from: "../fonoteka.go/plugins/golem15/fonoteka/controllers/albums/config_list.yaml"
|
|
to: "../fonoteka.go/plugins/golem15/fonoteka/controllers/albums/_stats.htm"
|
|
via: "headerPartial: stats"
|
|
pattern: "headerPartial: stats"
|
|
- from: "../fonoteka.go/plugins/golem15/fonoteka/controllers/albums_admin_controller.go"
|
|
to: "scopeAlbums"
|
|
via: "every stats count query"
|
|
pattern: "scopeAlbums"
|
|
- from: "../fonoteka.go/plugins/golem15/fonoteka/models/album/fields.yaml"
|
|
to: "discogsLookup action"
|
|
via: "widget action key"
|
|
pattern: "action: discogsLookup"
|
|
- from: "../fonoteka.go/plugins/golem15/fonoteka/admin.go"
|
|
to: "assets/js/discogs-lookup.js"
|
|
via: "//go:embed list"
|
|
pattern: "assets/js/discogs-lookup.js"
|
|
prohibitions:
|
|
- "No real Discogs HTTP client, job or credential lookup (Phase 14)."
|
|
- "No new permission code; the Winter registerPermissions catalog stays as ported."
|
|
- "The plugin JS performs no network request and reads no cookie or browser storage."
|
|
---
|
|
|
|
## Phase Goal
|
|
|
|
A plugin extends the compiled admin SPA without a Node rebuild: controller JS/CSS served same-origin from embedded files, `type: widget` custom elements whose actions the SPA posts, `type: partial` and list `headerPartial` rendered server-side without a raw-HTML sink, and registered toolbar actions (ADMIN-07).
|
|
|
|
<objective>
|
|
Prove the extension point on three real Albums surfaces in fonoteka.go: a statistics strip above the Albums list, a "Load from Discogs" widget on the Albums form whose stub fills Release year and Format, and a "Sync with Discogs" toolbar action whose stub toasts.
|
|
|
|
Purpose: D-01 requires the phase to land on Płytarium Albums, not only a toy fixture; D-02 keeps both Discogs actions as stubs that Phase 14 replaces. The work uses only Go, YAML, an html/template partial, plain JS and CSS; no Node step runs in this repository (D-04).
|
|
Output: Albums controller capabilities, YAML edits, template, JS and CSS assets, pl and en copy, updated Phase 10 tests and a smoke test.
|
|
|
|
Repo: fonoteka.go only (commits land in ../fonoteka.go). The framework it needs (10.1-01 routes, 10.1-02 SPA dist) is consumed through the go.work replace. Never add co-author tags.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@~/.claude/gsd-core/workflows/execute-plan.md
|
|
@~/.claude/gsd-core/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/10.1-runtime-admin-extension-point/10.1-CONTEXT.md
|
|
@.planning/phases/10.1-runtime-admin-extension-point/10.1-UI-SPEC.md
|
|
@.planning/phases/10.1-runtime-admin-extension-point/10.1-RESEARCH.md
|
|
@.planning/phases/10.1-runtime-admin-extension-point/10.1-01-SUMMARY.md
|
|
@.planning/phases/10.1-runtime-admin-extension-point/10.1-02-SUMMARY.md
|
|
@../fonoteka.go/plugins/golem15/fonoteka/controllers/albums_admin_controller.go
|
|
@../fonoteka.go/plugins/golem15/fonoteka/admin.go
|
|
|
|
<interfaces>
|
|
From 10.1-01 (modules/pact): `AdminClientAssets{AdminJS() []string; AdminCSS() []string}`; `AdminAction{Name, Label string; Permissions []string; Run func(ctx context.Context, in AdminActionInput) (AdminActionResult, error)}`; `AdminActionInput{Field string; RecordID *uint64; Record any; Values map[string]any}`; `AdminActionResult{Message string; Fill map[string]any}`; `HasAdminActions{AdminActions() []AdminAction}`; `AdminPartialData{PartialData(ctx context.Context, name string, record any) (any, error)}`. Partial templates live at `{ConfigDir}/_{name}.htm`, get `.Data` as the view model and a `trans "key"` function; output passes through the node allowlist (dl/dt/dd/div/span with class are allowed; style attributes are dropped). Widget tags must start with `golem15-fonoteka-`; fill keys must be writable scalar form fields; asset paths must start with `assets/` and be embedded in AdminFS.
|
|
From fonoteka.go: `albumsAdminController{db func() *gorm.DB}` with `scopeAlbums(ctx, db) *gorm.DB` (collection scope, `1 = 0` when unbound), `DropdownOptions("format")` returning `models.Album{}.DropdownOptions("format")` (values LP, 2LP, CD, 2CD, MC, Box, EP 7" with `golem15.fonoteka::lang.album_format.*` labels); `models.Album` columns `year` (*int, rule `nullable|integer|between:1889,2100`), `format` (*string, rule `in:LP,2LP,CD,2CD,MC,Box,EP 7"`), `shelf` (*string); permission `golem15.fonoteka.access_albums` (admin_permissions.go). Test helpers: `bootDB(t)`, `assembleTracer(t)`, `phase10CookieAdmin(t, gdb, h, permsJSON)`, `albumsFrontend(t, gdb, email, active)`, `phase10Call(t, h, method, path, body, cookie, ajax)`, `phase10GetJSON`, `adminAPI(rel)`, `testAdminPrefix` = /plytadmin.
|
|
Tests pinning the current Albums shape: admin_albums_test.go (`len(schema.Fields) != 5` in TestAlbumsAdminRegistration; `"toolbarButtons":["create","delete"]` in TestAlbumsAdminList), admin_phase10_copy_test.go (every controller's toolbarButtons equal "create,delete"), admin_phase10_controllers_test.go (`phase10BuiltinFieldTypes` must contain every served field type; served fields must equal the tracked fields.yaml keys).
|
|
</interfaces>
|
|
</context>
|
|
|
|
## Planning notes
|
|
|
|
- Spec-less probe fallback skipped: no requirement IDs were mapped for Phase 10.1 before this planning run; ADMIN-07 is introduced by it. Truths come from CONTEXT D-01..D-17 and the UI-SPEC "Application proof" rows.
|
|
- Discretion resolved per UI-SPEC and RESEARCH Open Questions 1 and 3: stats show the total, per-format counts and no-shelf; the widget fills `[year, format]`, which needs the new `year` field (D-19); action names `discogsLookup` and `discogsSync`; both reuse `golem15.fonoteka.access_albums` (Pitfall 12, no Winter catalog divergence); the widget renders on create and update.
|
|
- Albums has no real form partial; the form `type: partial` path is proven by the acme fixture in 10.1-01 (D-03 discretion).
|
|
- Tests here are smoke tests; the full Albums acceptance test is 10.1-04 (CLAUDE.md rule 3).
|
|
|
|
## Artifacts this phase produces
|
|
|
|
- fonoteka methods on `albumsAdminController`: `PartialData`, `AdminJS`, `AdminCSS`, `AdminActions`; view-model types `albumsStatsView` (`Total`, `Formats`, `NoShelf`) and `albumsStatItem` (`Label`, `Count`)
|
|
- Stub actions `discogsLookup` (fill `year`, `format`) and `discogsSync`
|
|
- Files: `controllers/albums/_stats.htm`, `assets/js/discogs-lookup.js` (custom element `golem15-fonoteka-discogs-lookup`), `assets/css/albums.css`
|
|
- YAML: config_list.yaml `headerPartial: stats`, `toolbar.buttons: [create, delete, discogsSync]`; fields.yaml `year` (number) and `discogs` (widget)
|
|
- Lang keys (en, pl): `golem15.fonoteka::lang.item.year`, `stats.total`, `stats.no_shelf`, `discogs.lookup_button`, `discogs.lookup_label`, `discogs.lookup_comment`, `discogs.stub_filled`, `discogs.sync_button`, `discogs.stub_not_implemented`
|
|
- Test: `TestPhase101AlbumsSmoke` (subtests stats, widget, assets, toolbar)
|
|
|
|
<tasks>
|
|
|
|
<task type="tracer">
|
|
<name>Task 1: The Albums list shows a statistics strip scoped to the admin's collection</name>
|
|
<precondition>10.1-01 and 10.1-02 are executed: `grep -q 'AdminPartialData' modules/pact/capabilities.go && grep -q 'summer-stats' admin/src/styles/main.css` succeeds from summercms.go.</precondition>
|
|
<files>../fonoteka.go/plugins/golem15/fonoteka/controllers/albums_admin_controller.go, ../fonoteka.go/plugins/golem15/fonoteka/controllers/albums/_stats.htm, ../fonoteka.go/plugins/golem15/fonoteka/controllers/albums/config_list.yaml, ../fonoteka.go/plugins/golem15/fonoteka/admin.go, ../fonoteka.go/plugins/golem15/fonoteka/lang/en/lang.yaml, ../fonoteka.go/plugins/golem15/fonoteka/lang/pl/lang.yaml, ../fonoteka.go/plugins/golem15/fonoteka/admin_phase101_smoke_test.go</files>
|
|
<read_first>../fonoteka.go/plugins/golem15/fonoteka/controllers/albums_admin_controller.go (scopeAlbums, orderedOptions nil-db idiom), ../fonoteka.go/plugins/golem15/fonoteka/models/album.go (albumFormatOptions, columns), ../fonoteka.go/plugins/golem15/fonoteka/controllers/albums/config_list.yaml, ../fonoteka.go/plugins/golem15/fonoteka/admin.go, ../fonoteka.go/plugins/golem15/fonoteka/lang/en/lang.yaml and pl/lang.yaml (item, album_format groups), ../fonoteka.go/plugins/golem15/fonoteka/admin_phase10_relations_test.go (phase10CookieAdmin), ../fonoteka.go/plugins/golem15/fonoteka/admin_albums_test.go (albumsFrontend), modules/cabana/partial_render.go and modules/cabana/README.md (template contract from 10.1-01), .planning/phases/10.1-runtime-admin-extension-point/10.1-UI-SPEC.md (Partial style kit markup, Application proof)</read_first>
|
|
<action>(1) View model (D-10, T-10.1-15): add `PartialData(ctx context.Context, name string, record any) (any, error)` to albumsAdminController. For `stats` it returns an `albumsStatsView` with `Total int`, `Formats []albumsStatItem` (`Label string` holding the format option's phrase key, `Count int`) and `NoShelf int`; any other name returns an error. Resolve the db like orderedOptions (nil db gives a zero view). Build each of three queries fresh from `db.WithContext(ctx).Model(&models.Album{})` and pass it through `c.scopeAlbums(ctx, q)` (never reuse a chained *gorm.DB between queries): a Count for Total; `Select("format, COUNT(*) AS count").Group("format")` scanned into rows; `Where("shelf IS NULL OR shelf = ''")` Count for NoShelf. Emit Formats in `models.Album{}.DropdownOptions("format")` order, keeping only counts above zero. The view model never holds an Album.
|
|
|
|
(2) Template `controllers/albums/_stats.htm` per the UI-SPEC partial style kit: a `<dl class="summer-stats">` whose first `<div class="summer-stat">` has `<dt class="summer-stat__label">{{ trans "golem15.fonoteka::lang.stats.total" }}</dt><dd class="summer-stat__value">{{ .Data.Total }}</dd>`, then `{{ range .Data.Formats }}` one item per format with `{{ trans .Label }}` and `{{ .Count }}`, then `{{ if .Data.NoShelf }}` an item with `{{ trans "golem15.fonoteka::lang.stats.no_shelf" }}` and `{{ .Data.NoShelf }}`. Kit classes only, no inline style, no plugin CSS. With zero albums it still renders the total item with 0.
|
|
|
|
(3) config_list.yaml gains top-level `headerPartial: stats`. admin.go's `//go:embed` list appends `controllers/albums/_stats.htm`. Add a `stats:` group with `total` and `no_shelf` to lang/en/lang.yaml ("All albums", "No shelf") and lang/pl/lang.yaml ("Wszystkie albumy", "Bez półki").
|
|
|
|
(4) Smoke test: new `admin_phase101_smoke_test.go` with `TestPhase101AlbumsSmoke` and its subtest `stats`: boot with bootDB and assembleTracer, create an admin with `{"golem15.fonoteka.access_albums":1}` through phase10CookieAdmin, bind its email to a collection with albumsFrontend, insert albums in that collection (two LP, one CD, one without shelf) and albums in a second collection; GET `adminAPI("/golem15/fonoteka/albums/schema/list")` shows `"headerPartial":"stats"`; GET `adminAPI("/golem15/fonoteka/albums/partials/stats")` with the cookie and locale en returns 200 whose node tree text contains "All albums", the admin's own total, "LP" with 2, "CD" with 1 and "No shelf", and never the other collection's total.</action>
|
|
<verify>
|
|
<automated>cd ../fonoteka.go && go vet ./... ./plugins/golem15/fonoteka/... && go test ./plugins/golem15/fonoteka -run '^(TestPhase101AlbumsSmoke|TestPhase10LangCatalog|TestAlbumsAdminList|TestAlbumsAdminRegistration|TestPhase10ControllerCopy)$' -count=1 -v</automated>
|
|
<fails_when>Non-zero exit; the output lacks "--- PASS: TestPhase101AlbumsSmoke" or "--- PASS: TestPhase101AlbumsSmoke/stats", or prints "no tests to run" or "--- SKIP"; boot fails with a "cabana: admin schema" error.</fails_when>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `grep -c '^headerPartial: stats' ../fonoteka.go/plugins/golem15/fonoteka/controllers/albums/config_list.yaml` prints 1.
|
|
- `grep -c 'controllers/albums/_stats.htm' ../fonoteka.go/plugins/golem15/fonoteka/admin.go` prints 1.
|
|
- `grep -c 'scopeAlbums' ../fonoteka.go/plugins/golem15/fonoteka/controllers/albums_admin_controller.go` prints at least 5 (the three stats queries plus the existing list and form scopes).
|
|
- `grep -c 'summer-stat__value' ../fonoteka.go/plugins/golem15/fonoteka/controllers/albums/_stats.htm` prints at least 2 and `grep -c 'style=' ../fonoteka.go/plugins/golem15/fonoteka/controllers/albums/_stats.htm` prints 0.
|
|
- TestPhase101AlbumsSmoke/stats proves the other collection's albums are not counted.
|
|
</acceptance_criteria>
|
|
<done>An admin opening Albums gets a server-rendered statistics strip for their own collection above the list.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: The Albums form offers "Load from Discogs", whose stub fills Release year and Format</name>
|
|
<reversibility rating="costly">D-06: this is the first application fields.yaml written in the widget key shape; every later ported widget copies it. User-locked, flagged without a checkpoint.</reversibility>
|
|
<files>../fonoteka.go/plugins/golem15/fonoteka/models/album/fields.yaml, ../fonoteka.go/plugins/golem15/fonoteka/controllers/albums_admin_controller.go, ../fonoteka.go/plugins/golem15/fonoteka/assets/js/discogs-lookup.js, ../fonoteka.go/plugins/golem15/fonoteka/assets/css/albums.css, ../fonoteka.go/plugins/golem15/fonoteka/admin.go, ../fonoteka.go/plugins/golem15/fonoteka/lang/en/lang.yaml, ../fonoteka.go/plugins/golem15/fonoteka/lang/pl/lang.yaml, ../fonoteka.go/plugins/golem15/fonoteka/admin_albums_test.go, ../fonoteka.go/plugins/golem15/fonoteka/admin_phase10_controllers_test.go, ../fonoteka.go/plugins/golem15/fonoteka/admin_phase101_smoke_test.go</files>
|
|
<read_first>../fonoteka.go/plugins/golem15/fonoteka/models/album/fields.yaml, ../fonoteka.go/plugins/golem15/fonoteka/controllers/albums_admin_controller.go, ../fonoteka.go/plugins/golem15/fonoteka/admin.go, ../fonoteka.go/plugins/golem15/fonoteka/admin_albums_test.go (TestAlbumsAdminRegistration, TestAlbumsAdminForm), ../fonoteka.go/plugins/golem15/fonoteka/admin_phase10_controllers_test.go (phase10BuiltinFieldTypes, phase10TrackedKeys), ../fonoteka.go/plugins/golem15/fonoteka/admin_phase10_auth_test.go (TestPhase10LangCatalog), modules/cabana/README.md (widget, assets and CSS variable contract from 10.1-01/02), admin/tests/fixtures/extension.form-schema.json (element contract as the SPA uses it), .planning/phases/10.1-runtime-admin-extension-point/10.1-UI-SPEC.md (S3 plugin element visual contract, Copywriting Contract application rows)</read_first>
|
|
<action>(1) fields.yaml (D-06, UI-SPEC Application proof): after `shelf` add `year` (label golem15.fonoteka::lang.item.year, span right, type number), so shelf and year share a row; as the last field add `discogs` with label golem15.fonoteka::lang.discogs.lookup_label, comment golem15.fonoteka::lang.discogs.lookup_comment, type widget, widget golem15-fonoteka-discogs-lookup, action discogsLookup, fill [year, format], span full. No context key: it renders on create and update.
|
|
|
|
(2) Controller (D-02, D-13): `AdminJS()` returns `assets/js/discogs-lookup.js`; `AdminCSS()` returns `assets/css/albums.css`; `AdminActions()` returns `discogsLookup` with Label golem15.fonoteka::lang.discogs.lookup_button, Permissions [golem15.fonoteka.access_albums] and a Run that returns Message golem15.fonoteka::lang.discogs.stub_filled and Fill {"year": 1977, "format": "LP"}. A comment says Phase 14 replaces this stub with the Discogs client; the stub reads no record, no credential and makes no outbound call. Add compile-time assertions that albumsAdminController implements pact.AdminClientAssets, pact.HasAdminActions and pact.AdminPartialData.
|
|
|
|
(3) `assets/js/discogs-lookup.js` (D-04; plain ES module, no import, no Vue, no user-facing string, no network request, no cookie or storage access): a class extending HTMLElement with observedAttributes label, busy-label, busy and state. connectedCallback creates one light-DOM `<button type="button">` if it does not exist yet, with text from the `label` attribute. A click dispatches `new CustomEvent('summer-action', { bubbles: true, composed: true })` unless the element has `busy`. attributeChangedCallback keeps the button in sync: with `busy` present the button is disabled, has aria-busy="true" and shows the `busy-label` text; otherwise it is enabled and shows `label`. Define it only when `customElements.get('golem15-fonoteka-discogs-lookup')` is undefined.
|
|
|
|
(4) `assets/css/albums.css` styles only `golem15-fonoteka-discogs-lookup button` per the UI-SPEC S3 table, reading only public `--c-*` variables: height 42px, padding 0 16px, min-width 160px, border-radius 10px, 1px solid var(--c-border-strong), background var(--c-surface), color var(--c-text), `font: inherit`, font-weight 600, white-space nowrap, background var(--c-hover) on hover with a 150ms ease-out transition, `:focus-visible` outline 3px solid var(--c-ring) with offset 2px, and `[disabled]` opacity .6 with cursor not-allowed. No hex colours.
|
|
|
|
(5) admin.go's `//go:embed` list appends `assets/js/discogs-lookup.js assets/css/albums.css`. Lang en and pl, using the UI-SPEC strings: item.year ("Release year" / "Rok wydania"); in the existing discogs group lookup_button ("Load from Discogs" / "Wczytaj z Discogs"), lookup_label ("Discogs"), lookup_comment ("Fills in Release year and Format. Click Save to keep them." / "Uzupełnia Rok wydania i Format. Kliknij Zapisz, aby je zachować."), stub_filled ("Release year and Format were filled with test data. Save to keep them." / "Uzupełniono Rok wydania i Format danymi testowymi. Zapisz, aby je zachować.").
|
|
|
|
(6) Tests pinning the old shape, in the same commit: admin_albums_test.go's form field count becomes 7; admin_phase10_controllers_test.go's phase10BuiltinFieldTypes gains "widget" (the SPA ships its renderer since 10.1-02). Extend TestPhase101AlbumsSmoke with subtests `widget` (the form schema has the discogs field with widget, action and fill [year format], an actionLabel resolved to "Load from Discogs" in en and "Wczytaj z Discogs" in pl, and assets.scripts naming `/plytadmin/assets/golem15/fonoteka/js/discogs-lookup.js?v=`; creating an album then POSTing `widgets/discogs` with its record_id and values returns 200 with fill exactly {year: 1977, format: "LP"} and the resolved stub_filled message; a PUT carrying year 1977 and format LP persists both) and `assets` (GET the script and stylesheet URLs from the schema returns 200 with `text/javascript; charset=utf-8` and `text/css; charset=utf-8`, nosniff and an ETag; GET `/plytadmin/assets/golem15/fonoteka/models/album/fields.yaml` is 404).</action>
|
|
<verify>
|
|
<automated>cd ../fonoteka.go && go vet ./... ./plugins/golem15/fonoteka/... && go test ./plugins/golem15/fonoteka -run '^(TestPhase101AlbumsSmoke|TestPhase10LangCatalog|TestAlbumsAdminForm|TestAlbumsAdminRegistration|TestAlbumsAdminCRUD|TestPhase10Controllers|TestPhase10AlbumRelations|TestPhase10AssembledAcceptance)$' -count=1 -v</automated>
|
|
<fails_when>Non-zero exit; the output lacks a "--- PASS" line for TestPhase101AlbumsSmoke/widget, TestPhase101AlbumsSmoke/assets, TestPhase10LangCatalog, TestPhase10Controllers or TestPhase10AssembledAcceptance, or prints "no tests to run" or "--- SKIP"; boot fails with a "cabana: admin schema" error.</fails_when>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `grep -c 'action: discogsLookup' ../fonoteka.go/plugins/golem15/fonoteka/models/album/fields.yaml` prints 1 and `grep -c 'widget: golem15-fonoteka-discogs-lookup' ../fonoteka.go/plugins/golem15/fonoteka/models/album/fields.yaml` prints 1.
|
|
- `grep -cE 'fetch\(|XMLHttpRequest|document\.cookie|localStorage|sessionStorage' ../fonoteka.go/plugins/golem15/fonoteka/assets/js/discogs-lookup.js` prints 0 and `grep -cE '^\s*import\s' ../fonoteka.go/plugins/golem15/fonoteka/assets/js/discogs-lookup.js` prints 0.
|
|
- `grep -cE '#[0-9a-fA-F]{3,6}' ../fonoteka.go/plugins/golem15/fonoteka/assets/css/albums.css` prints 0.
|
|
- `grep -c 'golem15.fonoteka.access_albums' ../fonoteka.go/plugins/golem15/fonoteka/controllers/albums_admin_controller.go` prints at least 2.
|
|
- `grep -c 'assets/js/discogs-lookup.js' ../fonoteka.go/plugins/golem15/fonoteka/admin.go` prints 1.
|
|
</acceptance_criteria>
|
|
<done>The Albums form serves a Discogs lookup widget whose stub returns Release year 1977 and Format LP into the unsaved form, with its plain-JS element and stylesheet served from the plugin's embedded files.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 3: A "Sync with Discogs" toolbar action toasts from its stub, and the three surfaces pass a browser check</name>
|
|
<reversibility rating="costly">D-12: first application list YAML to register a custom toolbar name in toolbar.buttons; user-locked, flagged without a checkpoint.</reversibility>
|
|
<files>../fonoteka.go/plugins/golem15/fonoteka/controllers/albums/config_list.yaml, ../fonoteka.go/plugins/golem15/fonoteka/controllers/albums_admin_controller.go, ../fonoteka.go/plugins/golem15/fonoteka/lang/en/lang.yaml, ../fonoteka.go/plugins/golem15/fonoteka/lang/pl/lang.yaml, ../fonoteka.go/plugins/golem15/fonoteka/admin_albums_test.go, ../fonoteka.go/plugins/golem15/fonoteka/admin_phase10_copy_test.go, ../fonoteka.go/plugins/golem15/fonoteka/admin_phase101_smoke_test.go</files>
|
|
<read_first>../fonoteka.go/plugins/golem15/fonoteka/controllers/albums/config_list.yaml, ../fonoteka.go/plugins/golem15/fonoteka/controllers/albums_admin_controller.go (Task 2 AdminActions), ../fonoteka.go/plugins/golem15/fonoteka/admin_albums_test.go (TestAlbumsAdminList), ../fonoteka.go/plugins/golem15/fonoteka/admin_phase10_copy_test.go, ../fonoteka.go/plugins/golem15/fonoteka/admin_phase101_smoke_test.go, .planning/phases/10.1-runtime-admin-extension-point/10.1-UI-SPEC.md (S4, Copywriting Contract), .planning/phases/10.1-runtime-admin-extension-point/10.1-VALIDATION.md (Manual-Only Verifications)</read_first>
|
|
<action>(1) config_list.yaml `toolbar.buttons` becomes `[create, delete, discogsSync]` (D-12). AdminActions gains `discogsSync` with Label golem15.fonoteka::lang.discogs.sync_button, Permissions [golem15.fonoteka.access_albums] and a Run returning Message golem15.fonoteka::lang.discogs.stub_not_implemented and no fill (D-02 stub; a comment names Phase 14; it changes nothing and makes no outbound call). Lang en and pl in the discogs group: sync_button ("Sync with Discogs" / "Synchronizuj z Discogs") and stub_not_implemented ("Discogs sync is in test mode. Nothing was changed." / "Synchronizacja z Discogs działa w trybie testowym. Nic nie zmieniono.").
|
|
|
|
(2) Tests pinning the old toolbar, in the same commit: admin_albums_test.go's expected `"toolbarButtons":["create","delete"]` for Albums becomes `["create","delete","discogsSync"]`; admin_phase10_copy_test.go keeps "create,delete" for the other four controllers and expects "create,delete,discogsSync" for albums (a per-case expectation). Extend TestPhase101AlbumsSmoke with subtest `toolbar`: the developer admin's list schema has toolbarActions [{name: discogsSync, label: "Sync with Discogs"}] in en and the pl label in pl; POST `adminAPI("/golem15/fonoteka/albums/toolbar/discogsSync")` with `{}`, the cookie and X-Requested-With returns 200 with the resolved stub_not_implemented message and an empty fill; the same POST without X-Requested-With is 403; an admin whose role grants only golem15.fonoteka.access_genres gets 403 on the toolbar, widget and partial routes.
|
|
|
|
(3) Run the whole fonoteka plugin suite and the Phase 10 hygiene gate from summercms.go to confirm nothing application-specific leaked into the framework.</action>
|
|
<verify>
|
|
<automated>(cd ../fonoteka.go && go vet ./... ./plugins/golem15/fonoteka/... && go test ./plugins/golem15/fonoteka/... -count=1 && go test ./plugins/golem15/fonoteka -run '^(TestPhase101AlbumsSmoke|TestAlbumsAdminList|TestPhase10ControllerCopy)$' -count=1 -v) && scripts/check-phase10.sh --hygiene</automated>
|
|
<fails_when>Any command exits non-zero; the verbose run lacks "--- PASS: TestPhase101AlbumsSmoke/toolbar", "--- PASS: TestAlbumsAdminList" or "--- PASS: TestPhase10ControllerCopy", or prints "no tests to run" or "--- SKIP"; check-phase10.sh prints a line starting with "refuse:".</fails_when>
|
|
<human-check>
|
|
<test>In ../fonoteka.go run `summer serve` (or the built binary's serve command), open http://localhost:8080/plytadmin, log in as an admin bound to a collection and open Albumy with the browser console open. Then: (a) check the statistics strip above the list; (b) click "Synchronizuj z Discogs"; (c) open an album, click "Wczytaj z Discogs", then Zapisz and reload; (d) open the create form; (e) narrow the window to 768px in pl and repeat (a) to (c); (f) switch to another controller such as Gatunki.</test>
|
|
<expected>No CSP or module-loading error in the console (RESEARCH A4: the same-origin module script loads under script-src 'self'); the strip shows Wszystkie albumy with the collection's counts and wraps without horizontal scroll at 768px with labels such as "Cassette (MC)" and "Bez półki" readable; the toolbar action shows its test-mode toast; the widget button shows its label, turns busy during the request, fills Rok wydania 1977 and Format LP without saving, and after Zapisz and reload both values persist; the widget also renders on the create form; button labels do not truncate at 768px; on Gatunki the Albums stylesheet no longer applies.</expected>
|
|
<why_human>CSP enforcement, real module loading and custom-element upgrade happen only in a browser, and the 768px layout backstops need visual judgment; Vitest's happy-dom does not enforce CSP.</why_human>
|
|
</human-check>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `grep -c 'buttons: \[create, delete, discogsSync\]' ../fonoteka.go/plugins/golem15/fonoteka/controllers/albums/config_list.yaml` prints 1.
|
|
- `grep -c 'discogsSync' ../fonoteka.go/plugins/golem15/fonoteka/controllers/albums_admin_controller.go` prints at least 1 and `grep -c 'stub_not_implemented' ../fonoteka.go/plugins/golem15/fonoteka/lang/pl/lang.yaml` prints 1.
|
|
- TestPhase101AlbumsSmoke/toolbar proves the limited admin's 403 on the toolbar, widget and partial routes.
|
|
- The whole `go test ./plugins/golem15/fonoteka/...` run passes.
|
|
</acceptance_criteria>
|
|
<done>The Albums list offers a permission-gated "Sync with Discogs" action that toasts from its stub, and the three Albums surfaces are ready for the in-browser check.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
## Source coverage (this plan)
|
|
|
|
| Source | Item | Task |
|
|
|--------|------|------|
|
|
| CONTEXT | D-01 proof on Albums (strip, widget, toolbar) | 1, 2, 3 |
|
|
| CONTEXT | D-02 enabled widget button, stub endpoint with fixture payload | 2, 3 |
|
|
| CONTEXT | D-03 strip is list chrome via headerPartial | 1 |
|
|
| CONTEXT | D-04 plain JS custom element | 2 |
|
|
| CONTEXT | D-06, D-07 widget YAML shape and fill write-back | 2 |
|
|
| CONTEXT | D-10 curated view model, escaping on | 1 |
|
|
| CONTEXT | D-11 headerPartial declared in config_list.yaml | 1 |
|
|
| CONTEXT | D-12 third toolbar action registered on the controller | 3 |
|
|
| CONTEXT | D-13 to D-16 controller JS/CSS through AdminJS/AdminCSS, embedded, same-origin | 2 |
|
|
| UI-SPEC | Application proof rows, S1 populated/zero/overflow, backstops | 1-3 |
|
|
| RESEARCH | Open Questions 1, 3; Pitfalls 3, 11, 12, 14; A4 manual UAT | 1-3 |
|
|
|
|
<threat_model>
|
|
## Trust Boundaries
|
|
|
|
| Boundary | Description |
|
|
|----------|-------------|
|
|
| Admin session → Albums stats view model | Counts must stay inside the admin's resolved collection |
|
|
| Plugin JS → admin origin | Same-origin script shipped by the application |
|
|
| Admin → Discogs stub actions | Stub endpoints reachable by any admin who passes the Albums permission |
|
|
|
|
## STRIDE Threat Register
|
|
|
|
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
|
|-----------|----------|-----------|----------|-------------|-----------------|
|
|
| T-10.1-10 | Elevation of Privilege | Albums plugin JS running in the admin origin | medium | mitigate | Plugin JS is trusted compiled code (like plugin Go); the element makes no network request and reads no cookie or storage, and signals only through summer-action (D-05); the session cookie is HttpOnly; 10.1-04's hygiene stage scans plugin asset JS for network and cookie access (Task 2). |
|
|
| T-10.1-15 | Information Disclosure | Albums statistics strip | high | mitigate | Every stats query is built fresh and scoped with scopeAlbums (collection binding, `1 = 0` when unbound); the view model holds labels and integers only; the smoke test and 10.1-04's two-collection test prove isolation (Task 1). |
|
|
| T-10.1-21 | Elevation of Privilege | discogsLookup and discogsSync stubs | medium | mitigate | Both require golem15.fonoteka.access_albums on top of the controller permission; a Genres-only admin gets 403 (Task 3). |
|
|
| T-10.1-22 | Tampering | stub fill payload | low | accept | The stub only patches the unsaved form; nothing persists until the admin saves, and the save runs the Album rules (year between 1889 and 2100, format in the option list). |
|
|
| T-10.1-SC | Tampering | dependencies | high | mitigate | No Go module or npm package added in fonoteka.go; the element is hand-written plain JS. |
|
|
</threat_model>
|
|
|
|
<verification>
|
|
After Task 3: `go vet ./... ./plugins/golem15/fonoteka/...` and `go test ./plugins/golem15/fonoteka/...` pass in fonoteka.go; TestPhase101AlbumsSmoke covers stats, widget, assets and toolbar; `scripts/check-phase10.sh --hygiene` passes in summercms.go. The browser check in Task 3 is collected at /gsd-verify-work.
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- The Albums list shows a collection-scoped statistics strip; the Albums form shows the Discogs lookup widget filling Release year and Format; the Albums toolbar offers Sync with Discogs, both Discogs actions as stubs.
|
|
- All application copy exists in pl and en; the Phase 10 Albums tests reflect the new shape; no framework file names the application.
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Create `.planning/phases/10.1-runtime-admin-extension-point/10.1-03-SUMMARY.md` when done.
|
|
</output>
|