Compare commits

...

15 Commits

Author SHA1 Message Date
b5d3a060f7 Deploy full import artifacts tree 2026-04-26 20:04:12 +02:00
995316c8ca Fix deploy remote script arguments 2026-04-26 19:53:47 +02:00
0608cd3d10 Reuse SSH session in deploy script 2026-04-26 19:51:31 +02:00
40925868bc vserver.env 2026-04-26 19:45:41 +02:00
7a3169d77e updated agents 2026-04-26 19:41:48 +02:00
61f11eed38 Document nginx vserver setup 2026-04-26 19:41:21 +02:00
26960a8a15 Add vserver Docker deploy script 2026-04-26 19:30:24 +02:00
4c3ed8a76c Persist app DB changes directly 2026-04-19 14:04:53 +02:00
adb61cbc13 Updated db 2026-04-19 13:59:32 +02:00
be0f7b728e Stabilize curation image height 2026-04-19 13:50:49 +02:00
0aac1bd734 Commit critical import artifacts 2026-04-19 13:38:52 +02:00
5fcfd3e381 Fix frontend theme coverage 2026-04-19 11:57:16 +02:00
b7f89b4e58 Added jb tool 2026-04-19 11:33:38 +02:00
531cb007bd linux agents 2026-04-19 11:33:11 +02:00
2c0568639c moved windows specific agents 2026-04-19 11:19:08 +02:00
2203 changed files with 239264 additions and 309 deletions

34
AGENTS.linux.md Normal file
View File

@@ -0,0 +1,34 @@
# Agent Guide
Also see the other related technical documentation in the docs folder.
## Tools
These tools are installed and available: Python3, MiKTeX, Tesseract, Playwright
## Rules
- Prefer extracting code to a shared helper to be reused instead of duplicating code. Always keep high maintainability standards.
- Always place each newly created class into its own file. The file name must match the class name.
- When asked to begin working on a task, create a detailed implementation plan first, present the plan to the user, and ask for approval before beginning with the actual implementation.
- Don't make assumptions in the plan. If necessary, ask all clarifying questions before presenting the final plan.
- When an task is finished, perform a code review to evaluate if the change is clean and maintainable with high software engineering standards. Iterate on the code and repeat the review process until satisfied.
- If there's documnentation present, always keep it updated.
- After every iteration, evaluate if the test coverage would fall below 100%, and write tests if necessary.
- After every iteration, run `dotnet jb cleanupcode --build=False '$file1' '$file2' ...` for every C# file you touched.
- After every frontend change, verify the results using an ephemeral Playwright run.
- For ad hoc verification in this repo, do not default to `npx playwright test` with a temp spec outside the repo.
- When browser verification needs the app running, launch the app against a temporary copy of `src\RolemasterDb.App\rolemaster.db` so verification does not mutate the canonical DB.
### Git
- Never change the .gitignore file without consent.
- Keep changes small with minimal churn and commit often. If one iteration encompasses many smaller tasks with more than one commit, create a git branch and do the commits there. Let me review the branch before merging it back to master.
- When multiple commits are necessary, pause after every commit and ask the user to give a command to proceed.
- After every iteration, do a git commit with a brief summary of the changes as a commit message.
- If you find unexpected changes in the code (deletions, changes, diff results that were not communicated), never revert them and never restore the old state. Assume that those changes happened with intent.
- Never use `git restore`, `git checkout --`, reset commands, or equivalent rollback actions to discard local changes unless the user explicitly asks for that exact rollback.
### Dotnet CLI
- If you need a separate output directory, use a subfolder under `artifacts`, and clean it up afterwards.
- Avoid running `dotnet build` and `dotnet test` in parallel in this repo; that can cause file-lock failures in `obj\Debug\net10.0`.

View File

