feat(categories): rename + delete a category (bookshelf-0v44o.2) #1091

Merged
zombor merged 4 commits from bd-bookshelf-0v44o.2 into main 2026-07-11 00:23:26 +00:00
Owner

Summary

  • PATCH /categories/{id} — rename a category; rejects with 409 on name conflict (merge is slice .3)
  • DELETE /categories/{id} — deletes category + cascade-removes all book_metadata_category_mapping rows via DB FK
  • Both endpoints are admin-gated (manage-metadata-config permission, same as authors)
  • Kebab button per card on /categories; category_manage_controller.js (Stimulus) drives rename + delete modals using canonical CSS classes (.modal-dialog--author-manage, .author-manage-menu, .am-error-msg)
  • CSRF token sent via X-CSRF-Token header from meta tag; no inline style= (CSP compliant)
  • MySQL 1062 duplicate-key detection via errors.As(*mysql.MySQLError)
  • Curried-function DI throughout; black-box unit tests; 100% Go coverage; 31 Vitest specs; Ordered browser e2e journey (kebab→rename modal→submit→reload; kebab→delete modal→confirm→reload) with 3 screenshots posted to PR

Test plan

  • go build ./internal/categories/... — clean compile
  • go test ./internal/categories/... — all unit tests pass
  • npm test — all 3766 JS tests pass (including 31 category_manage tests)
  • go build -tags e2e ./e2e/... — browser e2e compiles
  • Browser e2e journey: Ordered container, BeforeAll seeds DB, clicks kebab, asserts rename+delete modals open and mutations reload page

Closes bead bookshelf-0v44o.2 on merge.

## Summary - PATCH /categories/{id} — rename a category; rejects with 409 on name conflict (merge is slice .3) - DELETE /categories/{id} — deletes category + cascade-removes all book_metadata_category_mapping rows via DB FK - Both endpoints are admin-gated (manage-metadata-config permission, same as authors) - Kebab button per card on /categories; category_manage_controller.js (Stimulus) drives rename + delete modals using canonical CSS classes (.modal-dialog--author-manage, .author-manage-menu, .am-error-msg) - CSRF token sent via X-CSRF-Token header from meta tag; no inline style= (CSP compliant) - MySQL 1062 duplicate-key detection via errors.As(*mysql.MySQLError) - Curried-function DI throughout; black-box unit tests; 100% Go coverage; 31 Vitest specs; Ordered browser e2e journey (kebab→rename modal→submit→reload; kebab→delete modal→confirm→reload) with 3 screenshots posted to PR ## Test plan - [x] `go build ./internal/categories/...` — clean compile - [x] `go test ./internal/categories/...` — all unit tests pass - [x] `npm test` — all 3766 JS tests pass (including 31 category_manage tests) - [x] `go build -tags e2e ./e2e/...` — browser e2e compiles - [x] Browser e2e journey: `Ordered` container, BeforeAll seeds DB, clicks kebab, asserts rename+delete modals open and mutations reload page ## Closes bead bookshelf-0v44o.2 on merge.
feat(categories): add browser e2e journey for rename + delete (bookshelf-0v44o.2)
All checks were successful
/ JS Unit Tests (pull_request) Successful in 37s
/ E2E API (pull_request) Successful in 2m25s
/ Lint (pull_request) Successful in 3m10s
/ Integration (pull_request) Successful in 3m20s
/ E2E Browser (pull_request) Successful in 3m56s
/ Test (pull_request) Successful in 6m25s
7b06d39343
Ordered journey covering the full kebab→modal→submit flow for both the
rename (PATCH /categories/{id}) and delete (DELETE /categories/{id})
operations. Uploads three screenshots to PR (rename modal, rename
submitted, delete modal).

Justified as browser e2e (not Vitest/jsdom or API e2e):
- Requires real Chromium: Stimulus controller builds DOM dynamically;
  fetch calls must reach the live HTTP server.
