first commit
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
# Cashow error pages
|
||||
|
||||
Branded static error pages served by Traefik in front of the LNbits instances,
|
||||
replacing Traefik's default plain-text bodies (`no available server`,
|
||||
`Internal Server Error`).
|
||||
|
||||
```
|
||||
deploy/error-pages/
|
||||
├── README.md you are here — not deployed
|
||||
├── preview.html local review grid — not deployed
|
||||
└── pages/ ← the ConfigMap is built from this directory only
|
||||
├── 429.html
|
||||
├── 500.html
|
||||
├── 502.html
|
||||
├── 503.html
|
||||
├── 504.html
|
||||
└── onderhoud.html
|
||||
```
|
||||
|
||||
`README.md` and `preview.html` sit *outside* `pages/` on purpose: the ConfigMap
|
||||
is built with `--from-file=.`, which would otherwise sweep them both in as keys.
|
||||
|
||||
## Design constraints
|
||||
|
||||
Each page is a single self-contained HTML file making **zero external requests** —
|
||||
inline `<style>`, inline `<script>`, the logo inlined as a base64 `<image>` inside
|
||||
an `<svg>`. This is not a preference. Traefik's `errors` middleware fetches only
|
||||
the HTML document from the error service and serves that body on the *original*
|
||||
domain, so any relative subresource (`/logo.svg`, `/style.css`) would resolve
|
||||
against the LNbits instance that is currently down and 404.
|
||||
|
||||
System font stack throughout — no webfont, embedded or otherwise.
|
||||
|
||||
| file | size |
|
||||
|---|---|
|
||||
| `500.html` | 20.7 KB |
|
||||
| `429.html`, `502.html`, `503.html`, `504.html`, `onderhoud.html` | 22.1 KB each |
|
||||
| **total** | **131 KB** |
|
||||
|
||||
`500.html` is smaller because it has no retry block, and therefore ships no
|
||||
countdown JS, no `<noscript>` refresh and none of the rail CSS.
|
||||
|
||||
About 16 KB of every page is the base64 logo. Budget is 1 MB (ConfigMap hard cap),
|
||||
so there is room, but keep it in mind before adding anything per-page.
|
||||
|
||||
## Regenerate the ConfigMap
|
||||
|
||||
From `deploy/error-pages/pages`:
|
||||
|
||||
```bash
|
||||
kubectl -n kube-system create configmap error-pages --from-file=. --dry-run=client -o yaml | kubectl apply -f -
|
||||
```
|
||||
|
||||
The pod serving these mounts the ConfigMap; if it is mounted as a volume the
|
||||
kubelet refreshes it within a minute or so, but restart the deployment if you
|
||||
want the change immediately:
|
||||
|
||||
```bash
|
||||
kubectl -n kube-system rollout restart deploy/error-pages
|
||||
```
|
||||
|
||||
## Serving pod
|
||||
|
||||
The `errors` middleware needs a real backend to fetch pages from. Anything that
|
||||
serves static files works; this is the smallest version.
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: error-pages
|
||||
namespace: kube-system
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels: { app: error-pages }
|
||||
template:
|
||||
metadata:
|
||||
labels: { app: error-pages }
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginxinc/nginx-unprivileged:alpine
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
volumeMounts:
|
||||
- name: pages
|
||||
mountPath: /usr/share/nginx/html
|
||||
readinessProbe:
|
||||
httpGet: { path: /503.html, port: 8080 }
|
||||
volumes:
|
||||
- name: pages
|
||||
configMap:
|
||||
name: error-pages
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: error-pages
|
||||
namespace: kube-system
|
||||
spec:
|
||||
selector: { app: error-pages }
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8080
|
||||
```
|
||||
|
||||
## Middleware
|
||||
|
||||
```yaml
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: error-pages
|
||||
namespace: kube-system
|
||||
spec:
|
||||
errors:
|
||||
status:
|
||||
- "429"
|
||||
- "500-599"
|
||||
service:
|
||||
name: error-pages
|
||||
port: 80
|
||||
query: "/{status}.html"
|
||||
```
|
||||
|
||||
`query` substitutes the numeric status directly, which is why the filenames must
|
||||
be exactly `{status}.html`. A status in the range with no matching file (`501`,
|
||||
`507`, …) gets a 404 from the error service and Traefik falls back to its own
|
||||
plain body — so the six files cover the codes that actually occur, and anything
|
||||
else degrades to today's behaviour rather than breaking.
|
||||
|
||||
Attach it to an instance ingress with the usual annotation:
|
||||
|
||||
```yaml
|
||||
traefik.ingress.kubernetes.io/router.middlewares: kube-system-error-pages@kubernetescrd
|
||||
```
|
||||
|
||||
> **Cross-namespace reference.** The middleware lives in `kube-system` while the
|
||||
> instance ingresses live in their own namespaces. Traefik allows this by default,
|
||||
> but if `providers.kubernetesCRD.allowCrossNamespace=false` is set the reference
|
||||
> is silently ignored and you get the default error bodies back. Either enable it
|
||||
> or copy the Middleware object into each instance namespace.
|
||||
|
||||
## No 404 page
|
||||
|
||||
Deliberate. Traefik does not run the middleware chain when no router matches, so
|
||||
a 404 page would only ever fire for LNbits-generated 404s. Not worth the file.
|
||||
|
||||
## Planned maintenance
|
||||
|
||||
`onderhoud.html` is not wired to a status code — it is swapped in by hand.
|
||||
Repoint the middleware query for the duration of the window:
|
||||
|
||||
```yaml
|
||||
query: "/onderhoud.html"
|
||||
```
|
||||
|
||||
Apply it, and every 5xx from the instances renders the maintenance page instead
|
||||
of the per-status ones. Revert to `query: "/{status}.html"` when the window ends.
|
||||
|
||||
This only changes what is *rendered*. Traefik still returns the upstream status
|
||||
code, so `onderhoud.html` goes out as a 503 for as long as the instances are
|
||||
down — which is the correct signal for crawlers and uptime checks.
|
||||
|
||||
## Open TODOs
|
||||
|
||||
Both are marked in the HTML.
|
||||
|
||||
- **Logo.** Currently `www.cashow.be/img/cashow_extrasmall.png` base64-inlined —
|
||||
a 250×250 raster, not a vector. Drop `assets/cashow-logo.svg` in the repo and
|
||||
swap the marked `<!-- ==== CASHOW LOGO ==== -->` block in all six files; it is
|
||||
byte-identical across them, so it is one find-and-replace.
|
||||
- **Status host.** The ghost pill links to `https://status.cashow.be`, which did
|
||||
not resolve as of 2026-08-17 (NXDOMAIN). Point it at the real status page or
|
||||
remove the pill.
|
||||
|
||||
## Local review
|
||||
|
||||
Open `preview.html` in a browser to see all six side by side. It is a review aid
|
||||
only and is not part of the ConfigMap.
|
||||
+154
File diff suppressed because one or more lines are too long
+123
File diff suppressed because one or more lines are too long
+154
File diff suppressed because one or more lines are too long
+154
File diff suppressed because one or more lines are too long
+154
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,78 @@
|
||||
<!doctype html>
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>Preview — Cashow error pages (niet gedeployed)</title>
|
||||
<style>
|
||||
:root{
|
||||
--plum-900:#1A0F1A; --lime:#C6F24E; --paper:#F6F2F0; --muted:#A493A1;
|
||||
--line:rgba(246,242,240,.12);
|
||||
--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace;
|
||||
color-scheme:dark;
|
||||
}
|
||||
*,*::before,*::after{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; padding:24px; background:var(--plum-900); color:var(--paper);
|
||||
font:14px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
|
||||
}
|
||||
.warn{
|
||||
border:1px solid var(--lime); border-radius:10px; padding:14px 18px;
|
||||
margin:0 0 24px; font-family:var(--mono); font-size:13px; color:var(--lime);
|
||||
}
|
||||
.warn b{display:block; margin-bottom:4px; letter-spacing:.1em; text-transform:uppercase}
|
||||
.warn span{color:var(--muted)}
|
||||
.grid{display:grid; gap:24px; grid-template-columns:repeat(auto-fit,minmax(420px,1fr))}
|
||||
figure{margin:0}
|
||||
figcaption{
|
||||
font-family:var(--mono); font-size:12px; color:var(--muted);
|
||||
padding-bottom:8px; display:flex; justify-content:space-between; gap:12px;
|
||||
}
|
||||
figcaption a{color:var(--lime)}
|
||||
iframe{
|
||||
width:100%; height:620px; border:1px solid var(--line); border-radius:10px;
|
||||
background:var(--plum-900); display:block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<p class="warn">
|
||||
<b>Local review only — not deployed</b>
|
||||
<span>This file is not part of the <code>error-pages</code> ConfigMap. It lives one
|
||||
level above <code>pages/</code> so <code>--from-file=.</code> cannot pick it up.
|
||||
It is the only file here that makes relative requests; the six pages themselves
|
||||
make none. The frames reload themselves on their own countdown (20s, 60s for
|
||||
429) — that is the retry behaviour working, not a bug in the preview.</span>
|
||||
</p>
|
||||
|
||||
<div class="grid">
|
||||
<figure>
|
||||
<figcaption><span>503 · service unavailable</span><a href="pages/503.html">open</a></figcaption>
|
||||
<iframe src="pages/503.html" title="503 service unavailable" loading="lazy"></iframe>
|
||||
</figure>
|
||||
<figure>
|
||||
<figcaption><span>502 · bad gateway</span><a href="pages/502.html">open</a></figcaption>
|
||||
<iframe src="pages/502.html" title="502 bad gateway" loading="lazy"></iframe>
|
||||
</figure>
|
||||
<figure>
|
||||
<figcaption><span>504 · gateway timeout</span><a href="pages/504.html">open</a></figcaption>
|
||||
<iframe src="pages/504.html" title="504 gateway timeout" loading="lazy"></iframe>
|
||||
</figure>
|
||||
<figure>
|
||||
<figcaption><span>500 · internal server error</span><a href="pages/500.html">open</a></figcaption>
|
||||
<iframe src="pages/500.html" title="500 internal server error" loading="lazy"></iframe>
|
||||
</figure>
|
||||
<figure>
|
||||
<figcaption><span>429 · too many requests</span><a href="pages/429.html">open</a></figcaption>
|
||||
<iframe src="pages/429.html" title="429 too many requests" loading="lazy"></iframe>
|
||||
</figure>
|
||||
<figure>
|
||||
<figcaption><span>onderhoud · gepland onderhoud</span><a href="pages/onderhoud.html">open</a></figcaption>
|
||||
<iframe src="pages/onderhoud.html" title="Gepland onderhoud" loading="lazy"></iframe>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user