Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
213 commits
Select commit Hold shift + click to select a range
7acfb0a
Add a randomize-portrait button next to the Image URL input
codeGlaze Aug 16, 2026
50233fc
Revert "Add a randomize-portrait button next to the Image URL input"
codeGlaze Aug 28, 2026
7323d75
Add the paper-doll portrait compositor (v1 MVP)
codeGlaze Aug 28, 2026
5fb4b8c
Add character colors to the portrait compositor; fix its save path
codeGlaze Sep 5, 2026
ec16bed
Merge remote-tracking branch 'origin/integration' into claude/charact…
codeGlaze Sep 5, 2026
0f3d81b
Fix portrait draft not rehydrating on reopen; add compositor e2e
codeGlaze Sep 6, 2026
baf3c43
Print composed portraits in the exported PDF
codeGlaze Sep 6, 2026
15b5c53
Render composed portraits for og:image; fix empty share cards
codeGlaze Sep 6, 2026
2059a51
Cover schema deploy-safety for ::char5e/portrait
codeGlaze Sep 6, 2026
a58cbfe
Light theme for the portrait compositor; keep the launcher styled
codeGlaze Sep 6, 2026
ade244a
Add a second way into the portrait compositor: a pencil on the summar…
codeGlaze Sep 6, 2026
5251dde
Declare the AI opt-out on the pages, the portrait PNGs and robots.txt
codeGlaze Sep 12, 2026
50d87b6
Let link-preview crawlers through robots.txt
codeGlaze Sep 12, 2026
6e539b8
Add the desktop half of the asset pipeline
codeGlaze Sep 12, 2026
fbb3210
Credit the artists wherever the portrait goes
codeGlaze Sep 12, 2026
fc87ebf
Use the illustrator's actual inventory instead of invented placeholders
codeGlaze Sep 12, 2026
2e6ce1c
Stamp the exported sheet with honest metadata
codeGlaze Sep 12, 2026
6c04151
Bake the credit into the composed picture, and stop trusting the save…
codeGlaze Sep 12, 2026
2b22bf9
Add the site mark up the right edge of shared portraits
codeGlaze Sep 13, 2026
04507d8
Set both portrait marks in Vollkorn, the face the sheet already uses
codeGlaze Sep 13, 2026
8a68b38
Add the Portrait builder tab: the compositor in place, not only in a …
codeGlaze Sep 13, 2026
143112d
Show pieces the illustrator has not drawn yet in the picker
codeGlaze Sep 20, 2026
b028fd2
Merge origin/integration into the portrait branch
codeGlaze Sep 20, 2026
2d67b6b
Ignore a local CLAUDE.md
codeGlaze Sep 20, 2026
c4ad185
Make the portrait probes citizens of the probe runner
codeGlaze Sep 20, 2026
634be2b
Merge origin/integration into the portrait branch
codeGlaze Sep 20, 2026
360a8f2
Subset the credit face: 357 KB of Vollkorn becomes 26 KB
codeGlaze Sep 20, 2026
2b23a08
Stop the printed portrait coming out in a black box
codeGlaze Sep 20, 2026
a126f4a
Remove shots.js, a scratch file swept in by git add -A
codeGlaze Sep 20, 2026
f7729b4
Add the branch changelog
codeGlaze Sep 25, 2026
ff050de
Close the six findings from the PR #36 review
codeGlaze Sep 25, 2026
bfef1f4
Stop a first save from discarding the portrait being edited
codeGlaze Sep 25, 2026
bd75e95
Decide the portrait draft from what the save was dispatched with
codeGlaze Sep 25, 2026
72c9890
Give the three renderers one set of numbers
codeGlaze Sep 26, 2026
f6f95fa
Split :set-character in two, and stop a stale save replacing the screen
codeGlaze Sep 27, 2026
548b305
Settle the empty portrait slot, and show the clobber in a browser
codeGlaze Sep 27, 2026
e297469
Retire four assertions that could not fail, and settle the panel into…
codeGlaze Sep 27, 2026
713bb63
Read the save response where the effect actually puts it
codeGlaze Sep 27, 2026
79209a0
Answer a crawler that already has the portrait without rendering it a…
codeGlaze Sep 27, 2026
12991d3
Match If-None-Match the way the spec defines it, not as a string compare
codeGlaze Sep 27, 2026
c7a5528
Stop the wildcard being something a client can fabricate
codeGlaze Sep 27, 2026
6905f87
Show what the tint does to art that is not a silhouette
codeGlaze Sep 27, 2026
f07d25e
Show that multiply keeps the drawing masking throws away
codeGlaze Sep 27, 2026
8552a79
Add a harness for composing a real pack both ways
codeGlaze Sep 27, 2026
c5e4243
Compose four characters from one pack, not one
codeGlaze Sep 27, 2026
ad96451
Leave the layers that hold more than one thing alone
codeGlaze Sep 27, 2026
a0da4a2
Eyes want multiply too -- one exception, not two
codeGlaze Sep 27, 2026
7cf37ef
Render the pack through Java2D, and paint the iris rather than tint it
codeGlaze Sep 28, 2026
2e9fdd8
Soften the region edges, and let the pupil sit off centre
codeGlaze Sep 28, 2026
d3038d7
Test whether a grade is worth offering, before offering one
codeGlaze Sep 28, 2026
0b2a8fc
Find out why the eye colour went quiet, and by how much
codeGlaze Sep 28, 2026
5c48d75
Separate iris legibility from iris modelling with a gamma
codeGlaze Sep 28, 2026
e35f91a
Write down what the portrait tinting experiments settled
codeGlaze Sep 28, 2026
d80a978
Check the settled iris pair against every eye style, not just the bes…
codeGlaze Sep 28, 2026
f5435f9
The lips are drawn light and the irises dark, so one setting cannot s…
codeGlaze Sep 28, 2026
9e3aa2a
Stop re-reading every asset off disk on every compose
codeGlaze Sep 28, 2026
b4c3fac
Add a lips colour slot, owned by the asset rather than the layer
codeGlaze Sep 28, 2026
6ddc89c
Compose the full range of busts from the registry's own rules
codeGlaze Sep 28, 2026
66831c5
Show the actual range: vary the features, not just the palette
codeGlaze Sep 28, 2026
20d873b
Find every hair combination that leaves an island of scalp
codeGlaze Sep 28, 2026
ced70fa
Fill the scalp gap with a patch derived from the gap itself
codeGlaze Sep 28, 2026
76df70d
One patch under the crown, instead of a patch per combination
codeGlaze Sep 28, 2026
08e2afb
Hair has to be opaque before it counts as hiding the patch
codeGlaze Sep 28, 2026
64bb77c
Check the patch by painting it green, not by comparing masks
codeGlaze Sep 28, 2026
27cdc77
Add the scalp as a real layer, between the head and the hair
codeGlaze Sep 28, 2026
30cf041
Tint by multiply in all three renderers
codeGlaze Sep 28, 2026
760c418
Keep the scalp out of the hair piece list
codeGlaze Sep 28, 2026
2e0735e
Credit Fusspot, and let a deployment restate it without a fork
codeGlaze Sep 28, 2026
efd05a2
Make the credit on the character page a link
codeGlaze Sep 28, 2026
22f7343
Link Fusspot's homepage, and a Ko-fi link in the builder only
codeGlaze Sep 28, 2026
7ffa941
Give the artist a labelled link set, with Twitch where it belongs
codeGlaze Sep 28, 2026
c81d8ac
Stop the artist name's styling leaking onto the link pills
codeGlaze Sep 28, 2026
33a0a06
Make the artist's links read as a credit, not a button bar
codeGlaze Sep 28, 2026
85a51a8
Use the services' own marks for the artist's links
codeGlaze Sep 28, 2026
a2615ac
Design pass on the credit strip
codeGlaze Sep 28, 2026
22f1e4b
Put the credit under the art it credits
codeGlaze Sep 28, 2026
c4c63a4
Align the portrait frame with the column it sits in
codeGlaze Sep 28, 2026
9384728
Shimmer the swirl on hover
codeGlaze Sep 28, 2026
632db73
Fusspot's links: her site and her Twitch, which is what she asked for
codeGlaze Sep 29, 2026
a0f0012
Give the artist's links their own colours, worn at rest
codeGlaze Sep 29, 2026
7e38fbb
Settle the credit row on one rhythm and one alignment
codeGlaze Sep 29, 2026
ee5d5f2
Size the marks to the text's cap height, not its line box
codeGlaze Sep 29, 2026
80d20a2
Put credit lockups B, D and F behind a temporary switch
codeGlaze Sep 29, 2026
ec7f167
Let layout D hold for any number of links and artists
codeGlaze Sep 29, 2026
c88d213
Settle the credit on layout D and remove the switch
codeGlaze Sep 29, 2026
1420945
Order the credit by how much of the picture each artist drew
codeGlaze Sep 29, 2026
9737f74
Make the artist names read as links, and glow when reached for
codeGlaze Sep 29, 2026
94dda63
Give the artist names a prismatic highlight instead of a plain glow
codeGlaze Sep 29, 2026
2c39c67
Add account fields for artists and preferred names, and their encryption
codeGlaze Sep 29, 2026
3cef5ba
Link or create artist accounts from PORTRAIT_ARTISTS on startup
codeGlaze Sep 29, 2026
7126883
Send the artist welcome, upgrade and admin emails
codeGlaze Sep 29, 2026
2b8e88a
Layer artist credit edits under the deployment config
codeGlaze Sep 29, 2026
1861c4c
Let accounts set a preferred name and artists edit their credit
codeGlaze Sep 29, 2026
cec3084
Add preferred name and artist credit settings to My Account
codeGlaze Sep 29, 2026
b3e4caa
Greet people by their preferred name in site emails
codeGlaze Sep 29, 2026
3f43eac
Document artist accounts, credit layering and preferred names
codeGlaze Sep 29, 2026
176dfd5
Pick link icons from the link, and use a switch for email updates
codeGlaze Sep 29, 2026
707815a
Check credit links as they're typed, with the server's own rules
codeGlaze Sep 30, 2026
b42fe76
Fit many-artist credits everywhere, collapsing the tail into a count
codeGlaze Sep 30, 2026
a97e07d
Colour irises and lips through their shading, in all three renderers
codeGlaze Sep 30, 2026
4cf05d1
Add a Brightness slider to the eyes and lips
codeGlaze Sep 30, 2026
c6e0aba
Use the v1.5 iris placement from the Loom
codeGlaze Sep 30, 2026
9626f49
Shift eye style 1's right-hand iris left onto the drawn iris
codeGlaze Sep 30, 2026
2433a27
Restore the artist's placement for eye style 1's right-hand iris
codeGlaze Sep 30, 2026
d18bace
Feather the iris edge, and add a developer switch for one iris
codeGlaze Sep 30, 2026
807ee86
Move the far-iris developer switch under the portrait
codeGlaze Sep 30, 2026
beff7a2
Fill in eye whites for styles drawn without them, with a Whites colour
codeGlaze Sep 30, 2026
7602cd7
Add a hair ombre with controls, and shadows from hair over the face
codeGlaze Oct 1, 2026
91fa72d
Run the hair ombre down from each piece's crown line, not from a point
codeGlaze Oct 1, 2026
5dd47b1
Move per-piece portrait settings out of code, with a Loom import
codeGlaze Oct 1, 2026
5c7b59a
Add three hair tricks to the ombre, off until chosen
codeGlaze Oct 1, 2026
c8ada32
Keep the long fringe in the root colour, as colourists do
codeGlaze Oct 1, 2026
52aeac0
Add a Streaks slider to the ombre, on by default at 0.45
codeGlaze Oct 1, 2026
9e1831c
Hair streaks follow the strands drawn in the art
codeGlaze Oct 1, 2026
4b41da6
Document strand-following hair streaks
codeGlaze Oct 1, 2026
d44414c
Precompute strand fields beside the art; let bangs be dyed
codeGlaze Oct 1, 2026
09f2570
Document precomputed strand fields and the Bangs control
codeGlaze Oct 1, 2026
809df3a
Portrait eye and skin effects, hair shine and split
codeGlaze Oct 1, 2026
69b2274
Hair shine: one clean ring per crown piece
codeGlaze Oct 2, 2026
aaa225b
Shine at: place the shine ring, the same depth on every piece
codeGlaze Oct 2, 2026
d492241
Offer Split only in Developer mode
codeGlaze Oct 2, 2026
e68218a
Portrait roadmap: plans for everything still open
codeGlaze Oct 2, 2026
dcfa979
Skin and hair default to one colour; freckles on both cheeks and the …
codeGlaze Oct 2, 2026
1abb507
Freckles: Placement and Prominence controls
codeGlaze Oct 2, 2026
cef4bc0
Log hair pieces shipped without strand fields at server start
codeGlaze Oct 2, 2026
2585456
Glowing eyes, mood light and skin shade
codeGlaze Oct 2, 2026
6285ce7
Mood light: make the Edge band's width and softness adjustable
codeGlaze Oct 2, 2026
9213176
Whites can be left unfilled
codeGlaze Oct 2, 2026
49c0c32
Style 3 eye whites from manifest v1.6; loom.edn records its pack
codeGlaze Oct 3, 2026
6ce76fb
Draw every portrait piece with its inside made solid
codeGlaze Oct 4, 2026
daf133d
Lip colour follows how solid the stroke is
codeGlaze Oct 4, 2026
898d8f0
Make the solid-insides fill a developer tool, off in production
codeGlaze Oct 4, 2026
199509f
Retire the generated scalp piece
codeGlaze Oct 6, 2026
4350787
Merge integration into the portrait branch
codeGlaze Oct 6, 2026
96161ba
Portrait docs: what ships, how to deploy it, what comes later
codeGlaze Oct 6, 2026
647b1a8
Work out a missing strand file on the server instead of 404ing
codeGlaze Oct 6, 2026
f95c14e
Fold ASCII names and hosts the same way under any locale
codeGlaze Oct 6, 2026
699a4d1
Portrait art: serve only the images, each asking not to be used for AI
codeGlaze Oct 7, 2026
ec6fcb9
Generate hair strand files in the image build
codeGlaze Oct 8, 2026
a0fb1bd
Keep the licensed portrait art on the private branch, and out of here
codeGlaze Oct 8, 2026
81cf787
Escape the page's inline branding JSON so an artist credit cannot clo…
codeGlaze Oct 12, 2026
427eccb
Serve only registered portrait pieces from the art directory
codeGlaze Oct 12, 2026
c916cf0
Bound the server's portrait renders: two at once, last 64 kept
codeGlaze Oct 12, 2026
e22b957
Bake the portrait ahead so the PDF export submits inside the click
codeGlaze Oct 12, 2026
1f477f5
Artist accounts: exact race checks, resend a refused welcome, claim u…
codeGlaze Oct 12, 2026
5aeecd8
Pass portrait settings to the container; small fixes from review
codeGlaze Oct 12, 2026
7f41556
Move the portrait gallery tools to dev/, keep the art coverage check
codeGlaze Oct 12, 2026
b5cc99f
Add the scalp coverage test and the gallery's new namespace
codeGlaze Oct 12, 2026
f747039
Merge integration; fix the browser's Randomize seed hash
codeGlaze Oct 12, 2026
a0b61cb
Fold the portrait changelog into Summer Patch; add its What's New entry
codeGlaze Oct 12, 2026
ff7dd4c
Add the folded changelog lines and the What's New entry
codeGlaze Oct 12, 2026
e9a222b
One namespace for the portraits' seeded randomness
codeGlaze Oct 12, 2026
97da85e
One work pool and one bounded cache for the server's image work
codeGlaze Oct 12, 2026
f4464bd
Unfold the portrait changelog; no release entry until the owner names…
codeGlaze Oct 12, 2026
19d0ef4
Judge the decoded path in the portrait art guard
codeGlaze Oct 12, 2026
b129778
Serve artist credits as data, never in the page
codeGlaze Oct 12, 2026
a6ab75c
Guard every image in the repository, not only the portrait directory
codeGlaze Oct 12, 2026
e2f91af
Test that docker-compose passes the portrait settings to the app
codeGlaze Oct 12, 2026
0330b0c
The server draws each shared-link portrait once and keeps it as a file
codeGlaze Oct 13, 2026
d6a4c70
Branch changelog: one line per user-visible outcome
codeGlaze Oct 13, 2026
34cce50
Fix the dev gallery's calls into the seeded-random functions
codeGlaze Oct 13, 2026
6d71d2b
Carry the licensed-art mark into everything drawn from the art, and r…
codeGlaze Oct 13, 2026
06ce980
./run --force keeps the existing profile encryption key
codeGlaze Oct 13, 2026
1d75ef1
Credit links: a host may hold only hostname characters
codeGlaze Oct 13, 2026
c65b1ae
A crafted portrait can no longer break a character's page
codeGlaze Oct 13, 2026
c068299
Saving a portrait marks the character changed
codeGlaze Oct 13, 2026
6c76c48
Portrait store: a writable default, and a failed write still serves
codeGlaze Oct 13, 2026
c0eba4d
The art guard fails when it cannot list the repository's files
codeGlaze Oct 13, 2026
36551be
Serving doc: the real address of a shared portrait image
codeGlaze Oct 13, 2026
a05e7db
PDF export probe: await the Export, option and Create PDF actions
codeGlaze Oct 13, 2026
d77a634
Work pool: count a caller as waiting only until it has a permit
codeGlaze Oct 13, 2026
28e48cf
Tests: start the portrait-save test from a clean app-db; check the ru…
codeGlaze Oct 13, 2026
dc102fe
Fold the portrait changelog into Summer Patch
codeGlaze Oct 13, 2026
333625f
The page template escapes by default
codeGlaze Oct 14, 2026
4b5073e
Move the portrait compositor's styles into Garden
codeGlaze Oct 14, 2026
2b2f406
portrait_tab_e2e: declare that it needs the dev bundle
codeGlaze Oct 15, 2026
fd0f5f9
Artist credits load when a view shows one, not at start-up
codeGlaze Oct 15, 2026
db85391
Unsaved builder edits survive the sheet's saves and Level Up
codeGlaze Oct 16, 2026
3c110ab
Sheet edits survive a builder Save and a save already in flight; undo…
codeGlaze Oct 16, 2026
7d6187f
One builder draft per character, so tabs stop overwriting each other
codeGlaze Oct 16, 2026
ecb3776
Bring over the notice box from fix/hidden-multiclass-skill-pick
codeGlaze Oct 16, 2026
7120493
Offer a leftover draft once, hold a character open in another tab, li…
codeGlaze Oct 16, 2026
0d0ace8
Carry the stored version, and answer a save refused as out of date
codeGlaze Oct 16, 2026
2d48a23
Portrait fixes from review: undrawable thumbnails, the drawing digest…
codeGlaze Oct 16, 2026
b70262b
PORTRAIT-SERVING: the drawing code's digest is taken at compile time
codeGlaze Oct 16, 2026
bdd7916
An on-demand walkthrough of characters and homebrew, logged out and in
codeGlaze Oct 16, 2026
523e799
Drafts belong to the account that made them; offline sheet saves resend
codeGlaze Oct 16, 2026
aa6c583
Drafts expire after 30 days; a Save refused as out of date asks which…
codeGlaze Oct 16, 2026
63482f8
CodeRabbit reviews PRs into integration automatically
codeGlaze Oct 16, 2026
94a10bd
PDF export: nothing optional stops it
codeGlaze Oct 17, 2026
829f8e2
A save reply no longer writes the draft another tab is holding
codeGlaze Oct 17, 2026
57c936f
private-art.sh: refuse when pushes would reach GitHub, not only tracking
codeGlaze Oct 17, 2026
17db387
Branding settings are checked before use
codeGlaze Oct 17, 2026
4a3dea6
Portrait store: each write gets its own temporary file
codeGlaze Oct 17, 2026
1536f03
check-images.sh names a marked file with a space in its name whole
codeGlaze Oct 17, 2026
3ea7b2d
A stored portrait keeps only colour values it can use
codeGlaze Oct 17, 2026
2feed01
Drafts: write before moving the tab's key; re-mark on pageshow
codeGlaze Oct 17, 2026
ba117b0
PDF export test: no captured request fails cleanly
codeGlaze Oct 17, 2026
1c7f9e2
Portrait store: a failed write leaves no temporary file
codeGlaze Oct 17, 2026
b032c4d
Email counts as configured only with a usable sender
codeGlaze Oct 17, 2026
8ea5231
Remove the dead .fa-caret-square-o-down rule
codeGlaze Oct 17, 2026
8a69968
Merge integration into feature/portrait-builder
codeGlaze Oct 18, 2026
1a93e2c
Changelog: saving a character deleted elsewhere
codeGlaze Oct 18, 2026
3db467c
Changelog: one change per bullet for the unsaved-edits fixes
codeGlaze Oct 18, 2026
84db944
.env.example: the store-dir example holds only the path
codeGlaze Oct 18, 2026
3526e99
Startup banner: a portrait store path reads as set
codeGlaze Oct 18, 2026
06ce10f
A stored portrait keeps only colours that can tint
codeGlaze Oct 18, 2026
2697638
A tweak on the lips shows on the Lips chip
codeGlaze Oct 18, 2026
de51d0d
Account credit form: blank link rows count against the limit
codeGlaze Oct 18, 2026
55622c6
Compositor probe: stop when the launcher is missing
codeGlaze Oct 18, 2026
5b3ae06
PDF export probe: the stripped-payload check guards a missing request
codeGlaze Oct 18, 2026
bce9e98
New and Log Out keep the never-saved draft they replace
codeGlaze Oct 18, 2026
9da4eab
Portrait controls get the fonts their rules always named
codeGlaze Oct 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .clj-kondo/config.edn
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@
:unresolved-symbol
{:exclude
[read-string
;; test macros that bind their first argument -- clj-kondo cannot
;; see through them, so each use reads as an unresolved symbol
(orcpub.portrait-credit-test/with-conn)
(orcpub.portrait-og-test/with-conn)
(orcpub.portrait-artists-endpoint-test/with-conn)
(orcpub.portrait-store-test/with-conn)
(orcpub.page-escape-test/with-conn)
(clojure.test.check.clojure-test/defspec)
(clojure.core.match/match)
(cljs.core.match/match)
Expand Down Expand Up @@ -132,6 +139,8 @@
(orcpub.routes.share-test/with-conn)
(orcpub.routes.party-test/with-conn)
(orcpub.heartbeat-test/with-conn)
(orcpub.artist-accounts-test/with-conn)
(orcpub.artist-credit-test/with-conn)
(user/with-db)]}}
;; native/cljs and web/cljs are separate source roots; kondo doesn't know
;; about them so ns names appear to mismatch their file paths.
Expand Down
9 changes: 9 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# CodeRabbit settings for the codeGlaze/orcpub fork only. Leave this file out of pull requests to
# Orcpub/orcpub. The fork's default branch is develop, so without this CodeRabbit reviews only PRs
# into develop automatically; work lands on integration first.
reviews:
auto_review:
enabled: true
base_branches:
- develop
- integration
20 changes: 20 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,26 @@ EMAIL_TLS=FALSE
# to be empty; ignored otherwise. Private instances only.
ALLOW_UNVERIFIED_REGISTRATION=false

