AI Implementation feature(863): Challenges Page and Challenge Solve Modal (#45)
This commit was merged in pull request #45.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
---
|
||||
type: guide
|
||||
title: Notifications
|
||||
description: SPA-wide notification store + the global HTTP error interceptor that translates backend error codes and statuses into friendly user-facing messages.
|
||||
tags: [guide, notifications, error, interceptor, toast, ux]
|
||||
timestamp: 2026-07-23T00:10:00Z
|
||||
---
|
||||
|
||||
# Components
|
||||
|
||||
| Symbol | Path | Responsibility |
|
||||
|-------------------------------------|--------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------|
|
||||
| `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 catches `HttpErrorResponse`, looks up the error code, and pushes a friendly message into `NotificationService`. |
|
||||
| `NotificationService.messages` | signal (consumed by future toast UI) | Currently the messages signal is appended in-memory only; the list itself doubles as the integration point for a future toast/banner component. Each push also mirrors the message to the browser console (`console.error` for `error`, `console.info` for `info`). |
|
||||
|
||||
# Wiring
|
||||
|
||||
`frontend/src/main.ts` registers the interceptor **after** `authInterceptor` so
|
||||
the auth header is already attached when the error interceptor sees
|
||||
the request:
|
||||
|
||||
```ts
|
||||
provideHttpClient(
|
||||
withInterceptors([csrfInterceptor, authInterceptor, errorNotificationInterceptor]),
|
||||
)
|
||||
```
|
||||
|
||||
The interceptor:
|
||||
|
||||
1. Catches every `HttpErrorResponse` thrown from any HTTP call.
|
||||
2. Maps the response to a friendly message using `friendlyMessage(status, err.error)`.
|
||||
3. Suppresses notifications for endpoints that have their own
|
||||
user-facing error presentation (see *Suppression rules* below).
|
||||
4. Pushes the message via `NotificationService.error(...)`.
|
||||
5. Re-throws the original error so the caller still receives the
|
||||
`ApiErrorEnvelope` (e.g. `ChallengesApiService.submit` reads
|
||||
`envelope.code` to render the modal's inline error).
|
||||
|
||||
# Friendly message mapping
|
||||
|
||||
`friendlyMessage(status, body)` resolves in this order:
|
||||
|
||||
| Source | Message |
|
||||
|-----------------------------------------------------|------------------------------------------------------------------------------|
|
||||
| `status === 0` (network / CORS / aborted) | `Network error. Please try again.` |
|
||||
| `body.code` matches `FRIENDLY_MESSAGES` | The mapped friendly text (see table below). |
|
||||
| `status === 401` or `403` | `Your session is no longer valid.` (unless suppressed). |
|
||||
| `status === 404` | `Not found.` |
|
||||
| `status >= 500` | `Server error. Please try again later.` |
|
||||
| `body.message` (non-empty) | Falls back to that message. |
|
||||
| otherwise | `Request failed (<status>).` |
|
||||
|
||||
`FRIENDLY_MESSAGES` lookup table:
|
||||
|
||||
| Error code | Friendly text |
|
||||
|-----------------------|------------------------------------------------------------|
|
||||
| `EVENT_NOT_RUNNING` | `Submissions are only accepted while the event is running.` |
|
||||
| `FLAG_INCORRECT` | `Incorrect flag.` |
|
||||
| `UNAUTHORIZED` | `Your session is no longer valid.` |
|
||||
| `FORBIDDEN` | `You are not allowed to perform this action.` |
|
||||
| `NOT_FOUND` | `Not found.` |
|
||||
| `VALIDATION_FAILED` | `Invalid input.` |
|
||||
|
||||
# Suppression rules
|
||||
|
||||
Some errors are expected and have their own UI. To avoid double-toasting:
|
||||
|
||||
| URL pattern | Status | Why |
|
||||
|---------------------------------------|--------|---------------------------------------------------------------------------------------------------|
|
||||
| `/api/v1/auth/login` `/csrf` `/register` | `401` | A failed login or first-fetch of the CSRF cookie produces a `401`; the form already renders the inline error. |
|
||||
| `/api/v1/challenges/status` | `401` | The challenges page uses the snapshot as a bootstrap hint only — the SSE stream (`/api/v1/events`) is the source of truth and will deliver the next frame. |
|
||||
|
||||
In both cases the interceptor still re-throws the error so callers can
|
||||
react locally; only the notification push is skipped.
|
||||
|
||||
# See also
|
||||
|
||||
- [Challenges Board](/guides/challenges-board.md) (uses
|
||||
`errorNotificationInterceptor` for non-flag submission failures)
|
||||
- [Authenticated Shell](/guides/authenticated-shell.md) (cross-tab
|
||||
invalidation propagates from a 401 SSE response via
|
||||
`NotificationService` is **not** triggered — the shell uses its own
|
||||
peer-invalidation channel)
|
||||
- [REST API Overview](/api/rest-overview.md) (error envelope)
|
||||
Reference in New Issue
Block a user