8 Commits

Author SHA1 Message Date
lyrathorpe 6707504b88 Merge pull request 'Feat/why is the dlr shut' (#1) from feat/why-is-the-dlr-shut into main
Build and publish container / build (push) Failing after 40s
Reviewed-on: #1
2026-06-11 16:26:33 +01:00
Emma Thorpe e5dd090c45 docs: document container, CI and dependency updates
Build and publish container / build (pull_request) Successful in 5m18s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 16:18:04 +01:00
Emma Thorpe eb0186a5b8 build: add Renovate configuration
Track the Dockerfile base image, the actions used in the Gitea workflow,
and versioned front-end dependencies referenced in HTML (via renovate
comment annotations or jsDelivr/unpkg npm URLs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 16:18:04 +01:00
Emma Thorpe b28ec41ccb ci: build and publish the container via Gitea Actions
Build the image on pushes to main, version tags and pull requests, and
push to the Gitea container registry (except on PRs) using the
auto-provided GITEA_TOKEN. Tags are derived with docker/metadata-action.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 16:18:04 +01:00
Emma Thorpe d2bac6b8d3 feat: containerise the site with nginx-unprivileged
Serve the static site from a non-root nginx image listening on 8080,
with cache headers, gzip and a /healthz endpoint. Designed to run behind
an external reverse proxy that terminates TLS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 16:18:04 +01:00
Emma Thorpe a2c6408277 docs: add README
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 16:18:04 +01:00
Emma Thorpe 3b5c304002 feat: add random message selection and theme toggle
script.js picks a random entry from MESSAGES on load and on each "Check
again" click, and persists the chosen colour scheme in localStorage.
messages.js holds the MESSAGES array as a placeholder template to be
filled in with content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 16:18:04 +01:00
Emma Thorpe 59b8d969df feat: add page structure and DLR colour schemes
Single-page layout with a centred message, a colour-scheme toggle, and a
"Check again" button. styles.css defines two palettes selected via the
data-theme attribute: modern DLR turquoise/teal and the original 1987
red-and-blue livery.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 16:18:04 +01:00
10 changed files with 489 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
# Keep the build context minimal; only the site assets and nginx config
# referenced by the Dockerfile are needed.
.git
.gitea
.github
.idea
.vscode
*.md
renovate.json
.renovaterc*
Dockerfile
.dockerignore
+53
View File
@@ -0,0 +1,53 @@
name: Build and publish container
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Determine registry host
run: echo "REGISTRY=${GITHUB_SERVER_URL#*://}" >> "$GITHUB_ENV"
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Gitea container registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITEA_TOKEN }}
- name: Extract image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ github.repository }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+9
View File
@@ -0,0 +1,9 @@
# Lightweight, non-root nginx serving the static site.
# Runs as user "nginx" and listens on 8080, ready to sit behind an
# external reverse proxy that terminates TLS and forwards requests.
FROM nginxinc/nginx-unprivileged:1.27-alpine-slim
COPY default.conf /etc/nginx/conf.d/default.conf
COPY --chown=nginx:nginx index.html styles.css script.js messages.js /usr/share/nginx/html/
EXPOSE 8080
+85
View File
@@ -0,0 +1,85 @@
# Why is the DLR shut today?
A single-page site that displays one randomly chosen message in the centre of
the screen. The message changes on every page load and whenever the
**Check again** button is pressed.
The site is themed around the Docklands Light Railway colour scheme, with a
toggle between:
- **Modern colours** — the current DLR turquoise/teal branding.
- **Original colours** — the 1987 DLR red-and-blue livery.
The chosen theme is remembered between visits via `localStorage`.
## Adding messages
Edit `messages.js` and fill the `MESSAGES` array with your own reasons — one
string per entry. Entries are inserted as plain text. Until you add some, the
page shows a fallback prompt.
## Running
It is a static site with no build step. Open `index.html` in a browser, or
serve the directory with any static file server, for example:
```sh
python3 -m http.server
```
## Container
The site is packaged as a container based on `nginxinc/nginx-unprivileged`. It
runs as a non-root user and listens on port **8080**, serving the static files
and exposing a `/healthz` endpoint. It is designed to sit behind an external
reverse proxy that terminates TLS and routes by host.
Build and run locally:
```sh
docker build -t dlr .
docker run --rm -p 8080:8080 dlr
# then browse http://localhost:8080
```
## CI
`.gitea/workflows/build-and-publish.yml` builds the container with Gitea Actions
and publishes it to this Gitea instance's container registry on pushes to `main`
and on `v*` tags. Pull requests build the image but do not push. Authentication
uses the automatically provided `GITEA_TOKEN`; the registry host is derived from
the Gitea server URL.
The published image is `<gitea-host>/<owner>/<repo>`, tagged by branch, semver
(for `v*` tags), commit SHA, and `latest` on the default branch.
## Dependency updates
`renovate.json` configures Renovate to keep dependencies current:
- the Dockerfile base image,
- the actions used in the Gitea workflow,
- versioned front-end dependencies referenced in HTML.
There are currently no external front-end dependencies. When one is added via a
CDN, Renovate will track it if it is either annotated with a comment, e.g.
```html
<!-- renovate: datasource=npm depName=bootstrap -->
<link href="https://cdn.example.com/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
```
or referenced through a versioned jsDelivr / unpkg npm URL, which is detected
automatically.
## Files
| File | Purpose |
| ------------------------------------- | ------------------------------------------------ |
| `index.html` | Page structure. |
| `styles.css` | Both colour schemes, selected via `data-theme`. |
| `messages.js` | The list of messages (fill this in). |
| `script.js` | Random message selection and the theme toggle. |
| `Dockerfile` / `default.conf` | Container image and nginx static-serving config. |
| `.gitea/workflows/` | Gitea Actions build-and-publish pipeline. |
| `renovate.json` | Renovate dependency-update configuration. |
+42
View File
@@ -0,0 +1,42 @@
# Static file serving for "Why is the DLR shut today?".
# Intended to run behind an external reverse proxy (e.g. NGINX) which
# handles TLS, host routing and any X-Forwarded-* headers.
server {
listen 8080;
listen [::]:8080;
server_name _;
root /usr/share/nginx/html;
index index.html;
gzip on;
gzip_vary on;
gzip_types text/css application/javascript;
location / {
try_files $uri $uri/ =404;
}
# HTML should always be revalidated so deploys are picked up promptly.
location = /index.html {
add_header Cache-Control "no-cache";
}
# The message list is edited frequently; do not cache it.
location = /messages.js {
add_header Cache-Control "no-cache";
}
# Other static assets may be cached for a short period.
location ~* \.(?:css|js)$ {
add_header Cache-Control "public, max-age=3600";
}
# Health endpoint for the proxy / orchestrator.
location = /healthz {
access_log off;
default_type text/plain;
return 200 "ok\n";
}
}
+31
View File
@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Why is the DLR shut today?</title>
<link rel="stylesheet" href="styles.css">
</head>
<body data-theme="modern">
<header class="controls">
<fieldset class="theme-toggle">
<legend class="sr-only">Colour scheme</legend>
<button type="button" class="theme-button" data-set-theme="modern" aria-pressed="true">
Modern colours
</button>
<button type="button" class="theme-button" data-set-theme="original" aria-pressed="false">
Original colours
</button>
</fieldset>
</header>
<main class="stage">
<h1 class="title">Why is the DLR shut today?</h1>
<p class="message" id="message" role="status" aria-live="polite"></p>
<button type="button" class="refresh-button" id="refresh">Check again</button>
</main>
<script src="messages.js"></script>
<script src="script.js"></script>
</body>
</html>
+18
View File
@@ -0,0 +1,18 @@
/* Messages for "Why is the DLR shut today?"
*
* Fill this array with your own reasons. One string per entry.
* On every page load (and every "Check again" click) one entry is
* picked at random and shown in the centre of the screen.
*
* Keep them short — roughly a sentence — so they fit the large display
* text. HTML is NOT rendered; entries are inserted as plain text.
*
* Replace the placeholders below with your own content.
*/
const MESSAGES = [
"PLACEHOLDER: write your first reason here",
"PLACEHOLDER: write another reason here",
// Add as many entries as you like, one per line:
// "Your reason here",
];
+39
View File
@@ -0,0 +1,39 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":dependencyDashboard",
":semanticCommits"
],
"labels": ["renovate"],
"github-actions": {
"fileMatch": ["^\\.gitea/workflows/[^/]+\\.ya?ml$"]
},
"packageRules": [
{
"description": "Group nginx base image updates",
"matchManagers": ["dockerfile"],
"groupName": "docker base image"
}
],
"customManagers": [
{
"customType": "regex",
"description": "Update HTML dependencies annotated with a renovate comment, e.g. <!-- renovate: datasource=npm depName=bootstrap --> before the versioned URL",
"fileMatch": ["\\.html$"],
"matchStrings": [
"datasource=(?<datasource>\\S+) depName=(?<depName>\\S+)( versioning=(?<versioning>\\S+))?[\\s\\S]*?(?<currentValue>v?\\d+\\.\\d+\\.\\d+[\\w.-]*)"
]
},
{
"customType": "regex",
"description": "Auto-detect versioned jsDelivr / unpkg npm assets in HTML",
"fileMatch": ["\\.html$"],
"matchStrings": [
"https://cdn\\.jsdelivr\\.net/npm/(?<depName>@?[^@/]+(?:/[^@/]+)?)@(?<currentValue>\\d[^/\"']+)",
"https://unpkg\\.com/(?<depName>@?[^@/]+(?:/[^@/]+)?)@(?<currentValue>\\d[^/\"']+)"
],
"datasourceTemplate": "npm"
}
]
}
+59
View File
@@ -0,0 +1,59 @@
/* Why is the DLR shut today?
* Picks a random message on load and on demand, and handles the
* colour-scheme toggle. Messages live in messages.js (MESSAGES). */
(function () {
"use strict";
const FALLBACK = "Add some reasons in messages.js";
const messageEl = document.getElementById("message");
const refreshButton = document.getElementById("refresh");
const themeButtons = document.querySelectorAll("[data-set-theme]");
const THEME_KEY = "dlr-theme";
function pickMessage() {
if (!Array.isArray(MESSAGES) || MESSAGES.length === 0) {
return FALLBACK;
}
const index = Math.floor(Math.random() * MESSAGES.length);
return MESSAGES[index];
}
function showMessage() {
messageEl.textContent = pickMessage();
}
function applyTheme(theme) {
document.body.setAttribute("data-theme", theme);
themeButtons.forEach(function (button) {
const isActive = button.dataset.setTheme === theme;
button.setAttribute("aria-pressed", String(isActive));
});
try {
localStorage.setItem(THEME_KEY, theme);
} catch (err) {
/* localStorage unavailable; theme simply won't persist. */
}
}
function initTheme() {
let saved = null;
try {
saved = localStorage.getItem(THEME_KEY);
} catch (err) {
/* ignore */
}
applyTheme(saved === "original" ? "original" : "modern");
}
themeButtons.forEach(function (button) {
button.addEventListener("click", function () {
applyTheme(button.dataset.setTheme);
});
});
refreshButton.addEventListener("click", showMessage);
initTheme();
showMessage();
})();
+141
View File
@@ -0,0 +1,141 @@
/* Why is the DLR shut today?
Two colour schemes selected via the [data-theme] attribute on <body>:
- modern: current DLR turquoise/teal branding
- original: 1987 DLR red-and-blue livery */
:root {
--font-stack: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
/* Modern DLR — turquoise/teal */
[data-theme="modern"] {
--bg: #00afaa;
--bg-accent: #007e7a;
--surface: #ffffff;
--text: #ffffff;
--message: #ffffff;
--button-bg: #ffffff;
--button-text: #007e7a;
--button-active-bg: #00302e;
--button-active-text: #ffffff;
}
/* Original DLR — 1987 red and blue */
[data-theme="original"] {
--bg: #002b5c;
--bg-accent: #c8102e;
--surface: #f5f0e1;
--text: #f5f0e1;
--message: #f5f0e1;
--button-bg: #c8102e;
--button-text: #f5f0e1;
--button-active-bg: #f5f0e1;
--button-active-text: #002b5c;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
font-family: var(--font-stack);
color: var(--text);
background: linear-gradient(135deg, var(--bg) 0%, var(--bg-accent) 100%);
transition: background 0.4s ease, color 0.4s ease;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
.controls {
display: flex;
justify-content: flex-end;
padding: 1.5rem;
}
.theme-toggle {
display: flex;
gap: 0.5rem;
border: none;
margin: 0;
padding: 0;
}
.theme-button {
font-family: inherit;
font-size: 0.9rem;
font-weight: 600;
padding: 0.5rem 1rem;
border: 2px solid var(--button-bg);
border-radius: 999px;
background: transparent;
color: var(--text);
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease;
}
.theme-button[aria-pressed="true"] {
background: var(--button-active-bg);
color: var(--button-active-text);
border-color: var(--button-active-bg);
}
.stage {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 2rem;
gap: 2rem;
}
.title {
font-size: clamp(1.5rem, 4vw, 2.5rem);
font-weight: 700;
margin: 0;
letter-spacing: 0.02em;
}
.message {
font-size: clamp(1.75rem, 6vw, 4rem);
font-weight: 800;
line-height: 1.2;
margin: 0;
max-width: 22ch;
color: var(--message);
}
.refresh-button {
font-family: inherit;
font-size: 1rem;
font-weight: 600;
padding: 0.75rem 1.75rem;
border: 2px solid var(--button-bg);
border-radius: 999px;
background: var(--button-bg);
color: var(--button-text);
cursor: pointer;
transition: transform 0.1s ease, opacity 0.2s ease;
}
.refresh-button:hover {
opacity: 0.9;
}
.refresh-button:active {
transform: scale(0.97);
}