Files
EpicNext-Cms/.env.example
T
openhands 203399aab7
Gitea Actions Runner Test / test-job (push) Successful in 0s
CI / check (push) Successful in 32s
CI / tests-integration (push) Successful in 1m38s
CI / tests-unit (push) Successful in 1m42s
CI / tests-ui (push) Successful in 2m33s
CI / preflight (push) Skipped
CI / deploy (push) Successful in 2m43s
fix(cache): true LRU, stale-while-revalidate and cross-process invalidation
The in-process cache was a FIFO of 500 entries that was never touched on a
read, so a key polled on every request could be evicted by an unrelated burst
of dynamic keys. That looked exactly like the cache being cleared at random,
and it is what made the site fall back to the database unpredictably.

- Evict least-recently-used instead, and raise the default budget to 2000
  (CACHE_MEMORY_MAX_ENTRIES). Reading a key now marks it as used, so a hot key
  only leaves when a hotter one takes its place.
- Add opt-in stale-while-revalidate (CachedOptions.staleMs). The grace window
  lives on the entry, so one call site opting in protects every reader of that
  key. A failed background refresh keeps serving the last good value instead of
  falling through to the origin, and is reported once rather than per read.
- Invalidate across processes. invalidateKey() now clears memory, deletes the
  Redis key and publishes a signal, so a value written by one process is no
  longer served stale by the others for the rest of its TTL. A failed Redis
  delete no longer skips the broadcast.
- Guard against a refresh that started before an invalidation writing its
  outdated result back into the cache.
- Read the news revision at most once a second per process instead of on every
  call, with a pub/sub signal to drop the local copy when it rotates. A Redis
  outage now degrades to the in-process cache rather than to no cache at all.
- Warm the hot public keys on boot, so the first visitors after a deploy do not
  each pay for a miss.
- Count hits, misses, stale serves, errors and evictions per key, exposed at
  GET /api/admin/devops/cache. Without it a wrong REDIS_URL, a full budget and
  a dead origin all look identical from the outside.
- Enforce the imaging cache budget for real: records are .img/.json pairs, so
  the old cap counted files and never removed anything while entries were
  fresh. Sweeps are throttled per directory and prune to a low-water mark.
- Cap the JWT version map, and stop per-test scratch roots from littering the
  runtime imaging cache.

Public read-only endpoints get grace windows; admin, account and auth data
deliberately stays fresh. Redis TTLs get a little jitter so keys written
together no longer expire together.

3209 tests pass. next build could not be verified on this host: the optimized
build is OOM-killed before prerender, so this has not run in a real Next
runtime yet.
2026-09-25 18:26:45 +02:00

192 lines
8.2 KiB
Bash

