From 2fb807a4fba6bc83ad2a1825f8236cf819cd2f0c Mon Sep 17 00:00:00 2001 From: nickgreengithub Date: Mon, 17 Aug 2026 06:21:48 +0700 Subject: [PATCH] docs: Assessment 3 README, deployment runbook, video script and checklist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit README now describes the system as it is: the observability architecture, the dashboard, the two new models and why FeedFetch is separate from RequestLog, the tracing and metrics sections, and the testing results with links to the full analysis. docs/deployment.md is the EC2 runbook, including the failure this codebase is most likely to hit — client-side code that resolves only on localhost works perfectly over SSH and fails completely for anyone else, so the verification step insists on a browser on a different machine. docs/submission.md lists what has to be handed in, and the questions to have an answer ready for in the live defence. VIDEO_SCRIPT.md is timed for seven minutes, and puts two minutes on the dashboard because that is the largest single mark. Its most important beat is curling a feed on camera and watching the counters move: a dashboard that changes while you watch proves the figures are live in a way no amount of narration does. Also fixes something the docs revealed. All three documents told the reader to prove persistence with `sqlite3` inside the container — and sqlite3 was not in the image. That command is the one Tony asks for in the oral defence, so it is now installed in the runner stage rather than quietly removed from the instructions. Verified: it returns the same per-feed counts the dashboard displays. Full stack verified from a clean `docker compose down -v` and rebuild: all four services up, container healthcheck healthy, dashboard totals matching generated traffic, custom spans in Jaeger, all three Prometheus targets up, valid RSS, 404 on an unknown channel, and the rows visible in SQLite. Co-Authored-By: Claude --- Dockerfile | 7 +- README.md | 178 ++++++++++++++++++++-- VIDEO_SCRIPT.md | 359 ++++++++++++++++----------------------------- docs/submission.md | 73 +++++++++ 4 files changed, 373 insertions(+), 244 deletions(-) create mode 100644 docs/submission.md diff --git a/Dockerfile b/Dockerfile index e5b08a8..00ed821 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,8 +41,13 @@ ENV PORT=3000 ENV HOSTNAME=0.0.0.0 ENV DATABASE_URL="file:/data/rss.db" +# curl backs the HEALTHCHECK below. sqlite3 is here so the stored data can be +# inspected from inside the running container with plain SQL — being able to +# show the rows behind the dashboard is the difference between claiming +# persistence and demonstrating it, and it is not something you want to +# discover is missing during a live demonstration. RUN apt-get update && apt-get install -y --no-install-recommends \ - openssl ca-certificates curl \ + openssl ca-certificates curl sqlite3 \ && rm -rf /var/lib/apt/lists/* # Run as a non-root user. The database lives on a volume at /data, which must be diff --git a/README.md b/README.md index 7c82355..599137d 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,29 @@ Prometheus metrics, and end-to-end, load and accessibility testing. └──────────────────────────────┘ ``` +Assessment 3 adds the layer that watches all of it: + +``` + ┌──────────────────────────────┐ + │ Dashboard /dashboard │ health, alerts, per-feed and + └───────────────┬──────────────┘ per-client reporting + │ /api/dashboard — one collection, one point in time + ▼ + ┌──────────────────────────────┐ + │ RequestLog · FeedFetch │ every request and every feed delivery + └──────────────────────────────┘ + + rss-server ──OTLP/HTTP──▶ otel-collector ──▶ Jaeger :16686 traces + ▲ + └──────── scrape /api/metrics ──────── Prometheus :9090 metrics +``` + +Traces answer "what happened inside this one request"; metrics answer "what is +happening across all of them"; the database answers "which feed, which client, +how many items". The application exports OTLP and knows nothing about its +telemetry backend — replacing Jaeger is a change to +`otel-collector-config.yaml`, not to application code. + Everything runs in **one container**. SQLite was chosen over a separate database service deliberately: a single process has no start-up ordering to get wrong, and the data still persists because the database file lives on a named volume @@ -60,6 +83,15 @@ rather than in the container layer. docker compose up --build ``` +This starts four services: + +| Service | Port | What it is | +| --- | --- | --- | +| `rss-server` | 3000 | the application, its API and the SQLite database | +| `jaeger` | 16686 | trace inspection UI | +| `prometheus` | 9090 | metrics queries | +| `otel-collector` | 4318 | receives OTLP from the app, fans it out | + Then open . The entrypoint applies migrations and seeds the baseline channels before starting the server, so a clean checkout with an empty volume comes up working with no manual step. Both operations are @@ -93,12 +125,14 @@ npm run dev | `npm run db:seed` | seed baseline data (idempotent) | | `npm run db:studio` | browse the database in Prisma Studio | | `npm run db:reset` | drop and rebuild the database | +| `npx playwright test` | end-to-end tests (starts its own server) | +| `./load/run-stages.sh` | staged JMeter load test | --- ## Database schema -Prisma with SQLite. Seven models, each earning its place in the RSS use case. +Prisma with SQLite. Eight models, each earning its place in the RSS use case. | Model | Represents | Notable relationships | | --- | --- | --- | @@ -108,7 +142,8 @@ Prisma with SQLite. Seven models, each earning its place in the RSS use case. | `FeedPost` | Explicit join: which posts are in which channels | carries `assignedAt` | | `Enclosure` | Attached media (``) | cascades with its post | | `Subscriber` | A registered RSS client | powers polling stats | -| `RequestLog` | One row per API request | powers `/api/count` | +| `RequestLog` | One row per HTTP request, including feed polls | powers `/api/count` and `/dashboard` | +| `FeedFetch` | One row per RSS feed delivery | items served, duration, feed-level errors | Decisions worth stating: @@ -127,6 +162,16 @@ Decisions worth stating: that renders a feed. - **`status` is a string with a documented union** (`draft` | `published`) because SQLite has no native enum type. +- **`RequestLog.clientKey` is a hash, not an address.** Counting unique clients + needs to tell callers apart, not know who they are, so it stores the first 16 + characters of `sha256(ip + user-agent)`. The count is exact and there is no + personal information in the database to protect. +- **`FeedFetch` is separate from `RequestLog`** because they answer different + questions. `RequestLog` records that an HTTP request happened and what status + it returned; `FeedFetch` records what the feed itself did — how many items + went out, and whether the channel was unknown or merely empty. A feed + returning 200 with zero items is invisible in HTTP terms and is exactly the + failure worth alerting on. --- @@ -163,10 +208,21 @@ for an unknown channel slug. | `/api/health` | Heartbeat. Real `SELECT 1` probe; returns **503** and `status: "degraded"` if the database is unreachable. | | `/api/count` | Request counts from `RequestLog` — totals, per-path and per-status breakdowns, timing. Accepts `?since=1h`. | | `/api/stats` | Feed statistics — posts per channel, posts per author, draft/published split, subscriber polling. | +| `/api/dashboard` | Everything `/dashboard` needs, collected in one pass. Accepts `?since=15m\|1h\|24h\|7d`. | +| `/api/metrics` | Prometheus text exposition — request, feed poll, duration and unique-client series. | + +Request logging is written by `lib/metrics.ts` rather than by `proxy.ts`, +because Next's proxy runs on the Edge runtime and cannot open a database +connection. It is fire-and-forget: telemetry can never fail a request. -Request logging is written by the shared route wrapper rather than by -`proxy.ts`, because Next's proxy runs on the Edge runtime and cannot open a -database connection. It is fire-and-forget: telemetry can never fail a request. +The RSS routes call it themselves. They return XML directly and never pass +through the API wrapper, so until Assessment 3 the busiest route on the server +— the feed poll — was the one route the metrics could not see. + +`/api/dashboard` and `/api/metrics` are excluded from the request log. The +dashboard polls every ten seconds and Prometheus scrapes every fifteen; left +in, they become the busiest endpoints on the server within a minute and +inflate the very totals they are reporting. ### RSS @@ -197,6 +253,7 @@ curl -X POST http://localhost:3000/api/posts \ | `/feeds` | Post browser — search and channel filter run server-side | | `/feeds/[id]` | Post detail, rendered on demand | | `/feeds/new` | Publish a post to one or more channels | +| `/dashboard` | **Operational dashboard** — health, alerts, reporting views | | `/client` | **RSS Client** — subscribes to the feeds over HTTP | | `/about` | Student details and walkthrough video | | `/settings` | Theme, layout density, server connection check | @@ -216,6 +273,96 @@ being dressed up as one. --- +## Observability + +### The dashboard + +`/dashboard` reads one endpoint, not three. The alternative — the browser +calling `/api/health`, `/api/count` and `/api/stats` and stitching the results +together — is three round trips that must agree with each other, which is +three chances to render a panel contradicting the panel beside it. One +collection, one point in time. + +The first snapshot is rendered on the server, so the page arrives populated +rather than empty-then-filled; the client polls from there. + +Panels: health and uptime · rule-based alerts · totals (requests, unique +clients, feed polls, items served, latency, error rate) · requests per feed · +feed status table · requests per endpoint · requests per client · response +codes · stored content · recent activity. + +**Alerts have two levels.** A warning says something is drifting — error rate +above 2%, a channel that served zero items, a request past one second. A +critical says someone has to act — the database is unreachable, or errors are +above 10%. A single threshold only ever tells you once it is already too late. + +### Tracing + +`instrumentation.ts` at the project root registers the OpenTelemetry Node SDK. +**It must be at the root** — Next only looks for it there, and a copy under +`app/` silently exports nothing. + +Next instruments its own request handling, so every request already produces a +root span. What it cannot know is what this application is doing, so the spans +that matter are added by hand in `lib/otel.ts`: + +| Span | Where | Answers | +| --- | --- | --- | +| `api ` | `handle()` | which handler ran, and how long it took | +| `rss.lookup_channel` | `/rss/[slug]` | was the channel resolution slow | +| `rss.load_items` | both RSS routes | was the item query slow | +| `dashboard.aggregate` | `lib/dashboard.ts` | which dashboard query is expensive | + +A slow feed then shows *which part* was slow, rather than only that it was. + +### Metrics + +`/api/metrics` exposes `rss_requests_total`, `rss_request_duration_ms`, +`rss_feed_polls_total`, `rss_feed_items` and `rss_unique_clients`, plus Node +process metrics. Route labels collapse `/rss/careers` to `/rss/[slug]`: a +label per channel or per post id is unbounded cardinality, which is the usual +way a Prometheus instance is brought down. + +Per-client breakdowns are deliberately **not** Prometheus labels — those come +from the database, where a high-cardinality dimension belongs. + +--- + +## Testing + +| Tool | What it covers | Where | +| --- | --- | --- | +| Playwright | 10 end-to-end tests, server and client use cases | `e2e/` | +| JMeter | staged load, x1 → x10000 | `load/` | +| Lighthouse | accessibility and performance, before and after | `docs/lighthouse/` | + +```bash +npx playwright test # starts its own production build +./load/run-stages.sh # needs a server on :3100 and JMeter installed +``` + +The end-to-end tests drive the real interface and then check the API **and the +published RSS** agree with it — a UI test that asserts only on the UI can pass +while the feed it is supposed to publish stays empty. One test polls a feed +and then asserts the dashboard's count moved, which checks the dashboard's +central claim end to end. + +Load testing found that nothing degrades until roughly two thousand concurrent +clients, where latency rises about twentyfold while throughput also rises and +no request fails — queueing rather than breakage, most likely SQLite +serialising the `RequestLog` write. Full analysis, including an honest account +of why the x10000 stage is 10,000 sessions rather than 10,000 concurrent +threads, is in [`load/README.md`](load/README.md). + +The accessibility review is in [`docs/accessibility.md`](docs/accessibility.md). +Its headline: all four pages scored **100 for accessibility before any change +was made, and all four still had a real defect** — a Lighthouse score is a +floor, not a verdict. + +Deployment to EC2 is documented in [`docs/deployment.md`](docs/deployment.md). + +--- + ## Environment variables | Variable | Default | Purpose | @@ -224,20 +371,31 @@ being dressed up as one. | `SITE_URL` | derived from the request | Absolute base for links inside the RSS feed. | | `NEXT_PUBLIC_API_BASE` | `""` (same origin) | Lets the client target a different server. | | `BUILD_TARGET` | unset | `static` builds the Assessment 1 GitHub Pages export. | +| `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://localhost:4318` | Collector address. `http://otel-collector:4318` in Compose. | +| `OTEL_SERVICE_NAME` | `rss-server` | Name shown in Jaeger's service list. | --- ## Tech stack Next.js 16 (App Router) · React 19 · TypeScript · Prisma 7 with the -better-sqlite3 driver adapter · SQLite · Zod · Docker +better-sqlite3 driver adapter · SQLite · Zod · Docker · OpenTelemetry · +Jaeger · Prometheus · Playwright · JMeter --- ## Project continuity Assessment 1 established the frontend, usability and accessibility layer. -Assessment 2 adds the API, database and Docker packaging. Assessment 3 builds on -this with dashboard views, simulated input records, rule-based interpretation -and reporting — which is why `RequestLog`, `Subscriber` and `/api/stats` already -exist: the operational data those features need is being collected now. +Assessment 2 added the API, database and Docker packaging, and started +collecting operational data it did not yet display — `RequestLog`, +`Subscriber` and `/api/stats` were built there in anticipation of this stage. + +Assessment 3 makes the running system observable and proves it works: +`/dashboard` reads the data that was already being collected, `RequestLog` +grew the two columns it turned out to be missing, `FeedFetch` records what the +feeds themselves do, OpenTelemetry and Prometheus report on the system from +the outside, and Playwright, JMeter and Lighthouse establish that it behaves +under use. + +Assessment 4 presents this system live. diff --git a/VIDEO_SCRIPT.md b/VIDEO_SCRIPT.md index 28a8760..f72f929 100644 --- a/VIDEO_SCRIPT.md +++ b/VIDEO_SCRIPT.md @@ -1,263 +1,156 @@ -# Assessment 2 — video script +# Assessment 3 — video walkthrough script -**Target: 6 minutes** (brief allows 3–8). Bracketed lines are actions, not narration. +**Required:** 3–8 minutes, showing your **student ID card, your face and your +voice**, plus the application and its key features. Aim for **7 minutes** — +there is a lot to show and the marking is done alongside the live defence, so +this is the artefact that has to stand on its own if anything goes wrong. --- -## Pre-flight — do this before you hit record +## Before you press record -```bash -cd ~/development/latrobe_cloud_assessment_1 -docker compose down # optional: a fresh start looks better on camera -docker compose up -d --build -docker ps # wait until STATUS says (healthy) -``` - -Then set the stage: +- [ ] `docker compose up -d --build` — all four services `Up` +- [ ] Generate traffic so the dashboard is not empty: + `HOST=127.0.0.1 PORT=3000 ./load/run-stages.sh` (or a few dozen curls) +- [ ] Tabs open, in this order: `/dashboard` · `/feeds` · `/client` · + Jaeger `:16686` · Prometheus `:9090` · GitHub repo · terminal +- [ ] Student ID card within reach +- [ ] Terminal font large enough to read on a compressed recording +- [ ] Notifications off, other tabs closed -- **Settings page → turn OFF "compact list"** so post summaries are visible. -- Open browser tabs in this order, left to right: - 1. `localhost:3000` 2. `localhost:3000/feeds` 3. `localhost:3000/client` - 4. `localhost:3000/rss` 5. `localhost:3000/api/health` 6. `localhost:3000/api/stats` -- Have a terminal window ready, font size **16pt+** so it reads on video. -- Open `prisma/schema.prisma` in your editor. -- Have your student ID card in reach. +--- -Have this command copied ready to paste: +## 0:00 — Identity (20s) -```bash -curl -s -o /tmp/r.json -w 'HTTP %{http_code}\n' -X POST http://localhost:3000/api/posts \ - -H 'Content-Type: application/json' \ - -d '{"title":"Live demo post","summary":"Created on camera.","content":"Written to SQLite and published as RSS.","authorName":"Careers & Employability","feedSlugs":["careers"]}' -python3 -c " -import json;d=json.load(open('/tmp/r.json'))['data'] -print(' title :',d['title']); print(' id :',d['id']) -print(' channel:',[f['slug'] for f in d['feeds']]); print(' author :',d['author']['name'])" -``` +> "Nicholas Green, student number 22840097, CSE5006 Assessment 3 — a +> data-driven web application and reporting." -> Raw \`curl\` prints the JSON body but **not** the status code, and the full -> record is an unreadable wall of text on video. The \`-w\` flag prints -> \`HTTP 201\` and the second command shows four readable lines instead of forty. +Hold the student ID to camera. Face visible. Say the numbers aloud rather than +only showing them. ---- +## 0:20 — What the system is (40s) -## 0:00 — Identity (30s) +One sentence of continuity, then move: -> [**Face to camera, holding student ID up**] -> -> "Hi, I'm Nicholas Green, student number 22840097, and this is my Assessment 2 -> submission for Cloud Web Applications. -> -> Assessment 1 was the frontend. Assessment 2 puts a real server behind it — a -> database, a REST API, RSS output, and the whole thing running in Docker. I'll -> show you the server sending feeds to a client, and then how it works underneath." +> "This is an RSS server for university announcements. Assessment 1 built the +> interface, Assessment 2 built the API, database and Docker packaging. +> Assessment 3 is about knowing whether the thing is actually working — so +> it's a dashboard, tracing, metrics, and three kinds of testing." ---- +Show the architecture diagram in the README briefly. Do not read it out. -## 0:30 — It runs in Docker (45s) +## 1:00 — The dashboard (2:00) ← **the largest single mark, spend the time** -> [**Switch to terminal. Type `docker ps`**] -> -> "The application is running in a Docker container right now. You can see the -> status here reads **healthy** — that's not Docker just checking the process is -> alive. It's polling my own healthcheck endpoint every fifteen seconds." +Open `/dashboard`. Work top to bottom and say what each thing is *for*: -> [**Type `docker compose logs --tail 20 rss-server`**] -> -> "And on start-up the container applies its database migrations and seeds the -> baseline channels before the server boots. So `docker compose up` on a clean -> machine gives you a working server — there's no manual setup step." +- **Health strip** — "`/health` returns 200, and this database figure is a + real `SELECT 1`, not an assumption. The container's own healthcheck polls + the same endpoint." +- **Alerts** — "These are rules over the data, with two levels. A warning + means something is drifting; a critical means someone has to act. One + threshold only tells you once it's already too late." Point at a live + warning — the unknown-channel one is good: "someone's client is polling a + channel that doesn't exist, which is invisible unless something says so." +- **Operational metrics** — total requests, requests in window, **unique + clients**, feed polls, items served, RSS channel count, latency, error rate. + Name them; these are the rubric's list. +- **Requests per feed** — "measured per channel, not inferred from the path." +- **Feed status table** — "posts stored, polls received, items in the last + delivery, and its state. A feed serving zero items is a 200 as far as HTTP + is concerned — that's why there's a separate table recording what the feed + did, not just what the response code was." +- **Requests per client** — "identified by a hash of address and user agent, + so the count is real without the server storing who anyone is." ---- +**Then make it move.** In the terminal, `curl http://localhost:3000/rss` a few +times, and watch the counters change on the next refresh. This is the single +most convincing ten seconds in the video — it proves the numbers are live +rather than rendered once. -## 1:15 — The heartbeat and the database probe (45s) +## 3:00 — Data and persistence (1:00) -> [**Browser tab: `localhost:3000/api/health`**] -> -> "This is the healthcheck Docker is polling. Notice it's not just returning -> 'ok'. It runs an actual `SELECT 1` against the database and reports the -> latency — seven milliseconds here. If the database were unreachable this -> returns a 503 and the container gets marked unhealthy. A healthcheck that -> returns 200 no matter what tells an operator nothing." +> "All of that comes out of the database, through Prisma." ---- - -## 2:00 — The schema (60s) - -> [**Editor: `prisma/schema.prisma` — in VS Code, `Cmd+P` then type `schema.prisma`. -> Scroll slowly through the models.**] -> -> "The database is SQLite through Prisma. Seven models, and each one earns its -> place in the RSS use case. -> -> `Feed` is a channel — and deliberately, the channel *is* the category. -> `/rss/careers` **is** the Careers feed, so a separate category table would -> have been duplication. -> -> `Post` is an RSS item. `Author` is the poster. -> -> [**Point at `FeedPost`**] This is an explicit many-to-many join rather than an -> implicit one, so it can carry its own data — and it means one post can go to -> several channels. An internship notice is both Careers and General. -> -> [**Point at `onDelete: SetNull` on the author relation**] The delete rules are -> deliberate too. Removing an author sets null rather than cascading — deleting -> a person shouldn't destroy everything they published. But enclosures and join -> rows *do* cascade, so nothing is orphaned. -> -> [**Point at `@@index([status, pubDate])`**] And that composite index is -> precisely the query that renders a feed." - -**Optional — show the real rows** (a stronger shot than Prisma Studio, because it -reads the live database inside the running container): +- Show `prisma/schema.prisma` — point at `RequestLog` (with `feedSlug` and + `clientKey`) and `FeedFetch`. +- **Prove it from the database, not the UI** — expect this question in the + oral defence: ```bash -docker exec rss-server node --experimental-sqlite -e " -const {DatabaseSync}=require('node:sqlite'); -const db=new DatabaseSync('/data/rss.db'); -for (const t of ['Feed','Post','Author','FeedPost','Enclosure','Subscriber','RequestLog']) - console.log(t.padEnd(12), db.prepare('SELECT COUNT(*) c FROM '+t).get().c); -" +docker compose exec rss-server sh -c \ + 'sqlite3 /data/rss.db "select feedSlug, count(*) from RequestLog group by feedSlug;"' ``` -> "And those models aren't theoretical — here are the actual row counts from the -> database inside the container. Five channels, three posts, and four rows in -> the join table, because one post belongs to two channels." - -> **Do NOT run `npm run db:studio` on camera.** It reads `DATABASE_URL` from -> `.env`, which points at the local `./dev.db` — a *different* database from the -> container's `/data/rss.db`. They look almost identical, so the mistake is easy -> to miss, but a post you create on camera will not appear in Studio. If you -> want the Studio GUI, snapshot the live database first: -> -> ```bash -> docker cp rss-server:/data/rss.db /tmp/live.db -> DATABASE_URL="file:/tmp/live.db" npx prisma studio # opens localhost:5555 -> ``` - ---- - -## 3:00 — CRUD over the API (60s) - -> [**Terminal. Paste the prepared POST command, hit enter.**] -> -> "Here's a create through the REST API. It comes back **201**, and you can see -> the server has given it an ID and attached it to the Careers channel." - -> [**Type `curl -s localhost:3000/api/posts | head -c 400`**] -> -> "Every endpoint returns the same envelope — `ok`, `data`, `meta`, `error` — so -> the frontend never has to guess the shape of a response. The `meta` block -> carries the paging totals." - -> [**Type: `curl -s -X POST localhost:3000/api/posts -H 'Content-Type: application/json' -d '{"title":"x"}' -o /dev/null -w "%{http_code}\n"`**] -> -> "And the status codes are meaningful — that's a **422**, validation failure. -> Missing record gives 404, duplicate slug gives 409." - ---- - -## 4:00 — The money shot: server → client (75s) - -> [**Browser tab: `localhost:3000/rss`**] -> -> "This is the RSS server output. Valid RSS 2.0 — channel metadata, and one item -> per announcement. The dates are RFC-822 format, which the spec requires, and -> the text is CDATA-wrapped so ampersands and smart quotes can't break the XML." - -> [**Browser tab: `localhost:3000/client`**] -> -> "And this is the RSS Client. This page is a *subscriber* — it's not reading -> the database. It makes an HTTP request to `/rss`, gets XML back, and parses it -> with DOMParser exactly like any third-party feed reader would. -> -> You can see the transport details up here — 200, the round-trip time, the -> payload size." - -> [**Click `/rss/careers`, then `/rss/events`**] -> -> "And subscribing to a different category just means pointing the client at a -> different endpoint. Careers. Events. Same client, different URL — that's the -> whole story. Nothing else changes." - -> [**Click "Raw RSS 2.0 response" → Show**] -> -> "And here's the raw XML it received, so you can see this is genuinely RSS -> crossing the network, not an internal function call dressed up as one." - ---- - -## 5:15 — Frontend integration and operational output (45s) - -> [**Browser tab: `localhost:3000/feeds` — refresh**] -> -> "The Assessment 1 interface is intact — same components, same themes, same -> hamburger menu. What changed is where the data comes from. The local storage -> layer is deleted; this list is the database, read through the API. There's the -> post I created in the terminal a minute ago. -> -> Searching and filtering are pushed down to the server as query parameters, not -> filtering a local copy." - -> [**Browser tab: `localhost:3000/api/stats`**] -> -> "And there's a second operational endpoint — posts per channel, per author, -> and subscriber polling. Alongside `/api/count`, which reports request totals -> and timings from a log the API writes on every call." +> "Same numbers the dashboard is showing, straight out of SQLite." + +## 4:00 — Tracing and metrics (1:00) + +- **Jaeger** `:16686` → service `rss-server` → find a trace → expand it. + > "Next instruments its own request handling automatically. These + > — `rss.lookup_channel`, `rss.load_items` — are spans I added by hand, so + > when a feed is slow I can see whether it was the database or the render." +- **Prometheus** `:9090` → query `rss_feed_polls_total` → Graph. + > "Per channel, over time. The app exports OTLP to a collector and the + > collector decides where it goes — swapping Jaeger out doesn't touch + > application code." +- Show `/targets` — all three up. + +## 5:00 — Testing (1:15) + +- **Playwright** — run it live if you are confident, or show a recorded pass: + ```bash + npx playwright test + ``` + > "Ten tests. The server use case creates an announcement through the real + > form, then checks the API *and the published RSS* agree — a UI test that + > only checks the UI can pass while the feed stays empty. The client use + > case fetches and renders a feed. One test polls a feed and then asserts + > the dashboard's count moved." +- **JMeter** — show `load/results/summary.md`: + > "x1 through x1000, nothing moves — 3 or 4 milliseconds. At two thousand + > concurrent clients the mean goes to 58 and the p99 to 149, but throughput + > goes *up* and nothing fails. That's queueing, not breaking, and it's + > SQLite serialising the request-log write." + > "The x10000 stage is 10,000 sessions at 2,000 concurrency, because my + > machine won't create more than about 4,100 OS threads. That's the load + > generator hitting a limit, not the server — the server never failed a + > request." + Being upfront about this reads as competence, not as a shortfall. +- **Lighthouse** — open a before and after report side by side: + > "Accessibility scored 100 on every page before I changed anything — and + > every page was still failing the label/name mismatch check, because that + > audit doesn't carry any score weight. The header link announced 'Home' + > while the screen said something else, so anyone using voice control + > couldn't activate it by reading it. Fixed by letting the visible text be + > the accessible name. A Lighthouse score is a floor, not a verdict." + +## 6:15 — Repository and CI (30s) + +- GitHub: show the **branch list and the merged pull requests** — one branch + per feature, clean `main`, no `node_modules`. +- Show `.github/workflows/ci.yml`: + > "Every pull request lints, type checks, builds, runs the end-to-end tests + > and builds the container image before it can merge. Main is gated, not + > trusted." + +## 6:45 — Close (15s) + +> "That's the RSS server with a data-driven dashboard, OpenTelemetry tracing, +> Prometheus metrics, and Playwright, JMeter and Lighthouse testing. Running +> in Docker, deployed on EC2. Thanks." --- -## 6:00 — Persistence and repository (45s) - -> [**Terminal: `docker compose down && docker compose up -d`, wait, then refresh `/feeds`**] -> -> "One last thing — the database lives on a named Docker volume, not inside the -> container. So I can destroy and recreate the container, and the post I created -> is still there." - -> [**Browser: your GitHub repo → Pull requests → Closed**] -> -> "And the repository has each feature on its own branch, merged through pull -> requests, with CI that lints, type-checks and builds the container image on -> every push. -> -> That's Assessment 2 — thanks for watching." - ---- +## If something breaks on camera -## Timing check - -| Section | Runs | Cumulative | -| --- | --- | --- | -| Identity | 0:30 | 0:30 | -| Docker | 0:45 | 1:15 | -| Healthcheck | 0:45 | 2:00 | -| Schema | 1:00 | 3:00 | -| CRUD | 1:00 | 4:00 | -| **Server → Client** | 1:15 | 5:15 | -| Integration + stats | 0:45 | 6:00 | -| Persistence + repo | 0:45 | 6:45 | - -Comfortably inside the 3–8 minute window with room to breathe. - ---- - -## If you run short on time - -Cut in this order: the schema walk-through to 30 seconds, then the persistence -demo, then `/api/stats`. - -**Never cut:** your ID and face, `docker ps` showing healthy, and the -`/client` page switching channels. Those three cover the criteria the rubric -names explicitly. - ---- +Keep recording and narrate it. "That's a 500 — let me look at the logs" and +then finding it is worth more than a clean take, and this same system has to +survive live questioning in Assessment 4 anyway. -## Common on-camera failures +## Things not to do -- **Nothing loads.** Another container may have taken port 3000 — `docker ps` - and check. `markovcast-frontend` was the culprit before. -- **`/client` shows an error.** The container is still starting; wait for - `docker ps` to say healthy. -- **Stale content in the browser.** Hard reload with `Cmd+Shift+R`. -- **Summaries missing from post rows.** Compact list is on in Settings. +- Don't read the README aloud. Show the running system. +- Don't skip the "make the counters move" moment to save time — cut something + else. +- Don't claim 10,000 concurrent clients. Say what actually happened. +- Don't spend more than a minute on Assessment 1 and 2 material. diff --git a/docs/submission.md b/docs/submission.md new file mode 100644 index 0000000..78ef5f8 --- /dev/null +++ b/docs/submission.md @@ -0,0 +1,73 @@ +# Submission checklist + +## Assessment 3 + +- [ ] **Video, 3–8 minutes**, showing student ID, face and voice — script in + [`VIDEO_SCRIPT.md`](../VIDEO_SCRIPT.md) +- [ ] **Zip of the project code**, with `node_modules` removed: + +```bash +cd .. +zip -r latrobe-rss-a3-22840097.zip latrobe_cloud_assessment_1 \ + -x '*/node_modules/*' '*/.next/*' '*/.git/*' '*/out/*' \ + '*/test-results/*' '*/playwright-report/*' '*/assessment_3/*' +# confirm nothing slipped through +unzip -l latrobe-rss-a3-22840097.zip | grep -c node_modules # expect 0 +``` + +- [ ] **GitHub repository link:** + +- [ ] **AI acknowledgement form** — required, and its absence can be treated as + an academic integrity breach. The form is on the Assessments page in + Moodle. This assessment permits full AI use, so there is nothing to + declare beyond completing it honestly. +- [ ] Submitted via Moodle so it **generates a Turnitin similarity score** — + a submission that produces no score cannot be checked and will not be + marked. + +## Assessment 4 — the live defence + +- [ ] **Book a slot** as soon as the spreadsheet is announced. Slots are about + ten minutes and the whole cohort is marked in one week. +- [ ] **Start EC2 and the stack at least ten minutes before the slot.** Cold + boot plus image pull plus the first Next.js request takes longer than a + slot allows. See [`deployment.md`](deployment.md). +- [ ] Generate traffic before the session so the dashboard has data. +- [ ] No slides. It is a live demonstration and questions. +- [ ] Keep the Assessment 3 video accessible as a fallback if the deployment + fails on the day. + +### Questions to have an answer ready for + +These are the ones signalled in class, and the ones this codebase invites: + +- **"Show me the data is really in the database."** + `docker compose exec rss-server sh -c 'sqlite3 /data/rss.db "select feedSlug, count(*) from RequestLog group by feedSlug;"'` +- **"How do you know the application is healthy?"** `/api/health` does a real + `SELECT 1`; `docker compose ps` shows the container healthcheck polling it; + the dashboard health strip shows both. +- **"Where are your spans?"** `lib/otel.ts` defines `withSpan`; `handle()` in + `lib/api-response.ts` names one per API route; the RSS routes wrap the + channel lookup and the item query separately. Show one in Jaeger. +- **"What happens if I post an empty feed URL?"** Zod rejects it with 422 + before it reaches the database — there is an end-to-end test for exactly + this in `e2e/server.spec.ts`. +- **"What breaks first under load?"** SQLite serialises the `RequestLog` + write. See `load/README.md`. +- **"Why SQLite and not Postgres?"** One process, no start-up ordering to get + wrong, data on a named volume. The load results quantify what that choice + costs and where it would need to change. +- **"Why is `instrumentation.ts` at the root?"** Next only looks for it there. + Under `app/` it silently exports nothing and the service never appears in + Jaeger. + +## Known gaps, stated honestly + +- The x10000 load stage is 10,000 client sessions at 2,000 concurrency, not + 10,000 concurrent clients. The machine generating the load could not create + more than ~4,100 OS threads. Explained in `load/README.md`. +- `npm audit` reports advisories in `sharp`, a transitive dependency of the + pinned Next.js version. Clearing them requires a Next upgrade, which was not + worth the regression risk mid-assessment. +- Lighthouse performance sits at 91 on `/feeds`; the remaining opportunities + are in the framework bundle rather than in application code.