# --- Preferred names and portrait artists ---
# Encrypts the preferred names people give on My Account. Comma-separated id:key pairs, each
# key 32 random bytes in base64; the first encrypts, all decrypt (put a new key first to
# rotate). Keep it safe: lose it and stored names cannot be read. Without it, preferred
# names cannot be saved. Generate: echo "k1:$(openssl rand -base64 32)"
PROFILE_ENCRYPTION_KEYS=
#
# Portrait artist credits and accounts, as JSON (see ARTISTS.md). An "account" email gets an
# artist account and a welcome email when the server starts, so leave real addresses out of
# a test server unless that is wanted. Artist emails link to APP_URL (Branding, below).
# PORTRAIT_ARTISTS={"house-pack": {"name": "Fusspot", "account": "artist@example.test"}}
# Who hears about artist account changes (falls back to EMAIL_ERRORS_TO).
# EMAIL_ADMIN_TO=
#
# Where the server keeps its drawings of shared-link portraits (default: orcpub-portraits in the
# system temp directory, which the Docker image's user can write). Point several containers at one shared volume and each portrait is
# drawn once for all of them. They are derived files: deleting them only means they are drawn again.
# Mount a shared volume at this path; the image creates it for the app's user.
# PORTRAIT_STORE_DIR=/var/lib/orcpub/portraits