# ==============================================================================
# Epicnextcms — Ultimate Speed & Low-Latency Example Configuration
# ==============================================================================
# --- DATABASE (High Performance Pooling & Strict Timeouts) ---
DATABASE_URL="mysql://user:password@localhost:3306/dbname?charset=utf8mb4&connection_limit=150&connect_timeout=5"
DATABASE_POOL_SIZE=150
DATABASE_IDLE_TIMEOUT_MS=60000
DATABASE_CONNECT_TIMEOUT_MS=5000
# --- REDIS (Lightning Fast Caching & Sessions) ---
REDIS_URL=redis://127.0.0.1:6379?connect_timeout=2
REDIS_CACHE_TTL_DEFAULT=7200
# In-process cache entries kept per instance, evicted least-recently-used. Raise
# it if hot keys are evicted while memory headroom remains (default 2000).
CACHE_MEMORY_MAX_ENTRIES=2000
# Renders kept per imaging cache directory, counted as .img/.json pairs. A sweep
# every 5 minutes brings an over-budget directory back to 90% of this (default 20000).
IMAGING_CACHE_MAX_ENTRIES=20000
# --- CORE RUNTIME & PERFORMANCE FLAGS ---
NODE_ENV=production
PORT=3002
NEXT_TELEMETRY_DISABLED=1
UV_THREADPOOL_SIZE=16
# Production requires this kill switch plus housekeeping.preview.access.
HOUSEKEEPING_NEXT_PREVIEW_ENABLED=false
# --- HOTEL & URLS ---
HOTEL_NAME=EPIC WEB CONTROL
APP_URL=http://localhost:3002
AUTH_URL=http://localhost:3002
# --- IMAGER ---
# Avatar imager: Polaris-imager (avatar-imaging-pixinode) serves /avatarimage on 8082.
IMAGING_UPSTREAM_URL=http://127.0.0.1:8082/avatarimage
# Runtime values: changing these only requires recreating the container.
IMAGER_URL=http://127.0.0.1:8082/avatarimage
BADGE_URL=/swf/c_images/album1584
# Legacy NEXT_PUBLIC_IMAGER_URL / NEXT_PUBLIC_BADGE_URL are still read at runtime.
# --- SECURITY & HASHING ---
AUTH_SECRET=your-super-secret-auth-key-change-this-min-32-chars
APP_KEY=base64:your-app-key-here=
# Bcrypt cost factor for new password hashes.
BCRYPT_COST=12
# --- ANTI-DDOS (app-layer gate, production only) ---
# On by default in production. Set to "false" to disable (not recommended).
ANTI_DDOS_ENABLED=true
# Per-category request thresholds over the given window (per client IP).
ANTI_DDOS_PAGES_LIMIT=300
ANTI_DDOS_PAGES_WINDOW_SEC=60
ANTI_DDOS_API_LIMIT=600
ANTI_DDOS_API_WINDOW_SEC=60
ANTI_DDOS_AUTH_LIMIT=20
ANTI_DDOS_AUTH_WINDOW_SEC=60
# Whole-site safety valve per window (sheds everything for global_halt_ms when hit).
ANTI_DDOS_GLOBAL_LIMIT=18000
ANTI_DDOS_GLOBAL_WINDOW_SEC=60
ANTI_DDOS_GLOBAL_HALT_MS=10000
# Violations accumulate inside this window before an IP is hard-blocked.
ANTI_DDOS_VIOLATION_WINDOW_SEC=600
ANTI_DDOS_MAX_VIOLATIONS=10
# Escalation tiers "minViolations:ttlSeconds" — how long an offender stays blocked.
ANTI_DDOS_BLOCK_TIERS=5:600,20:3600,50:86400
# --- CLOUDFLARE API (automatic edge blocks, optional) ---
# When set, the anti-DDoS gate automatically mirrors hard-blocked IPs to the
# zone's IP Access Rules so repeat offenders are dropped at the Cloudflare
# edge (works on every plan, incl. Free). Token permissions required:
# Zone > Zone > Read and Zone > Firewall > Edit
CLOUDFLARE_API_TOKEN=
CLOUDFLARE_ZONE_ID=
# Runtime toggle; leave true to auto-create Cloudflare blocks at the block
# threshold. Also overridable live from the admin panel.
CLOUDFLARE_AUTO_BLOCK_ENABLED=true
# Override for tests/staging (production uses the public endpoint by default).
CLOUDFLARE_API_BASE_URL=https://api.cloudflare.com/client/v4
# --- CROWDSEC API (community reputation auto-block, optional) ---
# Free CTI API key: https://app.crowdsec.net/ → Settings → CTI API Keys.
# When set, the anti-DDoS gate checks the community reputation of repeat
# offenders (CTI GET /smoke/{ip}) and immediately hard-blocks known-bad IPs.
# Lookups only happen for IPs that already tripped a rate bucket and are
# cached in Redis for 1h, so quota usage stays minimal.
CROWDSEC_API_KEY=
# Runtime toggle for reputation-based auto-blocking (also overridable live
# from the admin panel). Requires CROWDSEC_API_KEY.
CROWDSEC_AUTO_BLOCK_ENABLED=true
# Minimum malevolence score 0-5 (CrowdSec scale; 4-5 = "malicious") before an
# IP is treated as known-bad. IPs with false-positive tags are never blocked.
CROWDSEC_BLOCK_SCORE=4
# How long a CrowdSec-confirmed bad IP stays blocked (seconds).
CROWDSEC_BLOCK_TTL_SECONDS=86400
# Endpoint — override only for tests/staging.
CROWDSEC_CTI_BASE_URL=https://cti.api.crowdsec.net/v2
# Daily enrichment-call ceiling (freemium plan ≈ 10k/day). Once today's
# counter reaches it, reputation lookups pause until tomorrow so a spread
# DDoS cannot silently burn the whole quota. 0 = unlimited.
CROWDSEC_CTI_DAILY_QUOTA=10000
# How many new community-reputation blocks within a 5-minute window justify an
# ops alert (quota/backoff/report alerts all use HEALTH_ALERT_COOLDOWN_MIN).
CROWDSEC_ALERT_BLOCK_BURST=10
# --- CROWDSEC SIGNAL PUSH (share our blocks back, optional) ---
# Opt-in: pushes blocked IPs + behaviors to the CrowdSec Central API (CAPI) so
# the community blocklist protects other members too. Set to "true" to enable.
# Requires watcher credentials — either set both CROWDSEC_REPORT_MACHINE_ID
# (48 chars, [A-Za-z0-9]) and CROWDSEC_REPORT_PASSWORD now, or leave them
# unset and let the app generate a stable pair persisted in Redis automatically.
CROWDSEC_REPORT_ENABLED=false
CROWDSEC_REPORT_MACHINE_ID=
CROWDSEC_REPORT_PASSWORD=
# Optional: attachment key from https://app.crowdsec.net → Console settings —
# links our watcher to your account so pushed signals show up there.
CROWDSEC_REPORT_ENROLL_KEY=
# Central API base — override only for tests/staging.
CROWDSEC_CAPI_BASE_URL=https://api.crowdsec.net/v3
# --- CROWDSEC LOCAL (opt-in engine on this Docker host, no proxy changes) ---
# App-layer LAPI bouncer: the anti-DDoS gate asks the local engine per client
# IP (short-cached) and blocks ban/captcha decisions before its own buckets.
# Start everything with `bash cms security`; it writes the key below into .env
# and starts the CrowdSec engine bound to 127.0.0.1. Set to "true" to load the
# bouncer without the local engine (not recommended).
CROWDSEC_LOCAL_ENABLED=false
# Host access-log directory mounted into the engine for detection (Nginx only).
CROWDSEC_NGINX_LOG_DIR=/var/log/nginx
# Change LAPI port AND LAPI URL together when 18080 is already taken.
CROWDSEC_LAPI_PORT=18080
CROWDSEC_LAPI_URL=http://127.0.0.1:18080
# Generated by `bash cms security`; keep in .env, never commit a value.
CROWDSEC_LAPI_API_KEY=
# IP blocklist sync (`bash cms security blocklists`): space-separated URLs, by
# default Spamhaus DROP/EDROP, DShield, CINS, Greensnow, StopForumSpam,
# blocklist.de, Emerging Threats, abuse.ch Feodo/SSLBL/URLhaus, IPsum,
# Firehol ipsets and Tor exit nodes. Requires internet to fetch; detection and
# blocking stay local.
#CROWDSEC_BLOCKLIST_SOURCES=https://www.spamhaus.org/drop/drop.txt https://example.org/list.txt
# Expiration for each blocklist decision (re-synced keeps them fresh).
#CROWDSEC_BLOCKLIST_DURATION=24h
# Combined cap per sync (safety valve against excessive decisions).
#CROWDSEC_BLOCKLIST_MAX_DECISIONS=1000000
# Comma-separated IPs/CIDRs that a sync must always skip (allowlist).
#CROWDSEC_BLOCKLIST_ALLOW=1.2.3.4,10.0.0.0/8
# --- PATHS ---
BADGE_UPLOAD_DIR=./public/assets/images/badges
EMULATOR_JAR_PATH=./emulator/Arcturus.jar
EMULATOR_BACKUP_DIR=./backups/emulator
EMULATOR_BACKUP_KEEP=7
# Optional mysqldump (jobs-worker daily 03:30). Requires mysqldump on PATH.
DB_BACKUP_DIR=
DB_BACKUP_KEEP=7
# Minutes between repeat health-fail alerts from jobs-worker (default 15).
HEALTH_ALERT_COOLDOWN_MIN=15
# --- RCON (Low Latency Loop) ---
RCON_HOST=127.0.0.1
RCON_PORT=3003
EMU_PORT=3004
RCON_TIMEOUT_MS=2000
# --- EMAIL & NOTIFICATIONS ---
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=[email protected]
# --- ALERTING & MONITORING ---
DISCORD_WEBHOOK_URL=
ALERT_EMAIL=
# --- MODERATION & PAYMENTS ---
OPENAI_API_KEY=
PAYPAL_CLIENT_ID=
PAYPAL_SECRET=
PAYPAL_API=https://api-m.sandbox.paypal.com
# --- LOGGING ---
LOG_LEVEL=error
# --- BYPARR (Cloudflare bypass for clone sources) ---
BYPARR_URL=http://localhost:8191
# Catalog Studio export: dedicated clean clone on Beta-3 with Git push credentials.
CATALOG_GIT_CHECKOUT=
# Persistent directory shared by CMS and worker, outside the catalog clone.
CATALOG_GIT_STATE_DIR=