@@ -1,55 +1 @@
# Agent Guide This is a linux environment, read `AGENTS.linux.md`.
Also see the other related technical documentation in the docs folder.
## Tools
These tool paths should be used instead of any entry in the PATH environment variable:
- Python is installed in `C:\Users\frank\AppData\Local\Programs\Python\Python314`.
- MiKTeX portable is installed in `D:\Code\miktex-portable\texmfs\install\miktex\bin\x64`.
- Tesseract is installed in `C:\Program Files\Sejda PDF Desktop\resources\vendor\tesseract-windows-x64`.
## Rules
- Prefer extracting code to a shared helper to be reused instead of duplicating code. Always keep high maintainability standards.
- Keep changes as small as possible, design solutions that achieve the goals with minimal churn.
- Always place each newly created class into its own file. The file name must match the class name.
- When asked to begin working on a task, create a detailed implementation plan first, present the plan to the user, and ask for approval before beginning with the actual implementation.
- Don't make assumptions in the plan. If necessary, ask all clarifying questions before presenting the final plan.
- When an task is finished, perform a code review to evaluate if the change is clean and maintainable with high software engineering standards. Iterate on the code and repeat the review process until satisfied.
- After the implementation is finished, verify all changed files, and run `python D:\Code\crlf.py $file1 $file2 ...` only for files you recognize, in order to normalize all line endings of all touched files to CRLF.
- If there's documnentation present, always keep it updated.
- After every iteration, evaluate if the test coverage would fall below 100%, and write tests if necessary.
- After every iteration, run `jb cleanupcode --build=False '$file1' '$file2' ...` for every file you touched.
- After every frontend change, verify the results using an ephemeral Playwright run.
- For ad hoc verification in this repo, do not default to `npx playwright test` with a temp spec outside the repo.
- Prefer a repo-local ephemeral Node script under `artifacts_verify/` that imports `playwright` with `require('playwright')` and drives the browser directly.
- If using the Playwright test runner, use the repo-local CLI at `node_modules\.bin\playwright.cmd` and keep the spec inside the repo so local `node_modules` resolution works.
- Do not mix the global Playwright CLI with the repo-local `@playwright/test` package.
- When browser verification needs the app running, launch the app against a temporary copy of `src\RolemasterDb.App\rolemaster.db` so verification does not mutate the canonical DB.
### Git
- Never change the .gitignore file without consent.
- Keep changes small and commit often. If one iteration encompasses many smaller tasks with more than one commit, create a git branch and do the commits there. Let me review the branch before merging it back to master.
- When multiple commits are necessary, pause after every commit and ask the user to give a command to proceed.
- After every iteration, do a git commit with a brief summary of the changes as a commit message.
- If you find unexpected changes in the code (deletions, changes, diff results that were not communicated), never revert them and never restore the old state. Assume that those changes happened with intent.
- Never use `git restore`, `git checkout --`, reset commands, or equivalent rollback actions to discard local changes unless the user explicitly asks for that exact rollback.
### PowerShell
- This is a Windows environment, WSL is not installed (i.e. sed is not available). You're running under PowerShell 7.5.5. Due to platform restrictions, file deletions are not possible. Replacing the entire file content via a context diff is a viable alternative.
- PowerShell doesn't support bash-style heredocs. If complex scripts need to be executed, consider using python as a last resort. Run Python code using python -c with inline commands instead of python - <<'PY'.
- Parallel PowerShell calls are flaky, stick to sequential reads and command execution.
- Commands like `rg` and `Get-Content` are always allowed.
### Dotnet CLI
- If a build fails with 0 errors / 0 warnings:
- Do not keep retrying the same build command
- Consider using --no-restore.
- Consider using `$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = '1'`
- Consider using `$env:NUGET_PACKAGES = Join-Path $env:USERPROFILE '.nuget\packages'`
- If you need a separate output directory, use a subfolder under `artifacts`, and clean it up afterwards.
- Avoid running `dotnet build` and `dotnet test` in parallel in this repo; that can cause file-lock failures in `obj\Debug\net10.0`.

55
AGENTS.windows.md Normal file
View File

