From 2f5650632f50218370a1916f845ea9ceb23cf1d7 Mon Sep 17 00:00:00 2001 From: willxue Date: Mon, 8 Jun 2026 12:14:28 +0800 Subject: [PATCH] Improve open-source adoption and maintenance paths The remaining polish work turns the template from a private-ready scaffold into something easier for outside users to evaluate, fork, configure, and maintain. It adds conservative metadata initialization, contributor issue and PR surfaces, dependency automation, hosted-demo/release guidance, backend contract documentation, and an explicit accessibility/E2E maturity path without expanding the dependency stack. Constraint: Item 1 was already merged on main before this branch; this commit covers the remaining open-source readiness items. Constraint: No release tag was created; release docs only describe the tag workflow for maintainers. Rejected: Add Playwright or axe as a new dependency now | no-dependency smoke/docs checks keep this polish slice low-risk and consistent with the current script-based verification model. Confidence: high Scope-risk: moderate Directive: Replace the docs-only e2e contract with real browser automation once the project accepts a browser test dependency and maintenance path. Tested: node scripts/init-template.mjs --help Tested: init-template preview and --write against a temporary root Tested: npx eslint scripts/init-template.mjs scripts/e2e-smoke.mjs Tested: npm run smoke:e2e Tested: npm run smoke:a11y Tested: npm audit --omit=dev --registry=https://registry.npmjs.org Tested: npm run verify Not-tested: GitHub-hosted CI and Pages deployment after push Co-authored-by: OmX --- .github/ISSUE_TEMPLATE/bug_report.yml | 85 ++++ .github/ISSUE_TEMPLATE/config.yml | 8 + .github/ISSUE_TEMPLATE/feature_request.yml | 67 +++ .github/dependabot.yml | 24 + .github/pull_request_template.md | 19 + README.md | 40 ++ docs/accessibility.md | 106 +++++ docs/backend-integration.md | 522 +++++++++++++++++++++ docs/demo.md | 33 ++ docs/release.md | 39 ++ package.json | 4 +- scripts/e2e-smoke.mjs | 57 +++ scripts/init-template.mjs | 357 ++++++++++++++ 13 files changed, 1360 insertions(+), 1 deletion(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/pull_request_template.md create mode 100644 docs/accessibility.md create mode 100644 docs/backend-integration.md create mode 100644 docs/demo.md create mode 100644 docs/release.md create mode 100644 scripts/e2e-smoke.mjs create mode 100644 scripts/init-template.mjs diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..08019d7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,85 @@ +name: Bug report +description: Report a reproducible problem in the admin template. +title: "[Bug]: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for the report. Include the smallest reproduction you can and the verification command output you already ran. + - type: textarea + id: summary + attributes: + label: Summary + description: What went wrong? + validations: + required: true + - type: textarea + id: steps + attributes: + label: Reproduction steps + description: List the smallest steps needed to reproduce the issue. + placeholder: | + 1. Run npm ci + 2. Run npm run dev + 3. Open ... + validations: + required: true + - type: dropdown + id: scope + attributes: + label: Affected area + options: + - Template initialization + - App shell and layout + - Auth and permissions + - API adapter and services + - CRUD pages and forms + - Documentation + - Tooling or build + - Other + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual behavior + validations: + required: true + - type: input + id: version + attributes: + label: Version or commit + placeholder: v0.1.0 or commit SHA + - type: textarea + id: environment + attributes: + label: Environment + placeholder: OS, Node version, browser, API mode + validations: + required: true + - type: textarea + id: verification + attributes: + label: Verification + description: Paste relevant command output, such as npm run verify or a focused smoke script. + placeholder: | + npm run verify + npm run smoke:template + validations: + required: true + - type: checkboxes + id: checks + attributes: + label: Checks + options: + - label: I searched for existing issues covering the same problem. + required: true + - label: I can still reproduce this issue on the current default branch. + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..05cc74e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Security policy + url: https://github.com/WeOpen/WeBase/blob/main/SECURITY.md + about: Report security issues privately instead of opening a public issue. + - name: Template extension docs + url: https://github.com/WeOpen/WeBase/blob/main/docs/add-admin-module.md + about: Review the module extension guide before opening implementation questions. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..05edbdd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,67 @@ +name: Feature request +description: Suggest an improvement for the template. +title: "[Feature]: " +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Prefer changes that keep the template reusable, dependency-light, and easy to verify. + - type: textarea + id: problem + attributes: + label: Problem + description: What user or maintainer problem would this solve? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposal + description: Describe the desired behavior or API. + validations: + required: true + - type: textarea + id: scope + attributes: + label: Proposed scope + description: Call out the files, modules, or workflows you expect to change. + placeholder: README, docs, route registry, API adapter, init script, etc. + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + - type: dropdown + id: area + attributes: + label: Area + options: + - Community and documentation + - Template initialization + - App shell + - API adapter + - Auth and permissions + - CRUD patterns + - Documentation + - Testing and CI + - Other + validations: + required: true + - type: textarea + id: verification + attributes: + label: Verification plan + description: What command output, smoke checks, or user flow should prove this is done? + - type: textarea + id: context + attributes: + label: Additional context + - type: checkboxes + id: checks + attributes: + label: Checks + options: + - label: I searched for existing requests covering this idea. + required: true + - label: This proposal preserves the template's generic, reusable positioning. + required: true diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..c5910ce --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,24 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + groups: + next-stack: + patterns: + - "next" + - "eslint-config-next" + - "@next/*" + react-stack: + patterns: + - "react" + - "react-dom" + - "@types/react" + - "@types/react-dom" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..085eb0d --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,19 @@ +## Summary + +- Describe the user-facing or maintainer-facing change. +- Link the issue, docs item, or polish checklist item when applicable. + +## Verification + +- [ ] `npm run verify` +- [ ] Focused command or smoke script for the changed area: + +```bash +# paste the exact command(s) you ran +``` + +## Checklist + +- [ ] I updated docs when the template workflow or extension surface changed. +- [ ] I avoided new dependencies or explained why they were necessary. +- [ ] I called out any remaining risk, follow-up, or intentionally deferred work. diff --git a/README.md b/README.md index d0fe68f..a08cb00 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,32 @@ npm run dev Open `http://localhost:3000`, then sign in with the mock credentials below. +## Template initialization + +The repository ships with WeBase branding in repository metadata and app metadata. To preview a conservative first-pass rename, run: + +```bash +npm run init:template -- --name "Acme Admin" --short-name "Acme" --description "A production-ready Next.js admin template for Acme operations." --github-owner acme --github-repo admin-console +``` + +Add `--write` to apply the changes after reviewing the preview output. + +The script is intentionally conservative: + +- It updates only `package.json`, `README.md`, `public/manifest.json`, and `src/app/layout.tsx`. +- It skips fields that no longer match the shipped WeBase defaults instead of overwriting customized values. +- If you change `package.json` name, refresh the lockfile metadata with `npm install --package-lock-only`. + +## Demo + +Run the template locally with `npm run dev`, or use the GitHub Pages build after it has deployed from `main`: `https://weopen.github.io/WeBase/`. + +The demo runs in mock mode by default, so it is safe to explore CRUD flows, permissions, navigation, audit logs, settings, and `/system/health` without a backend. For demo routes, screenshot guidance, and deployment notes, see [Demo guide](docs/demo.md). + +## Screenshots + +Screenshots are intentionally not checked in until they are captured from a verified build. Recommended captures for a release are the login page, dashboard, user management, role permissions, and system health views. Store approved images under `public/screenshots/` and reference them here after verifying the files render in GitHub and the Pages site. + ## Tech stack - Next.js 16 App Router @@ -66,6 +92,7 @@ Copy `.env.example` to `.env.local` and adjust as needed: npm run dev npm run verify npm run lint +npm run smoke:e2e npm run smoke:template npm run smoke:production npm run build @@ -75,11 +102,18 @@ npm run start - `npm run dev` starts the local development server. - `npm run verify` runs the full CI gate: standards lint, production smoke suite, TypeScript, and production build. - `npm run lint` runs ESLint across the project. +- `npm run smoke:e2e` verifies that the documented browser/E2E and backend integration workflows remain linked and reviewable without adding a browser test dependency. - `npm run smoke:template` verifies template extension contracts such as route registry, RBAC, API adapter, form validation, docs, status pages, theme tokens, and dashboard chart loading. - `npm run smoke:production` runs the production readiness smoke suite, including i18n, auth, API, permissions, CRUD, audit, accessibility, template, and `/system/health` checks. - `npm run build` creates a production Next.js build and verifies buildable routes. - `npm run start` serves the production build after `npm run build`. +## CI and automation + +GitHub Actions runs `CI` for every pull request and for every push to `main`. The Pages deployment workflow runs only after pushes to `main`. + +To verify workflow status in GitHub, open the pull request or commit and inspect the status checks section. Use the Actions tab when you need job logs for `CI` or `Deploy to GitHub Pages`. + ## Release verification Before connecting a real backend or deploying a release, run: @@ -90,6 +124,8 @@ npm run verify Then open `/system/health` to confirm API mode, backend/mock status, frontend version, build target, and operational indicators. +Treat the template as release-ready only after `npm run verify` passes on the release candidate and the GitHub `CI` workflow is green for the target commit. For the full release checklist, changelog alignment, and tag guidance, see [Release checklist](docs/release.md). + ## Production security boundary Frontend RBAC, route guards, mock sessions, and permission-aware UI controls are template conveniences. A production backend must enforce authentication, authorization, data-scope checks, session expiry, audit logging, CSRF protection where applicable, and rate limiting. Do not treat hidden buttons or client route redirects as security controls. @@ -102,11 +138,15 @@ PostCSS is pinned through npm `overrides` so transitive consumers use a patched - [Add an admin module](docs/add-admin-module.md) - [API adapter guide](docs/api-adapter.md) +- [Backend integration contract](docs/backend-integration.md) +- [Accessibility maturity](docs/accessibility.md) +- [Demo guide](docs/demo.md) - [Permissions and RBAC](docs/permissions.md) - [Contributing](CONTRIBUTING.md) - [Security policy](SECURITY.md) - [Code of conduct](CODE_OF_CONDUCT.md) - [Changelog](CHANGELOG.md) +- [Release checklist](docs/release.md) - [Admin template hardening plan](docs/plans/2026-06-05-admin-template-hardening.md) ## API adapter note diff --git a/docs/accessibility.md b/docs/accessibility.md new file mode 100644 index 0000000..204f45a --- /dev/null +++ b/docs/accessibility.md @@ -0,0 +1,106 @@ +# Accessibility Maturity + +WeBase currently uses source-level accessibility smoke checks plus manual browser verification. This keeps open-source maintenance lightweight while documenting the minimum quality bar for contributors and downstream adopters. + +## Current status + +- Automated coverage today is source-contract smoke, not a full browser accessibility runner. +- `npm run smoke:a11y` checks for accessible dialog, sheet, select, table, search, and icon-button patterns in source. +- The project does not currently ship Playwright or axe-based browser automation. +- Manual checks are still required before release or after meaningful UI changes. + +## Why there is no browser harness yet + +This repository already standardizes on small no-dependency smoke scripts under `scripts/` for template maturity checks. Adding Playwright only for open-source polish would introduce a new dependency stack, browser install flow, and maintenance surface without enough existing E2E infrastructure to justify it in this slice. + +The recommended near-term path is: + +1. Keep source smoke checks fast and explicit. +2. Run focused manual keyboard/screen-reader checks on changed surfaces. +3. Add browser automation later when the project needs real interaction coverage beyond static contract checks. + +## Automated source smoke coverage + +Run: + +```bash +npm run smoke:a11y +``` + +The current script validates these patterns: + +- dialogs and sheets expose `role="dialog"`, `aria-modal`, labels, focus management, and `Escape` handling +- select controls expose combobox/listbox semantics and keyboard navigation +- tables expose header scope, selection labels, and empty-state support +- global search exposes an accessible input and initial focus +- icon-only buttons expose `aria-label` +- contributor docs keep accessibility requirements visible + +This smoke suite is intentionally shallow. It proves that key source patterns still exist; it does not prove that the composed UI is fully accessible in a browser. + +## Manual browser checks + +Run the app locally: + +```bash +npm run dev +``` + +Check these routes after UI or layout changes: + +- `/login` +- `/dashboard` +- `/system/users` +- `/system/roles` +- `/system/menus` +- `/system/audit-logs` +- `/system/settings` +- `/system/components` + +For each changed route, verify: + +1. `Tab` and `Shift+Tab` move in a predictable order. +2. Focus is visible on links, buttons, inputs, and menu triggers. +3. `Enter` and `Space` activate buttons and selections where expected. +4. `Escape` closes dialogs, sheets, and search overlays and returns focus to the previous trigger. +5. Dialogs trap focus while open. +6. Table actions, filters, and pagination are reachable without a mouse. +7. Icon-only controls announce meaningful names. +8. Page headings and section labels are unique and understandable out of visual context. +9. Light/dark theme states preserve readable contrast for body text, labels, and status badges. +10. Forbidden, error, session-expired, and network-error routes remain understandable with keyboard-only navigation. + +## Screen reader spot checks + +At minimum, do a spot check with one screen reader before release: + +- Windows: Narrator or NVDA +- macOS: VoiceOver + +Suggested spot-check flow: + +1. Open `/login` and confirm form labels, password field, submit action, and validation copy are announced. +2. Open `/system/users` and confirm the page heading, table headers, row selection, and action buttons are announced. +3. Open a dialog or sheet and confirm title, description, and close behavior are announced correctly. +4. Trigger a forbidden or session-expired state and confirm the status message is understandable without visual styling. + +## E2E placeholder workflow + +Run: + +```bash +npm run smoke:e2e +``` + +This command is a no-dependency contract check that confirms the accessibility document, backend integration document, and README links stay present. It is not a browser test runner. Its purpose is to keep the documented manual E2E workflow from drifting or disappearing during template cleanup. + +## When to upgrade to Playwright + +Introduce Playwright or another browser runner when at least one of these becomes true: + +- auth/session flows need interaction-level regression coverage +- contributors start breaking focus management or overlay behavior repeatedly +- HTTP mode becomes a standard supported workflow instead of a documented integration path +- visual/a11y regressions are hard to catch with source smoke plus manual review + +Until then, the current maturity target is: fast source smoke, explicit manual checks, and documented browser verification expectations. diff --git a/docs/backend-integration.md b/docs/backend-integration.md new file mode 100644 index 0000000..a4db0a9 --- /dev/null +++ b/docs/backend-integration.md @@ -0,0 +1,522 @@ +# Backend Integration Contract + +WeBase routes backend calls through `src/lib/api/client.ts` and the service layer in `src/lib/services/*`. This document defines the concrete HTTP contract expected when `NEXT_PUBLIC_API_MODE=http`. + +Use [API adapter guide](api-adapter.md) for adapter behavior and error normalization. Use this document when wiring a real backend to the frontend service contracts. + +## Transport rules + +- Base URL comes from `NEXT_PUBLIC_API_BASE_URL`. +- Requests are JSON unless otherwise noted. +- The frontend sends same-origin credentials in HTTP mode. +- `GET` requests use `cache: "no-store"`. +- Every success payload should follow the shared wrapper: + +```json +{ + "code": 0, + "message": "ok", + "data": {}, + "requestId": "req_01JX9M7V8Y0B9QG1X2A3" +} +``` + +- Non-zero `code` values are treated as business errors. +- `401` should map to `session_expired`. +- `403` should map to `forbidden`. +- Return `requestId` in the body and preferably also as `x-request-id`. + +## OpenAPI-style summary + +```yaml +openapi: 3.1.0 +info: + title: WeBase Backend Contract + version: 0.1.0 +servers: + - url: https://api.example.com +paths: + /auth/login: + post: + summary: Validate credentials and create a session + /auth/current-user: + get: + summary: Return the authenticated user snapshot + /auth/permissions: + get: + summary: Return backend-authoritative permission keys + /auth/logout: + post: + summary: Revoke the current session + /users: + get: + summary: List users with paging and filters + post: + summary: Create a user + /users/{id}: + put: + summary: Update a user + delete: + summary: Delete a user + /roles: + get: + summary: List roles + post: + summary: Create a role + /roles/{id}: + put: + summary: Update a role + delete: + summary: Delete a role + /audit-logs: + get: + summary: List audit events with operational filters + /health: + get: + summary: Return backend health for the system health view +``` + +## Shared schemas + +### `CurrentUser` + +```json +{ + "id": "user_001", + "name": "System Administrator", + "username": "admin", + "email": "admin@example.com", + "role": "Super Admin", + "avatar": "https://cdn.example.com/avatar/admin.png", + "permissions": [ + "dashboard:view", + "system:users:view", + "system:users:write", + "system:roles:view", + "system:roles:write", + "system:health:view" + ] +} +``` + +### `LoginPayload` + +```json +{ + "username": "admin", + "password": "admin123", + "remember": true +} +``` + +### `LoginResult` + +```json +{ + "token": "session_token_abc123", + "user": { + "id": "user_001", + "name": "System Administrator", + "username": "admin", + "email": "admin@example.com", + "role": "Super Admin", + "permissions": [ + "dashboard:view", + "system:users:view", + "system:users:write" + ] + }, + "expiresAt": "2026-06-09T12:00:00.000Z" +} +``` + +### `PermissionListResult` + +```json +{ + "permissions": [ + "dashboard:view", + "system:users:view", + "system:users:write", + "system:roles:view", + "system:roles:write" + ] +} +``` + +### `PageResult` + +```json +{ + "list": [ + { + "id": "user_001", + "username": "admin", + "name": "System Administrator", + "email": "admin@example.com", + "role": "Super Admin", + "status": "enabled", + "createdAt": "2026-05-01T08:00:00.000Z" + } + ], + "total": 1, + "page": 1, + "pageSize": 10 +} +``` + +## Auth endpoints + +### `POST /auth/login` + +Request: + +```json +{ + "username": "admin", + "password": "admin123", + "remember": true +} +``` + +Success response: + +```json +{ + "code": 0, + "message": "ok", + "data": { + "token": "session_token_abc123", + "user": { + "id": "user_001", + "name": "System Administrator", + "username": "admin", + "email": "admin@example.com", + "role": "Super Admin", + "permissions": [ + "dashboard:view", + "system:users:view", + "system:users:write" + ] + }, + "expiresAt": "2026-06-09T12:00:00.000Z" + }, + "requestId": "req_login_001" +} +``` + +Failure example: + +```json +{ + "code": 10001, + "message": "Invalid username or password", + "data": null, + "requestId": "req_login_002" +} +``` + +### `GET /auth/current-user` + +Success response: + +```json +{ + "code": 0, + "message": "ok", + "data": { + "id": "user_001", + "name": "System Administrator", + "username": "admin", + "email": "admin@example.com", + "role": "Super Admin", + "permissions": [ + "dashboard:view", + "system:users:view", + "system:users:write", + "system:roles:view", + "system:roles:write" + ] + }, + "requestId": "req_me_001" +} +``` + +### `GET /auth/permissions` + +Success response: + +```json +{ + "code": 0, + "message": "ok", + "data": { + "permissions": [ + "dashboard:view", + "system:users:view", + "system:users:write", + "system:roles:view", + "system:roles:write" + ] + }, + "requestId": "req_perm_001" +} +``` + +### `POST /auth/logout` + +Success response: + +```json +{ + "code": 0, + "message": "ok", + "data": { + "ok": true + }, + "requestId": "req_logout_001" +} +``` + +`204 No Content` is also acceptable for logout. + +## CRUD endpoints + +### Users + +#### `GET /users?page=1&pageSize=10&keyword=admin&status=enabled` + +Success response: + +```json +{ + "code": 0, + "message": "ok", + "data": { + "list": [ + { + "id": "user_001", + "username": "admin", + "name": "System Administrator", + "email": "admin@example.com", + "role": "Super Admin", + "status": "enabled", + "createdAt": "2026-05-01T08:00:00.000Z" + } + ], + "total": 1, + "page": 1, + "pageSize": 10 + }, + "requestId": "req_users_001" +} +``` + +#### `POST /users` + +Request: + +```json +{ + "username": "jdoe", + "name": "Jane Doe", + "email": "jane.doe@example.com", + "role": "Operations Admin", + "status": "enabled", + "createdAt": "2026-06-08T09:00:00.000Z" +} +``` + +Success response returns the created `UserRecord`. + +#### `PUT /users/{id}` + +Request: + +```json +{ + "name": "Jane D. Doe", + "role": "Operations Lead", + "status": "enabled" +} +``` + +Success response returns the updated `UserRecord`. + +#### `DELETE /users/{id}` + +Success response: + +```json +{ + "code": 0, + "message": "ok", + "data": { + "id": "user_002" + }, + "requestId": "req_users_delete_001" +} +``` + +### Roles + +#### `GET /roles` + +Success response: + +```json +{ + "code": 0, + "message": "ok", + "data": [ + { + "id": "role_001", + "name": "Super Admin", + "code": "super_admin", + "description": "Full tenant administration", + "userCount": 2, + "status": "enabled", + "permissions": [ + "/dashboard", + "/system/users" + ], + "actionPermissions": [ + "system:users:write", + "system:roles:write" + ], + "dataScope": "all" + } + ], + "requestId": "req_roles_001" +} +``` + +#### `POST /roles` and `PUT /roles/{id}` + +Request body example: + +```json +{ + "name": "Operations Admin", + "code": "ops_admin", + "description": "Operates day-to-day user workflows", + "userCount": 5, + "status": "enabled", + "permissions": [ + "/dashboard", + "/system/users" + ], + "actionPermissions": [ + "system:users:view", + "system:users:write" + ], + "dataScope": "department" +} +``` + +#### `DELETE /roles/{id}` + +Success response: + +```json +{ + "code": 0, + "message": "ok", + "data": { + "id": "role_002" + }, + "requestId": "req_roles_delete_001" +} +``` + +## Audit endpoint + +### `GET /audit-logs` + +Supported query params: + +- `keyword` +- `level=info|warning|danger|all` +- `result=success|failure|all` +- `actor` +- `module` +- `createdFrom` +- `createdTo` + +Success response: + +```json +{ + "code": 0, + "message": "ok", + "data": [ + { + "id": "audit_001", + "actor": "admin", + "action": "update", + "target": "user:user_002", + "module": "system.users", + "level": "info", + "result": "success", + "ipAddress": "203.0.113.10", + "userAgent": "Mozilla/5.0", + "requestId": "req_users_update_010", + "createdAt": "2026-06-08T09:21:12.000Z", + "beforeSnapshot": { + "role": "Viewer" + }, + "afterSnapshot": { + "role": "Operations Admin" + } + } + ], + "requestId": "req_audit_001" +} +``` + +Mutation endpoints should emit audit events that use the same `requestId` as the originating API call. + +## Health endpoint + +### `GET /health` + +This endpoint is backend-facing and complements the frontend `getSystemHealth()` summary shown on `/system/health`. + +Success response example: + +```json +{ + "code": 0, + "message": "ok", + "data": { + "status": "ok", + "service": "webase-api", + "version": "1.4.2", + "time": "2026-06-08T09:30:00.000Z", + "dependencies": [ + { + "name": "postgres", + "status": "ok", + "detail": "Primary reachable" + }, + { + "name": "redis", + "status": "ok", + "detail": "Cache reachable" + } + ] + }, + "requestId": "req_health_001" +} +``` + +If your backend already exposes a different health shape, normalize it in the HTTP adapter or in a backend-for-frontend layer before consuming it from the UI. + +## Integration checklist + +1. Keep page/components calling `src/lib/services/*`, not adapters directly. +2. Return the backend-authoritative permission list during login and current-user refresh. +3. Enforce authorization and data scope on the server for every mutation and list endpoint. +4. Preserve `requestId` through success, business error, and transport error paths. +5. Emit audit events for create, update, delete, revoke, and high-risk auth/session actions. +6. Verify with `npm run smoke:api`, `npm run smoke:auth`, `npm run smoke:audit`, and `npm run verify`. diff --git a/docs/demo.md b/docs/demo.md new file mode 100644 index 0000000..3589f35 --- /dev/null +++ b/docs/demo.md @@ -0,0 +1,33 @@ +# Demo and Screenshots + +The template can be previewed locally or through GitHub Pages once the Pages workflow has deployed `main`. + +## Local demo + +```bash +npm ci +cp .env.example .env.local +npm run dev +``` + +Open `http://localhost:3000` and sign in with the mock account shown on the login page. + +## Recommended screenshot set + +When preparing a release page or project README, capture these views at desktop width and at a mobile width around 390px: + +- `/login` - branded entry and mock credential state +- `/dashboard` - metrics and operational charts +- `/system/users` - CRUD table, filters, bulk actions, and CSV export +- `/system/health` - runtime mode and production readiness indicators + +Store committed screenshots under `public/screenshots/` and reference them from `README.md` only after they are current for the release. + +## Online demo checklist + +Before linking a hosted demo: + +- GitHub Pages workflow is green. +- `PAGES_BUILD=true` deployment renders routes correctly. +- Demo copy clearly states that mock data is in-memory and not persisted. +- Demo credentials are mock-only and not valid for real deployments. diff --git a/docs/release.md b/docs/release.md new file mode 100644 index 0000000..9d30c05 --- /dev/null +++ b/docs/release.md @@ -0,0 +1,39 @@ +# Release Guide + +Use this checklist when publishing an open-source release or tagging a stable template snapshot. + +## Before tagging + +1. Confirm `main` is up to date with the release branch. +2. Run the full local gate: + +```bash +npm run verify +npm audit --omit=dev --registry=https://registry.npmjs.org +``` + +3. Confirm GitHub Actions is green for the merge commit on `main`. +4. Review `CHANGELOG.md` and move unreleased notes into the target version. +5. Check `/system/health` in mock mode and, when available, against a staging HTTP backend. + +## Tagging + +Use semantic version tags: + +```bash +git tag v0.1.0 +git push origin v0.1.0 +``` + +Create a GitHub release from the tag and include: + +- highlights from `CHANGELOG.md` +- verification commands and results +- known dependency or browser-support notes +- migration notes for template consumers + +## After release + +- Confirm the GitHub Pages deployment finishes successfully. +- Confirm Dependabot is enabled for npm and GitHub Actions updates. +- Open follow-up issues for any deferred accessibility, E2E, or backend integration work. diff --git a/package.json b/package.json index 0ba651e..0f20047 100644 --- a/package.json +++ b/package.json @@ -27,11 +27,13 @@ "scripts": { "dev": "next dev", "build": "next build", + "init:template": "node scripts/init-template.mjs", "start": "next start", "lint": "eslint .", "lint:standards": "node scripts/lint-standards-smoke.mjs", - "verify": "npm run lint:standards && npm run smoke:production && npx tsc --noEmit --pretty false --project tsconfig.json && npm run build", + "verify": "npm run lint:standards && npm run smoke:e2e && npm run smoke:production && npx tsc --noEmit --pretty false --project tsconfig.json && npm run build", "smoke:a11y": "node scripts/accessibility-smoke.mjs", + "smoke:e2e": "node scripts/e2e-smoke.mjs", "smoke:api": "node scripts/api-contract-smoke.mjs", "smoke:audit": "node scripts/audit-contract-smoke.mjs", "smoke:auth": "node scripts/auth-contract-smoke.mjs", diff --git a/scripts/e2e-smoke.mjs b/scripts/e2e-smoke.mjs new file mode 100644 index 0000000..8235562 --- /dev/null +++ b/scripts/e2e-smoke.mjs @@ -0,0 +1,57 @@ +import { existsSync, readFileSync } from "node:fs"; + +function read(path) { + return existsSync(path) ? readFileSync(path, "utf8") : ""; +} + +const files = { + packageJson: "package.json", + readme: "README.md", + accessibilityDoc: "docs/accessibility.md", + backendIntegrationDoc: "docs/backend-integration.md", +}; + +const source = Object.fromEntries(Object.entries(files).map(([key, path]) => [key, read(path)])); + +const checks = [ + { + name: "package exposes the e2e smoke command", + pass: source.packageJson.includes('"smoke:e2e"') && source.packageJson.includes("e2e-smoke.mjs"), + }, + { + name: "accessibility doc explains source smoke and manual browser verification", + pass: + source.accessibilityDoc.includes("npm run smoke:a11y") && + source.accessibilityDoc.includes("Manual browser checks") && + source.accessibilityDoc.includes("Screen reader spot checks") && + source.accessibilityDoc.includes("npm run smoke:e2e"), + }, + { + name: "backend integration doc covers auth, permissions, crud, audit, and health", + pass: + source.backendIntegrationDoc.includes("/auth/login") && + source.backendIntegrationDoc.includes("/auth/current-user") && + source.backendIntegrationDoc.includes("/auth/permissions") && + source.backendIntegrationDoc.includes("/users") && + source.backendIntegrationDoc.includes("/roles") && + source.backendIntegrationDoc.includes("/audit-logs") && + source.backendIntegrationDoc.includes("/health"), + }, + { + name: "README links the new maturity docs", + pass: + source.readme.includes("docs/accessibility.md") && + source.readme.includes("docs/backend-integration.md") && + source.readme.includes("smoke:e2e"), + }, +]; + +const failures = checks.filter((check) => !check.pass); + +if (failures.length > 0) { + console.error("E2E smoke failed:"); + for (const failure of failures) console.error(`- ${failure.name}`); + process.exitCode = 1; +} else { + console.log("PASS e2e smoke checks"); +} diff --git a/scripts/init-template.mjs b/scripts/init-template.mjs new file mode 100644 index 0000000..47581df --- /dev/null +++ b/scripts/init-template.mjs @@ -0,0 +1,357 @@ +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; + +const DEFAULTS = { + packageName: "webase-admin-template", + projectName: "WeBase Admin", + projectShortName: "WeBase", + templateName: "WeBase Admin Template", + packageDescription: "A production-ready Next.js admin template for generic management consoles.", + metadataDescription: "WeBase admin-system frontend template for operational management.", + manifestDescription: "WeBase admin-system frontend template.", + githubOwner: "WeOpen", + githubRepo: "WeBase", +}; + +const args = parseArgs(process.argv.slice(2)); + +if (args.help) { + printUsage(); + process.exit(0); +} + +const hasOverrides = + Boolean(args.name) || + Boolean(args.shortName) || + Boolean(args.description) || + Boolean(args.packageName) || + Boolean(args.githubOwner) || + Boolean(args.githubRepo); + +if (!hasOverrides) { + console.error("No template metadata overrides provided."); + printUsage(); + process.exit(1); +} + +if ((args.githubOwner && !args.githubRepo) || (!args.githubOwner && args.githubRepo)) { + console.error("Pass --github-owner and --github-repo together."); + process.exit(1); +} + +const rootDir = path.resolve(args.root ?? "."); +const projectName = args.name ?? DEFAULTS.projectName; +const shortName = args.shortName ?? args.name ?? DEFAULTS.projectShortName; +const templateName = args.name ? `${args.name} Template` : DEFAULTS.templateName; +const packageDescription = args.description ?? DEFAULTS.packageDescription; +const metadataDescription = args.description ?? DEFAULTS.metadataDescription; +const manifestDescription = args.description ?? DEFAULTS.manifestDescription; +const packageName = args.packageName ?? DEFAULTS.packageName; +const githubOwner = args.githubOwner ?? DEFAULTS.githubOwner; +const githubRepo = args.githubRepo ?? DEFAULTS.githubRepo; +const repositoryUrl = `https://github.com/${githubOwner}/${githubRepo}`; + +const results = [ + updatePackageJson({ + rootDir, + write: args.write, + packageName, + packageDescription, + repositoryUrl, + }), + updateTextFile({ + rootDir, + relativePath: "README.md", + write: args.write, + replacements: [ + { + label: "README title", + before: `# ${DEFAULTS.templateName}`, + after: `# ${templateName}`, + }, + { + label: "README intro sentence", + before: + "WeBase Admin Template is an independent Next.js App Router admin scaffold for building operational management consoles.", + after: `${templateName} is ${ensureSentence(packageDescription)}`, + }, + ], + }), + updateTextFile({ + rootDir, + relativePath: "public/manifest.json", + write: args.write, + replacements: [ + { + label: "manifest name", + before: `"name": "${DEFAULTS.projectName}"`, + after: `"name": "${projectName}"`, + }, + { + label: "manifest short_name", + before: `"short_name": "${DEFAULTS.projectShortName}"`, + after: `"short_name": "${shortName}"`, + }, + { + label: "manifest description", + before: `"description": "${DEFAULTS.manifestDescription}"`, + after: `"description": "${manifestDescription}"`, + }, + ], + }), + updateTextFile({ + rootDir, + relativePath: "src/app/layout.tsx", + write: args.write, + replacements: [ + { + label: "metadata default title", + before: `default: "${DEFAULTS.projectName}"`, + after: `default: "${projectName}"`, + }, + { + label: "metadata title template", + before: `template: "%s | ${DEFAULTS.projectName}"`, + after: `template: "%s | ${projectName}"`, + }, + { + label: "metadata description", + before: `description: "${DEFAULTS.metadataDescription}"`, + after: `description: "${metadataDescription}"`, + }, + { + label: "metadata applicationName", + before: `applicationName: "${DEFAULTS.projectName}"`, + after: `applicationName: "${projectName}"`, + }, + ], + }), +]; + +for (const result of results) { + if (result.missing) { + console.log(`SKIP ${result.relativePath} (missing)`); + continue; + } + + const status = args.write ? (result.changed ? "UPDATED" : "UNCHANGED") : result.changed ? "WOULD UPDATE" : "NO CHANGE"; + console.log(`${status} ${result.relativePath}`); + + for (const detail of result.changedDetails) { + console.log(` - ${detail}`); + } + + for (const detail of result.skipped) { + console.log(` - skipped: ${detail}`); + } +} + +if (!args.write) { + console.log(""); + console.log("Preview only. Re-run with --write to apply changes."); +} + +if (args.packageName && args.write) { + console.log(""); + console.log("Note: package-lock.json is unchanged. Run npm install --package-lock-only if you want the root package name there to match."); +} + +function updatePackageJson({ rootDir, write, packageName, packageDescription, repositoryUrl }) { + const relativePath = "package.json"; + const filePath = path.join(rootDir, relativePath); + + if (!existsSync(filePath)) { + return createMissingResult(relativePath); + } + + const data = JSON.parse(readFileSync(filePath, "utf8")); + const changedDetails = []; + const skipped = []; + + maybeUpdateJsonField(data, "name", packageName, DEFAULTS.packageName, changedDetails, skipped, "package name"); + maybeUpdateJsonField( + data, + "description", + packageDescription, + DEFAULTS.packageDescription, + changedDetails, + skipped, + "package description", + ); + maybeUpdateJsonField( + data, + "homepage", + `${repositoryUrl}#readme`, + "https://github.com/WeOpen/WeBase#readme", + changedDetails, + skipped, + "homepage", + ); + + const nextRepositoryUrl = `git+${repositoryUrl}.git`; + const currentRepositoryUrl = data.repository?.url; + if (currentRepositoryUrl === "git+https://github.com/WeOpen/WeBase.git") { + data.repository = { + ...(data.repository ?? {}), + type: "git", + url: nextRepositoryUrl, + }; + changedDetails.push("repository URL"); + } else if (currentRepositoryUrl !== nextRepositoryUrl) { + skipped.push("repository URL no longer matches the shipped default"); + } + + const nextBugsUrl = `${repositoryUrl}/issues`; + const currentBugsUrl = data.bugs?.url; + if (currentBugsUrl === "https://github.com/WeOpen/WeBase/issues") { + data.bugs = { + ...(data.bugs ?? {}), + url: nextBugsUrl, + }; + changedDetails.push("bugs URL"); + } else if (currentBugsUrl !== nextBugsUrl) { + skipped.push("bugs URL no longer matches the shipped default"); + } + + if (write && changedDetails.length > 0) { + writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`); + } + + return { + relativePath, + changed: changedDetails.length > 0, + changedDetails, + skipped, + missing: false, + }; +} + +function maybeUpdateJsonField(target, key, nextValue, defaultValue, changedDetails, skipped, label) { + if (target[key] === defaultValue) { + if (nextValue !== defaultValue) { + target[key] = nextValue; + changedDetails.push(label); + } + return; + } + + if (target[key] !== nextValue) { + skipped.push(`${label} no longer matches the shipped default`); + } +} + +function updateTextFile({ rootDir, relativePath, write, replacements }) { + const filePath = path.join(rootDir, relativePath); + + if (!existsSync(filePath)) { + return createMissingResult(relativePath); + } + + const original = readFileSync(filePath, "utf8"); + let next = original; + const changedDetails = []; + const skipped = []; + + for (const replacement of replacements) { + if (replacement.before === replacement.after) { + continue; + } + + if (next.includes(replacement.before)) { + next = next.replace(replacement.before, replacement.after); + changedDetails.push(replacement.label); + continue; + } + + if (!next.includes(replacement.after)) { + skipped.push(`${replacement.label} no longer matches the shipped default`); + } + } + + if (write && changedDetails.length > 0) { + writeFileSync(filePath, next); + } + + return { + relativePath, + changed: changedDetails.length > 0, + changedDetails, + skipped, + missing: false, + }; +} + +function createMissingResult(relativePath) { + return { + relativePath, + changed: false, + changedDetails: [], + skipped: [], + missing: true, + }; +} + +function ensureSentence(value) { + return /[.!?]$/.test(value) ? value : `${value}.`; +} + +function parseArgs(argv) { + const parsed = { + write: false, + help: false, + }; + + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + + if (!token.startsWith("--")) { + console.error(`Unexpected argument: ${token}`); + printUsage(); + process.exit(1); + } + + const key = token.slice(2); + + if (key === "write") { + parsed.write = true; + continue; + } + + if (key === "help") { + parsed.help = true; + continue; + } + + const value = argv[index + 1]; + if (!value || value.startsWith("--")) { + console.error(`Missing value for --${key}`); + printUsage(); + process.exit(1); + } + + parsed[toCamelCase(key)] = value; + index += 1; + } + + return parsed; +} + +function toCamelCase(value) { + return value.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()); +} + +function printUsage() { + console.log(`Usage: + node scripts/init-template.mjs --name "Acme Admin" --short-name "Acme" --description "A production-ready Next.js admin template for Acme operations." --github-owner acme --github-repo admin-console [--package-name acme-admin-console] [--write] + +Options: + --name Project display name used in metadata and README title + --short-name Short manifest name; defaults to --name + --description Repository and app metadata description + --github-owner GitHub owner or org for homepage, repository, and issue links + --github-repo GitHub repository name for homepage, repository, and issue links + --package-name package.json name to use when it still matches the shipped default + --root Override the target root directory + --write Apply changes; without this flag the script only previews + --help Show this help text`); +}