# --- Branding (optional) ---
# Override app identity for forks. All have sensible defaults in fork/branding.clj.
# APP_NAME=Dungeon Master's Vault
Expand Down
32 changes: 32 additions & 0 deletions .github/workflows/portrait-art-guard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Guard: the licensed portrait art lives only on the private deployment branch (Gitea), never
# here. scripts/private-art.sh writes resources/public/image/portraits/PRIVATE-ART when it lays
# the art in, so a push or pull request carrying that marker is the private branch arriving in
# the public repository. Art committed without the marker is caught by scripts/check-images.sh
# (and orcpub.portrait-art-guard-test): every image, anywhere, must be a recorded silhouette or a
# reviewed image.

name: Portrait art guard

on:
push:
pull_request:
workflow_dispatch:

permissions:
contents: read

jobs:
no-private-art:
name: No private portrait art
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: The private-art marker must not be in the public repository
run: |
if [ -e resources/public/image/portraits/PRIVATE-ART ]; then
echo "::error file=resources/public/image/portraits/PRIVATE-ART::This branch carries the licensed portrait art. It belongs only on the private deployment branch. Remove it from GitHub (and from the history if it was pushed) and tell the art owner."
exit 1
fi
echo "OK - no private portrait art on $GITHUB_REF_NAME."
- name: Every image is a recorded silhouette or reviewed
run: scripts/check-images.sh
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,6 @@ _*.js
# Python bytecode from scripts/ (scripts/lint-changelog.py is run, not imported)
__pycache__/
*.pyc