- Multi-step user journey: kebab click → menu → modal → submit → reload.
- DOM assertion after controller-driven page reload is not testable in
  Vitest (no real HTTP) or e2e/api/ (no DOM interaction).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Author
Owner

Category management screenshot (rename-modal)

Category rename/delete — kebab menu and modal on /categories page

rename-modal

**Category management screenshot** (rename-modal) Category rename/delete — kebab menu and modal on /categories page ![rename-modal](/attachments/5369cb6d-814e-4f4a-94e9-49ed88228bcc)
Author
Owner

Category management screenshot (rename-submitted)

Category rename/delete — kebab menu and modal on /categories page

rename-submitted

**Category management screenshot** (rename-submitted) Category rename/delete — kebab menu and modal on /categories page ![rename-submitted](/attachments/363b2d4c-faf4-433b-b929-f85e489d301d)
Author
Owner

Category management screenshot (delete-modal)

Category rename/delete — kebab menu and modal on /categories page

delete-modal

**Category management screenshot** (delete-modal) Category rename/delete — kebab menu and modal on /categories page ![delete-modal](/attachments/983dcd6f-d62c-40bf-8ab5-879bc0186d37)
Author
Owner

Security Review — PR #1091 (feat(categories): rename + delete)

Authorization

PATCH /categories/{id} and DELETE /categories/{id} are both wrapped by manageRequired(eh.Wrap(...)) in routes.go (lines 19–20 of that file). In wire.go, manageRequired resolves to d.LibraryManageMetadataConfigRequired, which is wired in app.go as users.PermissionRequired(check: row.PermissionManageMetadataConfig, logger). The gate:

  • Denies unauthenticated requests (nil claims → denyRequest).
  • Denies requests where no permissions row exists in the DB → forbidRequest.
  • Denies on DB error during permission lookup → forbidRequest.
  • Allows admin short-circuit without a DB lookup.

Result: fails closed. No authorization gap.

SQL injection

RenameCategory uses UPDATE category SET name = ? WHERE id = ? with bound parameters. DeleteCategory uses DELETE FROM category WHERE id = ?. categoryExists uses SELECT 1 FROM category WHERE id = ? LIMIT 1. No string concatenation in SQL. Clean.

XSS

Server side: both data-category-name="{{.Name}}" and aria-label="Actions for {{.Name}}" in categories_index.html are in double-quoted attribute context — html/template escapes ", <, >, & automatically. Clean.

Client side (category_manage_controller.js):

  • input.value = this._activeName — sets the .value property, not innerHTML. Clean.
  • Confirm message: _el("p", null, "Delete " + name + "?…")_el sets el.textContent, not innerHTML. Even a name containing <script> would be inert. Clean.
  • _showError(el, msg) sets el.textContent. Clean.

The _activeID value flows from the server-rendered data-category-id="{{.ID}}" (a Go int64 → decimal literal) into the fetch URL. strconv.ParseInt on the server rejects anything non-numeric with a 400. Clean.

CSRF

Both _patchRename and _deleteCategory send "X-CSRF-Token": this._csrfToken(). The _csrfToken() implementation reads meta[name="csrf-token"], which is the same pattern used in the sibling author_manage_controller.js (lines 519–521 of that file). The global CSRF middleware enforces the header on unsafe methods server-side. Clean.

Architecture boundary

internal/categories imports: stdlib, github.com/go-sql-driver/mysql, and internal/middleware. No workflow engine import. Clean.

Secrets / PII

No tokens, keys, or PII appear in log messages or error strings. Category names in error wraps ("rename category %d: %w") are not sensitive. Clean.

Findings

[MINOR] internal/categories/manage_handler.go:409 — no maximum length cap on the rename name field

req.Name is validated for non-empty but not for maximum length. The DB column is varchar(255), so a name longer than 255 bytes reaches MySQL and returns a "Data too long for column" error that surfaces as an opaque 500 rather than a 400. No security impact, but the error signal is poor. Fix: add if len(req.Name) > 255 { return fmt.Errorf("name too long: %w", middleware.ErrValidation) } after the empty check.


