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
|
# 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).
|
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
|
## License
|
||||||
|
|
||||||
MIT — see [LICENSE](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 charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>bggpipe review</title>
|
<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>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--felt: #2c4136;
|
--felt: #2c4136;
|
||||||
@@ -35,10 +35,15 @@
|
|||||||
position: sticky; top: 0; z-index: 5;
|
position: sticky; top: 0; z-index: 5;
|
||||||
background: var(--felt-deep);
|
background: var(--felt-deep);
|
||||||
color: var(--paper);
|
color: var(--paper);
|
||||||
padding: .6rem 1.2rem;
|
padding: .45rem 1.2rem;
|
||||||
display: flex; align-items: baseline; gap: 1.2rem; flex-wrap: wrap;
|
display: flex; align-items: center; gap: 1.2rem; flex-wrap: wrap;
|
||||||
border-bottom: 1px solid rgba(255,255,255,.12);
|
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 {
|
.wordmark {
|
||||||
font-family: "Iowan Old Style", Palatino, Georgia, serif;
|
font-family: "Iowan Old Style", Palatino, Georgia, serif;
|
||||||
font-size: 1.25rem; letter-spacing: .02em;
|
font-size: 1.25rem; letter-spacing: .02em;
|
||||||
@@ -158,6 +163,10 @@
|
|||||||
background: var(--paper); border-radius: 8px; padding: 1.6rem;
|
background: var(--paper); border-radius: 8px; padding: 1.6rem;
|
||||||
text-align: center; box-shadow: 0 6px 18px rgba(0,0,0,.4);
|
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 h2 { color: var(--ink); margin-top: 0; }
|
||||||
.done .nums { display: flex; justify-content: center; gap: 2rem; margin: 1rem 0; }
|
.done .nums { display: flex; justify-content: center; gap: 2rem; margin: 1rem 0; }
|
||||||
.done .nums div { font-size: 1.6rem; font-weight: 700; }
|
.done .nums div { font-size: 1.6rem; font-weight: 700; }
|
||||||
@@ -216,7 +225,10 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<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 id="tally"></span>
|
||||||
<span class="keyhelp">
|
<span class="keyhelp">
|
||||||
<kbd>j</kbd>/<kbd>k</kbd> move · <kbd>1</kbd>–<kbd>9</kbd> pick ·
|
<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;
|
const waiting = s.summary.unresolved;
|
||||||
html += `
|
html += `
|
||||||
<div class="done">
|
<div class="done">
|
||||||
|
<img class="piper" src="/static/logo-full.jpg"
|
||||||
|
alt="the bggpipe piper plays a celebratory tune">
|
||||||
<h2>${waiting
|
<h2>${waiting
|
||||||
? "Resolved set fully reviewed"
|
? "Resolved set fully reviewed"
|
||||||
: "All reviewed — this catalog is diff-ready"}</h2>
|
: "All reviewed — this catalog is diff-ready"}</h2>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from pathlib import Path
|
|||||||
import typer
|
import typer
|
||||||
from defusedxml.ElementTree import fromstring as _safe_fromstring
|
from defusedxml.ElementTree import fromstring as _safe_fromstring
|
||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI, HTTPException
|
||||||
from fastapi.responses import FileResponse, HTMLResponse
|
from fastapi.responses import FileResponse, HTMLResponse, Response
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from rich.console import Console
|
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")
|
raise HTTPException(404, "no such photo")
|
||||||
return FileResponse(cfg.photos_dir / name)
|
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
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -324,3 +324,13 @@ def test_merge_veto_roundtrip(tmp_path):
|
|||||||
json={"title_raw": "Jokin Ha...", "source_photos": "shelf.jpg"},
|
json={"title_raw": "Jokin Ha...", "source_photos": "shelf.jpg"},
|
||||||
)
|
)
|
||||||
assert bad.status_code == 400
|
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