# strand fields are derived from the real portrait art and travel with it
resources/public/image/portraits/**/*.strands.png
139 changes: 139 additions & 0 deletions ARTISTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Artists

The portrait art in this repository was drawn by hand. It is not generated.

## Fusspot

- Site: <https://fusspot.rip/>
- Twitch: <https://www.twitch.tv/fusspot> — where most of the work happens, live

These are the two she asked to be listed. She has others; they are on her site,
which is the point of linking it.

Every portrait layer in `resources/public/image/portraits/` is Fusspot's work:
the heads, hair, ears, eyes, noses, mouths and shirts that the portrait maker
composes.

The files currently committed are **silhouettes** — alpha shapes derived from
her drawings, at the source proportions, without line detail. They are derived
works of her art and carry the same credit. The full line art replaces them at
the same paths.

`scalp/l2b_scalp_01.png` is the one exception: a generated blob that fills a
gap between two hair pieces. Its shape is derived from hers, so it is listed
here for completeness rather than claimed as anyone's drawing.

## How the credit is rendered

`orcpub.dnd.e5.portrait-assets/credit-line` produces one line — `Art: Fusspot`
— and the character sheet, the share card and the portrait summary all use it,
so the three always say the same thing. Attribution resolves **per asset**, so
a portrait built from two artists' pieces credits both.