REVIEW VERDICT: 0 blocker, 0 major, 1 minor

## Security Review — PR #1091 (`feat(categories): rename + delete`) ### Authorization **PATCH /categories/{id} and DELETE /categories/{id}** are both wrapped by `manageRequired(eh.Wrap(...))` in `routes.go` (lines 19–20 of that file). In `wire.go`, `manageRequired` resolves to `d.LibraryManageMetadataConfigRequired`, which is wired in `app.go` as `users.PermissionRequired(check: row.PermissionManageMetadataConfig, logger)`. The gate: - Denies unauthenticated requests (nil claims → `denyRequest`). - Denies requests where no permissions row exists in the DB → `forbidRequest`. - Denies on DB error during permission lookup → `forbidRequest`. - Allows admin short-circuit without a DB lookup. **Result: fails closed. No authorization gap.** ### SQL injection `RenameCategory` uses `UPDATE category SET name = ? WHERE id = ?` with bound parameters. `DeleteCategory` uses `DELETE FROM category WHERE id = ?`. `categoryExists` uses `SELECT 1 FROM category WHERE id = ? LIMIT 1`. No string concatenation in SQL. **Clean.** ### XSS **Server side:** both `data-category-name="{{.Name}}"` and `aria-label="Actions for {{.Name}}"` in `categories_index.html` are in double-quoted attribute context — `html/template` escapes `"`, `<`, `>`, `&` automatically. **Clean.** **Client side (`category_manage_controller.js`):** - `input.value = this._activeName` — sets the `.value` property, not `innerHTML`. **Clean.** - Confirm message: `_el("p", null, "Delete " + name + "?…")` — `_el` sets `el.textContent`, not `innerHTML`. Even a name containing `<script>` would be inert. **Clean.** - `_showError(el, msg)` sets `el.textContent`. **Clean.** The `_activeID` value flows from the server-rendered `data-category-id="{{.ID}}"` (a Go `int64` → decimal literal) into the fetch URL. `strconv.ParseInt` on the server rejects anything non-numeric with a 400. **Clean.** ### CSRF Both `_patchRename` and `_deleteCategory` send `"X-CSRF-Token": this._csrfToken()`. The `_csrfToken()` implementation reads `meta[name="csrf-token"]`, which is the same pattern used in the sibling `author_manage_controller.js` (lines 519–521 of that file). The global CSRF middleware enforces the header on unsafe methods server-side. **Clean.** ### Architecture boundary `internal/categories` imports: stdlib, `github.com/go-sql-driver/mysql`, and `internal/middleware`. No workflow engine import. **Clean.** ### Secrets / PII No tokens, keys, or PII appear in log messages or error strings. Category names in error wraps (`"rename category %d: %w"`) are not sensitive. **Clean.** ### Findings [MINOR] internal/categories/manage_handler.go:409 — no maximum length cap on the rename name field `req.Name` is validated for non-empty but not for maximum length. The DB column is `varchar(255)`, so a name longer than 255 bytes reaches MySQL and returns a "Data too long for column" error that surfaces as an opaque 500 rather than a 400. No security impact, but the error signal is poor. Fix: add `if len(req.Name) > 255 { return fmt.Errorf("name too long: %w", middleware.ErrValidation) }` after the empty check. --- REVIEW VERDICT: 0 blocker, 0 major, 1 minor
Author
Owner

Code Review: bookshelf-0v44o.2

Phase 0: DEMO Verification

CI is green (confirmed via commit status API). PR is mergeable. No DEMO block is present — this is a UI/mutation feature reviewed via diff + CI as the behavioral source of truth per project convention.

Phase 1: Spec Compliance

All stated requirements are present:

  • PATCH /categories/{id} rename handler — returns 409 on name conflict, 404 not-found, 204 success
  • DELETE /categories/{id} delete handler — cascades via FK, 404 not-found, 204 success
  • Admin-gated via d.LibraryManageMetadataConfigRequired on both routes (internal/categories/routes.go:19-20)
  • Kebab modal UI, Vitest controller tests, ordered browser e2e journey
  • ./internal/categories/... added to UNIT_PKGS in check-coverage.sh (inclusion, not exclusion — correct)

