commit 42b6815277209693b0df9900b970ff32f6a17b8a
Author: William Lindholm <william_lindholm@outlook.com>
Date: Sun, 20 Sep 2026 23:22:54 +0200
Created Curriculum vitae page
Diffstat:
7 files changed, 365 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,6 @@
+__pycache__/
+*.py[cod]
+.venv/
+.env
+.DS_Store
+Thumbs.db
diff --git a/README.md b/README.md
@@ -0,0 +1,135 @@
+# Start the container
+
+Install Docker with Docker Compose v2.23.0 or newer. From this directory, run:
+
+```sh
+docker compose up -d
+```
+
+Open <http://localhost:8000>.
+
+To start with automatic application updates during development, run:
+
+```sh
+docker compose watch
+```
+
+# Software Requirements Specification
+
+## Personal Résumé Website
+
+### 1. Intent
+
+Build a permanent, lightweight résumé website for a software developer.
+
+Treat the site as a document published on the Web, not as a web application.
+
+### 2. Application
+
+**REQ-01 — Runtime**\
+Use Python and Flask. Keep all Flask logic in `app/main.py`.
+
+**REQ-02 — Page**\
+The application shall contain exactly one page. Serve the résumé at `/`. Keep HTML in `app/index.html` and CSS in `app/style.css`.
+
+**REQ-03 — JavaScript**\
+Do not use JavaScript.
+
+### 3. Project Layout
+
+**REQ-04 — Files**\
+Use this structure:
+```
+/
+├── .gitignore
+├── README.md
+├── docker-compose.yml
+└── app/
+ ├── main.py
+ ├── Dockerfile
+ ├── index.html
+ └── style.css
+```
+
+Do not add directories.
+
+**REQ-05 — README**\
+The README shall contain only:
+
+1. Instructions to start the container.
+2. This SRS.
+
+Place the start instructions first.
+
+### 4. Container
+
+**REQ-06 — Operation**\
+Package the application with Docker. `docker compose up -d` shall start the site. Restarting the container shall restore normal operation.
+
+**REQ-07 — Compose Watch**\
+Configure `develop.watch` in `docker-compose.yml`. `docker compose watch` shall detect changes to application files and update or rebuild the service as required.
+
+**REQ-08 — Runtime Dependencies**\
+Do not require external CDNs, hosted fonts, analytics, databases, build systems, JavaScript packages, or third-party runtime services.
+
+### 5. Visual Design
+
+**REQ-09 — Direction**\
+Use the visual restraint of long-lived FOSS and Unix websites. Suitable references include CRUX Linux, kernel.org, OpenBSD, NetBSD, suckless.org, SQLite, musl libc, BusyBox, Alpine Linux, and SourceHut. Do not copy a reference site.
+
+Do not imitate a terminal. Do not use fake prompts, simulated commands, or monospace text as decoration.
+
+**REQ-10 — Palette**\
+Use this palette:
+```less
+Accent blue #007acc
+Light highlight #d4d4d4
+Primary text #333333
+Dark highlight #252525
+Dark background #1e1e1e
+```
+
+Declare colors as custom properties in `:root`. Reference those properties throughout the stylesheet. Use accent blue mainly for links and small accents.
+
+**REQ-11 — Typography**\
+Use system or web-safe fonts only. Declare each font stack once as a custom property in `:root`.
+
+**REQ-12 — Style**\
+Use typography, spacing, and hierarchy to structure the page. Avoid gradients, shadows, glass effects, animation, oversized hero sections, card grids, carousels, and decorative image galleries.
+
+### 6. Content and Layout
+
+**REQ-13 — Sections**\
+Include:
+
+- Introduction
+- Work experience
+- Education
+- Selected projects
+- Technical skills
+- Contact and external links
+
+Present experience and projects as concise text with ordinary links.
+
+**REQ-14 — Responsive Layout**\
+Use a restrained maximum width on large displays. Use one readable column on small displays. Require no JavaScript for responsive behavior.
+
+### 7. Document Quality
+
+**REQ-15 — HTML**\
+Use semantic HTML5 and a logical heading order. The document shall remain readable without CSS.
+
+**REQ-16 — CSS**\
+Keep the stylesheet small and conventional. Prefer standard layout methods over frameworks or design-system abstractions.
+
+**REQ-17 — Priorities**\
+Prioritize, in order:
+
+1. Maintainability
+2. Accessibility
+3. Readability
+4. Browser compatibility
+5. Load time
+6. Visual character
+
+When several solutions meet a requirement, use the simplest one.
diff --git a/app/Dockerfile b/app/Dockerfile
@@ -0,0 +1,14 @@
+FROM python:3.13-slim
+
+ENV PYTHONDONTWRITEBYTECODE=1 \
+ PYTHONUNBUFFERED=1
+
+WORKDIR /app
+RUN pip install --no-cache-dir Flask==3.1.2 waitress==3.0.2 \
+ && useradd --uid 10001 --create-home resume \
+ && chown resume:resume /app
+
+COPY --chown=resume:resume main.py index.html style.css ./
+USER resume
+EXPOSE 8000
+CMD ["python", "main.py"]
diff --git a/app/index.html b/app/index.html
@@ -0,0 +1,96 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <meta name="description" content="[sample]Alex Morgan — software developer. Résumé, selected projects, and contact information.[endsample]">
+ <title>[sample]Alex Morgan — Résumé[endsample]</title>
+ <link rel="stylesheet" href="/style.css">
+</head>
+<body>
+ <a class="skip-link" href="#main">Skip to résumé</a>
+ <header>
+ <p class="document-label">Curriculum vitae</p>
+ <h1><span class="sample-marker">[sample]</span>Alex Morgan<span class="sample-marker">[endsample]</span></h1>
+ <p class="subtitle">[sample]Software developer · Stockholm, Sweden[endsample]</p>
+ <nav aria-label="Résumé sections">
+ <a href="#experience">Experience</a>
+ <a href="#education">Education</a>
+ <a href="#projects">Projects</a>
+ <a href="#skills">Skills</a>
+ <a href="#contact">Contact</a>
+ </nav>
+ </header>
+
+ <main id="main">
+ <section aria-labelledby="introduction">
+ <h2 id="introduction">Introduction</h2>
+ <p>[sample]I build dependable software for the web, with a focus on Python, Linux, and straightforward systems. I enjoy turning complicated requirements into tools that are easy to use, understand, and maintain.[endsample]</p>
+ <p>[sample]My work spans backend services, developer tooling, and the small details that make software reliable over time.[endsample]</p>
+ </section>
+
+ <section aria-labelledby="experience">
+ <h2 id="experience">Work experience</h2>
+ <article>
+ <h3>[sample]Software developer · Northline Systems[endsample]</h3>
+ <p class="entry-meta">[sample]2022–present · Stockholm, Sweden[endsample]</p>
+ <p>[sample]Develop and maintain Python services for an internal operations platform. Work with a small team on API design, database changes, and production support.[endsample]</p>
+ <ul>
+ <li>[sample]Simplified background processing and added checks that made failed jobs easier to diagnose and recover.[endsample]</li>
+ <li>[sample]Documented deployment procedures and maintained automated tests for critical workflows.[endsample]</li>
+ </ul>
+ </article>
+ <article>
+ <h3>[sample]Junior software developer · Fieldwork Studio[endsample]</h3>
+ <p class="entry-meta">[sample]2020–2022 · Uppsala, Sweden[endsample]</p>
+ <p>[sample]Built accessible websites and small backend services. Collaborated with designers and clients, reviewed code, and supported applications after release.[endsample]</p>
+ </article>
+ </section>
+
+ <section aria-labelledby="education">
+ <h2 id="education">Education</h2>
+ <h3>[sample]BSc in Computer Science · Example University[endsample]</h3>
+ <p class="entry-meta">[sample]2017–2020 · Sweden[endsample]</p>
+ <p>[sample]Coursework in algorithms, operating systems, databases, and software engineering. Final project explored practical monitoring for small distributed services.[endsample]</p>
+ </section>
+
+ <section aria-labelledby="projects">
+ <h2 id="projects">Selected projects</h2>
+ <article>
+ <h3>[sample]Plainlog[endsample]</h3>
+ <p>[sample]A small command-line tool for searching and summarizing application logs. Designed around readable output, predictable behavior, and Unix pipes. <a href="https://example.com/plainlog">Project and source</a>.[endsample]</p>
+ <p class="entry-meta">[sample]Python · Linux · Automated testing[endsample]</p>
+ </article>
+ <article>
+ <h3>[sample]Community archive[endsample]</h3>
+ <p>[sample]A lightweight, searchable archive for a local association. Server-rendered pages, accessible forms, and a documented backup process keep day-to-day maintenance simple. <a href="https://example.com/community-archive">Project and source</a>.[endsample]</p>
+ <p class="entry-meta">[sample]Python · Flask · SQLite · HTML & CSS[endsample]</p>
+ </article>
+ </section>
+
+ <section aria-labelledby="skills">
+ <h2 id="skills">Technical skills</h2>
+ <dl>
+ <dt>Languages</dt>
+ <dd>[sample]Python, SQL, HTML, CSS, shell scripting[endsample]</dd>
+ <dt>Tools & systems</dt>
+ <dd>[sample]Linux, Git, Docker, PostgreSQL, SQLite[endsample]</dd>
+ <dt>Practice</dt>
+ <dd>[sample]API design, automated testing, accessibility, technical documentation[endsample]</dd>
+ </dl>
+ </section>
+
+ <section aria-labelledby="contact">
+ <h2 id="contact">Contact and external links</h2>
+ <address>
+ <p>[sample]Email: <a href="mailto:alex@example.com">alex@example.com</a>[endsample]</p>
+ <p>[sample]<a href="https://example.com/alex/source">Source code profile</a> · <a href="https://example.com/alex/profile">Professional profile</a>[endsample]</p>
+ </address>
+ </section>
+ </main>
+
+ <footer>
+ <p>Sample résumé. All information enclosed in [sample] and [endsample] is illustrative.</p>
+ </footer>
+</body>
+</html>
diff --git a/app/main.py b/app/main.py
@@ -0,0 +1,21 @@
+"""Serve the résumé document and its stylesheet."""
+
+from flask import Flask, send_from_directory
+
+app = Flask(__name__, static_folder=None)
+
+
+@app.get("/")
+def resume():
+ return send_from_directory(app.root_path, "index.html")
+
+
+@app.get("/style.css")
+def stylesheet():
+ return send_from_directory(app.root_path, "style.css")
+
+
+if __name__ == "__main__":
+ from waitress import serve
+
+ serve(app, host="0.0.0.0", port=8000)
diff --git a/app/style.css b/app/style.css
@@ -0,0 +1,74 @@
+:root {
+ color-scheme: light;
+ --accent-blue: #007acc;
+ --light-highlight: #d4d4d4;
+ --primary-text: #333333;
+ --dark-highlight: #252525;
+ --dark-background: #1e1e1e;
+ --paper: #ffffff;
+ --font-body: Georgia, "Times New Roman", serif;
+ --font-heading: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+}
+
+* { box-sizing: border-box; }
+
+html { background: var(--paper); }
+
+body {
+ max-width: 50rem;
+ margin: 0 auto;
+ padding: 3rem 1.5rem 2rem;
+ color: var(--primary-text);
+ font-family: var(--font-body);
+ font-size: 1.0625rem;
+ line-height: 1.65;
+ overflow-wrap: anywhere;
+}
+
+header { border-bottom: 2px solid var(--dark-background); padding-bottom: 1.5rem; }
+
+h1, h2, h3, nav, .document-label, .entry-meta, dt, footer {
+ font-family: var(--font-heading);
+}
+
+h1, h2, h3 { color: var(--dark-highlight); line-height: 1.35; }
+h1 { margin: 0.25rem 0; font-size: 2.25rem; font-weight: 650; }
+h2 { margin: 0 0 1rem; font-size: 1.25rem; }
+h3 { margin: 0 0 0.25rem; font-size: 1rem; font-weight: 650; }
+p { margin: 0 0 0.8rem; }
+.document-label { margin: 0; font-size: 0.8rem; letter-spacing: 0.08em; text-transform: uppercase; }
+.sample-marker { font-size: 0.75rem; font-weight: 400; vertical-align: middle; }
+.subtitle { margin-bottom: 1.25rem; }
+nav { display: flex; flex-wrap: wrap; gap: 0.5rem 1.4rem; font-size: 0.9rem; }
+a { color: var(--accent-blue); text-underline-offset: 0.18em; }
+a:hover { color: var(--dark-highlight); }
+a:focus-visible { outline: 2px solid var(--dark-highlight); outline-offset: 4px; }
+section { padding: 1.75rem 0; border-bottom: 1px solid var(--light-highlight); }
+section > :last-child, article > :last-child { margin-bottom: 0; }
+article + article { margin-top: 1.5rem; }
+.entry-meta { margin-bottom: 0.65rem; font-size: 0.85rem; }
+ul { margin: 0.5rem 0 0; padding-left: 1.35rem; }
+li + li { margin-top: 0.35rem; }
+dl { margin: 0; }
+dt { font-size: 0.9rem; font-weight: 650; }
+dd { margin: 0 0 0.75rem; }
+dd:last-child { margin-bottom: 0; }
+address { font-style: normal; }
+footer { padding-top: 1.25rem; font-size: 0.8rem; }
+.skip-link { position: absolute; top: -5rem; padding: 0.5rem; background: var(--paper); }
+.skip-link:focus { top: 0.5rem; }
+
+@media (max-width: 36rem) {
+ body { padding: 1.5rem 1rem; }
+ h1 { font-size: 1.8rem; }
+ .sample-marker { display: block; }
+ section { padding: 1.5rem 0; }
+}
+
+@media print {
+ body { max-width: none; padding: 0; font-size: 10pt; }
+ nav, .skip-link { display: none; }
+ a { color: var(--primary-text); }
+ h2, h3 { break-after: avoid; }
+ article { break-inside: avoid; }
+}
diff --git a/docker-compose.yml b/docker-compose.yml
@@ -0,0 +1,19 @@
+services:
+ resume:
+ build: ./app
+ ports:
+ - "8000:8000"
+ restart: unless-stopped
+ develop:
+ watch:
+ - action: sync+restart
+ path: ./app/main.py
+ target: /app/main.py
+ - action: sync
+ path: ./app/index.html
+ target: /app/index.html
+ - action: sync
+ path: ./app/style.css
+ target: /app/style.css
+ - action: rebuild
+ path: ./app/Dockerfile