feat(portal): implement dynamic drawer/page navigation management

Replace hardcoded sidebar drawer configuration with admin-manageable
dynamic system. Extend page_status.json with drawer definitions and
page assignments, add drawer CRUD API endpoints, render portal sidebar
via Jinja2 loops, and extend /admin/pages UI with drawer management.
Fix multi-worker cache invalidation via mtime-based staleness detection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
egg
2026-02-09 11:34:04 +08:00
parent 706c8ba52c
commit 9b1d2edc52
20 changed files with 2269 additions and 735 deletions

View File

@@ -0,0 +1,55 @@
## Purpose
Define stable requirements for drawer-management.
## Requirements
### Requirement: Admin SHALL be able to create drawers
The system SHALL allow admin users to create new navigation drawers via API, specifying a name, an order value, and an optional `admin_only` flag.
#### Scenario: Create a new drawer
- **WHEN** admin sends POST `/admin/api/drawers` with `{"name": "自訂分類", "order": 4}`
- **THEN** the system SHALL create a new drawer with a generated kebab-case id and persist it to `page_status.json`
#### Scenario: Create drawer with duplicate name
- **WHEN** admin sends POST `/admin/api/drawers` with a name that already exists
- **THEN** the system SHALL return 409 Conflict with an error message
### Requirement: Admin SHALL be able to rename drawers
The system SHALL allow admin users to update a drawer's name via API.
#### Scenario: Rename a drawer
- **WHEN** admin sends PUT `/admin/api/drawers/<id>` with `{"name": "新名稱"}`
- **THEN** the system SHALL update the drawer name and persist the change
### Requirement: Admin SHALL be able to reorder drawers
The system SHALL allow admin users to change a drawer's sort order via API.
#### Scenario: Change drawer order
- **WHEN** admin sends PUT `/admin/api/drawers/<id>` with `{"order": 2}`
- **THEN** the system SHALL update the drawer order and the sidebar SHALL reflect the new order on next page load
### Requirement: Admin SHALL be able to delete empty drawers
The system SHALL allow admin users to delete a drawer only when no pages are assigned to it.
#### Scenario: Delete an empty drawer
- **WHEN** admin sends DELETE `/admin/api/drawers/<id>` and the drawer has no assigned pages
- **THEN** the system SHALL remove the drawer from the configuration
#### Scenario: Attempt to delete a drawer with assigned pages
- **WHEN** admin sends DELETE `/admin/api/drawers/<id>` and the drawer still has assigned pages
- **THEN** the system SHALL return 409 Conflict with an error listing the assigned pages
### Requirement: Admin SHALL be able to list all drawers
The system SHALL provide an API to retrieve all drawers with their metadata.
#### Scenario: List all drawers
- **WHEN** admin sends GET `/admin/api/drawers`
- **THEN** the system SHALL return all drawers sorted by their `order` field
### Requirement: Drawer management SHALL be accessible from the admin UI
The existing `/admin/pages` page SHALL include a drawer management section where admin can create, rename, reorder, and delete drawers.
#### Scenario: Admin opens page management
- **WHEN** admin navigates to the page management UI
- **THEN** the UI SHALL display a drawer list with controls for add, rename, reorder, and delete

View File

