Commit Graph

38 Commits

Author SHA1 Message Date
darroyo 6e3db41ba4 fix(ci): force musl linking for the static native binary
CI / Build Native (push) Failing after 6m13s
binary-type=STATIC alone picks glibc on Ubuntu jammy hosts, hence
the GLIBC_2.35 error at runtime. Explicitly force musl:

- -Dquarkus.native.libc=musl
- -Dquarkus.native.additional-build-args=--libc=musl
- delete target/*-runner first so we don't reuse a previously-built
  glibc-based binary that the Quarkus plugin cache might return

Verify step now exits 1 if the binary isn't statically linked, so
the next CI run tells us definitively whether we shipped static.
2026-08-14 14:53:39 -04:00
darroyo 1ad8e17473 fix(ci): drop 'file' from verify step (not installed in maven base image)
CI / Build Native (push) Successful in 46m9s
The 'file' package isn't installed in maven:3.9.6-eclipse-temurin-21, so
'file target/...-runner' returned 127 and failed the verify step.
Use only 'ldd'; on a static binary ldd exits non-zero with 'not a
dynamic executable', which we suppress with '|| echo'.
2026-08-14 13:58:43 -04:00
darroyo 0ce8d401d8 fix(ci): build static natively with musl on the build host (drop container-build)
CI / Build Native (push) Failing after 6m7s
container-build=true runs the native-image inside a Mandrel container,
but in the act runner the workspace mount doesn't resolve
(/workspace/... is a container path, not a host path), so the Mandrel
container can't see shot-crafter-calculator-1.0.0-runner.jar.

Fix: install musl + musl-tools in the CI container, build locally.
Quarkus 3.x's binary-type=STATIC will then produce a genuinely static
binary linked against musl, which has no dependency on the host's
glibc and runs cleanly on quarkus-micro-image:2.0 (UBI 9, glibc 2.34).
2026-08-14 13:40:59 -04:00
darroyo e095547142 fix(ci): build inside a Mandrel container to get a truly-static binary
CI / Build Native (push) Failing after 2m2s
The local native-image (GraalVM on Ubuntu jammy) produces a binary
linked against the runner's glibc 2.35. quarkus-micro-image:2.0 ships
glibc 2.34, hence the 'GLIBC_2.35 not found' runtime error.

binary-type=STATIC alone is not enough without musl on the build host.
-Dquarkus.native.container-build=true runs the native build inside the
Mandrel builder image, which ships musl and produces a genuinely static
binary that has no host-libc dependency.

Also adds a verification step that runs file and ldd on the runner
binary so the next CI log shows whether we actually shipped static.
2026-08-14 09:05:28 -04:00
darroyo 07e62dd481 fix(ci): drop --max-concurrent-uploads (not a valid docker push flag)
CI / Build Native (push) Successful in 17m58s
That flag only exists for docker buildx / docker buildx imagetools.
docker push in this version rejects it as unknown. Revert to plain
docker push with the retry loop.
2026-08-13 22:04:34 -04:00
darroyo 425011d9c8 fix(ci): make docker push retry more aggressive against Gitea registry
CI / Build Native (push) Failing after 9m10s
The Gitea registry sometimes returns 'net/http: timeout awaiting
response headers' on blob uploads. The existing retry loop (3x15s)
isn't enough.

- Bump retries 3 -> 5
- Backoff 15s -> 30s
- --max-concurrent-uploads=1 to avoid hammering a small registry

Total worst-case wait: ~4 minutes (4 retries * 30s + 5 pushes ~ 30s).
2026-08-13 21:46:02 -04:00
darroyo 3c189ae9c4 fix(docker): stage curl libs under /usr/lib64, not /lib64
CI / Build Native (push) Failing after 24m54s
ldd reports library paths as /lib64/... (the legacy short path).
quarkus-micro-image:2.0 has /lib64 as a symlink to /usr/lib64, so
COPY --from=curl-builder /out/ / collides when trying to write into
/lib64 (cannot copy to non-directory).

The fix: in the curl-builder stage, translate /lib64 -> /usr/lib64
(and /lib -> /usr/lib) before installing each library, so the final
image gets libs under /usr/lib64 and doesn't touch the /lib64 symlink.
2026-08-13 21:09:56 -04:00
darroyo 378b46cbd8 fix(docker): don't ignore build-output/, the COPY needs it in the build context
CI / Build Native (push) Failing after 6m47s
Earlier cleanup added build-output to .dockerignore, but the runtime
stage does COPY build-output/*-runner /work/application, so the
directory must be present in the build context.
2026-08-13 20:47:53 -04:00
darroyo 6421a91148 ci: revert to Docker image build + registry push (no deploy)
CI / Build Native (push) Failing after 14m36s
Drop the LXC deploy step. Pipeline now stops at publishing the image
to the Gitea registry; deployment is handled out of band.

Restored:
- Dockerfile (multi-stage: curl-builder + quarkus-micro-image:2.0,
  generic via build-output/*-runner wildcard, COPY --chown=1001:1001)
- compose.yaml (one-shot install of the published image)
- .dockerignore (excludes build-output/)

CI workflow:
- Installs docker-buildx (needed for COPY --chown)
- Uses docker buildx build
- chmod 775 and echo securerandom happen in the 'Stage binary for Docker'
  step; the final image has no RUN commands
- Tags :latest and :<short-sha>, pushes with retry

No deploy step. Pull the image with docker compose / run it manually.
2026-08-13 20:25:43 -04:00
darroyo a2fb5cc521 ci: deploy native binary directly to LXC via SSH (drop Docker/registry)
CI / Build Native (push) Failing after 12m4s
Switch the runtime from a Docker image to a systemd service running the
native binary on the LXC host. The CI still uses Docker for the build
environment (maven:3.9.6-eclipse-temurin-21), but stops at producing the
static native binary.

Pipeline changes:
- Drop docker.io, docker-buildx, docker buildx, docker push, registry.
- Drop Dockerfile, compose.yaml, .dockerignore (no longer needed).
- Build native binary in CI container, SCP to LXC, run deploy script.
- Deploy script stops the service, swaps the binary, starts it, hits
  /q/health/live to verify.

LXC one-time setup (manual, run on the host):
- useradd runner (UID 1001)
- mkdir /opt/shot-crafter-calculator/{data,keys,deploy}
- copy RSA JWT keys into keys/
- install /etc/systemd/system/shot-crafter-calculator.service
- install /usr/local/bin/deploy-shot-crafter-calculator.sh
- useradd deployer + ssh keypair for the CI
- store DEPLOY_SSH_KEY secret in Gitea

Bootstrap the first deploy manually with scp + ssh before relying on CI.
2026-08-13 20:05:17 -04:00
darroyo ac349c1b34 fix(docker): drop RUN commands - quarkus-micro-image:2.0 has no shell
CI / Build Native (push) Failing after 15m1s
Removed the RUN chmod/chown/echo steps from the Dockerfile. The Quarkus
micro image 2.0 ships without /bin/sh and /usr/bin/sh, so any RUN
instruction fails. The chmod and securerandom.source append now happen
in the CI 'Stage binary for Docker' step, and COPY --chown=1001:1001
takes ownership of the binary in the image.

COPY --chown requires BuildKit, so:
- Install docker-buildx in the CI
- Switch docker build -> docker buildx build

The final image stays minimal (no shell, no microdnf, no extra packages).
2026-08-13 17:13:54 -04:00
darroyo 11c58aa614 fix(docker): set SHELL to /usr/bin/sh for quarkus-micro-image:2.0
CI / Build Native (push) Failing after 15m24s
The Quarkus micro image 2.0 no longer ships /bin/sh (only /usr/bin/sh).
Docker's default shell is /bin/sh, so RUN commands fail with
'exec /bin/sh: no such file or directory'. Set SHELL explicitly to
/usr/bin/sh to keep the small final image while letting Docker run
RUN commands.
2026-08-13 16:39:04 -04:00
darroyo af92363f18 fix(docker): mkdir /out/etc/pki and /out/etc before cp
CI / Build Native (push) Failing after 8m48s
cp -rP /etc/pki/ca-trust /out/etc/pki/ requires /out/etc/pki to exist.
Consolidated mkdir -p to create both /out/etc/pki and /out/etc.
2026-08-13 16:24:02 -04:00
darroyo 9e81097f53 fix(docker): use install -D to create leading dirs for curl
CI / Build Native (push) Failing after 16m26s
The previous install -m 0755 /usr/bin/curl /out/usr/bin/curl failed because
/out/usr/bin/ did not exist. install -D creates the leading dirs.
2026-08-13 15:45:58 -04:00
darroyo ef8d95b7da fix(docker): use multi-stage build to install curl (quarkus-micro-image:2.0 ships no microdnf)
CI / Build Native (push) Failing after 14m50s
The 2.0 rebuild of quarkus-micro-image removed microdnf to slim the image,
so the inline 'microdnf install curl-minimal' step now fails with
'command not found'.

Build curl-minimal in a ubi9/ubi-minimal builder stage, then copy only the
curl binary + its runtime shared libs + CA bundle into the final
quarkus-micro-image layer. Final image stays slim and the docker-compose
healthcheck keeps working.
2026-08-13 14:59:21 -04:00
darroyo 227c4e2e6e fix(ci): build static native binary (musl) to fix glibc version mismatch
CI / Build Native (push) Failing after 14m45s
Container `maven:3.9.6-eclipse-temurin-21` is Ubuntu 22.04 jammy
with glibc 2.35. Container `quarkus-micro-image:2.0` is UBI 9
minimal with glibc 2.34. Native-image links against the build
host's glibc, so the resulting binary needs GLIBC_2.34+ symbols
that the UBI 9 runtime doesn't provide:

  ./application: /lib64/libc.so.6: version `GLIBC_2.34' not found
  ./application: /lib64/libc.so.6: version `GLIBC_2.33' not found
  ./application: /lib64/libc.so.6: version `GLIBC_2.32' not found

Set quarkus.native.binary-type=STATIC so GraalVM embeds musl into
the binary and removes the glibc dependency. The static binary
runs on any Linux distribution, including UBI minimal. Trade-off:
~50MB -> ~80MB image size.
2026-08-13 14:29:51 -04:00
darroyo d101d6df0f feat: add HTTP healthcheck endpoint via quarkus-smallrye-health
CI / Build Native (push) Failing after 16m25s
- pom.xml: add quarkus-smallrye-health dependency.
  Exposes /q/health/live and /q/health/ready endpoints (liveness
  and readiness probes for Quarkus apps).
- Dockerfile: install curl-minimal in the microdnf layer so the
  container has a real HTTP client. quarkus-micro-image is based
  on UBI 9 minimal and doesn't ship with curl by default.
- compose.yaml: healthcheck now hits GET /q/health/live with
  curl -f instead of the previous kill -0 1 (which only proved
  the process was alive, not that the HTTP server was responding).

The next CI run will rebuild the native binary with the health
extension baked in; old images pulled from :latest will keep
working since this is additive.
2026-08-13 13:01:20 -04:00
darroyo 12d382d253 feat: add compose.yaml for one-line install
CI / Build Native (push) Has been cancelled
docker compose pull
  docker compose up -d
  docker compose logs -f

  - Single service `shot-crafter` pulling from
    gitea.danielarroyo.cl/proyectos/shot-crafter-calculator:latest
  - Named volume `shot-crafter-data` mounted at /work/data
    so the H2 DB persists across `down`/`up` cycles
  - env vars inline (overridable via -e or .env): DB URL,
    HTTP port/host, cookie name + secure flag
  - healthcheck using `kill -0 1` (the native binary is PID 1,
    POSIX-portable, no extra binaries needed)
  - restart: unless-stopped so it survives Docker daemon restarts

Ver .env.example para la lista completa de variables operacionales.
2026-08-13 12:48:03 -04:00
darroyo 9646236fa8 feat: externalize config via env vars
CI / Build Native (push) Successful in 20m1s
- Add .env.example with all configurable variables documented
  (DB URL, HTTP port/host, cookie, JWT issuer/keys, log level)
- Remove hardcoded -Dquarkus.http.host from Dockerfile ENTRYPOINT
  (application.properties already sets the default; env vars
  can now override it at runtime without conflicting with -D flags)

All env vars follow Quarkus's auto-binding convention:
property.key → PROPERTY_KEY (uppercase)

Most useful for production:
- QUARKUS_DATASOURCE_JDBC_URL: DB file path
- QUARKUS_HTTP_PORT: HTTP port
- QUARKUS_HTTP_HOST: bind interface
- APP_AUTH_COOKIE_SECURE: enable Secure flag behind HTTPS
- MP_JWT_VERIFY_PUBLICKEY_LOCATION: externalize RSA keys
2026-08-13 09:27:53 -04:00
darroyo ee1363535c fix(ci): add retry loop to docker push (handles Gitea registry timeout)
CI / Build Native (push) Successful in 18m38s
The Gitea Container Registry responded with 'net/http: timeout
awaiting response headers' midway through the push. The image
uploads ~50MB of layers, and the registry can be slow under load.

Add a push_with_retry() shell function that retries each push up
to 3 times with 15s backoff. Most importantly, it retries for
both the 'latest' and the SHA tag, so a partial failure doesn't
leave the registry in a half-pushed state.

Uses POSIX sh-compatible while loop (no bashisms like {1..3}).
2026-08-12 21:12:22 -04:00
darroyo 9bae7dfe29 fix(ci): use cut instead of bash substring for short SHA (POSIX sh)
CI / Build Native (push) Failing after 9m5s
The act runner uses /bin/sh (dash on Debian/Ubuntu), not bash.
The syntax ${GITHUB_SHA::7} is bash-specific substring expansion
and throws 'Bad substitution' under dash.

Replace with POSIX-portable equivalent:
    SHORT_SHA=$(echo "$GITHUB_SHA" | cut -c1-7)

Works in any sh-compatible shell (dash, bash, zsh, etc.).
2026-08-12 19:58:09 -04:00
darroyo d067110103 fix(ci): stage native binary in build-output/ to bypass .dockerignore
CI / Build Native (push) Failing after 8m52s
The .dockerignore has 'target' which excludes the target/
directory from the docker build context. The Dockerfile was
COPYing the binary from target/, so docker build failed with
'file not found in build context or excluded by .dockerignore'.

Fix: copy the binary to build-output/ (a non-excluded path)
before docker build, and update the Dockerfile to copy from
build-output/.

- Add 'Stage binary for Docker' step that does:
    mkdir -p build-output
    cp target/shot-crafter-calculator-1.0.0-runner build-output/
- Dockerfile COPY now reads build-output/shot-crafter-calculator-1.0.0-runner
- build-output/ is not in .dockerignore -> only the binary
  (~117MB) ships in the build context, not the whole target/
  tree (~200MB with classes, generated-sources, node binaries, etc.)
2026-08-12 19:44:14 -04:00
darroyo 888c3b95ee fix(ci): drop volumes block — act runner already mounts docker.sock
CI / Build Native (push) Failing after 8m34s
act (the runner the user is running) auto-mounts the docker
socket via volume `GITEA-ACTIONS-TASK-...-env Target:/var/run/act`.
Declaring volumes: in the workflow causes a duplicate mount
(once via the volumes block, once via act's auto-mount):

  Binds:[/var/run/docker.sock:/var/run/docker.sock
         /var/run/docker.sock:/var/run/docker.sock]

  failed to create container:
    'Error response from daemon: Duplicate mount point'

Drop the explicit volumes block. The socket is already there.
The docker CLI inside the container can talk to the host daemon
via the already-mounted socket.
2026-08-12 19:27:15 -04:00
darroyo 97dedd3424 fix(ci): install docker in container and mount host docker.sock
CI / Build Native (push) Failing after 0s
The container doesn't have docker, so docker build/docker push
fails with 'docker: not found'. Fix:

- Add docker.io to the apt-get install step (inside the container)
- Mount the host's docker.sock into the container via volumes:
  so the in-container docker CLI talks to the host's docker daemon
  (same model as Docker-in-Docker but using the host socket).

The runner has the docker label, so the host socket is available.
Now the whole pipeline (build native binary, build container image,
push to registry) runs in a single job without any artifact
upload/download between jobs.
2026-08-12 19:26:13 -04:00
darroyo fb26361f85 fix(ci): merge build-native and docker into one job to avoid 117MB artifact upload
CI / Build Native (push) Failing after 11m27s
The runner (runner-lxc-quarkus, using act) fails to upload the
117MB native binary via actions/upload-artifact@v4 with exit code 1
after 3 minutes. Avoid the artifact transfer entirely by collapsing
build-native and docker-native into a single job: build native
binary, then build the Docker image directly from the working tree
in the same container.

The container has the binary at target/shot-crafter-calculator-1.0.0-runner
and the Dockerfile expects it at the same path, so the build
chain works without any artifact download/upload.

Push only happens on push to main (not on PRs).
2026-08-12 19:04:48 -04:00
darroyo 08c0686aae fix(ci): add verify step before upload to debug missing binary
CI / Build Native (push) Failing after 11m6s
CI / Build & Push Native Image (push) Has been skipped
The upload-artifact step failed with exit code 1 but no details.
The most likely cause is that the binary file does not exist
at the expected path `target/shot-crafter-calculator-1.0.0-runner`
because the native build may have failed silently.

Add a 'Verify native binary' step that runs `ls -la target/` and
`file target/shot-crafter-calculator-1.0.0-runner` so the next
log shows exactly what's in target/ and what type the runtime
sees the binary as. Also make the upload's 'if-no-files-found'
explicit so the failure mode is unambiguous.
2026-08-12 18:45:49 -04:00
darroyo a7d3b39610 fix(ci): bump container memory to 8g and cap native-image xmx at 4g
CI / Build Native (push) Failing after 13m51s
CI / Build & Push Native Image (push) Has been skipped
Native-image was OOM-killed (exit 137) during inlining phase. The
runner has no implicit memory limit on container jobs, so native-image
probed all available memory and got killed by the host OOM killer.

Fix:
- Set container options: --memory=8g (hard cap for the build container)
- Pass -Dquarkus.native.native-image-xmx=4g to mvn (limits the
  native-image JVM heap to 4g, leaves 4g headroom for the rest
  of the toolchain: GraalVM, glibc, Maven, etc.)
2026-08-12 18:25:59 -04:00
darroyo 0b95c48975 fix(ci): install gcc and zlib headers for native-image compilation
CI / Build Native (push) Failing after 6m54s
CI / Build & Push Native Image (push) Has been skipped
GraalVM native-image requires a C compiler (gcc) and zlib dev
headers to compile native binaries. The maven:3.9.6-eclipse-temurin-21
image is JDK-only and lacks these build tools, causing the build
to fail with:

  Error: Default native-compiler executable 'gcc' not found via PATH

Extend the install step with build-essential (gcc, g++, make)
and zlib1g-dev so the native-image pipeline has the required
toolchain.
2026-08-12 18:16:44 -04:00
darroyo 7b89066ef3 fix(ci): gitignore was excluding src/frontend/src/data/defaults.ts
CI / Build Native (push) Failing after 3m18s
CI / Build & Push Native Image (push) Has been skipped
The .gitignore had 'data/' which matches any 'data' directory at
any depth. That excluded src/frontend/src/data/ from git, so the
file defaults.ts (with DEFAULT_INSUMOS, DEFAULT_FORMULAS, etc.)
was never committed. CI was failing with TS2307 because the file
was in the local working tree but not in the git checkout.

Tighten the rule to '/data/' so only the root runtime H2 database
folder is ignored, not the source data directory.
2026-08-12 17:57:23 -04:00
darroyo 355183c03f fix(ci): install node in container for actions/* post steps
CI / Build Native (push) Failing after 5m20s
CI / Build & Push Native Image (push) Has been skipped
Gitea Actions run actions as JavaScript via `node` inside the
job container. The maven:3.9.6-eclipse-temurin-21 image has no
node, so action post steps (e.g. Checkout cleanup) fail with
'exec: "node": executable file not found in $PATH'.

Extend the install step to add git AND node 22 via NodeSource
apt repo, so the runner can complete every action's pre/post step.
2026-08-12 17:20:42 -04:00
darroyo bcad46715e fix(ci): drop build-jvm job, install git inside build-native container
CI / Build Native (push) Failing after 49s
CI / Build & Push Native Image (push) Has been skipped
The runner lacks git even though it has Docker access. The
maven:3.9.6-eclipse-temurin-21 image is Ubuntu-based without
git installed, which broke actions/checkout.

- Remove build-jvm entirely (it was failing too, and the JMV
  artifact is not needed for the native pipeline)
- Add 'Install git and basic tools' step inside build-native
  (apt-get update + apt-get install -y git ca-certificates)
- docker-native job unchanged: still depends on build-native,
  builds the image with the pre-built native binary
2026-08-12 17:18:35 -04:00
darroyo 698e52eefc fix(ci): run build-jvm and build-native inside maven container image
CI / Build JVM (push) Failing after 16s
CI / Build Native (push) Failing after 51s
CI / Build & Push Native Image (push) Has been skipped
Runner has no Maven or JDK installed. Instead of installing
both manually each job, run the jobs inside the official
maven:3.9.6-eclipse-temurin-21 image which has both pre-installed.

- Drop actions/setup-java (image has JDK 21)
- Drop 'Install Maven' step (image has Maven 3.9.6)
- Add 'container: image: maven:3.9.6-eclipse-temurin-21' to each job
- Add cache for ~/.m2/repository (key on pom.xml hash)
- Keep graalvm/setup-graalvm only for the native job (adds GraalVM
  inside the container)
- docker-native job unchanged: uses the runner's docker daemon
  directly to build the image with the pre-built native binary
2026-08-12 17:04:33 -04:00
darroyo f3af0f8975 fix(ci): install Maven in each job (runner has no mvn binary)
CI / Build JVM (push) Failing after 2m56s
CI / Build Native (push) Failing after 21m20s
CI / Build & Push Native Image (push) Has been skipped
The runner has Java (via setup-java) but no Maven. Add an
Install Maven step to build-jvm and build-native that downloads
Maven 3.9.6 from Apache to runner.workspace/.maven and adds it
to the PATH via $GITHUB_PATH. Idempotent: the if-guard skips
redownload if the binary already exists on the runner.
2026-08-12 16:33:02 -04:00
darroyo f2c758eff1 fix(ci): drop container image pull, use setup-java + setup-graalvm
CI / Build Native (push) Failing after 3m48s
CI / Build & Push Native Image (push) Has been skipped
CI / Build JVM (push) Has been cancelled
The Quarkus image (docker.io/quarkusio/quarkus-images:tooling-21)
cannot be pulled by the runner (access denied). Replace the
container: block with the standard actions/setup-java (Temurin 21)
and graalvm/setup-graalvm (with native-image component) actions
so the jobs run on the host directly.
2026-08-12 16:26:49 -04:00
darroyo 14e892539a ci: rename registry secrets to REGISTRY_USERNAME/REGISTRY_TOKEN
CI / Build Native (push) Failing after 2s
CI / Build JVM (push) Failing after 1s
CI / Build & Push Native Image (push) Has been skipped
Aligns with the secret names configured in the Gitea repo
settings instead of the originally assumed GITEA_*/GITEA_* names.
2026-08-12 16:22:33 -04:00
darroyo 9db3f7a914 ci: build native Docker image and push to Gitea Container Registry
CI / Build Native (push) Failing after 2s
CI / Build JVM (push) Failing after 7s
CI / Build & Push Native Image (push) Has been skipped
Adds:
- Dockerfile based on quarkus-micro-image:2.0 (~50MB base)
  Runs the native binary as non-root user 1001, exposes 8080