An artist who has not said how they want to be credited is skipped rather than
given a byline nobody chose.

`:artist/link` is the single link, for surfaces that can only hold one -- the
character-page credit is a 100px strip. It points at the artist's own homepage,
because that is the list they maintain, so it cannot go stale the way a copied
handle can.

`:artist/links` is the full labelled set and appears only in the builder, which
has room. It is chosen with the artist rather than harvested from their site:
these are the accounts they want on this work, not every account they have.

Neither is part of `credit-line`. That string is burned into share-card PNGs
and PDF sheets, where a URL cannot be followed, and it would ride along on
every copy of somebody else's character.

## For forks

A deployment can restate a credit without editing shared code, via
`PORTRAIT_ARTISTS`:

```
PORTRAIT_ARTISTS='{"house-pack": {"name": "…", "link": "https://…"}}'
```

Only `name`, `link`, `links` and `license` are honoured. Which artist drew which asset
is structural, lives in the registry, and is not overridable — a configuration
cannot quietly re-attribute someone's work to a different pack.

**This does not grant a licence.** If you fork this repository, the art is
still Fusspot's. Ask her before shipping it.

## Artist accounts

An artist can have an account that controls how they're credited: their
name, where it links, and which link icons sit beside it. The account can't
change which art is theirs. That stays in the registry.