@@ -0,0 +1,42 @@
## Purpose
Define stable requirements for page-drawer-assignment.
## Requirements
### Requirement: Admin SHALL be able to assign a page to a drawer
The system SHALL allow admin users to assign a page to a specific drawer by setting its `drawer_id` via the existing page update API.
#### Scenario: Assign page to a drawer
- **WHEN** admin sends PUT `/admin/api/pages/<route>` with `{"drawer_id": "queries"}`
- **THEN** the page SHALL be associated with the specified drawer
#### Scenario: Assign page to non-existent drawer
- **WHEN** admin sends PUT `/admin/api/pages/<route>` with a `drawer_id` that does not exist
- **THEN** the system SHALL return 400 Bad Request with an error message
### Requirement: Admin SHALL be able to set page order within a drawer
The system SHALL allow admin users to control the display order of pages within a drawer.
#### Scenario: Set page order
- **WHEN** admin sends PUT `/admin/api/pages/<route>` with `{"order": 3}`
- **THEN** the page SHALL be displayed at position 3 within its drawer on next portal load
### Requirement: Pages without a drawer assignment SHALL NOT appear in the sidebar
Pages that have no `drawer_id` (e.g., sub-pages like `/wip-detail`, `/hold-detail`) SHALL NOT be rendered in the portal sidebar, but SHALL remain accessible via their direct routes.
#### Scenario: Sub-page without drawer assignment
- **WHEN** a page exists in `page_status.json` without a `drawer_id`
- **THEN** the page SHALL NOT appear in any sidebar drawer
- **THEN** the page SHALL still be accessible via its direct URL
### Requirement: Page drawer assignment SHALL be configurable from the admin UI
The existing `/admin/pages` page table SHALL include a drawer assignment dropdown and order controls for each page.
#### Scenario: Admin changes page drawer via UI
- **WHEN** admin selects a different drawer from the dropdown for a page
- **THEN** the UI SHALL call the page update API with the new `drawer_id`
#### Scenario: Admin clears drawer assignment via UI
- **WHEN** admin selects "未分類" (unassigned) from the dropdown
- **THEN** the page's `drawer_id` SHALL be removed and the page SHALL no longer appear in the sidebar

View File

@@ -5,15 +5,41 @@ Define stable requirements for portal-drawer-navigation.
### Requirement: Portal Navigation SHALL Group Entries by Functional Drawers
The portal SHALL group navigation entries into functional drawers: reports, queries, and development tools.
The portal SHALL group navigation entries into functional drawers as defined in the `drawers` configuration of `page_status.json`, rendered dynamically via Jinja2 loop instead of hardcoded HTML.
#### Scenario: Drawer grouping visibility
- **WHEN** users open the portal
- **THEN** report pages and query pages SHALL appear in separate drawer groups
- **THEN** the sidebar SHALL display drawers in the order defined by each drawer's `order` field
- **THEN** each drawer SHALL show only the pages assigned to it via `drawer_id`, sorted by each page's `order` field
#### Scenario: Admin-only drawer visibility
- **WHEN** a drawer has `admin_only: true` and the current user is not admin
- **THEN** the drawer and all its pages SHALL NOT be rendered in the sidebar
#### Scenario: Empty drawer visibility
- **WHEN** a drawer has no visible pages (all filtered out by `can_view_page()`)
- **THEN** the drawer group title SHALL NOT be rendered
### Requirement: Existing Page Behavior SHALL Remain Compatible
The portal navigation refactor SHALL preserve existing target routes and lazy-load behavior for content frames.
#### Scenario: Route continuity
- **WHEN** a user selects an existing page entry from the new drawer
- **WHEN** a user selects an existing page entry from a dynamically rendered drawer
- **THEN** the corresponding original route SHALL be loaded without changing page business logic behavior
#### Scenario: Iframe lazy-load continuity
- **WHEN** a sidebar item is clicked for the first time
- **THEN** the iframe SHALL lazy-load its content from the page's route, consistent with current behavior
### Requirement: First-run migration SHALL populate drawer configuration automatically
When `page_status.json` does not contain a `drawers` field, the system SHALL automatically create the default drawer structure matching the current hardcoded layout and assign existing pages to their corresponding drawers.
#### Scenario: First startup after deployment
- **WHEN** the application starts and `page_status.json` has no `drawers` field
- **THEN** the system SHALL create three default drawers (報表類, 查詢類, 開發工具)
- **THEN** the system SHALL assign each existing page to its historically correct drawer
- **THEN** the system SHALL persist the updated configuration immediately
#### Scenario: Subsequent startup
- **WHEN** the application starts and `page_status.json` already contains a `drawers` field
- **THEN** the system SHALL NOT modify the existing drawer configuration