- .dockerignore to exclude build artifacts
- Gitea Actions workflow with 3 parallel jobs:
  - build-jvm: standard JAR (mvn package)
  - build-native: GraalVM native binary (mvn package -Pnative)
  - docker-native: takes the native binary artifact, builds
    the container image and pushes to
    gitea.danielarroyo.cl/proyectos/shot-crafter-calculator
    with tags 'latest' and short SHA

Triggers: push to main and PRs (docker job only on push).
Required secrets: GITEA_USERNAME, GITEA_TOKEN (write:packages).
2026-08-12 16:16:51 -04:00
darroyo 47e24df40d chore: confirm working tree clean
All files committed in 9c9fb3a. Branch is up to date with origin/main.
- 57 files committed
- 6260 insertions
- Working tree clean
- 35/35 browser tests passing
2026-08-12 16:04:37 -04:00
darroyo 9c9fb3a2ca feat: shot-crafter-calculator with H2 persistence and production history
Quarkus 3.20.1 monolith serving React 18 + TypeScript + Tailwind SPA.

Features:
- Three-tab calculator (Insumos, Fórmulas, Calculadora) for
  Soulshot, Spiritshot and Blessed Spiritshot crafting in Lineage 2
  Interlude/Clásico with all 15 grades and pre-loaded recipes
- Real-time profitability computation (cristales → ore →
  crafteos → shots → cost → sale → ganancia)
- Multi-user auth with JWT in httpOnly cookie (bcrypt + RSA 2048)
- H2 file-based persistence in ./data/shots.mv.db (file-based, H2)
- Auto-save on state changes (debounced 500ms)
- Production history with stats (total/avg/best/worst/last5avg)
  and per-run detail modal with snapshot of insumos+formulas

Stack:
- Backend: Quarkus REST + Hibernate ORM Panache + smallrye-jwt
- Frontend: React 18 + TypeScript + Vite + Tailwind 3
- Build: Maven runs frontend-maven-plugin (Node 22 + npm ci)
  then copies dist to META-INF/resources for Quarkus to serve

Verified:
- 5 backend endpoints + 5 history endpoints with curl
- 35/35 browser tests via Playwright + Chromium
- All TS strict, all builds green
2026-08-12 16:03:51 -04:00