35 Commits

Author SHA1 Message Date
Janis 35097e22ff revert: "feat(spark-analytics): per-row free-text questions in webview"
This reverts commit af3310297b.
2026-06-24 09:54:16 +02:00
Janis af3310297b feat(spark-analytics): per-row free-text questions in webview
Each expandable row now has a text input + Ask button alongside the
auto-analysis. Type any question about that row, press Ask or Enter, and
the answer streams in below via the existing /explain SSE path.

Server: row mode accepts an optional "question" field — when set it uses
a grounded Q&A prompt instead of the default analysis. Adds askRow() JS
handler, row-ask markup, and supporting CSS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:47:15 +02:00
Janis 9a5b62c2ab fix(spark-analytics): remove raw newline from webview JS comment
The SEP comment contained "\n\n", which Python's triple-quoted JS block
turned into real newline characters in the emitted script. That produced
an inline-<script> SyntaxError ("string literal contains an unescaped
line break"), so every handler went undefined and clicking the analyse
buttons did nothing (ReferenceError: analyzeRow is not defined).

Replace the comment text with plain words — no backslash escapes. Verified
by exec'ing serve.py (psycopg2 stubbed) to capture the Python-evaluated
JS and parsing it with node --check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:37:15 +02:00
Janis 9bb8f119b5 fix(spark-analytics): stream NIM AI responses to kill webview 504s
The /explain endpoint blocked on a synchronous NIM call (llama-3.3-70b,
30-60s+) while nginx's default 60s proxy-read-timeout fired first,
surfacing every AI request as a 504 Gateway Timeout.

Switch the NIM proxy to streaming (stream:true) and relay its SSE to the
browser via a new _emit() helper; the client now reads resp.body
incrementally and renders tokens as they arrive. With chunks flowing,
nginx's read-timeout resets per chunk so the 60s wall is gone, and the
frozen spinner is replaced by live text.

Ingress: disable proxy-buffering (else nginx buffers the whole response,
defeating streaming) and set proxy-read-timeout to 120s as the
inter-chunk gap budget. Server urlopen timeout 60->110s.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 09:22:31 +02:00
Janis Eccarius f64b838030 feat(spark-analytics): add AccuracyBlunder, ClockPressure, SmurfAnomaly jobs and webview datasets
Adds three new CronJobs (spark-accuracy-blunder, spark-clock-pressure,
spark-smurf-anomaly) and registers their six output tables in the
analytics webview (accuracy_by_rating, blunder_outcome, clock_by_rating,
scramble_outcome, smurf_anomaly, flagged_smurfs).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 00:18:22 +02:00
Janis Eccarius 587ea19e0e fix(spark-analytics): increase NIM timeout and trim max_tokens to avoid read timeouts
70B model at 30-50 tok/s was exceeding the 30s urllib timeout for larger
outputs. Raise server timeout to 60s, add browser AbortController at 65s
so the UI fails gracefully. Trim max_tokens (row: 300, dataset: 512) to
keep generation well inside the window.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 17:58:29 +02:00
Janis Eccarius 1ab9d60dd2 fix(spark-analytics): use ThreadingHTTPServer to prevent probe starvation
Single-threaded HTTPServer blocked readiness probes during long NIM API
calls (~30s), causing Kubernetes to flip the pod unhealthy until the
request completed. ThreadingHTTPServer handles each request in its own
thread so probes are never starved.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 17:53:31 +02:00
Janis Eccarius ac51ad09a4 feat(spark-analytics): per-row AI analysis with queue and rate-limit handling
- Add  button on each row (first 200) — clicking analyses that specific
  row with a chess-expert prompt tailored to the entry type (opening,
  player, etc.)
- Cache results per row so already-analysed rows skip re-requests
- Serialise all NIM requests through a promise queue to avoid bursting
- Pass 429 + Retry-After through from NIM to the browser; JS surfaces
  "Rate limited — retry in Ns" error and re-enables the button for retry
- Dataset-level "Analyse top 10 rows" now sends a per-row numbered prompt
  asking for chess-specific analysis of each entry rather than a vague
  dataset summary
- Store first 200 rows in window.DATASET.rows for client-side row access

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 17:48:10 +02:00
Janis Eccarius 7f11de365c fix(spark-analytics): proxy NIM API calls through server to avoid CORS
Browser cannot call integrate.api.nvidia.com directly — no CORS headers.
Add POST /explain endpoint: browser sends key + dataset, server calls NIM
with urllib, returns JSON. Key is never logged or stored server-side.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 17:36:04 +02:00
Janis Eccarius 60467d8ff6 fix(spark-analytics): escape \\n in JS userPrompt strings
Python triple-quoted string interprets \n as literal newlines,
producing invalid JS string literals and a SyntaxError that
killed the entire script block.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 17:25:20 +02:00
Janis Eccarius 9074c3fbd0 fix(spark-analytics): replace multiline JS template literals with concatenation
Multi-line template literals spanning unindented continuation lines break the
YAML block scalar indentation, causing kustomize to fail with MalformedYAMLError.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 17:19:00 +02:00
Janis Eccarius 252c543fed feat(spark-analytics): add per-user NVIDIA NIM AI explanations to webview
Each dataset page now has an "Explain this dataset" button that calls the
NVIDIA NIM chat completions API (meta/llama-3.3-70b-instruct) directly from
the browser with headers + first 10 rows as context. API key is entered via
a settings modal and stored in localStorage only — never sent to the server.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 17:16:26 +02:00
Janis Eccarius f2d13efcd7 fix: co-locate analytics driver pods with spark-worker for RWO PVC access
do-block-storage only supports ReadWriteOnce. Executor tasks failed with
`Mkdirs failed` because the executor ran on the spark-worker node which
had no mount of the spark-analytics-output PVC.

- Mount spark-analytics-output PVC on spark-worker pod at /spark-output
- Add podAffinity to all 11 analytics CronJob driver pods to schedule on
  the same node as spark-worker, ensuring the RWO PVC is accessible by
  both driver and executor

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 16:06:51 +02:00
Janis Eccarius 283c6728d4 fix(spark-analytics): batch index COUNT queries in one connection, suppress BrokenPipeError
13 separate DB connections on index page caused readiness probe timeout.
Now all counts share one connection. BrokenPipeError on probe disconnect silenced.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 15:52:34 +02:00
Janis Eccarius 609a222211 fix(spark-analytics): robust JDBC URL parsing, log resolved DB host on startup
Handle URLs without explicit port, show DB_HOST:PORT/DB_NAME in error messages
and startup log to simplify connection debugging.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 15:47:48 +02:00
Janis Eccarius bdc3efc878 fix(spark-analytics): install psycopg2 to /tmp/pkg, set PYTHONPATH so it is importable
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 15:44:09 +02:00
Janis Eccarius 6765c5162d fix(spark-analytics): set HOME=/tmp so pip can write under non-root user 65534
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 15:42:50 +02:00
Janis Eccarius 4395415985 fix(spark-analytics): add --no-cache-dir to pip install for nonexistent home dir
User 65534 has no home directory; pip cache write to /nonexistent fails. Skip cache.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 15:41:41 +02:00
Janis Eccarius ca78173a56 feat(spark-analytics): switch webview to PostgreSQL, always write analytics to DB
- Rewrite webview serve.py to query PostgreSQL analytics tables via psycopg2
  instead of reading CSV files from PVC; PVC mount removed from Deployment
- Remove isPgnMode guard from all 4 original jobs so results always write to DB
  regardless of whether input comes from game_records or a Lichess PGN dump
- Add JDBC write-back to all 7 new analytics jobs (game_length_distribution,
  game_length_by_result, color_advantage, elo_distribution, time_control_stats,
  hourly_activity, weekly_activity, rating_mismatch, termination_stats)
- Add analytics_component_sizes JDBC write to PlayerGraphJob
- Add NOWCHESS_PGN_PATH staging patches for the 7 new CronJobs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 15:35:32 +02:00
Janis Eccarius a190d50a09 feat(spark-analytics): add 7 CronJobs, results webview, and LiChess staging demo
Adds CronJobs and headless driver Services for GameLengthJob, ColorAdvantageJob,
EloDistributionJob, TimeControlJob, DailyActivityJob, RatingMismatchJob, and
TerminationStatsJob. All suspended by default; scheduled Saturday mornings.

Adds spark-analytics-webview: a stdlib Python HTTP server (ConfigMap + Deployment
+ Service) that reads CSV output from the spark-analytics-output PVC and renders
13 datasets as browsable HTML tables. Disabled (replicas=0) in base.

Staging overlay enables the webview (replicas=1), exposes it via Ingress at
spark-results-st.nowchess.janis-eccarius.de, and sets NOWCHESS_PGN_PATH on all
CronJobs to the Lichess Jan-2013 rated standard PGN dump for demo runs without
a NowChess database.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 15:09:49 +02:00
Janis Eccarius 95dc346064 fix(spark): use file:/// so driver serves analytics.jar to executors over HTTP 2026-06-17 01:33:13 +02:00
Janis Eccarius fe75a75a2e fix(spark): add headless Services + driver.host/port so executors can reach driver pods 2026-06-17 01:29:35 +02:00
Janis Eccarius 27920182cb fix(spark): raise executor.memory to 512m - JVM minimum heap is 471m 2026-06-17 01:23:25 +02:00
Janis Eccarius a0f96e9985 fix(spark): raise driver.memory to 512m - Spark 4.x minimum is 450m 2026-06-17 01:20:31 +02:00
Janis Eccarius 54e0561b43 fix(spark): fit executor within 256m worker - instances=1, memory=200m, driver=256m 2026-06-17 01:17:17 +02:00
Janis Eccarius d34a78c921 chore(spark): scale down all Spark workloads to free cluster resources 2026-06-17 01:05:30 +02:00
Janis Eccarius 1b75466b3e fix(spark): cap executor cores to prevent job starvation
Spark standalone gives executors all worker cores by default.
LiveDashboard was consuming all 8 cores (2 workers x 4 cores).
- streaming: spark.executor.cores=1 (uses 1 core total)
- batch jobs: spark.executor.cores=2 (uses 4 cores total, 2 per executor)
Leaves headroom for concurrent streaming + batch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 00:44:16 +02:00
Janis Eccarius 081d7807c3 fix(spark): use Recreate strategy to avoid RWO PVC deadlock
Rolling update deadlocked: new pod couldn't attach the RWO checkpoint
PVC while the old pod still held it, so ContainerCreating stalled for
39+ minutes. Recreate terminates the old pod first.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 00:04:45 +02:00
Janis Eccarius 23f814ae29 fix(spark): add headless service so executors can reach driver
Spark executors crashed immediately (exit 1) because the driver
advertised its pod name as the RPC hostname, which is not DNS-resolvable
in-cluster. Added headless Service `spark-live-dashboard-driver` and
pinned spark.driver.host/port + spark.blockManager.port so workers can
connect back to the driver. Stops the JVM fork storm that saturated all
three nodes at ~100% CPU.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 00:03:06 +02:00
Janis Eccarius cb9de632d9 fix(spark): drop driver pod requests to 50m CPU / 64Mi memory
Nodes have only ~60-210Mi free. Requests are scheduler hints only —
pods burst to 1Gi limit once placed. 64Mi fits on all three nodes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 23:21:11 +02:00
Janis Eccarius 342e1f60b3 fix(spark): lower driver pod requests to 100m CPU / 256Mi memory
Nodes are saturated on requested resources. Driver pods (streaming +
cronjobs) were requesting 500m CPU + 768Mi each. Limits unchanged at
1 CPU / 1Gi — pods can still burst when resources are free.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 22:47:51 +02:00
Janis Eccarius 0b69b04b1c fix(spark): add fsGroup 185 so spark user can write to PVCs
local-path PVCs are created root-owned. The apache/spark image runs as
spark (UID 185). fsGroup: 185 makes K8s chown the mounted volume to that
group on attach, fixing the mkdir IOException on /spark-checkpoint.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 22:22:50 +02:00
Janis Eccarius ab95b4f101 fix(spark): replace shared spark-events PVC with emptyDir to fix Multi-Attach error
local-path provisioner is RWO-only; attaching a single PVC to master, two
workers, history-server, and streaming pod on separate nodes causes
Multi-Attach errors. Each pod now gets its own ephemeral emptyDir for
/spark-events. Event logging confs removed from CronJobs and streaming
Deployment (logs are pod-local and do not persist across restarts).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 20:33:48 +02:00
Janis Eccarius eefa6e728a feat(spark): deploy persistent Spark cluster with Web UI
- Add spark-cluster/base with Bitnami Spark chart 9.2.10 (master +
  2 workers), spark-events PVC (RWO/local-path), and Ingress resources
  for spark.nowchess.janis-eccarius.de (master UI) and
  spark-history.nowchess.janis-eccarius.de (history placeholder)
- Add spark-cluster/eu-central-1-prod overlay and ArgoCD Application
  (automated prune + selfHeal)
- Migrate all spark-analytics CronJobs and streaming Deployment from
  K8s-native mode (k8s://...) to Spark standalone (spark://spark-master:7077);
  drop kubernetes-specific --conf flags; add eventLog confs; mount
  spark-events PVC on all job pods

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 22:41:33 +02:00
Janis Eccarius a3eb172916 feat(nci): add Spark analytics Kubernetes manifests
Wire five Spark jobs (OpeningBook, PlayerStats, PlayerClustering,
PlayerGraph, LiveDashboard) to Kubernetes via native Spark-on-K8s mode.

- spark-analytics/base: ServiceAccount, RBAC Role/RoleBinding (driver
  executor pod management), two PVCs (10Gi output, 2Gi checkpoint),
  four CronJobs for batch analytics, one Deployment for the Structured
  Streaming live dashboard
- spark-analytics/eu-central-1-prod: namespace overlay pinning image
  to 0.1.0 with strategic merge patches that update both the container
  image field and the spark.kubernetes.container.image conf arg
- eu-central-1/argo-apps/spark-analytics: ArgoCD Application pointing
  at the prod overlay with automated prune+selfHeal sync

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 22:25:28 +02:00