@@ -0,0 +1,55 @@
# Agent Guide
Also see the other related technical documentation in the docs folder.
## Tools
These tool paths should be used instead of any entry in the PATH environment variable:
- Python is installed in `C:\Users\frank\AppData\Local\Programs\Python\Python314`.
- MiKTeX portable is installed in `D:\Code\miktex-portable\texmfs\install\miktex\bin\x64`.
- Tesseract is installed in `C:\Program Files\Sejda PDF Desktop\resources\vendor\tesseract-windows-x64`.
## Rules
- Prefer extracting code to a shared helper to be reused instead of duplicating code. Always keep high maintainability standards.
- Keep changes as small as possible, design solutions that achieve the goals with minimal churn.
- Always place each newly created class into its own file. The file name must match the class name.
- When asked to begin working on a task, create a detailed implementation plan first, present the plan to the user, and ask for approval before beginning with the actual implementation.
- Don't make assumptions in the plan. If necessary, ask all clarifying questions before presenting the final plan.
- When an task is finished, perform a code review to evaluate if the change is clean and maintainable with high software engineering standards. Iterate on the code and repeat the review process until satisfied.
- After the implementation is finished, verify all changed files, and run `python D:\Code\crlf.py $file1 $file2 ...` only for files you recognize, in order to normalize all line endings of all touched files to CRLF.
- If there's documnentation present, always keep it updated.
- After every iteration, evaluate if the test coverage would fall below 100%, and write tests if necessary.
- After every iteration, run `jb cleanupcode --build=False '$file1' '$file2' ...` for every C# file you touched.
- After every frontend change, verify the results using an ephemeral Playwright run.
- For ad hoc verification in this repo, do not default to `npx playwright test` with a temp spec outside the repo.
- Prefer a repo-local ephemeral Node script under `artifacts_verify/` that imports `playwright` with `require('playwright')` and drives the browser directly.
- If using the Playwright test runner, use the repo-local CLI at `node_modules\.bin\playwright.cmd` and keep the spec inside the repo so local `node_modules` resolution works.
- Do not mix the global Playwright CLI with the repo-local `@playwright/test` package.
- When browser verification needs the app running, launch the app against a temporary copy of `src\RolemasterDb.App\rolemaster.db` so verification does not mutate the canonical DB.
### Git
- Never change the .gitignore file without consent.
- Keep changes small and commit often. If one iteration encompasses many smaller tasks with more than one commit, create a git branch and do the commits there. Let me review the branch before merging it back to master.
- When multiple commits are necessary, pause after every commit and ask the user to give a command to proceed.
- After every iteration, do a git commit with a brief summary of the changes as a commit message.
- If you find unexpected changes in the code (deletions, changes, diff results that were not communicated), never revert them and never restore the old state. Assume that those changes happened with intent.
- Never use `git restore`, `git checkout --`, reset commands, or equivalent rollback actions to discard local changes unless the user explicitly asks for that exact rollback.
### PowerShell
- This is a Windows environment, WSL is not installed (i.e. sed is not available). You're running under PowerShell 7.5.5. Due to platform restrictions, file deletions are not possible. Replacing the entire file content via a context diff is a viable alternative.
- PowerShell doesn't support bash-style heredocs. If complex scripts need to be executed, consider using python as a last resort. Run Python code using python -c with inline commands instead of python - <<'PY'.
- Parallel PowerShell calls are flaky, stick to sequential reads and command execution.
- Commands like `rg` and `Get-Content` are always allowed.
### Dotnet CLI
- If a build fails with 0 errors / 0 warnings:
- Do not keep retrying the same build command
- Consider using --no-restore.
- Consider using `$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = '1'`
- Consider using `$env:NUGET_PACKAGES = Join-Path $env:USERPROFILE '.nuget\packages'`
- If you need a separate output directory, use a subfolder under `artifacts`, and clean it up afterwards.
- Avoid running `dotnet build` and `dotnet test` in parallel in this repo; that can cause file-lock failures in `obj\Debug\net10.0`.

14
deploy/vserver.Dockerfile Normal file
View File

@@ -0,0 +1,14 @@
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
ENV ASPNETCORE_URLS=http://+:8080
ENV DOTNET_EnableDiagnostics=0
EXPOSE 8080
COPY publish/ ./
RUN mkdir -p /app/data
ENTRYPOINT ["dotnet", "RolemasterDb.App.dll"]

28
deploy/vserver.env Normal file
View File

@@ -0,0 +1,28 @@
# SSH host or alias from ~/.ssh/config.
# This can also be a literal user@host value, for example root@myvserver.
REMOTE_HOST=root@myvserver
# Remote filesystem layout.
REMOTE_APP_DIR=/root/docker/rolemasterdb
REMOTE_DATA_DIR=/root/docker/rolemasterdb/data
# Docker names on the remote host.
IMAGE_NAME=rolemasterdb
CONTAINER_NAME=rolemasterdb
# Bind the container only on loopback so nginx is the public entrypoint.
HOST_BIND_ADDRESS=127.0.0.1
# Port mapping: host:container.
HOST_PORT=8080
CONTAINER_PORT=8080
# Set to 1 if docker on the server must be run through sudo.
REMOTE_USE_SUDO=0
# Optional env file that already exists on the server and should be passed
# through to docker run with --env-file.
REMOTE_ENV_FILE=
# Prefix used for the generated local tarball name.
RELEASE_PREFIX=rolemasterdb

View File

@@ -0,0 +1,28 @@
# SSH host or alias from ~/.ssh/config.
# This can also be a literal user@host value, for example root@myvserver.
REMOTE_HOST=root@myvserver
# Remote filesystem layout.
REMOTE_APP_DIR=/root/docker/rolemasterdb
REMOTE_DATA_DIR=/root/docker/rolemasterdb/data
# Docker names on the remote host.
IMAGE_NAME=rolemasterdb
CONTAINER_NAME=rolemasterdb
# Bind the container only on loopback so nginx is the public entrypoint.
HOST_BIND_ADDRESS=127.0.0.1
# Port mapping: host:container.
HOST_PORT=8080
CONTAINER_PORT=8080
# Set to 1 if docker on the server must be run through sudo.
REMOTE_USE_SUDO=0
# Optional env file that already exists on the server and should be passed
# through to docker run with --env-file.
REMOTE_ENV_FILE=
# Prefix used for the generated local tarball name.
RELEASE_PREFIX=rolemasterdb

