Quellcode durchsuchen

Add public /about page with feature tour and quickstart

Static, anonymous-accessible page that explains Sprint Planner to
non-technical users. Hero + plain-English workflow walk-through + a
gallery of the eight existing screenshots with click-to-enlarge into
a backdrop-blurred modal (Alpine CSP component, Esc / backdrop / pill
button to close). Also includes a Docker-only Quickstart, source repo
link, and Apache-2.0 license attribution. Header gains an About link
visible to signed-in and anonymous visitors alike.

- views/about.twig: the page; reuses the standard layout.
- views/layout.twig: About link added to the nav.
- public/index.php: GET /about route — public, no auth.
- public/assets/js/app.js: Alpine `screenshotModal` data component,
  registered alongside the existing appMenu / themeToggle. Reads
  `data-src` / `data-alt` off the trigger via $event.currentTarget so
  the strict CSP build of Alpine has no expression to evaluate.
- assets/css/input.css: `[x-cloak] { display: none !important; }` in
  the @layer base — needed because the strict CSP forbids inline
  `style="display:none"` on the modal during the pre-Alpine boot
  window.
- public/assets/img/screenshots/: the eight PNGs from doc/screenshots/
  copied under public/ so Apache serves them and the
  `img-src 'self'` CSP allows them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ClaudePriv@chiappa.zhdk.ch vor 6 Stunden
Ursprung
Commit
0fa978e8be

+ 8 - 0
assets/css/input.css

@@ -15,6 +15,14 @@
         -moz-appearance: textfield;
         appearance: textfield;
     }
+
+    /* Alpine x-cloak: keep elements hidden until Alpine boots and applies
+       the matching x-show directive. Strict CSP forbids inline `style=`
+       attributes, so we can't rely on the conventional pre-boot inline
+       `display:none` — this rule fills the same gap. */
+    [x-cloak] {
+        display: none !important;
+    }
 }
 
 @layer utilities {

BIN
public/assets/img/screenshots/Auditlog.png


BIN
public/assets/img/screenshots/Sprint_presenter_screen.png


BIN
public/assets/img/screenshots/Sprint_recources_view.png


BIN
public/assets/img/screenshots/Sprint_settings.png


BIN
public/assets/img/screenshots/Sprint_task_view.png


BIN
public/assets/img/screenshots/Sprints_view.png


BIN
public/assets/img/screenshots/Task_edit_menu.png


BIN
public/assets/img/screenshots/Task_user_time_status.png


+ 24 - 0
public/assets/js/app.js

@@ -118,6 +118,30 @@
             };
         });
 
+        // Screenshot lightbox used by /about. Each trigger button carries
+        // `data-src` and `data-alt`; clicking it opens a fullscreen modal
+        // that closes on backdrop click, the close button, or Escape.
+        window.Alpine.data('screenshotModal', function () {
+            return {
+                isOpen: false,
+                src: '',
+                alt: '',
+                open(ev) {
+                    const t = ev && ev.currentTarget;
+                    if (!t) { return; }
+                    this.src = t.getAttribute('data-src') || '';
+                    this.alt = t.getAttribute('data-alt') || '';
+                    if (this.src === '') { return; }
+                    this.isOpen = true;
+                    document.body.classList.add('overflow-hidden');
+                },
+                close() {
+                    this.isOpen = false;
+                    document.body.classList.remove('overflow-hidden');
+                },
+            };
+        });
+
         // Theme toggle — flips <html class="dark">, persists in localStorage,
         // mirrors a "Dark" / "Light" label into the menu row.
         window.Alpine.data('themeToggle', function () {

+ 10 - 0
public/index.php

@@ -237,6 +237,16 @@ $router->get('/', function (Request $req) use ($view, $pdo, $users, $sprints, $a
     ]));
 });
 