There's no admin page and no invite endpoint. Accounts come from the same
`PORTRAIT_ARTISTS` entry as the credit, read when the server starts:

```
PORTRAIT_ARTISTS='{"house-pack": {
"name": "Fusspot",
"account": "hello@fusspot.rip",
"username": "fusspot",
"preferred_name": "Fuss",
"welcome_subject": "We did it, Fuss! Your art is officially on OrcPub",
"welcome_note": "We did it! Your art is officially on OrcPub! …"
}}'
```

On each start:

| The address has… | What happens |
|-----------------------------------|-------------------------------------------------------------------------|
| no account | one is made and linked, and a welcome email with a set-password link that lasts a week is sent |
| a confirmed account | it's linked, and an upgrade email is sent |
| an unconfirmed account | it is claimed: given a new random password and linked, and the welcome email is sent to the address, so only the address's owner can get in |
| an account that's already linked | nothing, so restarts never resend (a welcome the mail server refused is sent again) |

Only `account` is needed; the rest just make the welcome warmer. Changing
`account` moves the link to the new address and tells the old account.
`"account": null` removes the link. Deleting the entry does nothing, so a
bad edit can't quietly strip an artist's access. Several containers
starting together make one account and send one email.

What the site shows is layered, and later layers win field by field:

1. the registry in `portrait_assets.cljc` (the public default)
2. the artist's own edits, from My Account → Artist credit
3. `name`, `link` and `links` in `PORTRAIT_ARTISTS`