View File

@@ -13,6 +13,8 @@ The tool is intentionally separate from the web application startup path. Critic
The tool currently lives in `src/RolemasterDb.ImportTool` and operates against the same SQLite schema used by the web app. The tool currently lives in `src/RolemasterDb.ImportTool` and operates against the same SQLite schema used by the web app.
For the committed app database workflow, the web app forces SQLite `journal_mode=DELETE` during startup so local curation changes land in `src/RolemasterDb.App/rolemaster.db` directly instead of accumulating in a long-lived WAL sidecar.
## Goals ## Goals
The importer is designed around the following requirements: The importer is designed around the following requirements:
@@ -70,7 +72,7 @@ The current implementation supports:
The current implementation does not yet support: The current implementation does not yet support:
- OCR/image-based PDFs such as `Void.pdf` - full OCR bootstrap on machines where Tesseract language data is unavailable
- automatic confidence scoring beyond validation errors - automatic confidence scoring beyond validation errors
## High-Level Architecture ## High-Level Architecture
@@ -381,15 +383,18 @@ Example:
dotnet run --project .\src\RolemasterDb.ImportTool\RolemasterDb.ImportTool.csproj -- import slash dotnet run --project .\src\RolemasterDb.ImportTool\RolemasterDb.ImportTool.csproj -- import slash
``` ```
### `reimport-images <table>` ### `reimport-images [table]`
Reuses `source.xml`, regenerates page PNGs and cell PNGs, rewrites the JSON artifacts, and refreshes only source-image metadata in SQLite. Reuses the extracted source artifact, regenerates page PNGs and cell PNGs, rewrites the JSON artifacts, and optionally refreshes source-image metadata in SQLite.
For OCR-backed tables, `--update-metadata false` also enables a database-provenance fallback: if the OCR source artifact cannot be regenerated in the current environment, the command can still repopulate the committed page and cell PNGs from the existing `SourceImagePath` and `SourceImageCropJson` values stored in SQLite.
Use this when: Use this when:
- crop resolution or render settings changed - crop resolution or render settings changed
- you want better source images without reloading result text - you want better source images without reloading result text
- you want to keep curated and uncurated content untouched while refreshing artifacts - you want to keep curated and uncurated content untouched while refreshing artifacts
- you need to repopulate the committed artifact tree without touching the database
Example: Example:
@@ -397,6 +402,19 @@ Example:
dotnet run --project .\src\RolemasterDb.ImportTool\RolemasterDb.ImportTool.csproj -- reimport-images slash dotnet run --project .\src\RolemasterDb.ImportTool\RolemasterDb.ImportTool.csproj -- reimport-images slash
``` ```
Refresh all enabled manifest entries without modifying SQLite metadata:
```powershell
dotnet run --project .\src\RolemasterDb.ImportTool\RolemasterDb.ImportTool.csproj -- reimport-images --all --update-metadata false
```
Command options:
- pass a table slug to refresh one enabled manifest entry
- pass `--all` to refresh every enabled manifest entry
- `--update-metadata` defaults to `true`; set it to `false` when you only want to regenerate committed artifacts
- when `--update-metadata false` is used, OCR tables may fall back to existing SQLite source-image metadata to regenerate page and cell PNGs without changing the database
## Manifest ## Manifest
The importer manifest is stored at: The importer manifest is stored at:
@@ -425,7 +443,7 @@ For the currently enabled entries:
Artifacts are written under: Artifacts are written under:
- `artifacts/import/critical/<slug>/` - `src/RolemasterDb.App/import-artifacts/critical/<slug>/`
The current artifact set is: The current artifact set is:

View File