+$router->get('/about', function () use ($view, $users): Response {
+    return Response::html($view->render('about', [
+        'title'       => 'About — Sprint Planner',
+        'currentUser' => SessionGuard::currentUser($users),
+        'csrfToken'   => SessionGuard::csrfToken(),
+        'appVersion'  => Meta::VERSION,
+        'appCreator'  => Meta::CREATOR,
+    ]));
+});
+
 $router->get('/healthz', fn() => Response::text('ok'));
 
 // R01-N19: browser-fired CSP violation reports. Public POST (no auth, no

+ 510 - 0
views/about.twig

@@ -0,0 +1,510 @@
+{% extends "layout.twig" %}
+
+{% block content %}
+<section class="space-y-12" x-data="screenshotModal">
+
+    {# --------------- Hero --------------- #}
+    <header class="text-center max-w-3xl mx-auto pt-2">
+        <div class="flex justify-center mb-4">
+            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="96" height="96" aria-hidden="true" class="block" fill="none">
+                <defs>
+                    <radialGradient id="brand-cycle-glow-about" cx="32" cy="32" r="20" gradientUnits="userSpaceOnUse">
+                        <stop offset="0"   stop-color="#6366f1" stop-opacity="0.55"/>
+                        <stop offset="0.6" stop-color="#6366f1" stop-opacity="0.12"/>
+                        <stop offset="1"   stop-color="#6366f1" stop-opacity="0"/>
+                    </radialGradient>
+                </defs>
+                <circle cx="32" cy="32" r="20" fill="url(#brand-cycle-glow-about)"/>
+                <path d="M52 32 A20 20 0 1 1 32 12" stroke="currentColor" stroke-width="3.5" stroke-linecap="round"/>
+                <path d="M44 8 L52 12 L48 20" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/>
+                <circle cx="32" cy="32" r="5"  fill="#6366f1"/>
+                <circle cx="48" cy="20" r="2.5" fill="currentColor" opacity="0.55"/>
+                <circle cx="52" cy="40" r="2.5" fill="currentColor" opacity="0.55"/>
+                <circle cx="40" cy="50" r="2.5" fill="currentColor" opacity="0.55"/>
+                <circle cx="20" cy="48" r="2.5" fill="currentColor" opacity="0.55"/>
+                <circle cx="14" cy="32" r="2.5" fill="#10b981"/>
+            </svg>
+        </div>
+        <h1 class="text-4xl font-bold tracking-tight">Sprint Planner</h1>
+        <p class="mt-3 text-lg text-slate-600 dark:text-slate-300">
+            One shared sprint board for the whole team — replacing the
+            spreadsheet that everybody copies and nobody keeps in sync.
+        </p>
+    </header>
+
+    {# --------------- What it is --------------- #}
+    <div class="rounded-lg border bg-white p-6 dark:bg-slate-800 dark:border-slate-700">
+        <h2 class="text-2xl font-semibold tracking-tight">What it is</h2>
+        <p class="mt-3 text-slate-700 dark:text-slate-300">
+            Sprint Planner is a small web app for ops/dev teams that plan their
+            work in two- to four-week sprints. It replaces an Excel sprint
+            workbook with a proper website: shared by everyone, edited live,
+            backed by a database, and protected with sign-in. Every change is
+            recorded so it is always clear who moved a number and when.
+        </p>
+        <p class="mt-3 text-slate-700 dark:text-slate-300">
+            Each sprint has its own page. The top of the page is a
+            <em>working-days matrix</em> — for every week of the sprint, every
+            team member tells the planner how many days they will be available.
+            The bottom is the <em>task list</em> — every piece of work the
+            team will tackle, with the days each person is going to spend on
+            it. The page sums these numbers up live so the team can see
+            whether they are over- or under-committed for the sprint.
+        </p>
+    </div>
+
+    {# --------------- How it works --------------- #}
+    <div class="space-y-6">
+        <h2 class="text-2xl font-semibold tracking-tight text-center">How a sprint runs end-to-end</h2>
+
+        <ol class="grid gap-4 md:grid-cols-2">
+            <li class="rounded-lg border bg-white p-5 dark:bg-slate-800 dark:border-slate-700">
+                <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-indigo-100 text-indigo-700 font-semibold dark:bg-indigo-900 dark:text-indigo-200">1</span>
+                <h3 class="mt-3 font-semibold">Create the sprint</h3>
+                <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
+                    An admin gives the sprint a name and picks a start and end
+                    date. The planner splits that range into calendar weeks
+                    automatically — up to twenty-six.
+                </p>
+            </li>
+
+            <li class="rounded-lg border bg-white p-5 dark:bg-slate-800 dark:border-slate-700">
+                <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-indigo-100 text-indigo-700 font-semibold dark:bg-indigo-900 dark:text-indigo-200">2</span>
+                <h3 class="mt-3 font-semibold">Pick the team</h3>
+                <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
+                    Add the people who will work on this sprint, set the
+                    reserve buffer (how much capacity to keep aside for
+                    interruptions), and tick the public holidays so the
+                    available-days count is right out of the box.
+                </p>
+            </li>
+
+            <li class="rounded-lg border bg-white p-5 dark:bg-slate-800 dark:border-slate-700">
+                <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-indigo-100 text-indigo-700 font-semibold dark:bg-indigo-900 dark:text-indigo-200">3</span>
+                <h3 class="mt-3 font-semibold">Capture the work</h3>
+                <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
+                    Add tasks with a title, an owner, a priority, and an
+                    estimate of total days. Re-order them by drag-and-drop;
+                    filter by owner, priority, or text search.
+                </p>
+            </li>
+
+            <li class="rounded-lg border bg-white p-5 dark:bg-slate-800 dark:border-slate-700">
+                <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-indigo-100 text-indigo-700 font-semibold dark:bg-indigo-900 dark:text-indigo-200">4</span>
+                <h3 class="mt-3 font-semibold">Allocate days</h3>
+                <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
+                    For every task, type how many days each person will spend.
+                    The planner re-totals the row, deducts the reserve, and
+                    shows what each person still has free — in green when
+                    there is room, in red when overcommitted.
+                </p>
+            </li>
+
+            <li class="rounded-lg border bg-white p-5 dark:bg-slate-800 dark:border-slate-700">
+                <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-indigo-100 text-indigo-700 font-semibold dark:bg-indigo-900 dark:text-indigo-200">5</span>
+                <h3 class="mt-3 font-semibold">Track progress</h3>
+                <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
+                    Mark each cell as <em>started</em>, <em>done</em>, or
+                    <em>cancelled</em> as the sprint runs. Use the focus
+                    filter to look at one person's plate at a time without
+                    losing the rest.
+                </p>
+            </li>
+
+            <li class="rounded-lg border bg-white p-5 dark:bg-slate-800 dark:border-slate-700">
+                <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-indigo-100 text-indigo-700 font-semibold dark:bg-indigo-900 dark:text-indigo-200">6</span>
+                <h3 class="mt-3 font-semibold">Present and audit</h3>
+                <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
+                    A big-screen view trims the chrome down for the daily
+                    stand-up or the sprint review. The audit log captures
+                    every edit — who, when, before, after — so questions
+                    like &ldquo;wait, who changed that number?&rdquo; have a
+                    factual answer.
+                </p>
+            </li>
+        </ol>
+    </div>
+
+    {# --------------- Tour of the interface (gallery) --------------- #}
+    <div class="space-y-6">
+        <h2 class="text-2xl font-semibold tracking-tight text-center">A tour of the interface</h2>
+        <p class="text-center text-slate-600 dark:text-slate-400 max-w-2xl mx-auto text-sm">
+            Click any picture to enlarge it.
+        </p>
+
+        <div class="grid gap-6 md:grid-cols-2">
+
+            {# 1. Sprints overview #}
+            <figure class="rounded-lg border bg-white overflow-hidden flex flex-col dark:bg-slate-800 dark:border-slate-700">
+                <button type="button"
+                        x-on:click="open($event)"
+                        data-src="/assets/img/screenshots/Sprints_view.png"
+                        data-alt="Sprints overview"
+                        aria-label="Enlarge: Sprints overview"
+                        class="block w-full bg-slate-50 dark:bg-slate-900/50 focus:outline-none focus:ring-2 focus:ring-indigo-400 group">
+                    <img src="/assets/img/screenshots/Sprints_view.png"
+                         alt="Sprints overview"
+                         class="w-full h-56 object-cover object-top transition group-hover:opacity-90">
+                </button>
+                <figcaption class="p-4 flex-1 flex flex-col">
+                    <div class="flex items-baseline justify-between gap-3">
+                        <h3 class="font-semibold">Sprints overview</h3>
+                        <button type="button"
+                                x-on:click="open($event)"
+                                data-src="/assets/img/screenshots/Sprints_view.png"
+                                data-alt="Sprints overview"
+                                class="text-xs text-indigo-700 hover:underline dark:text-indigo-400 whitespace-nowrap">Click to enlarge</button>
+                    </div>
+                    <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
+                        The home page lists every sprint with its dates, worker
+                        count, task count, and reserve percentage. Click a row
+                        to open it.
+                    </p>
+                </figcaption>
+            </figure>
+
+            {# 2. Sprint settings #}
+            <figure class="rounded-lg border bg-white overflow-hidden flex flex-col dark:bg-slate-800 dark:border-slate-700">
+                <button type="button"
+                        x-on:click="open($event)"
+                        data-src="/assets/img/screenshots/Sprint_settings.png"
+                        data-alt="Sprint settings"
+                        aria-label="Enlarge: Sprint settings"
+                        class="block w-full bg-slate-50 dark:bg-slate-900/50 focus:outline-none focus:ring-2 focus:ring-indigo-400 group">
+                    <img src="/assets/img/screenshots/Sprint_settings.png"
+                         alt="Sprint settings"
+                         class="w-full h-56 object-cover object-top transition group-hover:opacity-90">
+                </button>
+                <figcaption class="p-4 flex-1 flex flex-col">
+                    <div class="flex items-baseline justify-between gap-3">
+                        <h3 class="font-semibold">Sprint settings</h3>
+                        <button type="button"
+                                x-on:click="open($event)"
+                                data-src="/assets/img/screenshots/Sprint_settings.png"
+                                data-alt="Sprint settings"
+                                class="text-xs text-indigo-700 hover:underline dark:text-indigo-400 whitespace-nowrap">Click to enlarge</button>
+                    </div>
+                    <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
+                        Edit the dates and reserve buffer, add or remove team
+                        members, mark which weekdays are working days for each
+                        week, and set a per-person reserve.
+                    </p>
+                </figcaption>
+            </figure>
+
+            {# 3. Working-days matrix #}
+            <figure class="rounded-lg border bg-white overflow-hidden flex flex-col dark:bg-slate-800 dark:border-slate-700">
+                <button type="button"
+                        x-on:click="open($event)"
+                        data-src="/assets/img/screenshots/Sprint_recources_view.png"
+                        data-alt="Working-days matrix (Arbeitstage)"
+                        aria-label="Enlarge: Working-days matrix"
+                        class="block w-full bg-slate-50 dark:bg-slate-900/50 focus:outline-none focus:ring-2 focus:ring-indigo-400 group">
+                    <img src="/assets/img/screenshots/Sprint_recources_view.png"
+                         alt="Working-days matrix (Arbeitstage)"
+                         class="w-full h-56 object-cover object-top transition group-hover:opacity-90">
+                </button>
+                <figcaption class="p-4 flex-1 flex flex-col">
+                    <div class="flex items-baseline justify-between gap-3">
+                        <h3 class="font-semibold">Working-days matrix</h3>
+                        <button type="button"
+                                x-on:click="open($event)"
+                                data-src="/assets/img/screenshots/Sprint_recources_view.png"
+                                data-alt="Working-days matrix (Arbeitstage)"
+                                class="text-xs text-indigo-700 hover:underline dark:text-indigo-400 whitespace-nowrap">Click to enlarge</button>
+                    </div>
+                    <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
+                        For every team member, type the number of days they
+                        will be available each week. Half-day steps are
+                        allowed. The summary row at the top shows the total
+                        capacity for the whole sprint.
+                    </p>
+                </figcaption>
+            </figure>
+
+            {# 4. Task list #}
+            <figure class="rounded-lg border bg-white overflow-hidden flex flex-col dark:bg-slate-800 dark:border-slate-700">
+                <button type="button"
+                        x-on:click="open($event)"
+                        data-src="/assets/img/screenshots/Sprint_task_view.png"
+                        data-alt="Task list with per-worker allocations"
+                        aria-label="Enlarge: Task list"
+                        class="block w-full bg-slate-50 dark:bg-slate-900/50 focus:outline-none focus:ring-2 focus:ring-indigo-400 group">
+                    <img src="/assets/img/screenshots/Sprint_task_view.png"
+                         alt="Task list with per-worker allocations"
+                         class="w-full h-56 object-cover object-top transition group-hover:opacity-90">
+                </button>
+                <figcaption class="p-4 flex-1 flex flex-col">
+                    <div class="flex items-baseline justify-between gap-3">
+                        <h3 class="font-semibold">Task list</h3>
+                        <button type="button"
+                                x-on:click="open($event)"
+                                data-src="/assets/img/screenshots/Sprint_task_view.png"
+                                data-alt="Task list with per-worker allocations"
+                                class="text-xs text-indigo-700 hover:underline dark:text-indigo-400 whitespace-nowrap">Click to enlarge</button>
+                    </div>
+                    <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
+                        Every task is one row: title, owner, priority, total
+                        days, and one cell per team member. Search, sort,
+                        filter by owner or priority, hide the columns you
+                        don't need, and drag rows to reorder.
+                    </p>
+                </figcaption>
+            </figure>
+
+            {# 5. Cell time + status picker #}
+            <figure class="rounded-lg border bg-white overflow-hidden flex flex-col dark:bg-slate-800 dark:border-slate-700">
+                <button type="button"
+                        x-on:click="open($event)"
+                        data-src="/assets/img/screenshots/Task_user_time_status.png"
+                        data-alt="Per-cell time and status picker"
+                        aria-label="Enlarge: Per-cell time and status picker"
+                        class="block w-full bg-slate-50 dark:bg-slate-900/50 focus:outline-none focus:ring-2 focus:ring-indigo-400 group">
+                    <img src="/assets/img/screenshots/Task_user_time_status.png"
+                         alt="Per-cell time and status picker"
+                         class="w-full h-56 object-contain transition group-hover:opacity-90">
+                </button>
+                <figcaption class="p-4 flex-1 flex flex-col">
+                    <div class="flex items-baseline justify-between gap-3">
+                        <h3 class="font-semibold">Cell time &amp; status</h3>
+                        <button type="button"
+                                x-on:click="open($event)"
+                                data-src="/assets/img/screenshots/Task_user_time_status.png"
+                                data-alt="Per-cell time and status picker"
+                                class="text-xs text-indigo-700 hover:underline dark:text-indigo-400 whitespace-nowrap">Click to enlarge</button>
+                    </div>
+                    <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
+                        Click a cell to slide the day count and tag progress
+                        — assigned, started, done, or cancelled. The cell tints
+                        yellow / green / red so the team can scan progress at
+                        a glance.
+                    </p>
+                </figcaption>
+            </figure>
+
+            {# 6. Task menu #}
+            <figure class="rounded-lg border bg-white overflow-hidden flex flex-col dark:bg-slate-800 dark:border-slate-700">
+                <button type="button"
+                        x-on:click="open($event)"
+                        data-src="/assets/img/screenshots/Task_edit_menu.png"
+                        data-alt="Task menu — edit, move, copy, delete"
+                        aria-label="Enlarge: Task menu"
+                        class="block w-full bg-slate-50 dark:bg-slate-900/50 focus:outline-none focus:ring-2 focus:ring-indigo-400 group">
+                    <img src="/assets/img/screenshots/Task_edit_menu.png"
+                         alt="Task menu — edit, move, copy, delete"
+                         class="w-full h-56 object-contain transition group-hover:opacity-90">
+                </button>
+                <figcaption class="p-4 flex-1 flex flex-col">
+                    <div class="flex items-baseline justify-between gap-3">
+                        <h3 class="font-semibold">Task menu</h3>
+                        <button type="button"
+                                x-on:click="open($event)"
+                                data-src="/assets/img/screenshots/Task_edit_menu.png"
+                                data-alt="Task menu — edit, move, copy, delete"
+                                class="text-xs text-indigo-700 hover:underline dark:text-indigo-400 whitespace-nowrap">Click to enlarge</button>
+                    </div>
+                    <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
+                        Each task carries a small menu: edit the title and
+                        description, attach a link, move or copy the task to
+                        another sprint, or delete it. The right-hand pane
+                        shows the read-only details.
+                    </p>
+                </figcaption>
+            </figure>
+
+            {# 7. Presenter view #}
+            <figure class="rounded-lg border bg-white overflow-hidden flex flex-col dark:bg-slate-800 dark:border-slate-700">
+                <button type="button"
+                        x-on:click="open($event)"
+                        data-src="/assets/img/screenshots/Sprint_presenter_screen.png"
+                        data-alt="Big-screen presenter view"
+                        aria-label="Enlarge: Big-screen presenter view"
+                        class="block w-full bg-slate-50 dark:bg-slate-900/50 focus:outline-none focus:ring-2 focus:ring-indigo-400 group">
+                    <img src="/assets/img/screenshots/Sprint_presenter_screen.png"
+                         alt="Big-screen presenter view"
+                         class="w-full h-56 object-cover object-top transition group-hover:opacity-90">
+                </button>
+                <figcaption class="p-4 flex-1 flex flex-col">
+                    <div class="flex items-baseline justify-between gap-3">
+                        <h3 class="font-semibold">Big-screen presenter</h3>
+                        <button type="button"
+                                x-on:click="open($event)"
+                                data-src="/assets/img/screenshots/Sprint_presenter_screen.png"
+                                data-alt="Big-screen presenter view"
+                                class="text-xs text-indigo-700 hover:underline dark:text-indigo-400 whitespace-nowrap">Click to enlarge</button>
+                    </div>
+                    <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
+                        A stripped-down view tuned for the projector at a
+                        stand-up. Worker columns can rotate to fit a wide
+                        team into one screen without scrolling.
+                    </p>
+                </figcaption>
+            </figure>
+
+            {# 8. Audit log #}
+            <figure class="rounded-lg border bg-white overflow-hidden flex flex-col dark:bg-slate-800 dark:border-slate-700">
+                <button type="button"
+                        x-on:click="open($event)"
+                        data-src="/assets/img/screenshots/Auditlog.png"
+                        data-alt="Audit log"
+                        aria-label="Enlarge: Audit log"
+                        class="block w-full bg-slate-50 dark:bg-slate-900/50 focus:outline-none focus:ring-2 focus:ring-indigo-400 group">
+                    <img src="/assets/img/screenshots/Auditlog.png"
+                         alt="Audit log"
+                         class="w-full h-56 object-cover object-top transition group-hover:opacity-90">
+                </button>
+                <figcaption class="p-4 flex-1 flex flex-col">
+                    <div class="flex items-baseline justify-between gap-3">
+                        <h3 class="font-semibold">Audit log</h3>
+                        <button type="button"
+                                x-on:click="open($event)"
+                                data-src="/assets/img/screenshots/Auditlog.png"
+                                data-alt="Audit log"
+                                class="text-xs text-indigo-700 hover:underline dark:text-indigo-400 whitespace-nowrap">Click to enlarge</button>
+                    </div>
+                    <p class="mt-1 text-sm text-slate-600 dark:text-slate-400">
+                        Every create, update, and delete is logged with user,
+                        time, and a before/after snapshot. Filter by user,
+                        action, entity, or date range — useful when something
+                        looks off and someone wants to know what happened.
+                    </p>
+                </figcaption>
+            </figure>
+
+        </div>
+    </div>
+
+    {# --------------- Quickstart --------------- #}
+    <div class="rounded-lg border bg-white p-6 dark:bg-slate-800 dark:border-slate-700">
+        <h2 class="text-2xl font-semibold tracking-tight">Quickstart</h2>
+        <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">
+            For a pilot install on one host. You need <strong>Docker</strong>
+            (with the <code>docker compose</code> plugin) and not much else.
+        </p>
+
+        <ol class="mt-5 space-y-5 text-sm">
+            <li>
+                <p class="font-semibold">1. Clone the repository.</p>
+                <pre class="mt-2 p-3 rounded bg-slate-900 text-slate-100 text-xs overflow-x-auto"><code>git clone https://git.chiapparini.org/chiappa/sprint_planer_web.git
+cd sprint_planer_web</code></pre>
+            </li>
+
+            <li>
+                <p class="font-semibold">2. Make a config file.</p>
+                <pre class="mt-2 p-3 rounded bg-slate-900 text-slate-100 text-xs overflow-x-auto"><code>cp .env.example .env
+chmod 600 .env</code></pre>
+                <p class="mt-2 text-slate-600 dark:text-slate-400">
+                    Open <code>.env</code> and either fill in the
+                    <code>ENTRA_*</code> values (to sign in with a Microsoft
+                    work account) or set
+                    <code>LOCAL_ADMIN_EMAIL</code> and
+                    <code>LOCAL_ADMIN_PASSWORD_HASH</code> for a local sign-in.
+                </p>
+            </li>
+
+            <li>
+                <p class="font-semibold">3. Generate the local-admin password hash (only if you use the local fallback).</p>
+                <pre class="mt-2 p-3 rounded bg-slate-900 text-slate-100 text-xs overflow-x-auto"><code>docker run --rm -it php:8.3-cli php -r \
+  'echo password_hash(readline("Password: "), PASSWORD_DEFAULT), PHP_EOL;'</code></pre>
+                <p class="mt-2 text-slate-600 dark:text-slate-400">
+                    Paste the resulting <code>$2y$…</code> string into
+                    <code>.env</code> as <code>LOCAL_ADMIN_PASSWORD_HASH</code>
+                    — keep the single quotes around it.
+                </p>
+            </li>
+
+            <li>
+                <p class="font-semibold">4. Build and start the stack.</p>
+                <pre class="mt-2 p-3 rounded bg-slate-900 text-slate-100 text-xs overflow-x-auto"><code>docker compose up -d --build</code></pre>
+                <p class="mt-2 text-slate-600 dark:text-slate-400">
+                    The default port is <code>8080</code>; change
+                    <code>HTTP_PORT</code> in <code>.env</code> if you need
+                    something else.
+                </p>
+            </li>
+
+            <li>
+                <p class="font-semibold">5. Open the app and sign in.</p>
+                <pre class="mt-2 p-3 rounded bg-slate-900 text-slate-100 text-xs overflow-x-auto"><code>http://localhost:8080</code></pre>
+                <p class="mt-2 text-slate-600 dark:text-slate-400">
+                    The first person to sign in via local admin (or whoever
+                    you nominated through <code>BOOTSTRAP_ADMIN_*</code> on
+                    the Microsoft path) becomes the administrator.
+                    Subsequent admin promotions happen on the
+                    <strong>Users</strong> page.
+                </p>
+            </li>
+        </ol>
+
+        <p class="mt-5 text-xs text-slate-500 dark:text-slate-400">
+            Backups, upgrades, reverse-proxy setup, Microsoft Entra
+            registration: see <code>doc/admin-manual.md</code> in the
+            repository.
+        </p>
+    </div>
+
+    {# --------------- Source / license / version --------------- #}
+    <div class="rounded-lg border bg-slate-50 p-6 dark:bg-slate-800/60 dark:border-slate-700 text-sm">
+        <div class="grid gap-6 md:grid-cols-3">
+            <div>
+                <h3 class="font-semibold uppercase tracking-wider text-xs text-slate-500 dark:text-slate-400">Source</h3>
+                <a href="https://git.chiapparini.org/chiappa/sprint_planer_web"
+                   class="mt-1 inline-block text-blue-700 hover:underline dark:text-blue-400 break-all"
+                   rel="noopener noreferrer">
+                    git.chiapparini.org/chiappa/sprint_planer_web
+                </a>
+            </div>
+            <div>
+                <h3 class="font-semibold uppercase tracking-wider text-xs text-slate-500 dark:text-slate-400">License</h3>
+                <p class="mt-1">
+                    <a href="https://www.apache.org/licenses/LICENSE-2.0"
+                       class="text-blue-700 hover:underline dark:text-blue-400"
+                       rel="noopener noreferrer">Apache License 2.0</a><br>
+                    <span class="text-slate-600 dark:text-slate-400">© 2026 {{ appCreator }}</span>
+                </p>
+            </div>
+            <div>
+                <h3 class="font-semibold uppercase tracking-wider text-xs text-slate-500 dark:text-slate-400">Version</h3>
+                <p class="mt-1 font-mono">{{ appVersion }}</p>
+            </div>
+        </div>
+    </div>
+
+    {# --------------- Modal overlay --------------- #}
+    <div x-show="isOpen"
+         x-on:keydown.escape.window="close()"
+         x-cloak
+         role="dialog"
+         aria-modal="true"
+         aria-label="Enlarged screenshot"
+         class="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-8 bg-slate-900/85 backdrop-blur-sm">
+
+        {# Click-to-dismiss backdrop. Sits behind the image / close button so
+           clicks on either don't propagate. #}
+        <button type="button"
+                x-on:click="close()"
+                aria-label="Close enlarged screenshot"
+                tabindex="-1"
+                class="absolute inset-0 w-full h-full cursor-default focus:outline-none"></button>
+
+        {# Prominent close button — top-right, white pill. #}
+        <button type="button"
+                x-on:click="close()"
+                aria-label="Close"
+                class="absolute top-4 right-4 z-10 inline-flex items-center gap-2 rounded-full bg-white px-4 py-2 text-sm font-semibold text-slate-900 shadow-lg hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-white">
+            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
+                <line x1="5" y1="5" x2="15" y2="15"></line>
+                <line x1="15" y1="5" x2="5" y2="15"></line>
+            </svg>
+            <span>Close</span>
+        </button>
+
+        {# The enlarged image itself — `pointer-events-none` would block our
+           backdrop button below from the image's footprint, so we leave it
+           interactive but it has no click handler — clicking it does nothing. #}
+        <img x-bind:src="src"
+             x-bind:alt="alt"
+             class="relative max-w-[95vw] max-h-[90vh] object-contain rounded shadow-2xl">
+    </div>
+</section>
+{% endblock %}

+ 1 - 0
views/layout.twig

@@ -44,6 +44,7 @@
             </a>
 
             <nav class="ml-auto flex items-center gap-4 text-sm">
+                <a href="/about" class="text-slate-600 hover:text-slate-900 hover:underline dark:text-slate-300 dark:hover:text-slate-100">About</a>
                 {% if currentUser is not null %}
                     <a href="/" class="text-slate-600 hover:text-slate-900 hover:underline dark:text-slate-300 dark:hover:text-slate-100">Sprints</a>
                     {% if currentUser.isAdmin %}