Phase 2: Code Quality

Authz — Both mutation routes are correctly gated: mux.Handle("PATCH /categories/{id}", manageRequired(eh.Wrap(rename))) and mux.Handle("DELETE /categories/{id}", manageRequired(eh.Wrap(delete))) (internal/categories/routes.go:19-20). Wire passes d.LibraryManageMetadataConfigRequired — the same field used by the author/series pattern.

Multi-user — Category browse remains library-scoped (via getUserLibraryIDs in ListHandler). Mutations are admin-gated; categories are global metadata, so admin-only global mutation is the correct and intended model, matching authors/series.

SQL injection — All queries use parameterized placeholders. No string concatenation in SQL paths.

Duplicate key detectionisDuplicateCategoryKey uses errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 — typed error check, not string matching (internal/categories/manage_service.go:784-786). Correct.

Rename semantics — Rename to current name: RowsAffected == 0 → existence check → category exists → returns nil (no-op success). Tested in manage_service_test.go "Category exists (same name, no-op)" context. Rename nonexistent: RowsAffected == 0 → existence check → empty rows → ErrNotFound → 404. Correct.

DELETE cascade — Relies on MySQL FK ON DELETE CASCADE on book_metadata_category_mapping. The single DELETE FROM category WHERE id = ? statement is atomic; cascade is automatic. No orphaned rows. 404 on missing via RowsAffected == 0.

CSRF — Both _patchRename and _deleteCategory send X-CSRF-Token read from <meta name="csrf-token">. This matches the established project pattern (base.html line 6 renders the meta tag; author_manage_controller.js reads it identically).

XSS — Category names flow through: server → {{.Name}} (html/template-escaped) → data-category-name attribute → btn.dataset.categoryNameel.textContent. All safe. Delete modal body uses el.textContent = text via _el(), never innerHTML.

CSP — No inline style= attributes in the template. The JS controller uses menu.style.setProperty("--menu-top", ...) — CSS custom property via JS, which is the project-approved alternative per the no-inline-style rule.

Tests — All test files declare package categories_test (black-box). Curried-DI pattern throughout. Handler tests fold nil-error into value assertion (Expect(resp.StatusCode, reqErr).To(...)). Service tests cover all branches: success, duplicate key, generic error, zero-rows-affected (both exists and not-exists sub-cases), RowsAffected error, and existence-check query failure. Vitest covers the full controller lifecycle — connect, disconnect, open menu, rename modal (all paths), delete modal (all paths), CSRF header, keyboard/backdrop dismissal.

Browser e2e — Ordered journey, BeforeAll with single shared browser/server, BeforeEach resets page timeout (go-rod gotcha respected). Justification comment explains why real Chromium is required.


[MINOR] static/js/test/category_manage_controller.test.js:1484-1490 — The DISCONNECT test sets ctrl._activeID, ctrl._activeName, and calls ctrl._openRenameModal() directly (white-box JS). JavaScript has no language-level module privacy, so this is workable and CI passes, but it couples the test to implementation naming. Matches the likely author controller test pattern. Does not block.

REVIEW VERDICT: 0 blocker, 0 major, 1 minor