@@ -0,0 +1,182 @@
# Docker Deployment To A Linux VServer
This repo now includes a deployment script for shipping `RolemasterDb.App` to a remote Linux server that you reach as `root`, for example with:
```bash
ssh myvserver
```
The intended public URL is:
```text
https://rolemasterdb.franktovar.de
```
The script publishes the app locally, uploads a release bundle over `scp`, builds the Docker image on the server, and replaces the running container. Nginx remains a one-time manual setup on the host and proxies to the container over localhost.
## Files
- `scripts/deploy-vserver.sh`
- `deploy/vserver.Dockerfile`
- `deploy/vserver.env.example`
## Remote Prerequisites
On the vserver:
1. Install Docker.
2. Install nginx.
3. Install a certificate tool such as `certbot` plus the nginx plugin if you want automated certificate provisioning.
4. Make sure `rolemasterdb.franktovar.de` resolves to this server. Your wildcard `A` and `AAAA` records already satisfy that.
The deploy script stores releases under `REMOTE_APP_DIR/releases/<timestamp>` and keeps the SQLite database in `REMOTE_DATA_DIR/rolemaster.db`.
The container should stay behind nginx, so the default deploy config binds it to `127.0.0.1:8080`.
## Local Setup
Create a local deploy config from the example:
```bash
cp deploy/vserver.env.example deploy/vserver.env
```
Adjust the values in `deploy/vserver.env` as needed:
- `REMOTE_HOST`: your SSH host or alias, for example `root@myvserver`
- `REMOTE_APP_DIR`: base directory on the server, default `/root/docker/rolemasterdb`
- `REMOTE_DATA_DIR`: persistent directory for the SQLite database
- `HOST_BIND_ADDRESS`: default `127.0.0.1` so only nginx can reach the container
- `HOST_PORT`: nginx upstream port on the host, default `8080`
- `REMOTE_USE_SUDO`: keep `0` when you deploy as `root`
- `REMOTE_ENV_FILE`: optional existing env file on the server to pass through to `docker run`
## One-Time Server Setup
Create the base directories on the server:
```bash
ssh myvserver
mkdir -p /root/docker/rolemasterdb/data
```
Deploy once so the container exists:
```bash
./scripts/deploy-vserver.sh
```
Create an nginx site config at `/etc/nginx/sites-available/rolemasterdb.franktovar.de`:
```nginx
server {
listen 80;
listen [::]:80;
server_name rolemasterdb.franktovar.de;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300;
}
}
```
Enable it and reload nginx:
```bash
ln -s /etc/nginx/sites-available/rolemasterdb.franktovar.de /etc/nginx/sites-enabled/rolemasterdb.franktovar.de
nginx -t
systemctl reload nginx
```
At this point `http://rolemasterdb.franktovar.de` should work.
## HTTPS Setup
If `certbot` and the nginx plugin are installed, request the certificate:
```bash
certbot --nginx -d rolemasterdb.franktovar.de
```
That should update the nginx config to serve HTTPS and normally also install an HTTP-to-HTTPS redirect. Verify the generated config and then reload nginx if needed:
```bash
nginx -t
systemctl reload nginx
```
After that, `https://rolemasterdb.franktovar.de` should terminate TLS in nginx and proxy traffic to `127.0.0.1:8080`.
## Deploy
Run:
```bash
./scripts/deploy-vserver.sh
```
Or point at a different config file:
```bash
./scripts/deploy-vserver.sh /path/to/custom.env
```
## What The Script Does
1. Publishes `src/RolemasterDb.App/RolemasterDb.App.csproj` in `Release`.
2. Precreates `publish/wwwroot/components/layout` and `publish/wwwroot/components/shared` before publish.
This is required because the current project otherwise fails to publish two JavaScript static assets.
3. Replaces `publish/import-artifacts` with a full copy of `src/RolemasterDb.App/import-artifacts`.
This is required because the current publish output does not reliably include deep artifact folders such as `cells/` and `pages/`.
4. Bundles the publish output, `src/RolemasterDb.App/rolemaster.db`, and the runtime Dockerfile into `artifacts/deploy/<release-id>/`.
5. Uploads the bundle to the vserver.
6. Extracts the bundle on the vserver.
7. Seeds `REMOTE_DATA_DIR/rolemaster.db` on first deploy only.
8. Builds the Docker image on the vserver.
9. Recreates the container with:
```text
--restart unless-stopped
-p HOST_BIND_ADDRESS:HOST_PORT:CONTAINER_PORT
-v REMOTE_DATA_DIR:/app/data
ConnectionStrings__RolemasterDb=Data Source=/app/data/rolemaster.db
```
## Updating The App Later
Running the same deploy command again will:
- publish a fresh release bundle
- upload a new timestamped release
- keep the existing database in `REMOTE_DATA_DIR`
- keep the complete imported artifact tree inside the image for that release
- rebuild the image and restart the container
- leave the nginx config and TLS setup untouched
## Useful Remote Commands
Check the running container:
```bash
ssh myvserver docker ps
```
Follow logs:
```bash
ssh myvserver docker logs -f rolemasterdb
```
Check nginx:
```bash
ssh myvserver nginx -t
ssh myvserver systemctl status nginx
```

13
dotnet-tools.json Normal file
View File

@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"jetbrains.resharper.globaltools": {
"version": "2026.1.0.1",
"commands": [
"jb"
],
"rollForward": false
}
}
}

212
scripts/deploy-vserver.sh Executable file
View File

