docs: update documentation to OKF v0.1 format

This commit is contained in:
OpenVelo Agent
2026-07-23 10:17:52 +00:00
parent d468cca766
commit 456a6e3ee0
10 changed files with 218 additions and 32 deletions
+71
View File
@@ -0,0 +1,71 @@
---
type: api
title: Blog Endpoints
description: Public blog listing and administrator-only post management endpoints.
tags: [api, blog, admin, markdown]
timestamp: 2026-07-23T10:12:24Z
---
# Endpoints
| Method | Path | Access | Behavior |
|---|---|---|---|
| `GET` | `/api/v1/blog/posts` | Public | Returns published posts only, newest publication first. |
| `GET` | `/api/v1/admin/blog/posts` | Admin | Returns drafts and published posts, ordered by most recently updated. |
| `GET` | `/api/v1/admin/blog/posts/:id` | Admin | Returns one post or `404 NOT_FOUND`. |
| `POST` | `/api/v1/admin/blog/posts` | Admin + CSRF | Creates a draft or published post; defaults to draft. |
| `PUT` | `/api/v1/admin/blog/posts/:id` | Admin + CSRF | Partially updates title, Markdown body, or status. |
| `DELETE` | `/api/v1/admin/blog/posts/:id` | Admin + CSRF | Deletes a post and returns `204`; missing posts return `404 NOT_FOUND`. |
The public route is marked `@Public()`. Admin routes require a Bearer JWT with the administrator role, and unsafe requests use the standard CSRF cookie/header contract from [REST API Overview](/api/rest-overview.md).
# Schema
## Public list response
```json
{
"posts": [
{
"id": "post-id",
"title": "Event update",
"publishedAt": "2026-07-23T10:00:00.000Z",
"bodyMd": "# Welcome"
}
]
}
```
Drafts and administrative fields such as `status`, `createdAt`, and `updatedAt` are excluded.
## Admin post
| Field | Type | Description |
|---|---|---|
| `id` | string | Server-generated UUID. |
| `title` | string | Trimmed title, required, maximum 200 characters. |
| `bodyMd` | string | Markdown body, maximum 200,000 characters. |
| `status` | `draft` or `published` | Visibility state. |
| `createdAt` | ISO 8601 string | Creation time. |
| `updatedAt` | ISO 8601 string | Last persistence update. |
| `publishedAt` | ISO 8601 string or null | First publication time. |
`POST` accepts `title`, optional `bodyMd`, and optional `status`. `PUT` accepts any subset of those fields. Validation failures use HTTP 400 with code `VALIDATION_FAILED`.
# Publication behavior
* Creating with `status: "published"` sets `publishedAt`; omitted status creates a draft.
* Publishing a never-published draft sets `publishedAt` once.
* Editing a published post does not change `publishedAt`.
* Moving a post back to draft preserves `publishedAt`; republishing retains the original timestamp.
* Only rows currently marked `published` are returned by the public endpoint.
# Wiring
`BlogModule` registers `BlogController`/`BlogService` for the public list and `AdminBlogController`/`AdminBlogService` for management. Both services access `BlogPostEntity` through `TypeOrmModule.forFeature([BlogPostEntity])`. The Angular `BlogApiService` calls these routes for the admin page, authenticated Blog page, and landing-page post list.
# See also
- [Blog Publishing and Reading](/guides/blog.md)
- [Blog Post Table](/database/blog-posts.md)
- [REST API Overview](/api/rest-overview.md)
+3 -2
View File
@@ -3,7 +3,7 @@ type: api
title: REST API Overview title: REST API Overview
description: Base URL, versioning, auth, CSRF, and the standard error envelope. description: Base URL, versioning, auth, CSRF, and the standard error envelope.
tags: [api, rest, overview, csrf] tags: [api, rest, overview, csrf]
timestamp: 2026-07-23T00:10:00Z timestamp: 2026-07-23T10:12:24Z
--- ---
# Base URL & versioning # Base URL & versioning
@@ -98,4 +98,5 @@ statuses and the canonical messages produced by
- [Setup Endpoint](/api/setup.md) - [Setup Endpoint](/api/setup.md)
- [System Endpoints](/api/system.md) - [System Endpoints](/api/system.md)
- [Uploads Endpoints](/api/uploads.md) - [Uploads Endpoints](/api/uploads.md)
- [Challenges Endpoints](/api/challenges.md) - [Challenges Endpoints](/api/challenges.md)
- [Blog Endpoints](/api/blog.md)
+4 -2
View File
@@ -3,7 +3,7 @@ type: architecture
title: Backend Module Map title: Backend Module Map
description: NestJS modules, controllers, services, and how they are wired together. description: NestJS modules, controllers, services, and how they are wired together.
tags: [architecture, backend, nestjs, modules] tags: [architecture, backend, nestjs, modules]
timestamp: 2026-07-23T00:10:00Z timestamp: 2026-07-23T10:12:24Z
--- ---
# Module Map # Module Map
@@ -29,7 +29,7 @@ also registers two global providers:
| `SystemModule` | `backend/src/modules/system/system.module.ts` | `SystemController` (`/api/v1/bootstrap`, `/event/status`, SSE streams) + `SystemService`. | | `SystemModule` | `backend/src/modules/system/system.module.ts` | `SystemController` (`/api/v1/bootstrap`, `/event/status`, SSE streams) + `SystemService`. |
| `AdminModule` | `backend/src/modules/admin/admin.module.ts` | Four controllers (`AdminController` for `/api/v1/admin/users*`, `AdminGeneralController` for `/api/v1/admin/general/*`, `AdminCategoriesController` for `/api/v1/admin/categories*`, `AdminChallengesController` for `/api/v1/admin/challenges*`) plus their services (`AdminService`, `AdminGeneralService`, `AdminCategoriesService`, `AdminChallengesService`, `ChallengeFilesService`). Gated by `AdminGuard` + `@Roles('admin')`. Imports `TypeOrmModule.forFeature([UserEntity, CategoryEntity, ChallengeEntity, ChallengeFileEntity, SolveEntity])`, `AuthModule`, `UsersModule`, `SettingsModule`, and `CommonModule` (for `SseHubService`, `ThemeLoaderService`). | | `AdminModule` | `backend/src/modules/admin/admin.module.ts` | Four controllers (`AdminController` for `/api/v1/admin/users*`, `AdminGeneralController` for `/api/v1/admin/general/*`, `AdminCategoriesController` for `/api/v1/admin/categories*`, `AdminChallengesController` for `/api/v1/admin/challenges*`) plus their services (`AdminService`, `AdminGeneralService`, `AdminCategoriesService`, `AdminChallengesService`, `ChallengeFilesService`). Gated by `AdminGuard` + `@Roles('admin')`. Imports `TypeOrmModule.forFeature([UserEntity, CategoryEntity, ChallengeEntity, ChallengeFileEntity, SolveEntity])`, `AuthModule`, `UsersModule`, `SettingsModule`, and `CommonModule` (for `SseHubService`, `ThemeLoaderService`). |
| `UploadsModule` | `backend/src/modules/uploads/uploads.module.ts` | `UploadsController` (`/api/v1/uploads/*`). Admin-only multipart. | | `UploadsModule` | `backend/src/modules/uploads/uploads.module.ts` | `UploadsController` (`/api/v1/uploads/*`). Admin-only multipart. |
| `BlogModule` | `backend/src/modules/blog/blog.module.ts` | `BlogController` (`GET /api/v1/blog/posts`) + `BlogService` (lists `status='published'` rows). | | `BlogModule` | `backend/src/modules/blog/blog.module.ts` | `BlogController`/`BlogService` expose published posts at `GET /api/v1/blog/posts`; `AdminBlogController`/`AdminBlogService` provide guarded CRUD at `/api/v1/admin/blog/posts` using `BlogPostEntity`. |
| `ChallengesModule` | `backend/src/modules/challenges/challenges.module.ts` | `ChallengesController` (`/api/v1/challenges/{board,status,:id,:id/solves}`) + `ChallengesEventsController` (authenticated SSE `/api/v1/events`) + `ChallengesService` (board, detail, submit, scoring util). | | `ChallengesModule` | `backend/src/modules/challenges/challenges.module.ts` | `ChallengesController` (`/api/v1/challenges/{board,status,:id,:id/solves}`) + `ChallengesEventsController` (authenticated SSE `/api/v1/events`) + `ChallengesService` (board, detail, submit, scoring util). |
| `FrontendModule` | `backend/src/frontend/frontend.module.ts` | Registers `SpaFallbackMiddleware` and the uploads static helper. | | `FrontendModule` | `backend/src/frontend/frontend.module.ts` | Registers `SpaFallbackMiddleware` and the uploads static helper. |
@@ -47,6 +47,7 @@ also registers two global providers:
| `SystemController` | `/api/v1` | Mixed (bootstrap/event/scoreboard/settings are public; `events/status` SSE is JWT-protected) | `backend/src/modules/system/system.controller.ts` | | `SystemController` | `/api/v1` | Mixed (bootstrap/event/scoreboard/settings are public; `events/status` SSE is JWT-protected) | `backend/src/modules/system/system.controller.ts` |
| `UploadsController` | `/api/v1/uploads` | Admin only | `backend/src/modules/uploads/uploads.controller.ts` | | `UploadsController` | `/api/v1/uploads` | Admin only | `backend/src/modules/uploads/uploads.controller.ts` |
| `BlogController` | `/api/v1/blog` | Public | `backend/src/modules/blog/blog.controller.ts` | | `BlogController` | `/api/v1/blog` | Public | `backend/src/modules/blog/blog.controller.ts` |
| `AdminBlogController` | `/api/v1/admin/blog/posts` | Admin only | `backend/src/modules/blog/admin-blog.controller.ts` |
| `ChallengesController` | `/api/v1/challenges` | Authenticated | `backend/src/modules/challenges/challenges.controller.ts` | | `ChallengesController` | `/api/v1/challenges` | Authenticated | `backend/src/modules/challenges/challenges.controller.ts` |
| `ChallengesEventsController` | `/api/v1/events` | Authenticated (SSE) | `backend/src/modules/challenges/events.controller.ts` | | `ChallengesEventsController` | `/api/v1/events` | Authenticated (SSE) | `backend/src/modules/challenges/events.controller.ts` |
@@ -89,3 +90,4 @@ also registers two global providers:
- [System Overview](/architecture/overview.md) - [System Overview](/architecture/overview.md)
- [Key Files Index](/architecture/key-files.md) - [Key Files Index](/architecture/key-files.md)
- [REST API Overview](/api/rest-overview.md) - [REST API Overview](/api/rest-overview.md)
- [Blog API](/api/blog.md)
+15 -5
View File
@@ -1,9 +1,9 @@
--- ---
type: architecture type: architecture
title: Frontend Structure title: Frontend Structure
description: Angular routes, components, services, guards, interceptors, and authenticated SSE transport. description: Angular routes, components, services, guards, interceptors, authenticated SSE transport, and blog management and presentation wiring.
tags: [architecture, frontend, angular, shell, sse, challenges, scoreboard, notifications] tags: [architecture, frontend, angular, shell, sse, challenges, scoreboard, notifications, blog]
timestamp: 2026-07-23T05:10:00Z timestamp: 2026-07-23T10:12:24Z
--- ---
# Routes # Routes
@@ -17,9 +17,10 @@ Routes live in `frontend/src/app/app.routes.ts`:
| `/` | `HomeComponent` | `authGuard` | Authenticated shell and child outlet. | | `/` | `HomeComponent` | `authGuard` | Authenticated shell and child outlet. |
| `/challenges` | `ChallengesPage` | inherited | Full challenges board (category columns, modal, flag submit, live SSE). | | `/challenges` | `ChallengesPage` | inherited | Full challenges board (category columns, modal, flag submit, live SSE). |
| `/scoreboard` | `ScoreboardPage` | inherited | Live scoreboard page with 4 tabs (Ranking / Matrix / Event Log / Score Graph), live SSE `solve` updates. | | `/scoreboard` | `ScoreboardPage` | inherited | Live scoreboard page with 4 tabs (Ranking / Matrix / Event Log / Score Graph), live SSE `solve` updates. |
| `/blog` | `BlogPage` | inherited | Placeholder blog page. | | `/blog` | `BlogPage` | inherited | Loads and renders published posts with loading, error, and empty states. |
| `/admin` | `AdminUsersComponent` | `adminGuard` | Admin-only child route. | | `/admin` | `AdminShellComponent` | `adminGuard` | Admin-only layout; redirects to `/admin/general`. |
| `/admin/challenges` | `AdminChallengesComponent` | inherited | Sortable/searchable admin Challenges list, Add/Edit modal, import/export. | | `/admin/challenges` | `AdminChallengesComponent` | inherited | Sortable/searchable admin Challenges list, Add/Edit modal, import/export. |
| `/admin/blog` | `AdminBlogComponent` | inherited | Admin post table and create/edit/draft/publish/delete workflow. |
# Components and services # Components and services
@@ -45,6 +46,11 @@ Routes live in `frontend/src/app/app.routes.ts`:
| `RankingComponent` / `MatrixComponent` / `EventLogComponent` / `ScoreGraphComponent` | `frontend/src/app/features/scoreboard/` | Presentational components for the 4 tabs; each binds a single `@Input()` from `ScoreboardStore` and uses `PLAYER_COLOR_PALETTE` + `stablePlayerColorIndex` to color each player consistently. | | `RankingComponent` / `MatrixComponent` / `EventLogComponent` / `ScoreGraphComponent` | `frontend/src/app/features/scoreboard/` | Presentational components for the 4 tabs; each binds a single `@Input()` from `ScoreboardStore` and uses `PLAYER_COLOR_PALETTE` + `stablePlayerColorIndex` to color each player consistently. |
| `NotificationService` | `frontend/src/app/core/services/notification.service.ts` | Root-provided signal-backed store of `{ id, kind, message, ts }` records; `error()` / `info()` push, `dismiss(id)` / `clear()` remove. | | `NotificationService` | `frontend/src/app/core/services/notification.service.ts` | Root-provided signal-backed store of `{ id, kind, message, ts }` records; `error()` / `info()` push, `dismiss(id)` / `clear()` remove. |
| `errorNotificationInterceptor` | `frontend/src/app/core/interceptors/error-notification.interceptor.ts` | Functional HTTP interceptor that pushes a friendly message into `NotificationService` for every `HttpErrorResponse`; suppresses duplicate toasts for endpoints with their own error UI (login form, `/challenges/status` snapshot). | | `errorNotificationInterceptor` | `frontend/src/app/core/interceptors/error-notification.interceptor.ts` | Functional HTTP interceptor that pushes a friendly message into `NotificationService` for every `HttpErrorResponse`; suppresses duplicate toasts for endpoints with their own error UI (login form, `/challenges/status` snapshot). |
| `BlogApiService` | `frontend/src/app/core/services/blog.service.ts` | Promise-based HTTP client for the published list and admin CRUD routes. |
| `BlogPage` | `frontend/src/app/features/blog/blog.page.ts` | Smart `/blog` page that loads published posts and derives loading, error, empty, and list states. |
| `BlogPresenterComponent` | `frontend/src/app/features/blog/blog-presenter.component.ts` | Shared title/date/sanitized-Markdown renderer used by `/blog` and `/login`. |
| `AdminBlogComponent` | `frontend/src/app/features/admin/blog/blog.component.ts` | Smart `/admin/blog` page coordinating list refresh and create/edit/delete modal state. |
| `BlogFormModalComponent` / `BlogDeleteModalComponent` | `frontend/src/app/features/admin/blog/` | Admin Markdown editor with live preview and destructive-action confirmation. |
# Authenticated SSE wiring # Authenticated SSE wiring
@@ -81,6 +87,9 @@ on destruction.
| `frontend/src/app/core/services/event-status.pure.ts` | Defines the event payload and transport interface. | | `frontend/src/app/core/services/event-status.pure.ts` | Defines the event payload and transport interface. |
| `frontend/src/app/features/challenges/challenges.page.ts` | `/challenges` smart page; owns gate logic, modal lifecycle, SSE wiring, and the countdown-zero reload. | | `frontend/src/app/features/challenges/challenges.page.ts` | `/challenges` smart page; owns gate logic, modal lifecycle, SSE wiring, and the countdown-zero reload. |
| `frontend/src/app/features/challenges/challenges.store.ts` | Signal store backing `/challenges`: board, event state, per-card solve listeners, SSE solve-frame mutation, submit response application, public `reset()`, and per-user `setMyUserId` flush. | | `frontend/src/app/features/challenges/challenges.store.ts` | Signal store backing `/challenges`: board, event state, per-card solve listeners, SSE solve-frame mutation, submit response application, public `reset()`, and per-user `setMyUserId` flush. |
| `frontend/src/app/core/services/blog.service.ts` | Calls public and admin blog endpoints with typed payloads. |
| `frontend/src/app/features/blog/blog-presenter.component.ts` | Shared sanitized Markdown post rendering for authenticated and landing views. |
| `frontend/src/app/features/admin/blog/blog.component.ts` | Coordinates administrator blog CRUD UI and table refreshes. |
| `tests/frontend/authenticated-event-source.spec.ts` | Regression coverage for authenticated SSE transport behavior. | | `tests/frontend/authenticated-event-source.spec.ts` | Regression coverage for authenticated SSE transport behavior. |
# Event-status pure helpers # Event-status pure helpers
@@ -129,4 +138,5 @@ for the full contract and tester matrix.
- [Challenges Board](/guides/challenges-board.md) - [Challenges Board](/guides/challenges-board.md)
- [Scoreboard Page](/guides/scoreboard-page.md) - [Scoreboard Page](/guides/scoreboard-page.md)
- [Notifications](/guides/notifications.md) - [Notifications](/guides/notifications.md)
- [Blog Publishing and Reading](/guides/blog.md)
- [System Overview](/architecture/overview.md) - [System Overview](/architecture/overview.md)
+20 -3
View File
@@ -1,9 +1,9 @@
--- ---
type: architecture type: architecture
title: Key Files Index title: Key Files Index
description: One-line responsibility for important source and contract-test files, including strict event-window validation, the public bootstrap SSE listener, the challenges full-replace import flow, the authenticated player-facing challenges board, and the per-user `solvedByMe` reset on the session boundary. description: One-line responsibility for important source and contract-test files, including event-window, challenge, scoreboard, session, notification, and blog flows.
tags: [architecture, key-files, event-window, validation, default-challenge-ip, sse, bootstrap, migrations, challenges, import, board, notifications, per-user, session] tags: [architecture, key-files, event-window, validation, default-challenge-ip, sse, bootstrap, migrations, challenges, import, board, notifications, per-user, session, blog]
timestamp: 2026-07-23T04:05:00Z timestamp: 2026-07-23T10:12:24Z
--- ---
# Backend # Backend
@@ -41,6 +41,11 @@ timestamp: 2026-07-23T04:05:00Z
| `backend/src/modules/challenges/dto/challenges.dto.ts` | zod contracts (`SolveSubmitBodySchema`, `BoardQuerySchema`, `ChallengeIdParamSchema`) + DTO types for the player-facing board. | | `backend/src/modules/challenges/dto/challenges.dto.ts` | zod contracts (`SolveSubmitBodySchema`, `BoardQuerySchema`, `ChallengeIdParamSchema`) + DTO types for the player-facing board. |
| `backend/src/modules/challenges/scoring.util.ts` | Pure `computeLivePoints`, `rankBonusForPosition`, `compareDifficulty`, `computeAwardedPoints` (base + rank bonus), `PLAYER_COLOR_PALETTE` + `stablePlayerColorIndex` (FNV-1a → palette index), and constant-time `safeEqualString` for flag comparison. | | `backend/src/modules/challenges/scoring.util.ts` | Pure `computeLivePoints`, `rankBonusForPosition`, `compareDifficulty`, `computeAwardedPoints` (base + rank bonus), `PLAYER_COLOR_PALETTE` + `stablePlayerColorIndex` (FNV-1a → palette index), and constant-time `safeEqualString` for flag comparison. |
| `backend/src/common/errors/error-codes.ts` | Canonical error code map (now includes `EVENT_NOT_RUNNING`, `FLAG_INCORRECT`, `CHALLENGE_DISABLED`). | | `backend/src/common/errors/error-codes.ts` | Canonical error code map (now includes `EVENT_NOT_RUNNING`, `FLAG_INCORRECT`, `CHALLENGE_DISABLED`). |
| `backend/src/modules/blog/blog.module.ts` | Wires the public and admin blog controllers/services to `BlogPostEntity`. |
| `backend/src/modules/blog/admin-blog.controller.ts` | Registers administrator CRUD routes at `/api/v1/admin/blog/posts`. |
| `backend/src/modules/blog/admin-blog.service.ts` | Lists, creates, updates, publishes/drafts, and deletes posts while preserving first-publication time. |
| `backend/src/modules/blog/dto/blog.dto.ts` | Public/admin response contracts and zod validation for title, body, status, and post id. |
| `tests/backend/blog-admin.spec.ts` | Admin authorization, CRUD, validation, publication-timestamp, deletion, and public draft-hiding contracts. |
# Frontend # Frontend
@@ -94,6 +99,16 @@ timestamp: 2026-07-23T04:05:00Z
| `frontend/src/app/features/scoreboard/scoreboard.page.ts` | `/scoreboard` smart page (also listed above): gates the four tabs, wires `store.loadAll()` and `store.wireSse(...)` on init, and tears them down via `store.stop()` on destroy. | | `frontend/src/app/features/scoreboard/scoreboard.page.ts` | `/scoreboard` smart page (also listed above): gates the four tabs, wires `store.loadAll()` and `store.wireSse(...)` on init, and tears them down via `store.stop()` on destroy. |
| `frontend/src/app/core/services/notification.service.ts` | Root-provided signal-backed store of `{ id, kind, message, ts }` records; `error()` / `info()` push, `dismiss(id)` / `clear()` remove. | | `frontend/src/app/core/services/notification.service.ts` | Root-provided signal-backed store of `{ id, kind, message, ts }` records; `error()` / `info()` push, `dismiss(id)` / `clear()` remove. |
| `frontend/src/app/core/interceptors/error-notification.interceptor.ts` | Translates `HttpErrorResponse` into friendly messages and pushes them to `NotificationService` (with suppression for `/api/v1/auth/{login,csrf,register}` and `/api/v1/challenges/status`). | | `frontend/src/app/core/interceptors/error-notification.interceptor.ts` | Translates `HttpErrorResponse` into friendly messages and pushes them to `NotificationService` (with suppression for `/api/v1/auth/{login,csrf,register}` and `/api/v1/challenges/status`). |
| `frontend/src/app/core/services/blog.service.ts` | Typed Promise-based public blog list and admin CRUD HTTP client. |
| `frontend/src/app/features/blog/blog.page.ts` | `/blog` smart page with loading, error, empty, and published-list states. |
| `frontend/src/app/features/blog/blog-presenter.component.ts` | Shared sanitized Markdown post renderer used by the Blog and landing pages. |
| `frontend/src/app/features/blog/blog.pure.ts` | Pure blog-list state derivation. |
| `frontend/src/app/features/admin/blog/blog.component.ts` | `/admin/blog` post table and create/edit/delete workflow coordinator. |
| `frontend/src/app/features/admin/blog/blog-form-modal.component.ts` | Reactive title/Markdown editor with live preview and draft/publish actions. |
| `frontend/src/app/features/admin/blog/blog-delete-modal.component.ts` | Post deletion confirmation and inline failure UI. |
| `frontend/src/app/features/admin/blog/blog-form.pure.ts` | Admin form validation, form synchronization, request cleanup, and status display helpers. |
| `tests/frontend/blog-page.spec.ts` | Blog page-state and shared sanitized Markdown rendering contracts. |
| `tests/frontend/blog-admin-form.spec.ts` | Admin form validation, prefill/reset, payload, and status-label contracts. |
| `tests/frontend/challenges.pure.spec.ts` | Pure helpers: sorting, parsers, friendly error mapping, `formatDdHhMm` (`DD:HH:mm:ss`). | | `tests/frontend/challenges.pure.spec.ts` | Pure helpers: sorting, parsers, friendly error mapping, `formatDdHhMm` (`DD:HH:mm:ss`). |
| `tests/frontend/challenges.service.spec.ts` | HTTP-service contract: `getDetail` URL-encodes the id, attaches `?include=solvers`, and forwards `withCredentials: true`. | | `tests/frontend/challenges.service.spec.ts` | HTTP-service contract: `getDetail` URL-encodes the id, attaches `?include=solvers`, and forwards `withCredentials: true`. |
| `tests/frontend/challenges.store.spec.ts` | Signal store: board mutation, live solve merge, listeners, `stop()`. The "marks solvedByMe" and "does not double-count" tests now call `setMyUserId('me-1')` *before* `load()` to exercise the realistic page-mount sequence. | | `tests/frontend/challenges.store.spec.ts` | Signal store: board mutation, live solve merge, listeners, `stop()`. The "marks solvedByMe" and "does not double-count" tests now call `setMyUserId('me-1')` *before* `load()` to exercise the realistic page-mount sequence. |
@@ -118,3 +133,5 @@ timestamp: 2026-07-23T04:05:00Z
- [Scoreboard Page Guide](/guides/scoreboard-page.md) - [Scoreboard Page Guide](/guides/scoreboard-page.md)
- [Challenges Board](/guides/challenges-board.md) - [Challenges Board](/guides/challenges-board.md)
- [Notifications](/guides/notifications.md) - [Notifications](/guides/notifications.md)
- [Blog Publishing and Reading](/guides/blog.md)
- [Blog API](/api/blog.md)
+9 -3
View File
@@ -3,7 +3,7 @@ type: database
title: Blog Post Table title: Blog Post Table
description: blog_post table — draft/published Markdown posts surfaced on the public landing page. description: blog_post table — draft/published Markdown posts surfaced on the public landing page.
tags: [database, blog, markdown] tags: [database, blog, markdown]
timestamp: 2026-07-21T18:28:00Z timestamp: 2026-07-23T10:12:24Z
--- ---
# Schema # Schema
@@ -27,12 +27,18 @@ timestamp: 2026-07-21T18:28:00Z
# Behavior # Behavior
* Only rows with `status = 'published'` are exposed through the public * Only rows with `status = 'published'` are exposed through the public
endpoint `GET /api/v1/blog/posts` (see endpoint `GET /api/v1/blog/posts` (see [Blog API](/api/blog.md)). Drafts
[REST API Overview](/api/rest-overview.md)). Drafts are hidden. are hidden from the landing page and authenticated `/blog` page.
* The admin CRUD endpoints under `/api/v1/admin/blog/posts` expose both
statuses and all timestamps to administrators.
* First publication sets `published_at`. Later edits, demotion to draft,
and republication preserve that original timestamp.
* `BlogService.listPublished()` orders by `published_at DESC`, falling * `BlogService.listPublished()` orders by `published_at DESC`, falling
back to `created_at` when `published_at` is null. back to `created_at` when `published_at` is null.
# See also # See also
- [Database Schema Overview](/database/schema.md) - [Database Schema Overview](/database/schema.md)
- [Blog API](/api/blog.md)
- [Blog Publishing and Reading](/guides/blog.md)
- [Landing Page Guide](/guides/landing-page.md) - [Landing Page Guide](/guides/landing-page.md)
+14 -12
View File
@@ -1,9 +1,9 @@
--- ---
type: guide type: guide
title: Admin Shell & Side Navigation title: Admin Shell & Side Navigation
description: How an authenticated admin navigates the post-login admin area, the side-nav layout, and how the General and Categories pages are reached. description: How an authenticated admin navigates the post-login admin area, including General, Challenges, Players, Categories, and Blog management pages.
tags: [guide, admin, shell, navigation, tester] tags: [guide, admin, shell, navigation, tester]
timestamp: 2026-07-22T12:00:00Z timestamp: 2026-07-23T10:12:24Z
--- ---
# When this view is available # When this view is available
@@ -47,9 +47,9 @@ The side-nav is defined as a static `ENTRIES` array in
|-------------|-----------------------|---------|----------------------------------------------------------------------------------------------| |-------------|-----------------------|---------|----------------------------------------------------------------------------------------------|
| General | `/admin/general` | yes | [Admin — General Settings](/guides/admin-general-settings.md) (default — `/admin` redirects here). | | General | `/admin/general` | yes | [Admin — General Settings](/guides/admin-general-settings.md) (default — `/admin` redirects here). |
| Challenges | `/admin/challenges` | yes | [Admin — Challenges](/guides/admin-challenges.md) — sortable list, search, Add/Edit modal, file staging, import/export. | | Challenges | `/admin/challenges` | yes | [Admin — Challenges](/guides/admin-challenges.md) — sortable list, search, Add/Edit modal, file staging, import/export. |
| Players | `/admin/players` | no | Greyed-out placeholder. No route registered. | | Players | `/admin/players` | yes | User management page. |
| Blog | `/admin/blog` | no | Greyed-out placeholder. No route registered. | | Blog | `/admin/blog` | yes | [Blog Publishing and Reading](/guides/blog.md) — post list with draft/publish, edit, preview, and delete workflows. |
| System | `/admin/system` | no | Greyed-out placeholder. No route registered. | | System | `/admin/system` | no | Greyed-out placeholder. No route registered. |
Each `<li>` carries `data-testid="admin-nav-{id}"`. Disabled entries Each `<li>` carries `data-testid="admin-nav-{id}"`. Disabled entries
have `cursor: not-allowed` and `opacity: 0.45`. Clicking a disabled have `cursor: not-allowed` and `opacity: 0.45`. Clicking a disabled
@@ -95,17 +95,18 @@ current URL starts with the entry's path. General is the default child
| 4 | `frontend/src/app/features/admin/admin-shell.component.ts` | Renders the aside + body; `ENTRIES` is the static side-nav list. | | 4 | `frontend/src/app/features/admin/admin-shell.component.ts` | Renders the aside + body; `ENTRIES` is the static side-nav list. |
| 5 | `frontend/src/app/features/admin/general.component.ts` | `AdminGeneralComponent` (default `/admin/general`); embeds `AdminCategoriesComponent`. | | 5 | `frontend/src/app/features/admin/general.component.ts` | `AdminGeneralComponent` (default `/admin/general`); embeds `AdminCategoriesComponent`. |
| 6 | `frontend/src/app/features/admin/categories/categories.component.ts` | `AdminCategoriesComponent` (`/admin/categories`); also rendered inside the General page. | | 6 | `frontend/src/app/features/admin/categories/categories.component.ts` | `AdminCategoriesComponent` (`/admin/categories`); also rendered inside the General page. |
| 7 | `frontend/src/app/features/home/home.shell.ts` | `shouldShowAdminNav({isAuthenticated, role})` predicate (exported and unit-tested). | | 7 | `frontend/src/app/features/admin/blog/blog.component.ts` | `AdminBlogComponent` (`/admin/blog`); coordinates post listing, editing, publication, and deletion. |
| 8 | `frontend/src/app/features/home/home.component.ts` | Shell template renders header, conditional nav (`*ngIf="showAdminNav()"`), and `<router-outlet>`. | | 8 | `frontend/src/app/features/home/home.shell.ts` | `shouldShowAdminNav({isAuthenticated, role})` predicate (exported and unit-tested). |
| 9 | `frontend/src/app/core/services/admin.service.ts` | Typed wrappers for `/api/v1/admin/users`, `/admin/general/*`, `/admin/categories/*`, `/uploads/{logo,category-icon}`. | | 9 | `frontend/src/app/features/home/home.component.ts` | Shell template renders header, conditional nav (`*ngIf="showAdminNav()"`), and `<router-outlet>`. |
| 10 | `backend/src/modules/admin/admin.module.ts` | Registers `AdminController`, `AdminGeneralController`, `AdminCategoriesController` + their services. Imports `AuthModule`, `UsersModule`, `SettingsModule`, `CommonModule`, and `TypeOrmModule.forFeature([UserEntity, CategoryEntity, ChallengeEntity])`. | | 10 | `frontend/src/app/core/services/blog.service.ts` | Typed wrappers for public blog listing and `/api/v1/admin/blog/posts` CRUD. |
| 11 | `backend/src/modules/blog/blog.module.ts` | Registers public and admin blog controllers/services with the blog repository. |
# Notes # Notes
* The shell deliberately keeps `AdminShellComponent` as a thin layout * The shell deliberately keeps `AdminShellComponent` as a thin layout
owner — child routes (`general`, `categories`) render into its owner — enabled child routes render into its `<router-outlet>`. New admin
`<router-outlet>`. New admin sub-pages can be added by enabling sub-pages require both an enabled `ENTRIES` item and a child route in
entries in `ENTRIES` and registering a child route in `app.routes.ts`. `app.routes.ts`.
* The guard decision function (`decideAdminGuard`) is a pure module so * The guard decision function (`decideAdminGuard`) is a pure module so
it is straightforward to unit-test without Angular DI. See it is straightforward to unit-test without Angular DI. See
`tests/frontend/admin-shell.spec.ts` and `tests/frontend/admin-shell.spec.ts` and
@@ -124,5 +125,6 @@ current URL starts with the entry's path. General is the default child
- [Change Password](/guides/change-password.md) - [Change Password](/guides/change-password.md)
- [Admin — General Settings](/guides/admin-general-settings.md) - [Admin — General Settings](/guides/admin-general-settings.md)
- [Admin — Categories](/guides/admin-categories.md) - [Admin — Categories](/guides/admin-categories.md)
- [Blog Publishing and Reading](/guides/blog.md)
- [REST API Overview](/api/rest-overview.md) - [REST API Overview](/api/rest-overview.md)
- [Admin Endpoints](/api/admin.md) - [Admin Endpoints](/api/admin.md)
+71
View File
@@ -0,0 +1,71 @@
---
type: guide
title: Blog Publishing and Reading
description: How administrators create, edit, publish, and delete Markdown posts and how users read published posts.
tags: [guide, blog, admin, markdown, tester]
timestamp: 2026-07-23T10:12:24Z
---
# User views
Published posts are available in two places:
| View | Access | Expected content |
|---|---|---|
| `/login` | Unauthenticated users | The landing-page blog section lists published posts below the Login button. |
| `/blog` | Signed-in users | The Blog page loads and lists all published posts. |
Both views use `BlogPresenterComponent` to show the post title, publication date, and sanitized Markdown body. Draft posts never appear in either public list. The `/blog` page shows `Loading blog posts…` while loading, `No announcements yet.` when no posts are published, and an error message if loading fails.
# Admin workflow
1. Sign in as an administrator.
2. Open **Admin**, then select **Blog**, or navigate directly to `/admin/blog`.
3. Confirm the page displays a table with **Title**, **Status**, **Created**, **Updated**, **Published**, and **Actions** columns. If no posts exist, it displays `No posts yet.`.
4. Click **+ New post**.
5. Enter a required title and an optional Markdown body. The live preview updates beside the editor.
6. Click **Save draft** to keep the post hidden from users, or **Publish** to expose it in `/blog` and on `/login`.
7. Confirm a success banner displays `Draft saved.` or `Post published.`, and the refreshed table contains the post with the correct status badge.
# Editing and deleting
* Click the pencil action on a row to open the prefilled **Edit post** modal. It shows created, updated, and publication timestamps where available.
* Saving as draft changes the status to **Draft** and hides the post from public lists. Republishing preserves the post's original publication timestamp.
* Click the trash action to open **Delete post**. The confirmation includes the title and warns that deletion cannot be undone.
* Click **Delete** to remove the post. The table refreshes and displays `Post deleted.`. Click **Cancel** or the modal backdrop to leave the post unchanged.
# Validation and expected errors
| Input or operation | Expected behavior |
|---|---|
| Empty or whitespace-only title | `Title is required`; save and publish remain unavailable. |
| Title over 200 characters | `Title must be at most 200 characters`. |
| Markdown body over 200,000 characters | `Body must be at most 200000 characters`. |
| API list failure | An error banner replaces the table state. |
| Create or update failure | The form stays open and displays the server message. |
| Delete failure | The confirmation stays open and displays the server message. |
# Architecture map
| Layer | File | Responsibility |
|---|---|---|
| Routes | `frontend/src/app/app.routes.ts` | Registers authenticated `/blog` and admin-only `/admin/blog`. |
| Admin page | `frontend/src/app/features/admin/blog/blog.component.ts` | Loads all posts and coordinates create, edit, publish, draft, and delete operations. |
| Admin form | `frontend/src/app/features/admin/blog/blog-form-modal.component.ts` | Reactive title/body editor with sanitized live Markdown preview. |
| Admin form logic | `frontend/src/app/features/admin/blog/blog-form.pure.ts` | Validation, form synchronization, request cleanup, and status labels. |
| Delete confirmation | `frontend/src/app/features/admin/blog/blog-delete-modal.component.ts` | Confirms destructive deletion and displays failures. |
| User page | `frontend/src/app/features/blog/blog.page.ts` | Loads published posts and selects loading, error, empty, or list state. |
| Shared presenter | `frontend/src/app/features/blog/blog-presenter.component.ts` | Renders a published post consistently on `/blog` and `/login`. |
| HTTP client | `frontend/src/app/core/services/blog.service.ts` | Calls public list and admin CRUD endpoints. |
| Backend wiring | `backend/src/modules/blog/blog.module.ts` | Registers public and admin controllers and services with the blog repository. |
| Admin API | `backend/src/modules/blog/admin-blog.controller.ts` | Registers guarded CRUD routes under `/api/v1/admin/blog/posts`. |
| Persistence logic | `backend/src/modules/blog/admin-blog.service.ts` | Queries and mutates `blog_post` rows and controls publication timestamps. |
The Angular client uses HTTP JSON calls. Existing auth and CSRF interceptors attach the administrator JWT and CSRF token to unsafe requests. The backend controller is protected by `AdminGuard` and `@Roles('admin')`, then delegates to `AdminBlogService`, which uses the TypeORM `BlogPostEntity` repository.
# See also
- [Blog API](/api/blog.md)
- [Blog Post Table](/database/blog-posts.md)
- [Landing Page](/guides/landing-page.md)
- [Admin Shell & Side Navigation](/guides/admin-shell.md)
+4 -1
View File
@@ -3,7 +3,7 @@ type: guide
title: Landing Page title: Landing Page
description: How the public landing page renders, how the login + registration modal is opened, what each form does, and how the modal stays in sync with admin `registrationsEnabled` changes via SSE. description: How the public landing page renders, how the login + registration modal is opened, what each form does, and how the modal stays in sync with admin `registrationsEnabled` changes via SSE.
tags: [guide, landing, login, register, ui, sse, bootstrap] tags: [guide, landing, login, register, ui, sse, bootstrap]
timestamp: 2026-07-22T16:15:00Z timestamp: 2026-07-23T10:12:24Z
--- ---
# What you see # What you see
@@ -31,6 +31,8 @@ On a fresh visit the page shows, in order:
5. **Blog list**`<section data-testid="landing-blog">` listing every 5. **Blog list**`<section data-testid="landing-blog">` listing every
row returned by `GET /api/v1/blog/posts` (only `status='published'` row returned by `GET /api/v1/blog/posts` (only `status='published'`
rows), or the text `No announcements yet.` when the list is empty. rows), or the text `No announcements yet.` when the list is empty.
Each row is rendered by the shared `BlogPresenterComponent` with its
title, publication date, and sanitized Markdown body, matching `/blog`.
# Guard # Guard
@@ -144,4 +146,5 @@ response to the unsafe call itself.
- [Auth Endpoints](/api/auth.md) - [Auth Endpoints](/api/auth.md)
- [Database Schema Overview](/database/schema.md) (`blog_post`) - [Database Schema Overview](/database/schema.md) (`blog_post`)
- [Frontend Structure](/architecture/frontend-structure.md) - [Frontend Structure](/architecture/frontend-structure.md)
- [Blog Publishing and Reading](/guides/blog.md)
- [First-Run Bootstrap](/guides/bootstrap.md) - [First-Run Bootstrap](/guides/bootstrap.md)
+7 -4
View File
@@ -8,10 +8,11 @@ HIPCTF is a single-tenant CTF (Capture-The-Flag) platform built as a Node.js
monorepo containing a NestJS REST API and an Angular single-page application. monorepo containing a NestJS REST API and an Angular single-page application.
It supports user registration, authentication, challenge management, a live It supports user registration, authentication, challenge management, a live
scoreboard, an event window with a public countdown, theming, and admin scoreboard, an event window with a public countdown, theming, and admin
controls. controls, administrator-managed Markdown blog publishing, and public and
signed-in blog views.
The docs below are organized by purpose so agents can pull just the slice The docs below are organized by purpose so agents can pull just the slice
they need. Last regenerated 2026-07-23T08:42:24Z. they need. Last regenerated 2026-07-23T10:12:24Z.
# Architecture # Architecture
@@ -61,6 +62,7 @@ they need. Last regenerated 2026-07-23T08:42:24Z.
streams, public event-window settings, and the authenticated streams, public event-window settings, and the authenticated
`/events/status` SSE stream. `/events/status` SSE stream.
* [Uploads Endpoints](/api/uploads.md) - Admin-only multipart uploads for category icons, challenge files, and validated site logos. * [Uploads Endpoints](/api/uploads.md) - Admin-only multipart uploads for category icons, challenge files, and validated site logos.
* [Blog Endpoints](/api/blog.md) - Public published-post listing and admin-only post CRUD, validation, and publication behavior.
# Guides # Guides
@@ -70,8 +72,9 @@ they need. Last regenerated 2026-07-23T08:42:24Z.
helpers that translate reactive-form state into the inline validation helpers that translate reactive-form state into the inline validation
messages on the first-admin modal. messages on the first-admin modal.
* [Admin Shell & Side Navigation](/guides/admin-shell.md) - How admins * [Admin Shell & Side Navigation](/guides/admin-shell.md) - How admins
navigate the post-login admin area side-nav (General, Challenges, navigate the post-login admin area side-nav and reach General,
Players, Blog, System) and reach the enabled child pages. Challenges, Players, and Blog pages.
* [Blog Publishing and Reading](/guides/blog.md) - How administrators create, preview, draft, publish, edit, and delete Markdown posts and how users read them.
* [Admin — General Settings](/guides/admin-general-settings.md) - How an * [Admin — General Settings](/guides/admin-general-settings.md) - How an
admin edits platform-wide settings, including the required and strictly admin edits platform-wide settings, including the required and strictly
ordered event window, at `/admin/general`. ordered event window, at `/admin/general`.