## Code Review: bookshelf-0v44o.2 ### Phase 0: DEMO Verification CI is green (confirmed via commit status API). PR is mergeable. No DEMO block is present — this is a UI/mutation feature reviewed via diff + CI as the behavioral source of truth per project convention. ### Phase 1: Spec Compliance All stated requirements are present: - `PATCH /categories/{id}` rename handler — returns 409 on name conflict, 404 not-found, 204 success - `DELETE /categories/{id}` delete handler — cascades via FK, 404 not-found, 204 success - Admin-gated via `d.LibraryManageMetadataConfigRequired` on both routes (`internal/categories/routes.go:19-20`) - Kebab modal UI, Vitest controller tests, ordered browser e2e journey - `./internal/categories/...` added to `UNIT_PKGS` in `check-coverage.sh` (inclusion, not exclusion — correct) ### Phase 2: Code Quality **Authz** — Both mutation routes are correctly gated: `mux.Handle("PATCH /categories/{id}", manageRequired(eh.Wrap(rename)))` and `mux.Handle("DELETE /categories/{id}", manageRequired(eh.Wrap(delete)))` (`internal/categories/routes.go:19-20`). Wire passes `d.LibraryManageMetadataConfigRequired` — the same field used by the author/series pattern. **Multi-user** — Category browse remains library-scoped (via `getUserLibraryIDs` in `ListHandler`). Mutations are admin-gated; categories are global metadata, so admin-only global mutation is the correct and intended model, matching authors/series. **SQL injection** — All queries use parameterized placeholders. No string concatenation in SQL paths. **Duplicate key detection** — `isDuplicateCategoryKey` uses `errors.As(err, &mysqlErr) && mysqlErr.Number == 1062` — typed error check, not string matching (`internal/categories/manage_service.go:784-786`). Correct. **Rename semantics** — Rename to current name: `RowsAffected == 0` → existence check → category exists → returns nil (no-op success). Tested in `manage_service_test.go` "Category exists (same name, no-op)" context. Rename nonexistent: `RowsAffected == 0` → existence check → empty rows → `ErrNotFound` → 404. Correct. **DELETE cascade** — Relies on MySQL FK `ON DELETE CASCADE` on `book_metadata_category_mapping`. The single `DELETE FROM category WHERE id = ?` statement is atomic; cascade is automatic. No orphaned rows. 404 on missing via `RowsAffected == 0`. **CSRF** — Both `_patchRename` and `_deleteCategory` send `X-CSRF-Token` read from `<meta name="csrf-token">`. This matches the established project pattern (base.html line 6 renders the meta tag; `author_manage_controller.js` reads it identically). **XSS** — Category names flow through: server → `{{.Name}}` (html/template-escaped) → `data-category-name` attribute → `btn.dataset.categoryName` → `el.textContent`. All safe. Delete modal body uses `el.textContent = text` via `_el()`, never `innerHTML`. **CSP** — No inline `style=` attributes in the template. The JS controller uses `menu.style.setProperty("--menu-top", ...)` — CSS custom property via JS, which is the project-approved alternative per the no-inline-style rule. **Tests** — All test files declare `package categories_test` (black-box). Curried-DI pattern throughout. Handler tests fold nil-error into value assertion (`Expect(resp.StatusCode, reqErr).To(...)`). Service tests cover all branches: success, duplicate key, generic error, zero-rows-affected (both exists and not-exists sub-cases), RowsAffected error, and existence-check query failure. Vitest covers the full controller lifecycle — connect, disconnect, open menu, rename modal (all paths), delete modal (all paths), CSRF header, keyboard/backdrop dismissal. **Browser e2e** — Ordered journey, `BeforeAll` with single shared browser/server, `BeforeEach` resets page timeout (go-rod gotcha respected). Justification comment explains why real Chromium is required. --- [MINOR] `static/js/test/category_manage_controller.test.js:1484-1490` — The DISCONNECT test sets `ctrl._activeID`, `ctrl._activeName`, and calls `ctrl._openRenameModal()` directly (white-box JS). JavaScript has no language-level module privacy, so this is workable and CI passes, but it couples the test to implementation naming. Matches the likely author controller test pattern. Does not block. REVIEW VERDICT: 0 blocker, 0 major, 1 minor
Author
Owner

UI Review — PR #1091 (bookshelf-0v44o.2) — category rename/delete modals + kebab

Screenshots reviewed: rename-modal.png, delete-modal.png, rename-submitted.png.

What I see in the rendered screenshots

Rename modal — canonical chrome present: header ("Rename Category") with X close button top-right, padded body with "New name" label + pre-filled text input, right-aligned footer with Cancel (ghost) + Save (accent blue). Width is properly constrained (~480px). Input has focus ring. Clean, on-brand.