@@ -0,0 +1,212 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "$script_dir/.." && pwd)"
ssh_control_path=""
cleanup() {
local exit_code=$?
if [[ -n "$ssh_control_path" ]] && ssh -o ControlPath="$ssh_control_path" -O exit "$REMOTE_HOST" >/dev/null 2>&1; then
:
fi
exit "$exit_code"
}
trap cleanup EXIT
if [[ $# -gt 1 ]]; then
echo "Usage: $0 [deploy-config-file]" >&2
exit 1
fi
config_path="${1:-$repo_root/deploy/vserver.env}"
if [[ ! -f "$config_path" ]]; then
echo "Missing deploy config: $config_path" >&2
echo "Copy deploy/vserver.env.example to deploy/vserver.env and adjust it first." >&2
exit 1
fi
set -a
source "$config_path"
set +a
REMOTE_HOST="${REMOTE_HOST:-myvserver}"
REMOTE_APP_DIR="${REMOTE_APP_DIR:-/opt/rolemasterdb}"
REMOTE_DATA_DIR="${REMOTE_DATA_DIR:-$REMOTE_APP_DIR/data}"
IMAGE_NAME="${IMAGE_NAME:-rolemasterdb}"
CONTAINER_NAME="${CONTAINER_NAME:-rolemasterdb}"
HOST_BIND_ADDRESS="${HOST_BIND_ADDRESS:-}"
HOST_PORT="${HOST_PORT:-8080}"
CONTAINER_PORT="${CONTAINER_PORT:-8080}"
REMOTE_USE_SUDO="${REMOTE_USE_SUDO:-0}"
REMOTE_ENV_FILE="${REMOTE_ENV_FILE:-}"
RELEASE_PREFIX="${RELEASE_PREFIX:-rolemasterdb}"
release_id="$(date -u +%Y%m%d%H%M%S)"
artifact_root="$repo_root/artifacts/deploy/$release_id"
stage_dir="$artifact_root/stage"
tarball_path="$artifact_root/${RELEASE_PREFIX}-${release_id}.tar.gz"
publish_dir="$repo_root/src/RolemasterDb.App/bin/Release/net10.0/publish"
seed_db_path="$repo_root/src/RolemasterDb.App/rolemaster.db"
import_artifacts_source_dir="$repo_root/src/RolemasterDb.App/import-artifacts"
dockerfile_path="$repo_root/deploy/vserver.Dockerfile"
project_path="$repo_root/src/RolemasterDb.App/RolemasterDb.App.csproj"
remote_release_dir="$REMOTE_APP_DIR/releases/$release_id"
remote_tarball_path="$remote_release_dir/release.tar.gz"
ssh_control_path="/tmp/rolemasterdb-deploy-${USER:-$(id -un)}-$(date -u +%Y%m%d%H%M%S)-$$.sock"
if [[ ! -f "$seed_db_path" ]]; then
echo "Seed database not found: $seed_db_path" >&2
exit 1
fi
if [[ ! -f "$dockerfile_path" ]]; then
echo "Dockerfile not found: $dockerfile_path" >&2
exit 1
fi
if [[ ! -d "$import_artifacts_source_dir" ]]; then
echo "Import artifacts directory not found: $import_artifacts_source_dir" >&2
exit 1
fi
mkdir -p "$artifact_root"
echo "Opening shared SSH connection to $REMOTE_HOST..."
ssh \
-o ControlMaster=yes \
-o ControlPersist=10m \
-o ControlPath="$ssh_control_path" \
-Nf \
"$REMOTE_HOST"
echo "Publishing RolemasterDb.App..."
rm -rf "$publish_dir"
mkdir -p \
"$publish_dir/wwwroot/components/layout" \
"$publish_dir/wwwroot/components/shared"
env DOTNET_CLI_HOME="${DOTNET_CLI_HOME:-/tmp}" \
dotnet publish "$project_path" -c Release
echo "Copying full import-artifacts tree into publish output..."
rm -rf "$publish_dir/import-artifacts"
cp -a "$import_artifacts_source_dir" "$publish_dir/import-artifacts"
echo "Preparing deploy bundle..."
rm -rf "$stage_dir"
mkdir -p "$stage_dir/publish" "$stage_dir/seed"
cp -a "$publish_dir/." "$stage_dir/publish/"
cp "$seed_db_path" "$stage_dir/seed/rolemaster.db"
cp "$dockerfile_path" "$stage_dir/Dockerfile"
tar -C "$stage_dir" -czf "$tarball_path" .
echo "Creating remote release directory..."
ssh \
-o ControlPath="$ssh_control_path" \
"$REMOTE_HOST" bash -s -- "$remote_release_dir" "$REMOTE_DATA_DIR" <<'EOF'
set -euo pipefail
release_dir="$1"
data_dir="$2"
mkdir -p "$release_dir" "$data_dir"
EOF
echo "Uploading bundle to $REMOTE_HOST..."
scp \
-o ControlPath="$ssh_control_path" \
"$tarball_path" "$REMOTE_HOST:$remote_tarball_path"
echo "Building image and restarting container on $REMOTE_HOST..."
remote_env_file_arg="${REMOTE_ENV_FILE:-__ROLEMASTERDB_EMPTY__}"
ssh \
-o ControlPath="$ssh_control_path" \
"$REMOTE_HOST" bash -s -- \
"$release_id" \
"$REMOTE_APP_DIR" \
"$REMOTE_DATA_DIR" \
"$IMAGE_NAME" \
"$CONTAINER_NAME" \
"$HOST_BIND_ADDRESS" \
"$HOST_PORT" \
"$CONTAINER_PORT" \
"$REMOTE_USE_SUDO" \
"$remote_env_file_arg" <<'EOF'
set -euo pipefail
release_id="$1"
remote_app_dir="$2"
remote_data_dir="$3"
image_name="$4"
container_name="$5"
host_bind_address="$6"
host_port="$7"
container_port="$8"
remote_use_sudo="$9"
remote_env_file="${10}"
if [[ "$remote_env_file" == "__ROLEMASTERDB_EMPTY__" ]]; then
remote_env_file=""
fi
release_dir="$remote_app_dir/releases/$release_id"
tarball_path="$release_dir/release.tar.gz"
docker_cmd=(docker)
if [[ "$remote_use_sudo" == "1" ]]; then
docker_cmd=(sudo docker)
fi
tar -xzf "$tarball_path" -C "$release_dir"
rm -f "$tarball_path"
if [[ ! -f "$remote_data_dir/rolemaster.db" ]]; then
cp "$release_dir/seed/rolemaster.db" "$remote_data_dir/rolemaster.db"
fi
"${docker_cmd[@]}" build \
-t "$image_name:$release_id" \
-t "$image_name:latest" \
"$release_dir"
"${docker_cmd[@]}" rm -f "$container_name" >/dev/null 2>&1 || true
docker_run_args=(
run
-d
--name "$container_name"
--restart unless-stopped
-e "ASPNETCORE_ENVIRONMENT=Production"
-e "ASPNETCORE_URLS=http://+:$container_port"
-e "ConnectionStrings__RolemasterDb=Data Source=/app/data/rolemaster.db"
-v "$remote_data_dir:/app/data"
)
if [[ -n "$host_bind_address" ]]; then
docker_run_args+=(-p "$host_bind_address:$host_port:$container_port")
else
docker_run_args+=(-p "$host_port:$container_port")
fi
if [[ -n "$remote_env_file" ]]; then
docker_run_args+=(--env-file "$remote_env_file")
fi
docker_run_args+=("$image_name:$release_id")
"${docker_cmd[@]}" "${docker_run_args[@]}"
ln -sfn "$release_dir" "$remote_app_dir/current"
echo "Deployment complete."
echo "Release: $release_id"
echo "Container: $container_name"
if [[ -n "$host_bind_address" ]]; then
echo "Published at: $host_bind_address:$host_port -> $container_port"
else
echo "Published at: $host_port -> $container_port"
fi
EOF
echo "Local bundle ready at $tarball_path"

View File

@@ -1,9 +1,8 @@
#blazor-error-ui { #blazor-error-ui {
color-scheme: light only; background: color-mix(in srgb, var(--surface-danger-banner) 88%, var(--bg-elevated));
background: #682e24; color: var(--text-on-danger);
color: #fffaf2;
bottom: 0; bottom: 0;
box-shadow: 0 -1px 12px rgba(0, 0, 0, 0.22); box-shadow: 0 -1px 12px color-mix(in srgb, var(--shadow-strong) 72%, transparent);
box-sizing: border-box; box-sizing: border-box;
display: none; display: none;
left: 0; left: 0;
@@ -14,6 +13,7 @@
} }
#blazor-error-ui .dismiss { #blazor-error-ui .dismiss {
color: inherit;
cursor: pointer; cursor: pointer;
position: absolute; position: absolute;
right: 0.75rem; right: 0.75rem;

