Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
175 commits
Select commit Hold shift + click to select a range
19dd970
Add a randomize-portrait button next to the Image URL input
claude Aug 16, 2026
fbcbef2
Revert "Add a randomize-portrait button next to the Image URL input"
claude Aug 28, 2026
9e15b45
Add the paper-doll portrait compositor (v1 MVP)
claude Aug 28, 2026
efce42e
Add character colors to the portrait compositor; fix its save path
claude Sep 5, 2026
6f3a73c
Merge remote-tracking branch 'origin/integration' into claude/charact…
claude Sep 5, 2026
d0ba22f
Fix portrait draft not rehydrating on reopen; add compositor e2e
claude Sep 6, 2026
89ce91d
Print composed portraits in the exported PDF
claude Sep 6, 2026
150b78f
Render composed portraits for og:image; fix empty share cards
claude Sep 6, 2026
04e8d9d
Cover schema deploy-safety for ::char5e/portrait
claude Sep 6, 2026
a0a8605
Light theme for the portrait compositor; keep the launcher styled
claude Sep 6, 2026
63eddd2
Add a second way into the portrait compositor: a pencil on the summar…
claude Sep 6, 2026
c403f0d
Declare the AI opt-out on the pages, the portrait PNGs and robots.txt
claude Sep 12, 2026
850ce75
Let link-preview crawlers through robots.txt
claude Sep 12, 2026
62f0646
Add the desktop half of the asset pipeline
claude Sep 12, 2026
5450740
Credit the artists wherever the portrait goes
claude Sep 12, 2026
3b887f9
Use the illustrator's actual inventory instead of invented placeholders
claude Sep 12, 2026
6e12820
Stamp the exported sheet with honest metadata
claude Sep 12, 2026
3f679fd
Bake the credit into the composed picture, and stop trusting the save…
claude Sep 12, 2026
793fbd7
Add the site mark up the right edge of shared portraits
claude Sep 13, 2026
32e8eb7
Set both portrait marks in Vollkorn, the face the sheet already uses
claude Sep 13, 2026
a1081f1
Add the Portrait builder tab: the compositor in place, not only in a …
claude Sep 13, 2026
7e98065
Show pieces the illustrator has not drawn yet in the picker
claude Sep 20, 2026
ad0c492
Merge origin/integration into the portrait branch
claude Sep 20, 2026
3ebd305
Ignore a local CLAUDE.md
claude Sep 20, 2026
55c43ef
Make the portrait probes citizens of the probe runner
claude Sep 20, 2026
2e2e56e
Merge origin/integration into the portrait branch
codeGlaze Sep 20, 2026
0e0e140
Subset the credit face: 357 KB of Vollkorn becomes 26 KB
codeGlaze Sep 20, 2026
a674342
Stop the printed portrait coming out in a black box
codeGlaze Sep 20, 2026
b521ea9
Remove shots.js, a scratch file swept in by git add -A
codeGlaze Sep 20, 2026
f63cfce
Add the branch changelog
codeGlaze Sep 25, 2026
8fda24b
Close the six findings from the PR #36 review
codeGlaze Sep 25, 2026
421d50c
Stop a first save from discarding the portrait being edited
codeGlaze Sep 25, 2026
db87bd0
Decide the portrait draft from what the save was dispatched with
codeGlaze Sep 25, 2026
b2ce101
Give the three renderers one set of numbers
codeGlaze Sep 26, 2026
f8422a5
Split :set-character in two, and stop a stale save replacing the screen
codeGlaze Sep 27, 2026
4344b17
Settle the empty portrait slot, and show the clobber in a browser
codeGlaze Sep 27, 2026
60555f5
Retire four assertions that could not fail, and settle the panel into…
codeGlaze Sep 27, 2026
70e2cf7
Read the save response where the effect actually puts it
codeGlaze Sep 27, 2026
d71f943
Answer a crawler that already has the portrait without rendering it a…
codeGlaze Sep 27, 2026
bdce288
Match If-None-Match the way the spec defines it, not as a string compare
codeGlaze Sep 27, 2026
cf26556
Stop the wildcard being something a client can fabricate
codeGlaze Sep 27, 2026
aea1f0d
Show what the tint does to art that is not a silhouette
codeGlaze Sep 27, 2026
d47d596
Show that multiply keeps the drawing masking throws away
codeGlaze Sep 27, 2026
31f8e4d
Add a harness for composing a real pack both ways
codeGlaze Sep 27, 2026
676df6d
Compose four characters from one pack, not one
codeGlaze Sep 27, 2026
ab1869f
Leave the layers that hold more than one thing alone
codeGlaze Sep 27, 2026
31eab48
Eyes want multiply too -- one exception, not two
codeGlaze Sep 27, 2026
479126b
Render the pack through Java2D, and paint the iris rather than tint it
codeGlaze Sep 28, 2026
68da9d1
Soften the region edges, and let the pupil sit off centre
codeGlaze Sep 28, 2026
9030f1c
Test whether a grade is worth offering, before offering one
codeGlaze Sep 28, 2026
4420eec
Find out why the eye colour went quiet, and by how much
codeGlaze Sep 28, 2026
3f9613f
Separate iris legibility from iris modelling with a gamma
codeGlaze Sep 28, 2026
74193fa
Write down what the portrait tinting experiments settled
codeGlaze Sep 28, 2026
f662453
Check the settled iris pair against every eye style, not just the bes…
codeGlaze Sep 28, 2026
b85cf0d
The lips are drawn light and the irises dark, so one setting cannot s…
codeGlaze Sep 28, 2026
568ea98
Stop re-reading every asset off disk on every compose
codeGlaze Sep 28, 2026
f3220d0
Add a lips colour slot, owned by the asset rather than the layer
codeGlaze Sep 28, 2026
76e5db7
Compose the full range of busts from the registry's own rules
codeGlaze Sep 28, 2026
4bc8985
Show the actual range: vary the features, not just the palette
codeGlaze Sep 28, 2026
a40bfad
Find every hair combination that leaves an island of scalp
codeGlaze Sep 28, 2026
70fe632
Fill the scalp gap with a patch derived from the gap itself
codeGlaze Sep 28, 2026
04d6499
One patch under the crown, instead of a patch per combination
codeGlaze Sep 28, 2026
96def84
Hair has to be opaque before it counts as hiding the patch
codeGlaze Sep 28, 2026
96353d8
Check the patch by painting it green, not by comparing masks
codeGlaze Sep 28, 2026
3f09333
Add the scalp as a real layer, between the head and the hair
codeGlaze Sep 28, 2026
4ce3418
Tint by multiply in all three renderers
codeGlaze Sep 28, 2026
06c3bbf
Keep the scalp out of the hair piece list
codeGlaze Sep 28, 2026
14e5d48
Credit Fusspot, and let a deployment restate it without a fork
codeGlaze Sep 28, 2026
59577b3
Make the credit on the character page a link
codeGlaze Sep 28, 2026
653213a
Link Fusspot's homepage, and a Ko-fi link in the builder only
codeGlaze Sep 28, 2026
3c0a49f
Give the artist a labelled link set, with Twitch where it belongs
codeGlaze Sep 28, 2026
7dd59f0
Stop the artist name's styling leaking onto the link pills
codeGlaze Sep 28, 2026
6051324
Make the artist's links read as a credit, not a button bar
codeGlaze Sep 28, 2026
97c8fc9
Use the services' own marks for the artist's links
codeGlaze Sep 28, 2026
68ef2c2
Design pass on the credit strip
codeGlaze Sep 28, 2026
0e4faec
Put the credit under the art it credits
codeGlaze Sep 28, 2026
d3b3721
Align the portrait frame with the column it sits in
codeGlaze Sep 28, 2026
0198475
Shimmer the swirl on hover
codeGlaze Sep 28, 2026
a08ee9b
Fusspot's links: her site and her Twitch, which is what she asked for
codeGlaze Sep 29, 2026
d5dc8f4
Give the artist's links their own colours, worn at rest
codeGlaze Sep 29, 2026
0cd13ae
Settle the credit row on one rhythm and one alignment
codeGlaze Sep 29, 2026
5948550
Size the marks to the text's cap height, not its line box
codeGlaze Sep 29, 2026
f1593a3
Put credit lockups B, D and F behind a temporary switch
codeGlaze Sep 29, 2026
75cdd48
Let layout D hold for any number of links and artists
codeGlaze Sep 29, 2026
45b89ae
Settle the credit on layout D and remove the switch
codeGlaze Sep 29, 2026
79167e2
Order the credit by how much of the picture each artist drew
codeGlaze Sep 29, 2026
3d10532
Make the artist names read as links, and glow when reached for
codeGlaze Sep 29, 2026
1bb8c54
Give the artist names a prismatic highlight instead of a plain glow
codeGlaze Sep 29, 2026
b9a540c
Add account fields for artists and preferred names, and their encryption
codeGlaze Sep 29, 2026
4e27020
Link or create artist accounts from PORTRAIT_ARTISTS on startup
codeGlaze Sep 29, 2026
11bfe62
Send the artist welcome, upgrade and admin emails
codeGlaze Sep 29, 2026
1ebf4bd
Layer artist credit edits under the deployment config
codeGlaze Sep 29, 2026
74fb038
Let accounts set a preferred name and artists edit their credit
codeGlaze Sep 29, 2026
1da219d
Add preferred name and artist credit settings to My Account
codeGlaze Sep 29, 2026
443d860
Greet people by their preferred name in site emails
codeGlaze Sep 29, 2026
686789c
Document artist accounts, credit layering and preferred names
codeGlaze Sep 29, 2026
441344a
Pick link icons from the link, and use a switch for email updates
codeGlaze Sep 29, 2026
9efdbf9
Check credit links as they're typed, with the server's own rules
codeGlaze Sep 30, 2026
d413146
Fit many-artist credits everywhere, collapsing the tail into a count
codeGlaze Sep 30, 2026
d05c428
Colour irises and lips through their shading, in all three renderers
codeGlaze Sep 30, 2026
4d38ad6
Add a Brightness slider to the eyes and lips
codeGlaze Sep 30, 2026
ffa67f8
Use the v1.5 iris placement from the Loom
codeGlaze Sep 30, 2026
9e9f2e8
Shift eye style 1's right-hand iris left onto the drawn iris
codeGlaze Sep 30, 2026
e024cac
Restore the artist's placement for eye style 1's right-hand iris
codeGlaze Sep 30, 2026
acdae36
Feather the iris edge, and add a developer switch for one iris
codeGlaze Sep 30, 2026
2288244
Move the far-iris developer switch under the portrait
codeGlaze Sep 30, 2026
16e3f6d
Fill in eye whites for styles drawn without them, with a Whites colour
codeGlaze Sep 30, 2026
333a1ee
Add a hair ombre with controls, and shadows from hair over the face
codeGlaze Oct 1, 2026
f941889
Run the hair ombre down from each piece's crown line, not from a point
codeGlaze Oct 1, 2026
e48f8fc
Move per-piece portrait settings out of code, with a Loom import
codeGlaze Oct 1, 2026
f04077b
Add three hair tricks to the ombre, off until chosen
codeGlaze Oct 1, 2026
488a06b
Keep the long fringe in the root colour, as colourists do
codeGlaze Oct 1, 2026
faa0808
Add a Streaks slider to the ombre, on by default at 0.45
codeGlaze Oct 1, 2026
dc9678d
Hair streaks follow the strands drawn in the art
codeGlaze Oct 1, 2026
28400fe
Document strand-following hair streaks
codeGlaze Oct 1, 2026
fce7d6a
Precompute strand fields beside the art; let bangs be dyed
codeGlaze Oct 1, 2026
4ade967
Document precomputed strand fields and the Bangs control
codeGlaze Oct 1, 2026
44f91e6
Portrait eye and skin effects, hair shine and split
codeGlaze Oct 1, 2026
1b3175d
Hair shine: one clean ring per crown piece
codeGlaze Oct 2, 2026
133586e
Shine at: place the shine ring, the same depth on every piece
codeGlaze Oct 2, 2026
3163823
Offer Split only in Developer mode
codeGlaze Oct 2, 2026
5791e58
Portrait roadmap: plans for everything still open
codeGlaze Oct 2, 2026
cf86d22
Skin and hair default to one colour; freckles on both cheeks and the …
codeGlaze Oct 2, 2026
f1b187e
Freckles: Placement and Prominence controls
codeGlaze Oct 2, 2026
e85c54b
Log hair pieces shipped without strand fields at server start
codeGlaze Oct 2, 2026
1741fde
Glowing eyes, mood light and skin shade
codeGlaze Oct 2, 2026
e5415e1
Mood light: make the Edge band's width and softness adjustable
codeGlaze Oct 2, 2026
e04cf06
Whites can be left unfilled
codeGlaze Oct 2, 2026
191977c
Style 3 eye whites from manifest v1.6; loom.edn records its pack
codeGlaze Oct 3, 2026
e113526
Draw every portrait piece with its inside made solid
codeGlaze Oct 4, 2026
ec32164
Lip colour follows how solid the stroke is
codeGlaze Oct 4, 2026
b5fe10c
Make the solid-insides fill a developer tool, off in production
codeGlaze Oct 4, 2026
7da4e31
Retire the generated scalp piece
codeGlaze Oct 6, 2026
2a4c24b
Merge integration into the portrait branch
codeGlaze Oct 6, 2026
549d5e7
Portrait docs: what ships, how to deploy it, what comes later
codeGlaze Oct 6, 2026
635375d
Work out a missing strand file on the server instead of 404ing
codeGlaze Oct 6, 2026
a84a731
Fold ASCII names and hosts the same way under any locale
codeGlaze Oct 6, 2026
e71784b
Portrait art: serve only the images, each asking not to be used for AI
codeGlaze Oct 7, 2026
6872a03
Generate hair strand files in the image build
codeGlaze Oct 8, 2026
eb04164
Keep the licensed portrait art on the private branch, and out of here
codeGlaze Oct 8, 2026
44c490d
Escape the page's inline branding JSON so an artist credit cannot clo…
codeGlaze Oct 12, 2026
8f1be6a
Serve only registered portrait pieces from the art directory
codeGlaze Oct 12, 2026
953971e
Bound the server's portrait renders: two at once, last 64 kept
codeGlaze Oct 12, 2026
b5ad9c7
Bake the portrait ahead so the PDF export submits inside the click
codeGlaze Oct 12, 2026
8692067
Artist accounts: exact race checks, resend a refused welcome, claim u…
codeGlaze Oct 12, 2026
bdf8cbd
Pass portrait settings to the container; small fixes from review
codeGlaze Oct 12, 2026
312a4ef
Move the portrait gallery tools to dev/, keep the art coverage check
codeGlaze Oct 12, 2026
42389e1
Add the scalp coverage test and the gallery's new namespace
codeGlaze Oct 12, 2026
ceeb5ce
Merge integration; fix the browser's Randomize seed hash
codeGlaze Oct 12, 2026
eb4196f
Fold the portrait changelog into Summer Patch; add its What's New entry
codeGlaze Oct 12, 2026
9888084
Add the folded changelog lines and the What's New entry
codeGlaze Oct 12, 2026
b6f71d7
One namespace for the portraits' seeded randomness
codeGlaze Oct 12, 2026
c0a60ce
One work pool and one bounded cache for the server's image work
codeGlaze Oct 12, 2026
a80dcc1
Unfold the portrait changelog; no release entry until the owner names…
codeGlaze Oct 12, 2026
bbc7f13
Judge the decoded path in the portrait art guard
codeGlaze Oct 12, 2026
5011bcf
Serve artist credits as data, never in the page
codeGlaze Oct 12, 2026
37a0361
Guard every image in the repository, not only the portrait directory
codeGlaze Oct 12, 2026
0d2b015
Test that docker-compose passes the portrait settings to the app
codeGlaze Oct 12, 2026
b413475
The server draws each shared-link portrait once and keeps it as a file
codeGlaze Oct 13, 2026
2a42e87
Branch changelog: one line per user-visible outcome
codeGlaze Oct 13, 2026
c787511
Fix the dev gallery's calls into the seeded-random functions
codeGlaze Oct 13, 2026
3279d7c
Carry the licensed-art mark into everything drawn from the art, and r…
codeGlaze Oct 13, 2026
2fd8e8e
./run --force keeps the existing profile encryption key
codeGlaze Oct 13, 2026
4a26484
Credit links: a host may hold only hostname characters
codeGlaze Oct 13, 2026
0a8d2e8
A crafted portrait can no longer break a character's page
codeGlaze Oct 13, 2026
846a0e0
Saving a portrait marks the character changed
codeGlaze Oct 13, 2026
e23d9d3
Portrait store: a writable default, and a failed write still serves
codeGlaze Oct 13, 2026
5c1a688
The art guard fails when it cannot list the repository's files
codeGlaze Oct 13, 2026
ea86a06
Serving doc: the real address of a shared portrait image
codeGlaze Oct 13, 2026
278360a
PDF export probe: await the Export, option and Create PDF actions
codeGlaze Oct 13, 2026
78bed5c
Work pool: count a caller as waiting only until it has a permit
codeGlaze Oct 13, 2026
43549d1
Tests: start the portrait-save test from a clean app-db; check the ru…
codeGlaze Oct 13, 2026
6f7b6e2
Fold the portrait changelog into Summer Patch
codeGlaze Oct 13, 2026
3935fd0
The page template escapes by default
codeGlaze Oct 14, 2026
ab19efd
Move the portrait compositor's styles into Garden
codeGlaze Oct 14, 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
19 changes: 19 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,25 @@ 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.
# PORTRAIT_STORE_DIR=/var/lib/orcpub/portraits (mount a shared volume there; the image owns it for the app)

# --- 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 say "Hi there," 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: they fall back to "Hi there,". With no key set the field says
it isn't available yet, and nothing is ever stored readable.
32 changes: 31 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,26 @@ 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.

### Changed

Expand All @@ -59,6 +79,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.

### Fixed

Expand Down Expand Up @@ -97,6 +120,13 @@ 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:** a late save for one character no longer replaces another character open in the
builder, along with its unsaved edits.
- **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.

### Security

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