Feat/why is the dlr shut #1

Merged
lyrathorpe merged 7 commits from feat/why-is-the-dlr-shut into main 2026-06-11 16:26:33 +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);
}