So you can always correct a credit from config without touching the
account. Fields the config sets show as fixed on the artist's settings
page. Every edit is emailed to the artist and to the admin.

### Settings these need

| Setting | For |
|---------------------------------|---------------------------------------------------------------------|
| `APP_URL` | links in the startup emails; without it they're skipped (logged) and the artist can use "Forgot password" |
| `EMAIL_SERVER_URL` etc. | sending anything at all |
| `EMAIL_ADMIN_TO` | the admin's copies; falls back to `EMAIL_ERRORS_TO` |
| `APP_EMAIL_SIGNOFF` | who the warmer emails are signed from (default "The <sender name>") |
| `PROFILE_ENCRYPTION_KEYS` | preferred names (see below); a Docker secret `profile_encryption_keys` takes precedence |

## Preferred names

Every account can set "What should we call you?" on My Account. Site emails
open with it ("Hi Kaylee,") and greet the account by its username without it.

It's **encrypted, not hashed**: a hash can't be read back, and the site has
to write the name into the email. The database holds only ciphertext, and
the key lives outside it, so a copied database or backup doesn't reveal the
names. A compromised app server still could, because it holds the key.

```
# one key: id:base64 of 32 random bytes
PROFILE_ENCRYPTION_KEYS="k1:$(openssl rand -base64 32)"
```

