# BrevyScribe

BrevyScribe is a fork of [FancyScribe](https://github.com/NilsUeter/fancyscribe).
It prints **generic datasheets** instead of a record of one army list.

FancyScribe shows a BattleScribe or New Recruit roster as 10th-edition
datacards. The cards show only the wargear that you selected. That is correct
for a list that you play today. It is not correct for a reference card that you
keep, because the official cards show *every* option of a unit. BrevyScribe
rewrites the roster before it renders the cards. As a result, the printed cards
read like the official ones.

Three transforms do this work. You can switch each one on or off in the user
interface.

| Transform | What it does |
| --- | --- |
| **Merge duplicates** | Mutually exclusive wargear makes you take one datasheet two times. One Skatros has a radium jezzail, another has a transuranic arquebus. This transform folds the copies of a unit into one card that carries all options. |
| **Drop Leader/Support** | This transform removes the attachment rules. After the army is built, these rules tell you nothing that you need during a game. They are also long, and they push the necessary rules off the card. |
| **Split choice abilities** | An ability such as *Canticles of the Omnissiah* arrives as one block of text. This transform divides it into the intro rule and one titled row for each option. The official datasheets print it in this form. |

The app does all of its work in the browser. There is no server component, no
upload, no account and no analytics. Rosters stay in `localStorage` and never
leave your machine.

## Lore text

Official datasheets print a paragraph of flavor text to the right of the model
image. The **Show Lore Text** toggle prints this text. The card then gives the
right third of its image to the text, behind a gradient that fades the image
into the dark. Cards without an entry keep the full-width image.

[`public/Lore.csv`](public/Lore.csv) supplies the text. The file is a
pipe-delimited export in this form:

```
name|legend
Custodian Guard|These warriors form the backbone of the shield companies, ...
```

The app fetches this file only when a roster is on screen. A missing or bad
file means that the cards print without lore text.

The names in a roster do not match the export exactly, so the app searches for
the closest entry. This search is a heuristic, and you can correct it:

- To change the text of a card, edit it in place. The app keeps your edit in
  `localStorage` under `lore_<unit name>`. If you clear the edit, the text of
  the export comes back.
- If the export does not cover a unit, the card shows an **Add lore** button.

About 40 names carry lore for more than one faction. Chaos Daemons and Death
Guard both field Plaguebearers, and three armies field a Ministorum Priest. The
export has no column that separates them, so the longest entry wins.

Only the 10th-edition and 11th-edition cards show lore text. The 9th-edition
renderer lays out its header differently.

## Run it on your machine

```sh
npm install
npm run dev          # http://localhost:5173
npm test
npm run build        # static files in dist/
```

## Deploy to scribe.luxick.de

The build output is **static files**. There is no application server and no
socket, so nginx serves the files directly. The app does all of its work in the
browser. For the same reason, upstream FancyScribe can live on GitHub Pages.

The server needs no Node. Do the setup one time, in two stages. The two stages
are necessary because the real configuration names a certificate, and nginx
refuses to load a configuration whose certificate does not exist. Therefore
nginx first comes up on port 80 only. That is far enough for certbot to answer
the challenge.

```sh
sudo mkdir -p /var/www/brevyscribe /var/www/certbot
sudo chown "$USER" /var/www/brevyscribe

# Stage 1: HTTP only, so that nginx starts without a certificate.
sudo tee /etc/nginx/sites-available/scribe.luxick.de >/dev/null <<'EOF'
server {
	listen 80;
	listen [::]:80;
	server_name scribe.luxick.de;
	location /.well-known/acme-challenge/ { root /var/www/certbot; }
}
EOF
sudo ln -s /etc/nginx/sites-available/scribe.luxick.de /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

sudo certbot certonly --webroot -w /var/www/certbot -d scribe.luxick.de

# Stage 2: the real configuration, now that the certificate is on disk.
sudo cp deploy/nginx-scribe.luxick.de.conf /etc/nginx/sites-available/scribe.luxick.de
sudo nginx -t && sudo systemctl reload nginx
```

Use `certonly --webroot`, not `--nginx`. The configuration in the repository
carries its own redirect and TLS block. The nginx plugin rewrites the installed
file, and the installed file then drifts away from the one in the repository.
The port 80 block keeps its `acme-challenge` location for this reason, so
renewals continue to work without your attention.

CAUTION: The timer of certbot does not reload nginx. Put a one-line hook that
runs `systemctl reload nginx` in `/etc/letsencrypt/renewal-hooks/deploy/`.
Without this hook, the server serves a renewed certificate only after the next
restart.

Each deploy is then one command from a checkout on your own machine. The
command runs the tests, builds the app and copies `dist/` to the server with
rsync.

```sh
./deploy/deploy.sh
```

The script uses `--delete` on purpose. The asset filenames contain a hash of
the content, so old bundles collect on the server without this flag. There is
no state on the server and nothing to back up. Every roster stays in the
`localStorage` of the browser.

`index.html` loads Noto Sans from Google Fonts, so a page load contacts
`fonts.googleapis.com`. If you do not want this request, delete the `<link>`
tags. Then host the font next to the fonts in `public/fonts/`.

## Credit

The difficult parts are the work of
[Nils Ueter](https://github.com/NilsUeter/fancyscribe): the parser, the card
layout and the print CSS. The parsing logic comes in turn from
[PrettyScribe](https://github.com/rweyrauch/PrettyScribe). Upstream ships no
license file. Treat this fork as a private, personal deployment, not as
something that you redistribute.