Delete modal — canonical chrome: header ("Delete Category") with X close button, body with clear confirmation text naming the category, right-aligned footer with Cancel (ghost) + Delete (red outlined, .btn-danger affordance). Correct danger styling. Clean.

Post-rename page — modal closes cleanly, card updates to new name, no artifacts.

Canonical-component reuse

The modals correctly reuse:

  • .modal-overlay + .modal-dialog + .modal-dialog--author-manage (canonical modal shell + sizing variant)
  • .modal-header / .modal-title / .modal-close-btn / .modal-body / .modal-footer (canonical sections)
  • .btn, .btn-ghost, .btn-danger (canonical buttons)
  • .library-kebab-menu__item, .library-kebab-menu__item--danger (canonical kebab item buttons)
  • .author-manage-menu (canonical shared popup menu — correctly shared, not re-invented)
  • .metadata-field-label, .metadata-field-input (canonical form field classes)
  • .am-error-msg (shared error message style from the author-manage section)

No bespoke parallel class system was invented. No inline style= attributes (JS uses style.setProperty() for CSS custom properties, which is CSP-safe).

Findings

[MINOR] static/css/main.css (missing rule) / templates/pages/categories_index.html:19 — category-card-kebab class applied in template has no CSS definition
The kebab button has class="btn btn-icon category-card-kebab" but .category-card-kebab has zero CSS rules (grep confirms). The button renders correctly only because .btn + .btn-icon supply all visual styling. The undefined class is dead weight — either add minimal positioning/sizing rules if future CSS needs a hook, or remove it and rely on [data-action] for JS targeting. No visual defect visible; purely a convention issue.

[MINOR] static/js/controllers/category_manage_controller.js:69,78,138 — author-manage-* / am-* namespace reused for category UI without aliasing
The controller builds menu <ul> with class author-manage-menu, <li> elements with author-manage-menu-item, and error paragraphs with am-error-msg. These are defined in the author-manage CSS section and the <li> class author-manage-menu-item has no CSS definition at all (same gap exists in author_manage_controller.js — this is a shared latent nit). Functionally harmless but makes the category controller appear to be "borrowing" author-manage identity. Ideal fix is to either rename these to domain-neutral names (manage-menu, manage-menu-item, manage-error-msg) or document them as shared in main.css.


REVIEW VERDICT: 0 blocker, 0 major, 2 minor

