elpatron 58b9e0bb0a Harden app for production behind nginx Proxy Manager.
Remove path-based import, add rate limits and upload caps, security headers, proxy trust, bundled Chart.js, non-root Docker, and NPM deployment docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-19 16:23:27 +02:00

Idle Fantasy Save Viewer

A web viewer for backups of the Android game Idle Fantasy. Parses fantasyidler_save.json and displays skills, inventory, quests, and combat stats in a dark dashboard — with filters, item grouping, and history comparison via SQLite.

Features

  • Dashboard with character, coins, skills, inventory, equipment, quests, and combat
  • Inventory with text search, category filters, sorting, and grouped tables
  • SQLite history — import multiple backups, compare snapshots, coins/level charts
  • Import via CLI or browser upload
  • Multi-user without login — each player gets their own viewer via a secret link
  • Docker — ready to run behind nginx Proxy Manager
  • i18n — English as default/fallback, German optional; automatic browser language or manual selection in the sidebar

Requirements

  • Python 3.11+
  • An Idle Fantasy backup (fantasyidler_save.json from the in-game export)

Installation

python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt

Usage

Start server and import a backup

python app.py fantasyidler_save.json

The browser opens automatically at http://127.0.0.1:5000/v/local/.

Docker (host for other players)

Designed to run behind nginx Proxy Manager — the container port is not published publicly by default.

docker compose up -d --build
  1. Attach the viewer service to your NPM Docker network (see docker-compose.yml comments).
  2. In NPM: new Proxy Host → forward to viewer:5000, enable SSL.
  3. Open your public URL → Create my viewer
  4. Save the personal link (bookmark) — without the link, data cannot be recovered (no login)
  5. Import backups in the browser

Data is stored in the Docker volume viewer-data (/data/viewers/<id>.db).

# Logs
docker compose logs -f

# Stop
docker compose down

For local Docker testing without NPM, temporarily add to docker-compose.yml:

ports:
  - "5000:5000"

More options

# Import only, no server
python app.py --import backup2.json

# Different port, don't open browser
python app.py fantasyidler_save.json --port 8080 --no-browser

# Custom SQLite database (legacy single-file mode)
python app.py --db data\my_history.db fantasyidler_save.json

# Bind server for network/Docker
python app.py --host 0.0.0.0 --no-browser

Import backups in the browser

Sidebar at the bottom: Import backup — selects a .json file. Duplicates (same file hash) are skipped.

Multi-user (no login)

Each viewer has its own SQLite database at data/viewers/<viewer_id>.db.

Route Description
GET / Landing page — create a new viewer
POST /api/viewers Creates a viewer, returns { viewer_id, url }
GET /v/<viewer_id>/ Personal dashboard
GET /v/<viewer_id>/api/... API for this viewer

The viewer_id is a random URL-safe token. Anyone with the link has access — there is no password and no recovery if the link is lost.

Local CLI usage defaults to the local viewer (/v/local/). In Docker/production, /v/local/ is disabled (DISABLE_LOCAL_VIEWER=1).

Security

The app uses secret-link access (no accounts). Suitable for sharing with trusted players when deployed behind HTTPS.

Built-in protections

Measure Default (Docker)
Upload size limit 10 MB (MAX_UPLOAD_MB)
Viewer creation rate limit 5 / minute per IP
Import rate limit 20 / hour per IP
Path-based import Removed (upload only)
/v/local/ in production Disabled
Reverse-proxy headers TRUST_PROXY=1 (ProxyFix)
HTTPS links PREFERRED_URL_SCHEME=https
Security headers CSP, X-Frame-Options, nosniff, Referrer-Policy
Chart.js Bundled locally (no CDN)
Container user Non-root (appuser, uid 1000)

Environment variables

Variable Default Description
DATA_DIR ./data SQLite and upload storage
TRUST_PROXY 1 in Docker Trust X-Forwarded-* from nginx
PREFERRED_URL_SCHEME https in Docker Scheme for generated viewer URLs
DISABLE_LOCAL_VIEWER 1 in Docker Block predictable /v/local/
MAX_UPLOAD_MB 10 Max upload body size
RATE_LIMIT_VIEWER_CREATE 5 per minute Limit for POST /api/viewers
RATE_LIMIT_IMPORT 20 per hour Limit for POST .../import

nginx Proxy Manager

Recommended NPM settings:

  • SSL with Force SSL
  • Block Common Exploits enabled
  • Forward headers: X-Forwarded-For, X-Forwarded-Proto, X-Real-IP (default)
  • Optional Advanced config:
client_max_body_size 10m;

limit_req_zone $binary_remote_addr zone=viewer_create:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=viewer_api:10m rate=30r/s;

location /api/viewers {
    limit_req zone=viewer_create burst=2 nodelay;
    proxy_pass http://viewer:5000;
}

location / {
    limit_req zone=viewer_api burst=50 nodelay;
    proxy_pass http://viewer:5000;
}

Do not expose port 5000 publicly — only NPM should reach the container.

Language / i18n

  • Default: English (en) — also the fallback when a translation key is missing
  • Automatic: Sidebar → Language → Auto (browser) — uses navigator.language (de → German, otherwise English)
  • Manual: English or Deutsch — preference is stored in localStorage
  • Translation files: static/locales/en.json, static/locales/de.json
  • Import warnings from the server are coded in English (code + params); the UI translates them client-side

Project structure

idle-fantasy-viewer/
├── app.py              # Flask server and CLI
├── security.py         # Rate limits, headers, proxy trust
├── viewers.py          # Viewer IDs and isolation
├── parser.py           # Parse and normalize saves
├── categories.py       # Item categories (heuristics)
├── db.py               # SQLite snapshots, diff, timeline
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
├── static/
│   ├── vendor/           # chart.umd.min.js (bundled)
│   ├── i18n.js           # Locale loading, t(), en fallback
│   ├── locales/          # en.json, de.json
│   ├── landing.js        # Landing page
│   └── app.js            # Dashboard UI
├── templates/          # HTML
└── data/               # viewers/*.db (gitignored)

API

Endpoint Description
GET / Landing page
POST /api/viewers Create a new viewer (rate limited)
GET /v/<id>/api/snapshot/latest Latest save for the viewer
GET /v/<id>/api/snapshots All snapshots
GET /v/<id>/api/snapshots/<older>/diff/<newer> Compare two snapshots
GET /v/<id>/api/timeline Time series for charts
POST /v/<id>/api/import JSON file upload (.json only, rate limited)

Save format

The backup file contains doubly JSON-encoded fields (skillLevels, inventory, flags, …). The parser decodes these automatically.

Notes

  • data/viewers/ stores one SQLite file per player; do not commit to the repo (listed in .gitignore).
  • This viewer is an unofficial helper tool, not affiliated with the game.

Robustness for game updates

The game is actively developed — save files may contain new fields, items, or quest types. The viewer:

  • Parses tolerantly: unknown top-level fields are passed through in extensions and reported as info
  • Skips broken entries (e.g. individual quests/sessions) instead of aborting
  • Reports warnings for missing core fields, unreadable JSON in nested fields, or invalid numbers
  • Blocks import only for serious issues (invalid JSON file, empty object)

After import, errors and warnings appear as a banner in the dashboard; in the CLI on stderr.

License

Private project — use at your own risk.

S
Description
No description provided
Readme 1.5 MiB
Languages
Python 43%
JavaScript 40.5%
CSS 10%
HTML 4.5%
Shell 1.7%
Other 0.3%