deviant/admin
Local Dev b220d23a2d admin: pin dashboard <base> so deep links load CSS/JS
/apps/deviant-admin/errors/<id> was resolving ./app.css against the
full URL (→ /apps/deviant-admin/errors/app.css → 404), landing on
unstyled text. Inject a dynamic <base href> at the top of <head> that
rewinds the path to the directory above /errors/<id> or the current
dir — base stays correct regardless of mount or deep-link path.
2026-11-03 16:42:16 +02:00
..
public admin: pin dashboard <base> so deep links load CSS/JS 2026-11-03 16:42:16 +02:00
BRIEFING.md admin: ship MVP for mobile wallet error inbox 2026-11-03 16:39:01 +02:00
deviant-admin.service admin: ship MVP for mobile wallet error inbox 2026-11-03 16:39:01 +02:00
nginx-snippet.conf admin: ship MVP for mobile wallet error inbox 2026-11-03 16:39:01 +02:00
README.md admin: ship MVP for mobile wallet error inbox 2026-11-03 16:39:01 +02:00
server.js admin: ship MVP for mobile wallet error inbox 2026-11-03 16:39:01 +02:00

admin.deviant.x — error inbox

Receiver for mobile-wallet error reports + a small dashboard to browse them.

Layout

admin/
├── server.js              # Node service — ingest + admin API + static dashboard
├── deviant-admin.service  # systemd unit
├── nginx-snippet.conf     # two location blocks for silentmode-st
├── README.md              # this file
└── public/                # dashboard shell (vanilla HTML + CSS + JS)
    ├── index.html
    ├── app.css
    └── app.js

No build step. No npm deps. Node ≥ 20 (uses global fetch + URL).

Endpoints

URL on silentmode.st Behind nginx Auth
POST /apps/deviant/errors → Node /ingest none
GET /apps/deviant-admin/ → Node / (static dashboard) HTTP basic
GET /apps/deviant-admin/api/errors?… → Node /api/errors HTTP basic
GET /apps/deviant-admin/api/errors/<id> → Node /api/errors/<id> HTTP basic
GET /apps/deviant-admin/errors/<id> → Node /errors/<id> (SPA deep link) HTTP basic

Storage

One JSON file per report:

/opt/silent-mode/data/deviant/errors/<id>.json

Where <id> matches [A-Za-z0-9_-]{1,64} (sanitised server-side). The mobile client sends IDs shaped DEV-YYYYMMDD-HHMMSS-<4hex> which already pass.

The receiver atomically writes <id>.json.tmp-<uuid> → rename() to avoid half-written files from racing scans in the dashboard.

Rate limit

In-memory sliding window: 60 req / 60 s per X-Forwarded-For IP. Over → 429 with Retry-After: 60. Mobile client retries from its offline queue on the next online event — no need to be generous.

Install (first time)

# 1. Create data + install dirs on the VPS.
ssh silentmode '
  mkdir -p /opt/silent-mode/apps/deviant-admin/public
  mkdir -p /opt/silent-mode/data/deviant/errors
  chown -R www-data:www-data /opt/silent-mode/data/deviant
'

# 2. Ship the service + dashboard.
scp admin/server.js silentmode:/opt/silent-mode/apps/deviant-admin/server.js
scp admin/public/*  silentmode:/opt/silent-mode/apps/deviant-admin/public/

# 3. Install the systemd unit + start it.
scp admin/deviant-admin.service silentmode:/etc/systemd/system/deviant-admin.service
ssh silentmode '
  systemctl daemon-reload
  systemctl enable --now deviant-admin
  systemctl status deviant-admin --no-pager | head -20
'

# 4. Add nginx blocks (merge into /etc/nginx/sites-enabled/silentmode-st).
#    See admin/nginx-snippet.conf for the two location blocks.
ssh silentmode 'nginx -t && systemctl reload nginx'

# 5. Set up basic auth.
ssh silentmode '
  apt-get install -y apache2-utils  # once, if htpasswd not installed
  htpasswd -c /etc/nginx/.htpasswd-deviant-admin admin
  chown root:www-data /etc/nginx/.htpasswd-deviant-admin
  chmod 640 /etc/nginx/.htpasswd-deviant-admin
'

Deploy (subsequent)

scp admin/server.js silentmode:/opt/silent-mode/apps/deviant-admin/server.js
scp admin/public/*  silentmode:/opt/silent-mode/apps/deviant-admin/public/
ssh silentmode 'systemctl restart deviant-admin'

Dashboard changes (public/* only) don't need a restart; the service reads them on each request.

Smoke test

# Receiver — expect 201.
curl -sS -X POST https://silentmode.st/apps/deviant/errors \
  -H "Content-Type: application/json" \
  -d '{"id":"DEV-20261103-000000-test","ts":"2026-11-03T00:00:00.000Z","version":"0.0.0","scope":"test","message":"smoke","name":"Error","stack":"at smoke","ua":"curl","extra":{}}'

# Admin API — expect {ok:true, total: ≥ 1, rows: [...]}
curl -sS -u admin:<pw> https://silentmode.st/apps/deviant-admin/api/errors

# Dashboard in a browser:
#   https://silentmode.st/apps/deviant-admin/
#   → prompts for basic auth → list of reports → click a row → drawer with payload

BNS

admin.deviant.x resolves via a u-redirect BNS record:

{"u":"silentmode.st/apps/deviant-admin/"}

Published with the Argus update.js tool once the receiver + dashboard verify.

Field policy

Enforced client-side (shells/app/src/errorReport.ts writes extra explicitly — no auto-scraped globals). Reinforced here by the dashboard: raw payload is only shown under click-to-expand sections. No private keys, mnemonics, PINs, cookies or session fingerprints. Addresses, categories, amounts, URLs are fair game; sourceIp is kept for abuse debugging only.