Prefer the Docker secret `profile_encryption_keys` in swarm. To rotate, put a
new key first and keep the old ones after it (`k2:…,k1:…`); the first
encrypts and all of them decrypt. **Losing every key loses the names**, and
only the names: emails fall back to the username. With no key set the field says
it isn't available yet, and nothing is ever stored readable.
46 changes: 45 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ House style and the branch → release fold: [`docs/branch-changelog.template.md

### Highlights

Homebrew can be moved, switched off and repaired instead of lost, and a character that used to blank the page now recovers. Characters can be shared with the homebrew they use, printed sheets no longer drop spells and gain a spellbook, and accounts get new sign-in pages and stronger password rules. A new Rules tab holds the SRD's rules and conditions, with links that preview a spell, condition or rule as you read.
Homebrew can be moved, switched off and repaired instead of lost, and a character that used to blank the page now recovers. Characters can be shared with the homebrew they use and given a portrait drawn from an illustrator's layered art, printed sheets no longer drop spells and gain a spellbook, and accounts get new sign-in pages and stronger password rules. A new Rules tab holds the SRD's rules and conditions, with links that preview a spell, condition or rule as you read.

### Added

Expand Down Expand Up @@ -48,6 +48,30 @@ Homebrew can be moved, switched off and repaired instead of lost, and a characte
- **Rules:** clarification notes on rules, in our own words, linked to where Wizards published
the ruling.
- **Spells:** conditions named in spell and magic item text link to their page, with a preview.
- **Characters:** a portrait builder: pick or randomize layered art for hair, head, ears, eyes,
nose, mouth and shirt, from the summary thumbnail, beside the Image URL field, or in a Portrait
tab in the builder.
- **Characters:** portrait colours per character for hair, skin, eyes, eye whites, lips and shirt,
with a lighter, darker or different colour for any single piece.
- **Characters:** hair ombré with streaks that follow the drawn strands, a shine you can place,
dyed bangs, and soft shadows where hair falls over the face.
- **Characters:** eyes with slit or goat pupils, a different colour for each eye, brightness, and
glowing eyes.
- **Characters:** blush, freckles and shading under the hair.
- **Characters:** mood light: torch, moonlight or arcane, as a lit edge, a cast or a rim.
- **Characters:** the artist is credited under the portrait, on the sheet, in shared link previews
and in the picture itself, with the links the artist chose.
- **Printing:** the portrait prints on the exported character sheet, at full resolution.
- **Sharing:** a shared character link previews with the character's own portrait, and the
preview updates when the portrait changes.
- **Accounts:** a preferred name, used to greet you in site emails.
- **Accounts:** artists get an account with a welcome email and can change how they are credited
on My Account.
- **Site:** pages and portrait images ask AI crawlers not to train on them.
- **Characters:** unsaved changes left in a closed tab are offered once when you open that character
again, with Undo; older ones wait in an "Unsaved drafts" list in the builder and on the character
list for 30 days (a character never saved to an account is kept). A character open in another
tab waits for "Edit here".
- **Characters:** lowering a level or changing a class, subclass or race keeps the choices that depend on it while you edit; change your mind and they come back by themselves, with Undo.
- **Characters:** the builder shows choices waiting on a higher level beside the level picker, and says when each comes back.
- **Characters:** with more than one class, a "1st" badge marks the starting class; tap another class's badge to make it the starting class, and choices that only apply to the starting class follow.
Expand All @@ -63,6 +87,9 @@ Homebrew can be moved, switched off and repaired instead of lost, and a characte
- **Site:** on phones the header, menus and ability buttons fit the screen. (#711)
- **Server:** blank settings count as unset, Docker secrets work for the login key, and the Windows start scripts find their ports. (#701)
- **Server:** without email configured, registration is refused unless explicitly allowed. (#712)
- **Accounts:** site emails greet you by your preferred name, or by your username when you have
not set one, instead of "Hi there".
- **Characters:** the builder's tab bar wraps on narrow screens.
- **Characters:** opening a character whose choices no longer apply sets them aside and lists them in a notice; the saved copy changes only when you save.

### Fixed
Expand All @@ -78,6 +105,7 @@ Homebrew can be moved, switched off and repaired instead of lost, and a characte
- **Homebrew:** renamed items keep their links, and the characters using them. (#705)
- **Homebrew:** a custom magic item saved with no type no longer breaks. (#694)
- **Characters:** the Features tab loads when a trait has no name. (#680)
- **Characters:** saving a character that was deleted in another tab no longer sends you to the login page with nothing said; your changes stay, and Save stores them as a new character.
- **Characters:** the Robe of the Archmagi sets your AC to 15 + Dex instead of adding 5, so it no longer stacks with Draconic Resilience or Unarmored Defense.
- **Characters:** a tortle's Ring of Protection, Bracers of Defense and similar bonuses now count toward its AC of 17; only armor is ignored.
- **Characters:** anyone who can open a character sees its owner's custom items. (#252)
Expand All @@ -102,6 +130,22 @@ Homebrew can be moved, switched off and repaired instead of lost, and a characte
- **Equipment:** tools, musical instruments, gaming sets and equipment packs show their SRD prices,
and tools and instruments their weights.
- **Magic items:** the Alchemy Jug shows that its liquid can be created once a day.
- **Characters:** unsaved builder edits are no longer lost when a character's sheet or list saves,
whether for another character or the same one, or when it levels up.
- **Characters:** a sheet edit is no longer undone by a builder Save or by a save that was already
under way.
- **Characters:** closing the tab still warns while a save has not gone through.
- **Characters:** builder tabs open on different characters keep their own unsaved changes; a reload
no longer brings back the other tab's character. (#704)
- **Characters:** on a shared browser, logging in no longer opens the previous account's unsaved
character; each account's unsaved work stays its own, and work done logged out carries over.
- **Characters:** a change made on the character sheet while offline is saved when the connection
returns, instead of waiting for another change.
- **Sharing:** link previews of shared characters failed on Discord, Mastodon, Slack, Telegram and
X, because `robots.txt` blocked every crawler that honours it.
- **Sharing:** every shared character link had an empty title and image in its preview.
- **Printing:** exported sheets reported "Adobe InDesign CS6" as their creator and had no title or
author.
- **Characters:** a choice that no longer applied, such as a skill picked for a level since lowered, stayed hidden on the character and still counted on its sheet.
- **Characters:** deleting or changing a character's first class no longer wipes the other classes' choices.
- **Homebrew:** a homebrew class's choices that share a name show in level order; levels 10 and 17 came before level 3.
Expand Down
9 changes: 9 additions & 0 deletions dev/e2e_boot.clj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
(:require [com.stuartsierra.component :as component]
[datomic.api :as d]
[orcpub.system :as s]
[orcpub.artist-accounts :as artist-accounts]
[orcpub.routes :as routes]
[orcpub.entity.strict :as se]
[orcpub.dnd.e5.character :as char5e]
Expand Down Expand Up @@ -73,6 +74,14 @@
(item "kaylee" "Kaylee Seeded Beta")
(item "kaylee" "Kaylee Seeded Gamma")
(item "zoe" "Zoe Seeded Only")])
;; E2E_ARTIST=<artist id> makes kaylee that artist's account, through the
;; same linking the live site does. Not via PORTRAIT_ARTISTS: the server's
;; own startup pass runs before the seeding above, so it would find no
;; kaylee and create a separate account instead.
(when-let [artist (not-empty (System/getenv "E2E_ARTIST"))]
(println "e2e-boot: kaylee as artist"
(artist-accounts/reconcile! conn {(keyword artist) {:account "kaylee@example.com"}}
artist-accounts/log-only-notify)))
;; Printed so a check can open the character without logging in.
(let [{:keys [status body]} (routes/do-save-character (d/db conn) conn
(fighter-carrying "Bree Tinker" :kaylee-seeded-alpha)
Expand Down
Loading
Loading