Mascot: Juniper's bggpipe piper joins the header, favicon, and README
Original art (a bagpiper whose bag is a board game box) lives in assets/; web-sized derivatives ship in the package: a face-crop avatar in the header and favicon, and the framed full-length piper on the review done screen. Served via an allowlisted /static route. README leads with the full portrait, credited to Juniper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
# bggpipe — Shelf-to-BoardGameGeek Collection Pipeline
|
||||
|
||||
<img src="assets/logo-full.jpeg" alt="the bggpipe piper — a bagpiper whose bag is a board game box" width="220" align="right">
|
||||
|
||||
Photograph your board game shelves. End up with your whole collection — including which *edition* of each game you own — cataloged on [BoardGameGeek](https://boardgamegeek.com).
|
||||
|
||||
```
|
||||
@@ -42,3 +44,5 @@ This tool is **not affiliated with or supported by BoardGameGeek**. It uses only
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE](LICENSE).
|
||||
|
||||
Mascot art by Juniper, used with pride.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 507 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 483 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>bggpipe review</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🎲</text></svg>">
|
||||
<link rel="icon" type="image/png" href="/static/favicon.png">
|
||||
<style>
|
||||
:root {
|
||||
--felt: #2c4136;
|
||||
@@ -35,10 +35,15 @@
|
||||
position: sticky; top: 0; z-index: 5;
|
||||
background: var(--felt-deep);
|
||||
color: var(--paper);
|
||||
padding: .6rem 1.2rem;
|
||||
display: flex; align-items: baseline; gap: 1.2rem; flex-wrap: wrap;
|
||||
padding: .45rem 1.2rem;
|
||||
display: flex; align-items: center; gap: 1.2rem; flex-wrap: wrap;
|
||||
border-bottom: 1px solid rgba(255,255,255,.12);
|
||||
}
|
||||
.brand { display: flex; align-items: center; gap: .6rem; }
|
||||
.brand img {
|
||||
width: 38px; height: 38px; border-radius: 50%;
|
||||
border: 2px solid var(--brass); object-fit: cover; display: block;
|
||||
}
|
||||
.wordmark {
|
||||
font-family: "Iowan Old Style", Palatino, Georgia, serif;
|
||||
font-size: 1.25rem; letter-spacing: .02em;
|
||||
@@ -158,6 +163,10 @@
|
||||
background: var(--paper); border-radius: 8px; padding: 1.6rem;
|
||||
text-align: center; box-shadow: 0 6px 18px rgba(0,0,0,.4);
|
||||
}
|
||||
.done .piper {
|
||||
height: 165px; margin-bottom: .4rem;
|
||||
border-radius: 8px; border: 3px solid var(--felt);
|
||||
}
|
||||
.done h2 { color: var(--ink); margin-top: 0; }
|
||||
.done .nums { display: flex; justify-content: center; gap: 2rem; margin: 1rem 0; }
|
||||
.done .nums div { font-size: 1.6rem; font-weight: 700; }
|
||||
@@ -216,7 +225,10 @@
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<span class="wordmark">bggpipe <small>review</small></span>
|
||||
<span class="brand">
|
||||
<img src="/static/logo.jpg" alt="the bggpipe piper — a bagpiper whose bag is a board game box">
|
||||
<span class="wordmark">bggpipe <small>review</small></span>
|
||||
</span>
|
||||
<span id="tally"></span>
|
||||
<span class="keyhelp">
|
||||
<kbd>j</kbd>/<kbd>k</kbd> move · <kbd>1</kbd>–<kbd>9</kbd> pick ·
|
||||
@@ -362,6 +374,8 @@ function render() {
|
||||
const waiting = s.summary.unresolved;
|
||||
html += `
|
||||
<div class="done">
|
||||
<img class="piper" src="/static/logo-full.jpg"
|
||||
alt="the bggpipe piper plays a celebratory tune">
|
||||
<h2>${waiting
|
||||
? "Resolved set fully reviewed"
|
||||
: "All reviewed — this catalog is diff-ready"}</h2>
|
||||
|
||||
@@ -20,7 +20,7 @@ from pathlib import Path
|
||||
import typer
|
||||
from defusedxml.ElementTree import fromstring as _safe_fromstring
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.responses import FileResponse, HTMLResponse
|
||||
from fastapi.responses import FileResponse, HTMLResponse, Response
|
||||
from pydantic import BaseModel
|
||||
from rich.console import Console
|
||||
|
||||
@@ -286,6 +286,20 @@ def create_app(cfg: Config, *, client: BGGClient | None = None) -> FastAPI:
|
||||
raise HTTPException(404, "no such photo")
|
||||
return FileResponse(cfg.photos_dir / name)
|
||||
|
||||
_STATIC = {
|
||||
"logo.jpg": "image/jpeg",
|
||||
"logo-full.jpg": "image/jpeg",
|
||||
"favicon.png": "image/png",
|
||||
}
|
||||
|
||||
@app.get("/static/{name}")
|
||||
def static_asset(name: str):
|
||||
media_type = _STATIC.get(name) # allowlist: no traversal possible
|
||||
if media_type is None:
|
||||
raise HTTPException(404, "no such asset")
|
||||
data = (resources.files("bggpipe") / "static" / name).read_bytes()
|
||||
return Response(content=data, media_type=media_type)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
|
||||
@@ -324,3 +324,13 @@ def test_merge_veto_roundtrip(tmp_path):
|
||||
json={"title_raw": "Jokin Ha...", "source_photos": "shelf.jpg"},
|
||||
)
|
||||
assert bad.status_code == 400
|
||||
|
||||
|
||||
def test_static_assets_served_with_allowlist(tmp_path):
|
||||
web, _ = make_client(tmp_path)
|
||||
logo = web.get("/static/logo.jpg")
|
||||
assert logo.status_code == 200
|
||||
assert logo.headers["content-type"] == "image/jpeg"
|
||||
assert web.get("/static/favicon.png").status_code == 200
|
||||
assert web.get("/static/nope.js").status_code == 404
|
||||
assert web.get("/static/..%2Ftemplates%2Freview.html").status_code == 404
|
||||
|
||||
Reference in New Issue
Block a user