Pass the viewer id via a body data attribute instead of a blocked inline script so personal links still load saved data after restart. Co-authored-by: Cursor <cursoragent@cursor.com>
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.jsonfrom 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
- Attach the
viewerservice to your NPM Docker network (seedocker-compose.ymlcomments). - In NPM: new Proxy Host → forward to
viewer:5000, enable SSL. - Open your public URL → Create my viewer
- Save the personal link (bookmark) — without the link, data cannot be recovered (no login)
- 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
extensionsand 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.