## UI Review — PR #1091 (bookshelf-0v44o.2) — category rename/delete modals + kebab Screenshots reviewed: rename-modal.png, delete-modal.png, rename-submitted.png. ### What I see in the rendered screenshots **Rename modal** — canonical chrome present: header ("Rename Category") with X close button top-right, padded body with "New name" label + pre-filled text input, right-aligned footer with Cancel (ghost) + Save (accent blue). Width is properly constrained (~480px). Input has focus ring. Clean, on-brand. **Delete modal** — canonical chrome: header ("Delete Category") with X close button, body with clear confirmation text naming the category, right-aligned footer with Cancel (ghost) + Delete (red outlined, `.btn-danger` affordance). Correct danger styling. Clean. **Post-rename page** — modal closes cleanly, card updates to new name, no artifacts. ### Canonical-component reuse The modals correctly reuse: - `.modal-overlay` + `.modal-dialog` + `.modal-dialog--author-manage` (canonical modal shell + sizing variant) - `.modal-header` / `.modal-title` / `.modal-close-btn` / `.modal-body` / `.modal-footer` (canonical sections) - `.btn`, `.btn-ghost`, `.btn-danger` (canonical buttons) - `.library-kebab-menu__item`, `.library-kebab-menu__item--danger` (canonical kebab item buttons) - `.author-manage-menu` (canonical shared popup menu — correctly shared, not re-invented) - `.metadata-field-label`, `.metadata-field-input` (canonical form field classes) - `.am-error-msg` (shared error message style from the author-manage section) No bespoke parallel class system was invented. No inline `style=` attributes (JS uses `style.setProperty()` for CSS custom properties, which is CSP-safe). ### Findings [MINOR] static/css/main.css (missing rule) / templates/pages/categories_index.html:19 — `category-card-kebab` class applied in template has no CSS definition The kebab button has `class="btn btn-icon category-card-kebab"` but `.category-card-kebab` has zero CSS rules (grep confirms). The button renders correctly only because `.btn` + `.btn-icon` supply all visual styling. The undefined class is dead weight — either add minimal positioning/sizing rules if future CSS needs a hook, or remove it and rely on `[data-action]` for JS targeting. No visual defect visible; purely a convention issue. [MINOR] static/js/controllers/category_manage_controller.js:69,78,138 — `author-manage-*` / `am-*` namespace reused for category UI without aliasing The controller builds menu `<ul>` with class `author-manage-menu`, `<li>` elements with `author-manage-menu-item`, and error paragraphs with `am-error-msg`. These are defined in the author-manage CSS section and the `<li>` class `author-manage-menu-item` has no CSS definition at all (same gap exists in `author_manage_controller.js` — this is a shared latent nit). Functionally harmless but makes the category controller appear to be "borrowing" author-manage identity. Ideal fix is to either rename these to domain-neutral names (`manage-menu`, `manage-menu-item`, `manage-error-msg`) or document them as shared in main.css. --- REVIEW VERDICT: 0 blocker, 0 major, 2 minor
fix(categories): apply review-round minors (bookshelf-0v44o.2)
Some checks failed
/ JS Unit Tests (pull_request) Successful in 1m6s
/ E2E API (pull_request) Successful in 2m39s
/ Lint (pull_request) Successful in 3m24s
/ Integration (pull_request) Successful in 3m25s
/ E2E Browser (pull_request) Failing after 3m45s
/ Test (pull_request) Successful in 6m43s
2091cae1c1
- Add max-length (>255) guard on rename name input, returning 400 ErrValidation
  instead of an opaque MySQL "Data too long" 500; add matching handler test.
- Remove dead CSS class category-card-kebab from categories_index.html template;
  class had no rules in main.css and JS targets via data-action, not class.
- Refactor DISCONNECT test to open rename modal via public user flow
  (kebab button -> menu -> Rename item) instead of directly setting
  ctrl._activeID/_activeName and calling ctrl._openRenameModal().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(categories): update e2e browser test selector after class removal
All checks were successful
/ JS Unit Tests (pull_request) Successful in 36s
/ E2E API (pull_request) Successful in 2m28s
/ Lint (pull_request) Successful in 3m11s
/ Integration (pull_request) Successful in 3m17s
/ E2E Browser (pull_request) Successful in 3m53s
/ Test (pull_request) Successful in 6m21s
643f4c3dec
category-card-kebab was a dead CSS class (no rules in main.css) flagged by
UI review. The browser e2e test was targeting it as a DOM selector — update
to button[data-category-id] which is already on every kebab button via the
template data attribute, so the selector is semantic and not a ghost class.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
zombor force-pushed bd-bookshelf-0v44o.2 from 643f4c3dec
All checks were successful
/ JS Unit Tests (pull_request) Successful in 36s
/ E2E API (pull_request) Successful in 2m28s
/ Lint (pull_request) Successful in 3m11s
/ Integration (pull_request) Successful in 3m17s
/ E2E Browser (pull_request) Successful in 3m53s
/ Test (pull_request) Successful in 6m21s
to 8cd3eb5d2e
All checks were successful
/ JS Unit Tests (pull_request) Successful in 1m7s
/ Lint (pull_request) Successful in 3m47s
/ E2E API (pull_request) Successful in 1m39s
/ Integration (pull_request) Successful in 3m10s
/ E2E Browser (pull_request) Successful in 3m1s
/ Test (pull_request) Successful in 6m55s
2026-07-11 00:16:11 +00:00
Compare
zombor merged commit 2e95c6e8fa into main 2026-07-11 00:23:26 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
zombor/pergamum!1091
No description provided.