View File

@@ -21,13 +21,14 @@
#components-reconnect-modal { #components-reconnect-modal {
background-color: white; background-color: color-mix(in srgb, var(--surface-card-strong) 96%, var(--bg-elevated));
color: var(--text-primary);
width: 20rem; width: 20rem;
margin: 20vh auto; margin: 20vh auto;
padding: 2rem; padding: 2rem;
border: 0; border: 1px solid var(--border-default);
border-radius: 0.5rem; border-radius: 0.5rem;
box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3); box-shadow: var(--shadow-2);
opacity: 0; opacity: 0;
transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete; transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
animation: components-reconnect-modal-fadeOutOpacity 0.5s both; animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
@@ -41,7 +42,7 @@
} }
#components-reconnect-modal::backdrop { #components-reconnect-modal::backdrop {
background-color: rgba(0, 0, 0, 0.4); background-color: var(--bg-overlay);
animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out; animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out;
opacity: 1; opacity: 1;
} }
@@ -90,18 +91,18 @@
#components-reconnect-modal button { #components-reconnect-modal button {
border: 0; border: 0;
background-color: #6b9ed2; background: linear-gradient(135deg, var(--accent-4), var(--accent-5));
color: white; color: var(--text-on-accent);
padding: 4px 24px; padding: 4px 24px;
border-radius: 4px; border-radius: 4px;
} }
#components-reconnect-modal button:hover { #components-reconnect-modal button:hover {
background-color: #3b6ea2; background: linear-gradient(135deg, color-mix(in srgb, var(--accent-4) 88%, var(--accent-2)), var(--accent-5));
} }
#components-reconnect-modal button:active { #components-reconnect-modal button:active {
background-color: #6b9ed2; background: linear-gradient(135deg, var(--accent-4), var(--accent-5));
} }
.components-rejoining-animation { .components-rejoining-animation {
@@ -112,7 +113,7 @@
.components-rejoining-animation div { .components-rejoining-animation div {
position: absolute; position: absolute;
border: 3px solid #0087ff; border: 3px solid var(--info-5);
opacity: 1; opacity: 1;
border-radius: 50%; border-radius: 50%;
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite; animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;

View File

@@ -74,7 +74,7 @@
font-size: 0.85rem; font-size: 0.85rem;
letter-spacing: 0.12em; letter-spacing: 0.12em;
text-transform: uppercase; text-transform: uppercase;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22); box-shadow: inset 0 1px 0 color-mix(in srgb, var(--text-on-accent) 22%, transparent);
} }
.app-shell-brand-copy { .app-shell-brand-copy {

View File

@@ -33,7 +33,7 @@
color: var(--text-primary); color: var(--text-primary);
background: color-mix(in srgb, var(--accent-1) 84%, var(--surface-2)); background: color-mix(in srgb, var(--accent-1) 84%, var(--surface-2));
border-color: color-mix(in srgb, var(--accent-3) 45%, var(--border-default)); border-color: color-mix(in srgb, var(--accent-3) 45%, var(--border-default));
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.16); box-shadow: inset 0 1px 0 color-mix(in srgb, var(--text-on-accent) 16%, transparent);
} }
.shell-primary-nav-link.is-tools-link { .shell-primary-nav-link.is-tools-link {

View File

@@ -11,6 +11,7 @@ public static class RolemasterDbInitializer
await using var dbContext = await dbFactory.CreateDbContextAsync(cancellationToken); await using var dbContext = await dbFactory.CreateDbContextAsync(cancellationToken);
await dbContext.Database.EnsureCreatedAsync(cancellationToken); await dbContext.Database.EnsureCreatedAsync(cancellationToken);
await EnsureDeleteJournalModeAsync(dbContext, cancellationToken);
await RolemasterDbSchemaUpgrader.EnsureLatestAsync(dbContext, cancellationToken); await RolemasterDbSchemaUpgrader.EnsureLatestAsync(dbContext, cancellationToken);
RolemasterSeedData.BackfillAttackTableMetadata(dbContext); RolemasterSeedData.BackfillAttackTableMetadata(dbContext);
@@ -23,4 +24,18 @@ public static class RolemasterDbInitializer
RolemasterSeedData.SeedAttackStarterData(dbContext); RolemasterSeedData.SeedAttackStarterData(dbContext);
await dbContext.SaveChangesAsync(cancellationToken); await dbContext.SaveChangesAsync(cancellationToken);
} }
}
private static async Task EnsureDeleteJournalModeAsync(RolemasterDbContext dbContext,
CancellationToken cancellationToken)
{
await dbContext.Database.OpenConnectionAsync(cancellationToken);
await using var command = dbContext.Database.GetDbConnection().CreateCommand();
command.CommandText = "PRAGMA journal_mode=DELETE;";
var result = await command.ExecuteScalarAsync(cancellationToken);
if (!string.Equals(Convert.ToString(result), "delete", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException("Failed to configure SQLite journal_mode=DELETE.");
}
}
}

View File

@@ -33,12 +33,12 @@ public sealed class CriticalImportArtifactLocator
{ {
if (File.Exists(Path.Combine(probe.FullName, "RolemasterDB.slnx"))) if (File.Exists(Path.Combine(probe.FullName, "RolemasterDB.slnx")))
{ {
return Path.Combine(probe.FullName, "artifacts", "import", "critical"); return Path.Combine(probe.FullName, "src", "RolemasterDb.App", "import-artifacts", "critical");
} }
probe = probe.Parent; probe = probe.Parent;
} }
return Path.Combine(contentRootPath, "artifacts", "import", "critical"); return Path.Combine(contentRootPath, "import-artifacts", "critical");
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Some files were not shown because too many files have changed in this diff Show More