164 Commits

Author SHA1 Message Date
Kargo 9c0114a42e chore(kargo): update image tag official-bots to 0.36.0 on eu-central-1-prod
Delete PR Branch On Close / delete-branch (pull_request) Successful in 5s
2026-06-26 00:17:44 +00:00
svc.kargo e81fd1bc83 chore(kargo): update image tag kargo to 1.10.8 (#520) 2026-06-26 02:13:16 +02:00
svc.kargo ad6c0e7112 chore(kargo): update image tag official-bots to 0.40.0-x86_v2 on htwg-1-prod (#519) 2026-06-24 22:58:37 +02:00
svc.kargo 2d03547d58 chore(kargo): update image tag official-bots to 0.40.0 on staging (#518) 2026-06-24 22:53:18 +02:00
svc.kargo caad6681f7 chore(kargo): update image tag official-bots to 0.39.0-x86_v2 on htwg-1-prod (#517) 2026-06-24 22:32:29 +02:00
svc.kargo 8a2e175ba8 chore(kargo): update image tag official-bots to 0.39.0 on staging (#516) 2026-06-24 22:27:21 +02:00
svc.kargo ccf1a15901 chore(kargo): update image tag official-bots to 0.38.0-x86_v2 on htwg-1-prod (#515) 2026-06-24 20:37:01 +02:00
svc.kargo 1f9c5fc940 chore(kargo): update image tag official-bots to 0.38.0 on staging (#514) 2026-06-24 20:32:46 +02:00
svc.kargo 3ad1d3bbaf chore(kargo): update image tag official-bots to 0.37.0-x86_v2 on htwg-1-prod (#513) 2026-06-24 20:11:13 +02:00
svc.kargo 13ada3312d chore(kargo): update image tag official-bots to 0.37.0 on staging (#512) 2026-06-24 20:06:49 +02:00
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
svc.kargo b3fffec838 chore(kargo): update image tag official-bots to 0.36.0-x86_v2 on htwg-1-prod (#510) 2026-06-24 00:09:18 +02:00
svc.kargo 9ca55db829 chore(kargo): update image tag official-bots to 0.36.0 on staging (#509) 2026-06-24 00:05:01 +02:00
svc.kargo 069dc70bb8 chore(kargo): update image tag official-bots to 0.35.0-x86_v2 on htwg-1-prod (#508) 2026-06-23 23:54:17 +02:00
svc.kargo 799280a851 chore(kargo): update image tag official-bots to 0.35.0 on eu-central-1-prod (#507) 2026-06-23 23:53:39 +02:00
svc.kargo 9f23ef35fd chore(kargo): update image tag official-bots to 0.35.0 on staging (#506) 2026-06-23 23:53:29 +02:00
svc.kargo 5b79a71611 chore(kargo): update image tag official-bots to 0.34.0-x86_v2 on htwg-1-prod (#505) 2026-06-23 23:17:35 +02:00
svc.kargo 82b06b4d2b chore(kargo): update image tag official-bots to 0.34.0 on eu-central-1-prod (#504) 2026-06-23 23:16:37 +02:00
svc.kargo 873d6563e8 chore(kargo): update image tag official-bots to 0.34.0 on staging (#503) 2026-06-23 23:16:10 +02:00
svc.kargo 50d047e353 chore(kargo): update image tag tournament to 0.9.0 on eu-central-1-prod (#502) 2026-06-23 22:49:57 +02:00
svc.kargo 55c0644ea5 chore(kargo): update image tag tournament to 0.9.0-x86_v2 on htwg-1-prod (#501) 2026-06-23 22:42:22 +02:00
svc.kargo 093cb8b933 chore(kargo): update image tag tournament to 0.9.0 on staging (#500) 2026-06-23 22:40:23 +02:00
svc.kargo 6a700d46ac chore(kargo): update image tag official-bots to 0.33.0 on eu-central-1-prod (#499) 2026-06-23 21:47:22 +02:00
svc.kargo 4857ce2fe0 chore(kargo): update image tag official-bots to 0.33.0-x86_v2 on htwg-1-prod (#498) 2026-06-23 21:44:41 +02:00
svc.kargo 533b26a15a chore(kargo): update image tag official-bots to 0.33.0 on staging (#497) 2026-06-23 21:42:14 +02:00
svc.kargo 04dd75c2cd chore(kargo): update image tag tournament to 0.8.0 on eu-central-1-prod (#496) 2026-06-23 20:58:25 +02:00
svc.kargo 9824251acf chore(kargo): update image tag tournament to 0.8.0-x86_v2 on htwg-1-prod (#495) 2026-06-23 20:56:16 +02:00
svc.kargo bcaee070eb chore(kargo): update image tag tournament to 0.8.0 on staging (#494) 2026-06-23 20:53:57 +02:00
svc.kargo 11b8a205fc chore(kargo): update image tag official-bots to 0.32.0-x86_v2 on htwg-1-prod (#493) 2026-06-23 15:26:33 +02:00
svc.kargo 04ab316c83 chore(kargo): update image tag official-bots to 0.32.0 on eu-central-1-prod (#492) 2026-06-23 15:25:49 +02:00
svc.kargo 92faff4212 chore(kargo): update image tag official-bots to 0.32.0 on staging (#491) 2026-06-23 15:25:39 +02:00
svc.kargo f16d93c6c0 chore(kargo): update image tag official-bots to 0.31.0 on eu-central-1-prod (#490) 2026-06-23 15:09:25 +02:00
svc.kargo 1e1a7458e7 chore(kargo): update image tag frontend to 0.6.3 on eu-central-1-prod (#489) 2026-06-23 15:08:13 +02:00
svc.kargo 3d2167d00f chore(kargo): update image tag frontend to 0.6.2 on eu-central-1-prod (#488) 2026-06-23 15:07:26 +02:00
svc.kargo d296b0a26d chore(kargo): update image tag tournament to 0.7.0 on eu-central-1-prod (#487) 2026-06-23 15:06:35 +02:00
svc.kargo 72066fe215 chore(kargo): update image tag official-bots to 0.30.0 on eu-central-1-prod (#486) 2026-06-23 15:05:51 +02:00
svc.kargo 5d1fc138c1 chore(kargo): update image tag official-bots to 0.29.0 on eu-central-1-prod (#485) 2026-06-23 15:05:09 +02:00
svc.kargo 347cfcd0b1 chore(kargo): update image tag tournament to 0.6.0 on eu-central-1-prod (#484) 2026-06-23 15:04:23 +02:00
svc.kargo 6028e29680 chore(kargo): update image tag frontend to 0.6.1 on eu-central-1-prod (#483) 2026-06-23 15:03:40 +02:00
svc.kargo 2ca6f8830e chore(kargo): update image tag official-bots to 0.28.0 on eu-central-1-prod (#482) 2026-06-23 15:02:50 +02:00
svc.kargo b39393fce2 chore(kargo): update image tag frontend to 0.6.0 on eu-central-1-prod (#481) 2026-06-23 15:01:56 +02:00
svc.kargo 59b30804f3 chore(kargo): update image tag frontend to 0.6.3 on staging (#480) 2026-06-23 15:01:06 +02:00
svc.kargo e4467cc5b4 chore(kargo): update image tag official-bots to 0.27.0 on eu-central-1-prod (#479) 2026-06-23 15:00:25 +02:00
svc.kargo 7277518939 chore(kargo): update image tag frontend to 0.6.3-x86_v2 on htwg-1-prod (#478) 2026-06-23 15:00:22 +02:00
svc.kargo 3dfd4451ed chore(kargo): update image tag official-bots to 0.31.0-x86_v2 on htwg-1-prod (#477) 2026-06-23 14:57:22 +02:00
svc.kargo f8552a681c chore(kargo): update image tag official-bots to 0.31.0 on staging (#476) 2026-06-23 14:56:52 +02:00
svc.kargo f759c77b17 chore(kargo): update image tag official-bots to 0.26.0 on eu-central-1-prod (#475) 2026-06-23 14:56:36 +02:00
svc.kargo d3234b3347 chore(kargo): update image tag official-bots to 0.25.0 on eu-central-1-prod (#454)
chore(kargo): update image tag official-bots to 0.25.0 on eu-central-1-prod

[View in Kargo UI](https://kargo.nowchess.janis-eccarius.de/project/nowchess-kargo/stage/eu-central-1-prod)

---------

Co-authored-by: Kargo <no-reply@kargo.io>
Reviewed-on: #454
Co-authored-by: svc.kargo <kargo@blackhole.local>
Co-committed-by: svc.kargo <kargo@blackhole.local>
2026-06-23 14:55:09 +02:00
Janis d28bdcf062 chore(htwg-1): reduce log level from WARN to INFO 2026-06-23 14:42:58 +02:00
svc.kargo e5f13fb720 chore(kargo): update image tag frontend to 0.6.2-x86_v2 on htwg-1-prod (#474) 2026-06-23 14:25:21 +02:00
svc.kargo 21d4f34a7d chore(kargo): update image tag frontend to 0.6.2 on staging (#473) 2026-06-23 14:21:21 +02:00
svc.kargo 8c9659da75 chore(kargo): update image tag official-bots to 0.30.0 on staging (#472) 2026-06-23 14:04:19 +02:00
svc.kargo 5d08613cd7 chore(kargo): update image tag tournament to 0.7.0-x86_v2 on htwg-1-prod (#471) 2026-06-23 14:03:42 +02:00
svc.kargo ed863ae299 chore(kargo): update image tag official-bots to 0.30.0-x86_v2 on htwg-1-prod (#470) 2026-06-23 14:03:33 +02:00
svc.kargo 952e121c4e chore(kargo): update image tag tournament to 0.7.0 on staging (#469) 2026-06-23 14:03:28 +02:00
svc.kargo aa9e31f720 chore(kargo): update image tag official-bots to 0.29.0-x86_v2 on htwg-1-prod (#468) 2026-06-23 11:08:28 +02:00
svc.kargo 6d6d540303 chore(kargo): update image tag official-bots to 0.29.0 on staging (#467) 2026-06-23 11:04:25 +02:00
svc.kargo 75346b01fd chore(kargo): update image tag frontend to 0.6.1-x86_v2 on htwg-1-prod (#466) 2026-06-23 10:54:31 +02:00
svc.kargo efdd2774e9 chore(kargo): update image tag frontend to 0.6.1 on staging (#465) 2026-06-23 10:50:00 +02:00
svc.kargo 1c0ee49b47 chore(kargo): update image tag tournament to 0.6.0-x86_v2 on htwg-1-prod (#464) 2026-06-23 10:25:50 +02:00
svc.kargo 2a97c858b8 chore(kargo): update image tag official-bots to 0.28.0-x86_v2 on htwg-1-prod (#463) 2026-06-23 10:25:41 +02:00
svc.kargo a8f98a1419 chore(kargo): update image tag tournament to 0.6.0 on staging (#462) 2026-06-23 10:21:01 +02:00
svc.kargo 4adbf38b2b chore(kargo): update image tag official-bots to 0.28.0 on staging (#461) 2026-06-23 10:17:32 +02:00
svc.kargo 67bf5e4560 chore(kargo): update image tag frontend to 0.6.0-x86_v2 on htwg-1-prod (#460) 2026-06-23 09:49:05 +02:00
svc.kargo d8967f09f4 chore(kargo): update image tag official-bots to 0.27.0-x86_v2 on htwg-1-prod (#459) 2026-06-23 09:48:59 +02:00
svc.kargo 7a736c41d0 chore(kargo): update image tag frontend to 0.6.0 on staging (#458) 2026-06-23 09:45:02 +02:00
svc.kargo 1487b4b840 chore(kargo): update image tag official-bots to 0.27.0 on staging (#457) 2026-06-22 22:51:42 +02:00
svc.kargo 462a9830e5 chore(kargo): update image tag official-bots to 0.26.0-x86_v2 on htwg-1-prod (#456) 2026-06-22 21:58:29 +02:00
svc.kargo df0ff57707 chore(kargo): update image tag official-bots to 0.26.0 on staging (#455) 2026-06-22 21:54:28 +02:00
svc.kargo be4c9721ad chore(kargo): update image tag official-bots to 0.25.0-x86_v2 on htwg-1-prod (#453) 2026-06-22 20:56:10 +02:00
svc.kargo bc6bbc7b71 chore(kargo): update image tag official-bots to 0.25.0 on staging (#452) 2026-06-22 20:51:24 +02:00
svc.kargo 4efe1c2d92 chore(kargo): update image tag account to 0.26.0 on eu-central-1-prod (#451) 2026-06-22 19:59:44 +02:00
svc.kargo 6d481363ad chore(kargo): update image tag account to 0.26.0-x86_v2 on htwg-1-prod (#450) 2026-06-22 19:58:37 +02:00
svc.kargo 183bb029ff chore(kargo): update image tag official-bots to 0.24.0 on eu-central-1-prod (#449) 2026-06-22 19:58:31 +02:00
svc.kargo 574e849d46 chore(kargo): update image tag official-bots to 0.24.0-x86_v2 on htwg-1-prod (#448) 2026-06-22 19:58:30 +02:00
svc.kargo b65ddd0a49 chore(kargo): update image tag official-bots to 0.24.0 on staging (#447) 2026-06-22 19:54:29 +02:00
svc.kargo 674049e10e chore(kargo): update image tag account to 0.26.0 on staging (#446) 2026-06-22 19:52:16 +02:00
Janis Eccarius c133447ea6 chore(htwg-1): disable OTEL SDK to stop Grafana export noise
Sets OTEL_SDK_DISABLED=true in htwg-1-prod overlay so the OTel SDK
never initializes — eliminates export attempts to Grafana Alloy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-22 19:17:17 +02:00
svc.kargo 07a453a05e chore(kargo): update image tag tournament to 0.5.0-x86_v2 on htwg-1-prod (#444) 2026-06-21 22:31:41 +02:00
svc.kargo 4b1df75d7a chore(kargo): update image tag official-bots to 0.23.0-x86_v2 on htwg-1-prod (#443) 2026-06-21 22:24:22 +02:00
svc.kargo 4ab804f3bd chore(kargo): update image tag official-bots to 0.23.0 on eu-central-1-prod (#442) 2026-06-21 22:24:14 +02:00
svc.kargo df44bd176e chore(kargo): update image tag tournament to 0.5.0 on staging (#441) 2026-06-21 22:24:11 +02:00
svc.kargo 7a60f2865c chore(kargo): update image tag official-bots to 0.23.0 on staging (#440) 2026-06-21 22:20:03 +02:00
svc.kargo f823e06971 chore(kargo): update image tag frontend to 0.5.0 on eu-central-1-prod (#439) 2026-06-21 22:15:44 +02:00
svc.kargo 654e97957f chore(kargo): update image tag frontend to 0.5.0-x86_v2 on htwg-1-prod (#438) 2026-06-21 22:15:43 +02:00
svc.kargo 7d2b4774c7 chore(kargo): update image tag frontend to 0.5.0 on staging (#437) 2026-06-21 22:11:49 +02:00
Janis Eccarius 6abed9bb6e feat(nowchess): add TOURNAMENT_EXTERNAL_SERVERS env var to tournament rollout
htwg-1-prod seeds 141.37.123.132:8086 (primary bridge server) on startup
so the registry is populated without manual API calls after pod restarts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 21:34:05 +02:00
Janis Eccarius a84f62f972 fix(nowchess): correct tournament service port to 8088 and SELF_URL
- TOURNAMENT_SERVICE_URL was 8086 but K8s service and app both use 8088
- TOURNAMENT_SELF_URL on htwg-1 should be base ingress URL (port 80),
  not a direct service port — ingress routes /api/tournament to tournament service

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 21:31:16 +02:00
Janis Eccarius 3e6a7be274 fix(htwg-1): correct TOURNAMENT_SELF_URL to htwg-1 own IP
Was pointing at third-party server (141.37.123.132). Should be htwg-1's
own tournament service IP so remote clusters can proxy mutations back here.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 21:29:40 +02:00
Janis Eccarius b82a0919e9 feat(nowchess): wire TOURNAMENT_SERVICE_URL and TOURNAMENT_SELF_URL
- Official-bots now targets local tournament service (TOURNAMENT_SERVICE_URL)
  instead of calling remote cluster directly
- TOURNAMENT_SELF_URL tells each cluster's tournament and official-bots
  services their own public URL for cross-cluster replication
- htwg-1-prod: remove TOURNAMENT_SERVER_URL, add both new vars with correct
  values (tournament active service + node IP for self-announcement)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 21:23:58 +02:00
Janis Eccarius 521a39fa04 fix(official-bots): point ACCOUNT_SERVICE_URL to correct K8s service name
Service is named nowchess-account-active, not nowchess-account. DNS resolution
failed on startup causing official bots to never sync and appear unavailable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 20:59:39 +02:00
Janis Eccarius f2aeea1b40 fix(kustomization): update API and WebSocket URLs for frontend environment 2026-06-21 20:49:52 +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 7aa3bd3b12 fix: update CORS_ORIGINS to new IP address 2026-06-21 17:16:59 +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 c1c81f2507 chore(staging): bump analytics image to 0.7.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 16:55:52 +02:00
Janis Eccarius 42cb4efc05 fix(staging): add NOWCHESS_PGN_CACHE_DIR to all analytics CronJobs
Points to /spark-output/.pgn-cache so the decompressed Lichess PGN file
is written to the shared PVC, accessible by both driver and executor pods.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 16:31:20 +02:00
svc.kargo ce56b7cc8b chore(kargo): update image tag official-bots to 0.22.0-x86_v2 on htwg-1-prod (#436) 2026-06-21 16:30:58 +02:00
svc.kargo 433227e132 chore(kargo): update image tag official-bots to 0.22.0 on staging (#435) 2026-06-21 16:22:26 +02:00
svc.kargo e1e501766b chore(kargo): update image tag frontend to 0.4.4-x86_v2 on htwg-1-prod (#434) 2026-06-21 16:22:25 +02:00
svc.kargo 5423f0000a chore(kargo): update image tag frontend to 0.4.4 on staging (#433) 2026-06-21 16:12:22 +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 4379c0e867 fix(secret): update INTERNAL_SECRET value in ncs-internal-secret 2026-06-21 15:09:50 +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 2bc911ab81 fix(lf): Line Endings fixed 2026-06-21 15:09:47 +02:00
svc.kargo 49e9d60bb0 chore(kargo): update image tag official-bots to 0.21.0-x86_v2 on htwg-1-prod (#432) 2026-06-21 13:50:51 +02:00
svc.kargo e164e6d1a2 chore(kargo): update image tag official-bots to 0.21.0 on staging (#430) 2026-06-21 13:46:54 +02:00
svc.kargo bde04c0944 chore(kargo): update image tag tournament to 0.4.0-x86_v2 on htwg-1-prod (#431) 2026-06-21 13:46:53 +02:00
svc.kargo ea4728b111 chore(kargo): update image tag core to 0.53.0-x86_v2 on htwg-1-prod (#428) 2026-06-21 13:46:46 +02:00
svc.kargo 72e623bbf5 chore(kargo): update image tag tournament to 0.4.0 on staging (#427) 2026-06-21 13:42:52 +02:00
svc.kargo 79a2968950 chore(kargo): update image tag core to 0.53.0 on staging (#426) 2026-06-21 13:42:22 +02:00
svc.kargo 45284b6e36 chore(kargo): update image tag frontend to 0.4.3-x86_v2 on htwg-1-prod (#425) 2026-06-21 12:36:28 +02:00
lq64 0030998279 fix: add WebSocket timeout annotations and use-regex to ingresses (#419)
Add proxy-read-timeout and proxy-send-timeout (3600s) so nginx does not
drop long-lived WebSocket connections at the default 60s idle timeout.

Add use-regex: "true" to staging to match production — without it, the
ImplementationSpecific path for the game WebSocket
(/api/board/game/[A-Za-z0-9]{8}/ws) is treated as a literal string
instead of a regex and never matches real game IDs.

---------

Co-authored-by: LQ63 <lkhermann@web.de>
Reviewed-on: #419
Co-authored-by: Leon Hermann <lq@blackhole.local>
Co-committed-by: Leon Hermann <lq@blackhole.local>
2026-06-21 12:35:28 +02:00
svc.kargo fef2bb728b chore(kargo): update image tag ws to 0.17.0-x86_v2 on htwg-1-prod (#423)
chore(kargo): update image tag ws to 0.17.0-x86_v2 on htwg-1-prod

[View in Kargo UI](https://kargo.nowchess.janis-eccarius.de/project/nowchess-kargo/stage/htwg-1-prod)

---------

Co-authored-by: Kargo <no-reply@kargo.io>
Reviewed-on: #423
Co-authored-by: svc.kargo <kargo@blackhole.local>
Co-committed-by: svc.kargo <kargo@blackhole.local>
2026-06-21 12:35:21 +02:00
svc.kargo 3822f2a77f chore(kargo): update image tag frontend to 0.4.3 on eu-central-1-prod (#424) 2026-06-21 11:13:22 +02:00
svc.kargo 11ede2b94c chore(kargo): update image tag ws to 0.17.0 on eu-central-1-prod (#422) 2026-06-21 11:12:42 +02:00
svc.kargo 4916dc590f chore(kargo): update image tag frontend to 0.4.3 on staging (#421) 2026-06-21 11:08:31 +02:00
Janis Eccarius c39661828b fix: wire analysis service to ingress across all clusters
Add /api/analysis → nowchess-analysis-active:8087 path rule to
eu-central-1-staging, eu-central-1-prod, and htwg-1-prod ingresses.
Rule placed before the /api catch-all so nginx routes correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 10:32:01 +02:00
svc.kargo 994a709f44 chore(kargo): update image tag ws to 0.17.0 on staging (#420) 2026-06-19 12:35:52 +02:00
svc.kargo cef7823b44 chore(kargo): update image tag frontend to 0.4.2 on staging (#418) 2026-06-17 11:22:48 +02:00
Janis f655ce8ac6 fix(official-bots): resolve JWT verify key via MP env props (#417)
Reviewed-on: #417
2026-06-17 10:18:58 +02:00
Janis 93df9ee8d3 fix(official-bots): resolve JWT verify key via MP env props (#416)
The merged rollout set JWT_PUBLIC_KEY_PATH, but the running image's
config never references it, so mp.jwt.verify.publickey.location stayed
null and every token failed with "Verification key is unresolvable".

Replace it with the MicroProfile properties the runtime reads directly
(MP_JWT_VERIFY_PUBLICKEY_LOCATION, MP_JWT_VERIFY_ISSUER), pointing at the
already-mounted ncs-jwt-keys public key. No image rebuild needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Reviewed-on: #416
2026-06-17 10:01:38 +02:00
Janis 4563782626 fix(official-bots): mount JWT public key from ncs-jwt-keys (#414)
Reviewed-on: #414
2026-06-17 09:51:15 +02:00
Janis 2d5047cc63 fix(official-bots): mount JWT public key from ncs-jwt-keys (#413)
The official-bots rollout never mounted the ncs-jwt-keys secret nor set
JWT_PUBLIC_KEY_PATH, so the service fell back to the image-baked
public.pem, which does not match the deployed signing key. Every token
failed RS256 verification -> 401.

Mount ncs-jwt-keys at /secrets/jwt and point JWT_PUBLIC_KEY_PATH there,
mirroring account/core/tournament. Verify-only, no private key.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Reviewed-on: #413
2026-06-17 09:43:05 +02:00
svc.kargo 669159b098 chore(kargo): update image tag frontend to 0.4.1 on eu-central-1-prod (#412) 2026-06-17 09:39:33 +02:00
svc.kargo 55bcbe8ee9 chore(kargo): update image tag account to 0.25.0 on eu-central-1-prod (#411) 2026-06-17 09:38:50 +02:00
svc.kargo 5d77b37f57 chore(kargo): update image tag frontend to 0.4.1-x86_v2 on htwg-1-prod (#410) 2026-06-17 09:38:03 +02:00
svc.kargo f11b96a387 chore(kargo): update image tag account to 0.25.0-x86_v2 on htwg-1-prod (#409) 2026-06-17 09:37:57 +02:00
svc.kargo 6b2c67bd77 chore(kargo): update image tag official-bots to 0.19.0 on eu-central-1-prod (#408) 2026-06-17 09:37:51 +02:00
svc.kargo 4850303432 chore(kargo): update image tag official-bots to 0.19.0-x86_v2 on htwg-1-prod (#407) 2026-06-17 09:37:50 +02:00
svc.kargo 793b92e1e3 chore(kargo): update image tag official-bots to 0.19.0 on staging (#406) 2026-06-17 09:33:57 +02:00
svc.kargo b8b7d5aa47 chore(kargo): update image tag account to 0.25.0 on staging (#405) 2026-06-17 09:33:26 +02:00
svc.kargo 79dc37ffb5 chore(kargo): update image tag frontend to 0.4.1 on staging (#404) 2026-06-17 09:22:11 +02:00
Janis dbf9a551a7 fix(ingress): route /api/bots to official-bots service (#403)
/api/bots/official/join-tournament returned 404 because no ingress
rule matched the prefix; requests fell through to the /api catch-all
and hit nowchess-core (8080), which has no such endpoint.

Add /api/bots Prefix rule -> nowchess-official-bots-active:8088 in
all three environments (staging, eu-central-1-prod, htwg-1-prod).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Reviewed-on: #403
2026-06-17 08:58:49 +02:00
Janis 06e70a5e8a feat(spark): add staging overlay, scale down prod
- spark-cluster/eu-central-1-staging: namespace nowchess-staging, ingress
  patched to spark-st.* / spark-history-st.* with TLS + cert-manager
- spark-analytics/eu-central-1-staging: namespace nowchess-staging, image
  pinned to 0.2.0, all CronJob + live-dashboard patches carried over
- eu-central-1/argo-apps: spark-cluster-staging + spark-analytics-staging
  ArgoCD apps pointing at new staging overlays
- spark-cluster/eu-central-1-prod: scale spark-master and spark-worker to 0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 08:48:31 +02:00
svc.kargo f38fd0302c chore(kargo): update image tag frontend to 0.4.0 on staging (#402) 2026-06-17 08:24:42 +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 0dbfd4d35c fix(spark): raise worker memory to 512m/1Gi to fit executor overhead 2026-06-17 01:20:59 +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 7eecc23510 chore(spark): minimal cluster - 1 master + 1 worker, reduced limits, no streaming 2026-06-17 01:09:36 +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 0070139a37 fix(spark): reduce worker cores to 2 per worker (4 total) 2026-06-17 00:47:51 +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 d3feb65eb3 fix(spark): increase worker cores to 4 to allow concurrent jobs
With SPARK_WORKER_CORES=1 and 2 workers the cluster has 2 total cores.
LiveDashboard holds both — batch jobs wait indefinitely. 4 cores per
worker gives 8 total, enough to run streaming + batch concurrently.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 00:41:49 +02:00
Janis Eccarius 906a8cda2c fix(sealed-secrets): migrate Helm repo URL to bitnami
bitnami-labs GitHub Pages repo (bitnami-labs.github.io/sealed-secrets)
returns 404 — chart moved to charts.bitnami.com/bitnami. Updates chart
version from 2.16.1 to 2.5.19 (controller 0.27.1 → 0.31.0). Controller
is backward-compatible with existing sealed secrets.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 00:40:23 +02:00
Janis Eccarius e9926cbf62 chore(argocd): re-enable auto-sync for Spark apps
Cluster stable. Re-enabling automated sync on spark-analytics and
spark-cluster. Driver DNS fix and Recreate strategy already in main,
so next sync deploys the corrected manifests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 00:35:33 +02:00
29 changed files with 2832 additions and 62 deletions
+1
View File
@@ -0,0 +1 @@
- [Spark cluster deployment](project-spark-cluster.md) — chart 9.2.10 service names, history server gap, hostname pattern, standalone mode migration
@@ -0,0 +1,22 @@
---
name: project-spark-cluster
description: Spark cluster deployment facts — chart version, service names, history server gap, hostname pattern
metadata:
type: project
---
Bitnami Spark chart 9.2.10 (Spark 3.5.2) is pinned in `spark-cluster/base/kustomization.yaml`.
**Chart service names (rendered with releaseName=spark):**
- `spark-master-svc` — master Web UI on port 80 (targetPort: http/8080), cluster port 7077
- `spark-headless` — headless service for all nodes
**History server gap:** Chart 9.2.10 has NO standalone history server Deployment/Service. The `historyServer` chart key does not exist in this version. The `spark-history-ingress` in `spark-cluster/base/ingress.yaml` is a placeholder pointing to `spark-history-server-svc:18080` — this service does not exist until a separate Deployment is added.
**Why:** The Bitnami chart 9.2.10 only renders master + worker StatefulSets. A real history server requires either a newer chart version that adds it, or a hand-rolled Deployment.
**Hostnames:** `*.nowchess.janis-eccarius.de` — master UI at `spark.nowchess.janis-eccarius.de`, history at `spark-history.nowchess.janis-eccarius.de`. Ingress class: `traefik` (k3d default, different from production overlays which use `nginx`).
**Spark standalone mode:** All analytics CronJobs and the streaming Deployment use `spark://spark-master:7077` (not `k8s://https://kubernetes.default.svc:443`). Event logging enabled to `/spark-events` PVC (`spark-events`, RWO/local-path compatible).
**How to apply:** When working on Spark-related NCI tickets, check `spark-cluster/base/` for chart version and service names before writing ingress or spark-submit commands. If history server is needed, a new Deployment + Service named `spark-history-server-svc` on port 18080 must be added to the base.
+1 -1
View File
@@ -11,7 +11,7 @@ spec:
sources:
- repoURL: ghcr.io/akuity/kargo-charts
chart: kargo
targetRevision: 1.10.7
targetRevision: 1.10.8
helm:
valueFiles:
- $values/kargo/eu-central-1/values.yaml
@@ -9,9 +9,9 @@ spec:
namespace: kube-system
server: https://kubernetes.default.svc
sources:
- repoURL: https://bitnami-labs.github.io/sealed-secrets
- repoURL: https://charts.bitnami.com/bitnami
chart: sealed-secrets
targetRevision: 2.16.1
targetRevision: 2.5.19
helm:
valueFiles:
- $values/sealed-secrets/eu-central-1/values.yaml
@@ -0,0 +1,18 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: spark-analytics-staging
namespace: argocd
spec:
project: default
destination:
namespace: nowchess-staging
server: https://kubernetes.default.svc
sources:
- repoURL: git@git.janis-eccarius.de:NowChess/Gitops.git
path: ./spark-analytics/eu-central-1-staging
targetRevision: main
syncPolicy:
automated:
prune: true
selfHeal: true
@@ -12,4 +12,7 @@ spec:
- repoURL: git@git.janis-eccarius.de:NowChess/Gitops.git
path: ./spark-analytics/eu-central-1-prod
targetRevision: main
syncPolicy: {}
syncPolicy:
automated:
prune: true
selfHeal: true
@@ -0,0 +1,18 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: spark-cluster-staging
namespace: argocd
spec:
project: default
destination:
namespace: nowchess-staging
server: https://kubernetes.default.svc
sources:
- repoURL: git@git.janis-eccarius.de:NowChess/Gitops.git
path: ./spark-cluster/eu-central-1-staging
targetRevision: main
syncPolicy:
automated:
prune: true
selfHeal: true
@@ -12,4 +12,7 @@ spec:
- repoURL: git@git.janis-eccarius.de:NowChess/Gitops.git
path: ./spark-cluster/eu-central-1-prod
targetRevision: main
syncPolicy: {}
syncPolicy:
automated:
prune: true
selfHeal: true
+21 -1
View File
@@ -440,7 +440,19 @@ spec:
name: ncs-internal-secret
key: INTERNAL_SECRET
- name: ACCOUNT_SERVICE_URL
value: http://nowchess-account:8083
value: http://nowchess-account-active:8083
- name: TOURNAMENT_SERVICE_URL
value: http://nowchess-tournament-active:8088
- name: TOURNAMENT_SELF_URL
value: ""
- name: MP_JWT_VERIFY_PUBLICKEY_LOCATION
value: /secrets/jwt/public.pem
- name: MP_JWT_VERIFY_ISSUER
value: nowchess
volumeMounts:
- name: jwt-keys
mountPath: /secrets/jwt
readOnly: true
ports:
- containerPort: 8088
protocol: TCP
@@ -463,6 +475,10 @@ spec:
limits:
cpu: "500m"
memory: "256Mi"
volumes:
- name: jwt-keys
secret:
secretName: ncs-jwt-keys
rollbackWindow:
revisions: 3
strategy:
@@ -875,6 +891,10 @@ spec:
secretKeyRef:
name: ncs-db-secrets
key: ACCOUNT_DB_URL
- name: TOURNAMENT_SELF_URL
value: ""
- name: TOURNAMENT_EXTERNAL_SERVERS
value: ""
- name: MP_JWT_VERIFY_PUBLICKEY_LOCATION
value: /secrets/jwt/public.pem
volumeMounts:
+16
View File
@@ -8,6 +8,8 @@ metadata:
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
nginx.ingress.kubernetes.io/use-regex: "true"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
spec:
ingressClassName: nginx
rules:
@@ -56,6 +58,20 @@ spec:
name: nowchess-tournament-active
port:
number: 8088
- path: /api/bots
pathType: Prefix
backend:
service:
name: nowchess-official-bots-active
port:
number: 8088
- path: /api/analysis
pathType: Prefix
backend:
service:
name: nowchess-analysis-active
port:
number: 8087
- path: /api
pathType: Prefix
backend:
@@ -33,7 +33,7 @@ patches:
name: nowchess-frontend-env-config
images:
- name: ghcr.io/now-chess/now-chess-systems/account
newTag: 0.24.0
newTag: 0.26.0
- name: ghcr.io/now-chess/now-chess-systems/analysis
newTag: 0.2.0
- name: ghcr.io/now-chess/now-chess-systems/bot-platform
@@ -43,19 +43,19 @@ images:
- name: ghcr.io/now-chess/now-chess-systems/core
newTag: 0.51.0
- name: ghcr.io/now-chess/now-chess-systems/frontend
newTag: 0.3.0
newTag: 0.6.3
- name: ghcr.io/now-chess/now-chess-systems/io
newTag: 0.23.0
- name: ghcr.io/now-chess/now-chess-systems/official-bots
newTag: 0.17.0
newTag: 0.36.0
- name: ghcr.io/now-chess/now-chess-systems/rule
newTag: 0.18.0
- name: ghcr.io/now-chess/now-chess-systems/store
newTag: 0.24.0
- name: ghcr.io/now-chess/now-chess-systems/tournament
newTag: 0.3.0
newTag: 0.9.0
- name: ghcr.io/now-chess/now-chess-systems/ws
newTag: 0.16.0
newTag: 0.17.0
configurations:
- rollout-transform.yaml
openapi:
@@ -7,6 +7,9 @@ metadata:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
nginx.ingress.kubernetes.io/use-regex: "true"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
spec:
ingressClassName: nginx
rules:
@@ -55,6 +58,20 @@ spec:
name: nowchess-tournament-active
port:
number: 8088
- path: /api/bots
pathType: Prefix
backend:
service:
name: nowchess-official-bots-active
port:
number: 8088
- path: /api/analysis
pathType: Prefix
backend:
service:
name: nowchess-analysis-active
port:
number: 8087
- path: /api
pathType: Prefix
backend:
@@ -93,9 +93,24 @@ patches:
name: nowchess-core
spec:
replicas: 1
- patch: |-
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: nowchess-official-bots
spec:
template:
spec:
containers:
- name: nowchess-official-bots
env:
- name: QUARKUS_LOG_CATEGORY__IO_SMALLRYE_JWT__LEVEL
value: DEBUG
- name: QUARKUS_LOG_CATEGORY__IO_QUARKUS_SECURITY__LEVEL
value: DEBUG
images:
- name: ghcr.io/now-chess/now-chess-systems/account
newTag: 0.24.0
newTag: 0.26.0
- name: ghcr.io/now-chess/now-chess-systems/analysis
newTag: 0.2.0
- name: ghcr.io/now-chess/now-chess-systems/bot-platform
@@ -103,21 +118,21 @@ images:
- name: ghcr.io/now-chess/now-chess-systems/coordinator
newTag: 0.32.0
- name: ghcr.io/now-chess/now-chess-systems/core
newTag: 0.51.0
newTag: 0.53.0
- name: ghcr.io/now-chess/now-chess-systems/frontend
newTag: 0.3.0
newTag: 0.6.3
- name: ghcr.io/now-chess/now-chess-systems/io
newTag: 0.23.0
- name: ghcr.io/now-chess/now-chess-systems/official-bots
newTag: 0.18.0
newTag: 0.40.0
- name: ghcr.io/now-chess/now-chess-systems/rule
newTag: 0.18.0
- name: ghcr.io/now-chess/now-chess-systems/store
newTag: 0.24.0
- name: ghcr.io/now-chess/now-chess-systems/tournament
newTag: 0.3.0
newTag: 0.9.0
- name: ghcr.io/now-chess/now-chess-systems/ws
newTag: 0.16.0
newTag: 0.17.0
configurations:
- rollout-transform.yaml
openapi:
+14
View File
@@ -53,6 +53,20 @@ spec:
name: nowchess-tournament-active
port:
number: 8088
- path: /api/bots
pathType: Prefix
backend:
service:
name: nowchess-official-bots-active
port:
number: 8088
- path: /api/analysis
pathType: Prefix
backend:
service:
name: nowchess-analysis-active
port:
number: 8087
- path: /api
pathType: Prefix
backend:
+52 -10
View File
@@ -12,10 +12,11 @@ patches:
name: nowchess-env-config
data:
QUARKUS_PROFILE: deployed
CORS_ORIGINS: http://141.37.74.142
CORS_ORIGINS: http://141.37.123.122
NOWCHESS_COORDINATOR_ENABLED: "false"
REDIS_PREFIX: nowchess
QUARKUS_LOG_LEVEL: "WARN"
QUARKUS_LOG_LEVEL: "INFO"
OTEL_SDK_DISABLED: "true"
target:
kind: ConfigMap
name: nowchess-env-config
@@ -25,14 +26,55 @@ patches:
metadata:
name: nowchess-frontend-env-config
data:
API_URL: "http://141.37.74.142"
WEBSOCKET_URL: "ws://141.37.74.142"
API_URL: "http://141.37.123.122"
WEBSOCKET_URL: "ws://141.37.123.122"
target:
kind: ConfigMap
name: nowchess-frontend-env-config
- patch: |-
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: nowchess-official-bots
spec:
template:
spec:
containers:
- name: nowchess-official-bots
env:
- name: TOURNAMENT_SERVICE_URL
value: http://nowchess-tournament-active:8088
- name: TOURNAMENT_SELF_URL
value: http://141.147.25.239
- name: TOURNAMENT_BOT_TOKEN
valueFrom:
secretKeyRef:
name: ncs-tournament-bot-secret
key: TOURNAMENT_BOT_TOKEN
target:
kind: Rollout
name: nowchess-official-bots
- patch: |-
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: nowchess-tournament
spec:
template:
spec:
containers:
- name: nowchess-tournament
env:
- name: TOURNAMENT_SELF_URL
value: http://141.147.25.239
- name: TOURNAMENT_EXTERNAL_SERVERS
value: http://141.37.123.132:8086
target:
kind: Rollout
name: nowchess-tournament
images:
- name: ghcr.io/now-chess/now-chess-systems/account
newTag: 0.24.0-x86_v2
newTag: 0.26.0-x86_v2
- name: ghcr.io/now-chess/now-chess-systems/analysis
newTag: 0.1.0-x86_v2
- name: ghcr.io/now-chess/now-chess-systems/bot-platform
@@ -40,18 +82,18 @@ images:
- name: ghcr.io/now-chess/now-chess-systems/coordinator
newTag: 0.32.0-x86_v2
- name: ghcr.io/now-chess/now-chess-systems/core
newTag: 0.51.0-x86_v2
newTag: 0.53.0-x86_v2
- name: ghcr.io/now-chess/now-chess-systems/frontend
newTag: 0.3.0-x86_v2
newTag: 0.6.3-x86_v2
- name: ghcr.io/now-chess/now-chess-systems/io
newTag: 0.23.0-x86_v2
- name: ghcr.io/now-chess/now-chess-systems/official-bots
newTag: 0.18.0-x86_v2
newTag: 0.40.0-x86_v2
- name: ghcr.io/now-chess/now-chess-systems/rule
newTag: 0.18.0-x86_v2
- name: ghcr.io/now-chess/now-chess-systems/store
newTag: 0.24.0-x86_v2
- name: ghcr.io/now-chess/now-chess-systems/tournament
newTag: 0.3.0-x86_v2
newTag: 0.9.0-x86_v2
- name: ghcr.io/now-chess/now-chess-systems/ws
newTag: 0.16.0-x86_v2
newTag: 0.17.0-x86_v2
+1
View File
@@ -10,3 +10,4 @@ resources:
- ncs-internal-secret.yaml
- remotek6-certs.yaml
- postgres-credentials.yaml
- ncs-tournament-bot-secret.yaml
+13
View File
@@ -0,0 +1,13 @@
---
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: ncs-tournament-bot-secret
namespace: nowchess
spec:
encryptedData:
TOURNAMENT_BOT_TOKEN: AgBeVXB71v/88ksndl+bn4k8niG7rlKCDxWEinMgxh0vqLjtB/BWwwhhRp7064/jFH7Qt02nb9TF8N4+GIiBNykIGQLQtUBzvMp7I7EGSAHNsFSlXFTURQQJo5+C8L/H9c0UDQ+gunqCinz2mEZw1LlnMgQEqfMN3RCVK9YtW6f76jk3WLpet4g082lfy9cou2FZ+AgQq3zSBf6//OmlqrKRLkve1Nbxy4dS2IAvfLd/OdiC29sFs41nk3TFyrR9jPg8P4cYfd99O9XhabKRrCR8J1TxovCiWWQVBI+E1mmbQW5d6pcw1qO2fYsRGvdVb7DB+IiV3WjMHO6U3T/TV8sHhhAUSMELtD0NIfXyphkevK7Mtd3wPHgk6U2vTpNH3kionywff8jBeM8xWmTCOnHgRlCRoUrigysbAKNwGKjqEdWM3En8qKf4o6aTxFoQODIa09esivqcXs6U7WzqXWxjrZDVwsjauzC+eUyvJQv+yDPHSMjUt35r/CZF9xuOtRPG0ZmKmO6yAlbgLBnSYiOeEa9xPxhh3n7+kV9TijHgnxcsGhfzpxzQLJ49g7TPkSqudC+wZtMKt719HrW3SPiUnIT521ntqvaBGc+O8+AA1lEB5OaIfN1yKjUZmy7zPZhHG7wtTVBaXisXV0qdw62/V9aejgClb2S7OvXx7tuA4hrwKMXNyZ+YlN+10g6mIaxD62naiWRbrXdKy1dpgqtqq4erOUdGX09twWJ1kUWb0azUNV6BJTUinyTjh8bzbdq3cJ+IYm8gSe8QovAe9vpwAt9rRGTE8+lHBzUcEzQZwNQfK8FqqvHa2wDbFTaVt5G6wVNtGb2DLvlcIMKe5zSads0W0rwKXEKl2YUvZtBdVkdQvJD3/GlC4zBa7/TmIqCj
template:
metadata:
name: ncs-tournament-bot-secret
namespace: nowchess
File diff suppressed because it is too large Load Diff
+1
View File
@@ -7,3 +7,4 @@ resources:
- pvc.yaml
- cronjobs.yaml
- streaming.yaml
- webview.yaml
+4 -2
View File
@@ -25,7 +25,7 @@ metadata:
app.kubernetes.io/name: spark-analytics
app.kubernetes.io/part-of: nowchess
spec:
replicas: 1
replicas: 0
strategy:
type: Recreate
selector:
@@ -53,6 +53,8 @@ spec:
- --conf
- spark.executor.instances=1
- --conf
- spark.executor.cores=1
- --conf
- spark.executor.memory=512m
- --conf
- spark.driver.memory=512m
@@ -64,7 +66,7 @@ spec:
- spark.blockManager.port=37777
- --class
- de.nowchess.analytics.LiveDashboardJob
- local:///app/analytics.jar
- file:///app/analytics.jar
- /spark-checkpoint
env:
- name: NOWCHESS_JDBC_URL
+741
View File
@@ -0,0 +1,741 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: spark-analytics-webview
labels:
app.kubernetes.io/name: spark-analytics
app.kubernetes.io/part-of: nowchess
data:
serve.py: |
#!/usr/bin/env python3
"""Spark analytics results viewer — queries PostgreSQL analytics tables, serves HTML tables."""
import html
import json
import os
import re
import urllib.error
import urllib.request
import psycopg2
import psycopg2.extras
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
PORT = int(os.environ.get("PORT", "8080"))
_url = os.environ.get("NOWCHESS_JDBC_URL", "")
_m = re.match(r"jdbc:postgresql://([^:/]+)(?::(\d+))?/([^?]+)", _url)
DB_HOST = _m.group(1) if _m else "localhost"
DB_PORT = int(_m.group(2)) if (_m and _m.group(2)) else 5432
DB_NAME = _m.group(3) if _m else "nowchess"
DB_USER = os.environ.get("NOWCHESS_DB_USER", "nowchess")
DB_PASS = os.environ.get("NOWCHESS_DB_PASS", "")
# Each tuple: (url-slug, display label, postgres table name)
DATASETS = [
("opening-book", "Opening Book (Top 1000)", "analytics_opening_stats"),
("player-stats", "Player Statistics", "analytics_player_stats"),
("cluster-archetypes", "Cluster Archetypes", "analytics_cluster_archetypes"),
("component-sizes", "Graph Component Sizes", "analytics_component_sizes"),
("game-length", "Game Length Distribution", "analytics_game_length_distribution"),
("game-length-by-result", "Game Length by Result", "analytics_game_length_by_result"),
("color-advantage", "Color Advantage", "analytics_color_advantage"),
("elo-distribution", "ELO Distribution", "analytics_elo_distribution"),
("time-control", "Time Control Analysis", "analytics_time_control_stats"),
("hourly-activity", "Hourly Activity", "analytics_hourly_activity"),
("weekly-activity", "Weekly Activity", "analytics_weekly_activity"),
("rating-mismatch", "Rating Mismatch (Upsets)", "analytics_rating_mismatch"),
("termination-stats", "Termination Types", "analytics_termination_stats"),
("accuracy-by-rating", "Accuracy by Rating", "analytics_accuracy_by_rating"),
("blunder-outcome", "Blunder Outcome", "analytics_blunder_outcome"),
("clock-by-rating", "Clock Pressure by Rating", "analytics_clock_by_rating"),
("scramble-outcome", "Scramble Outcome", "analytics_scramble_outcome"),
("smurf-anomaly", "Smurf Anomaly Scores", "analytics_smurf_anomaly"),
("flagged-smurfs", "Flagged Smurfs", "analytics_flagged_smurfs"),
]
CSS = """
* { box-sizing: border-box; }
body { font-family: 'Segoe UI', sans-serif; margin: 0; background: #0d1117; color: #c9d1d9; }
header { background: #161b22; border-bottom: 1px solid #30363d; padding: 1rem 2rem; display: flex; align-items: center; gap: 1rem; }
header h1 { margin: 0; color: #58a6ff; font-size: 1.25rem; font-weight: 600; flex: 1; }
header span { color: #8b949e; font-size: 0.875rem; }
.settings-btn { background: none; border: 1px solid #30363d; border-radius: 6px; color: #8b949e;
cursor: pointer; padding: 4px 8px; font-size: 0.8rem; display: flex; align-items: center; gap: 4px; }
.settings-btn:hover { border-color: #58a6ff; color: #58a6ff; }
.settings-btn.active { border-color: #3fb950; color: #3fb950; }
main { padding: 1.5rem 2rem; }
h2 { color: #e6edf3; font-size: 1rem; font-weight: 600; margin: 0 0 1rem; }
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 1rem; }
.card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 1rem; }
.card h3 { margin: 0 0 0.5rem; font-size: 0.9rem; color: #58a6ff; }
.card a { text-decoration: none; color: inherit; display: block; }
.card a:hover .card-label { text-decoration: underline; }
.badge { display: inline-block; background: #21262d; border: 1px solid #30363d;
border-radius: 12px; padding: 1px 8px; font-size: 0.75rem; color: #8b949e; }
.badge.ready { border-color: #238636; color: #3fb950; }
.back { color: #58a6ff; text-decoration: none; font-size: 0.875rem; }
.back:hover { text-decoration: underline; }
.meta { color: #8b949e; font-size: 0.8rem; margin: 0.5rem 0 1rem; }
table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
thead th { background: #161b22; position: sticky; top: 0; padding: 6px 12px;
text-align: left; color: #8b949e; border-bottom: 1px solid #30363d;
font-weight: 600; white-space: nowrap; }
tbody td { padding: 5px 12px; border-bottom: 1px solid #21262d; }
tbody tr:hover td { background: #161b22; }
.table-wrap { overflow-x: auto; border: 1px solid #30363d; border-radius: 6px; }
.notice { background: #161b22; border: 1px solid #30363d; border-radius: 6px;
padding: 1.5rem; color: #8b949e; }
/* Settings modal */
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.6);
z-index: 100; align-items: center; justify-content: center; }
.modal-overlay.open { display: flex; }
.modal { background: #161b22; border: 1px solid #30363d; border-radius: 10px;
padding: 1.5rem; width: 420px; max-width: 95vw; }
.modal h3 { margin: 0 0 0.25rem; color: #e6edf3; font-size: 0.95rem; }
.modal p { margin: 0 0 1rem; color: #8b949e; font-size: 0.8rem; }
.modal input { width: 100%; background: #0d1117; border: 1px solid #30363d; border-radius: 6px;
color: #c9d1d9; font-size: 0.85rem; padding: 8px 10px; margin-bottom: 0.75rem;
font-family: monospace; }
.modal input:focus { outline: none; border-color: #58a6ff; }
.modal-actions { display: flex; gap: 0.5rem; justify-content: flex-end; }
.btn { border-radius: 6px; padding: 6px 14px; font-size: 0.8rem; cursor: pointer; border: 1px solid transparent; }
.btn-primary { background: #238636; color: #fff; border-color: #2ea043; }
.btn-primary:hover { background: #2ea043; }
.btn-danger { background: none; color: #f85149; border-color: #f85149; }
.btn-danger:hover { background: rgba(248,81,73,0.1); }
.btn-ghost { background: none; color: #8b949e; border-color: #30363d; }
.btn-ghost:hover { color: #c9d1d9; border-color: #8b949e; }
/* Explain panel */
.explain-bar { display: flex; align-items: center; gap: 0.75rem; margin: 1rem 0; }
.explain-btn { background: #1f3a5f; border: 1px solid #388bfd; color: #58a6ff; border-radius: 6px;
padding: 6px 14px; font-size: 0.8rem; cursor: pointer; display: flex; align-items: center; gap: 6px; }
.explain-btn:hover { background: #263d6a; }
.explain-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.no-key-hint { color: #8b949e; font-size: 0.78rem; }
.explain-panel { background: #161b22; border: 1px solid #388bfd; border-radius: 8px;
padding: 1.25rem; margin-bottom: 1rem; }
.explain-panel h4 { margin: 0 0 0.5rem; color: #58a6ff; font-size: 0.85rem; font-weight: 600; }
.explain-panel .explain-text { color: #c9d1d9; font-size: 0.85rem; line-height: 1.6; white-space: pre-wrap; }
.explain-panel .explain-error { color: #f85149; font-size: 0.82rem; }
.spinner { display: inline-block; width: 12px; height: 12px; border: 2px solid #388bfd;
border-top-color: transparent; border-radius: 50%; animation: spin 0.7s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
/* Per-row AI */
.row-ai-btn { background: none; border: 1px solid #30363d; border-radius: 4px; color: #8b949e;
cursor: pointer; padding: 1px 5px; font-size: 0.72rem; line-height: 1.4; }
.row-ai-btn:hover { border-color: #388bfd; color: #58a6ff; }
.row-ai-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.row-ai-btn.done { border-color: #238636; color: #3fb950; }
.row-expand-tr td { background: #0a0e14; padding: 0.6rem 1rem 0.6rem 2rem;
font-size: 0.8rem; line-height: 1.6; border-bottom: 1px solid #30363d; }
.row-explain-text { color: #c9d1d9; white-space: pre-wrap; }
.row-explain-error { color: #f85149; }
"""
JS = """
const NIM_KEY_STORAGE = 'nim_api_key';
const rowCache = {};
let nimQueue = Promise.resolve();
function getKey() { return localStorage.getItem(NIM_KEY_STORAGE) || ''; }
function hasKey() { return !!getKey(); }
function updateSettingsBtn() {
const btn = document.getElementById('settings-btn');
if (!btn) return;
if (hasKey()) {
btn.classList.add('active');
btn.title = 'NIM API key set — click to change';
} else {
btn.classList.remove('active');
btn.title = 'Configure NVIDIA NIM API key for AI explanations';
}
}
function openSettings() {
document.getElementById('nim-modal').classList.add('open');
const inp = document.getElementById('nim-key-input');
inp.value = getKey();
inp.focus();
updateNoKeyHint();
}
function closeSettings() {
document.getElementById('nim-modal').classList.remove('open');
}
function saveSettings() {
const val = document.getElementById('nim-key-input').value.trim();
if (val) localStorage.setItem(NIM_KEY_STORAGE, val);
else localStorage.removeItem(NIM_KEY_STORAGE);
closeSettings();
updateSettingsBtn();
updateNoKeyHint();
}
function clearKey() {
localStorage.removeItem(NIM_KEY_STORAGE);
document.getElementById('nim-key-input').value = '';
updateSettingsBtn();
updateNoKeyHint();
}
function updateNoKeyHint() {
const hint = document.getElementById('no-key-hint');
if (!hint) return;
hint.style.display = hasKey() ? 'none' : 'inline';
const btn = document.getElementById('explain-btn');
if (btn) btn.disabled = !hasKey();
}
async function nimStream(payload, onDelta) {
const ctrl = new AbortController();
const tid = setTimeout(() => ctrl.abort(), 180000);
try {
const resp = await fetch('/explain', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: ctrl.signal,
});
if (resp.status === 429) {
const retryAfter = parseInt(resp.headers.get('Retry-After') || '10', 10);
const err = new Error('Rate limited — retry in ' + retryAfter + 's');
err.retryAfter = retryAfter;
throw err;
}
if (!resp.ok) {
const msg = await resp.text();
throw new Error('Proxy ' + resp.status + ': ' + msg);
}
const reader = resp.body.getReader();
const dec = new TextDecoder();
const SEP = String.fromCharCode(10, 10); // SSE frame separator (two newlines)
let buf = '', full = '';
for (;;) {
const { done, value } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
let idx;
while ((idx = buf.indexOf(SEP)) >= 0) {
const frame = buf.slice(0, idx).trim();
buf = buf.slice(idx + 2);
if (!frame.startsWith('data:')) continue;
const obj = JSON.parse(frame.slice(5).trim());
if (obj.error) throw new Error(obj.error);
if (obj.delta) { full += obj.delta; onDelta(full); }
}
}
return full || '(no response)';
} finally {
clearTimeout(tid);
}
}
function queueNim(payload, onDelta) {
const next = nimQueue.then(() => nimStream(payload, onDelta));
nimQueue = next.catch(() => {});
return next;
}
async function explainDataset() {
const key = getKey();
if (!key) { openSettings(); return; }
const btn = document.getElementById('explain-btn');
const panel = document.getElementById('explain-panel');
const textEl = document.getElementById('explain-text');
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> Thinking…';
panel.style.display = 'block';
textEl.className = 'explain-text';
textEl.textContent = '';
try {
const text = await queueNim(
{ key: key, mode: 'dataset', dataset: window.DATASET },
partial => { textEl.textContent = partial; },
);
textEl.textContent = text;
} catch (e) {
textEl.className = 'explain-error';
textEl.textContent = 'Error: ' + e.message;
} finally {
btn.disabled = false;
btn.innerHTML = '✦ Analyse top 10 rows';
updateNoKeyHint();
}
}
async function analyzeRow(idx) {
const key = getKey();
if (!key) { openSettings(); return; }
if (rowCache[idx] !== undefined) return;
const btn = document.getElementById('row-btn-' + idx);
const expandRow = document.getElementById('row-expand-' + idx);
const textEl = document.getElementById('row-explain-' + idx);
if (!btn || !expandRow || !textEl) return;
const dataset = window.DATASET;
const row = dataset.rows && dataset.rows[idx];
if (!row) {
expandRow.style.display = 'table-row';
textEl.className = 'row-explain-error';
textEl.textContent = 'Row data not available.';
return;
}
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span>';
expandRow.style.display = 'table-row';
textEl.className = 'row-explain-text';
textEl.textContent = 'Analyzing…';
try {
const text = await queueNim(
{
key: key, mode: 'row',
dataset: { label: dataset.label, headers: dataset.headers, row: row },
},
partial => { textEl.textContent = partial; },
);
rowCache[idx] = text;
textEl.textContent = text;
btn.innerHTML = '✓';
btn.classList.add('done');
} catch (e) {
textEl.className = 'row-explain-error';
textEl.textContent = e.message;
btn.disabled = false;
btn.innerHTML = '⚡';
}
}
document.addEventListener('DOMContentLoaded', () => {
updateSettingsBtn();
updateNoKeyHint();
document.getElementById('nim-modal')?.addEventListener('click', e => {
if (e.target === e.currentTarget) closeSettings();
});
document.getElementById('nim-key-input')?.addEventListener('keydown', e => {
if (e.key === 'Enter') saveSettings();
if (e.key === 'Escape') closeSettings();
});
});
"""
SETTINGS_MODAL = """
<div class="modal-overlay" id="nim-modal">
<div class="modal">
<h3>NVIDIA NIM API Key</h3>
<p>Used client-side only — stored in your browser's localStorage, never sent to this server.</p>
<input type="password" id="nim-key-input" placeholder="nvapi-…" autocomplete="off" spellcheck="false" />
<div class="modal-actions">
<button class="btn btn-danger" onclick="clearKey()">Clear</button>
<button class="btn btn-ghost" onclick="closeSettings()">Cancel</button>
<button class="btn btn-primary" onclick="saveSettings()">Save</button>
</div>
</div>
</div>
"""
def get_conn():
return psycopg2.connect(host=DB_HOST, port=DB_PORT, dbname=DB_NAME, user=DB_USER, password=DB_PASS)
def all_row_counts() -> dict:
counts = {}
try:
with get_conn() as conn:
with conn.cursor() as cur:
for _, _, table in DATASETS:
try:
cur.execute(f"SELECT COUNT(*) FROM {table}")
counts[table] = cur.fetchone()[0]
except Exception:
conn.rollback()
counts[table] = -1
except Exception:
for _, _, table in DATASETS:
counts.setdefault(table, -1)
return counts
def fetch_rows(table: str, limit: int = 10_000):
with get_conn() as conn:
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur:
cur.execute(f"SELECT * FROM {table} LIMIT %s", (limit,))
rows = cur.fetchall()
if not rows:
return [], []
headers = list(rows[0].keys())
data = [
[str(row[h]) if row[h] is not None else "" for h in headers]
for row in rows
]
return headers, data
def page(title: str, body: str, dataset_json: str = "null") -> str:
return (
f"<!doctype html><html lang=en><head><meta charset=utf-8>"
f"<meta name=viewport content='width=device-width,initial-scale=1'>"
f"<title>{html.escape(title)} — Spark Analytics</title>"
f"<style>{CSS}</style></head><body>"
f"<header>"
f"<h1>Spark Analytics</h1><span>NowChess · staging</span>"
f"<button class='settings-btn' id='settings-btn' onclick='openSettings()' title='Configure NIM API key'>⚙ NIM Key</button>"
f"</header>"
f"<main>{body}</main>"
f"{SETTINGS_MODAL}"
f"<script>window.DATASET={dataset_json};\n{JS}</script>"
f"</body></html>"
)
def index_html() -> str:
counts = all_row_counts()
cards = ""
for slug, label, table in DATASETS:
count = counts.get(table, -1)
if count > 0:
badge = f'<span class="badge ready">{count} rows</span>'
elif count == 0:
badge = '<span class="badge">no data yet</span>'
else:
badge = '<span class="badge">table missing</span>'
cards += (
f'<div class="card"><a href="/{slug}">'
f'<div class="card-label"><h3>{html.escape(label)}</h3></div>'
f"{badge}</a></div>"
)
return page("Results", f"<h2>Datasets</h2><div class='cards'>{cards}</div>")
def table_html(slug: str, label: str, table: str) -> str:
back = '<a class="back" href="/">← All datasets</a>'
try:
headers, rows = fetch_rows(table)
except Exception as e:
return page(
label,
f"{back}<h2>{html.escape(label)}</h2>"
f"<div class='notice'>Error querying {html.escape(table)} on {html.escape(DB_HOST)}:{DB_PORT}/{html.escape(DB_NAME)}: {html.escape(str(e))}</div>",
)
if not headers:
return page(
label,
f"{back}<h2>{html.escape(label)}</h2>"
"<div class='notice'>No data yet. Run the CronJob first, then check back.</div>",
)
ai_rows = rows[:200]
dataset_json = json.dumps({
"label": label,
"headers": headers,
"sample": rows[:10],
"rows": ai_rows,
"total_rows": len(rows),
})
explain_bar = (
"<div class='explain-bar'>"
"<button class='explain-btn' id='explain-btn' onclick='explainDataset()'>✦ Analyse top 10 rows</button>"
"<span class='no-key-hint' id='no-key-hint'>— <a href='#' onclick='openSettings();return false;' style='color:#58a6ff'>add NIM key</a> to enable AI explanations</span>"
"</div>"
"<div class='explain-panel' id='explain-panel' style='display:none'>"
"<h4>AI Explanation</h4>"
"<div class='explain-text' id='explain-text'></div>"
"</div>"
)
num_cols = len(headers)
ths = "".join(f"<th>{html.escape(h)}</th>" for h in headers) + "<th></th>"
trs = ""
for i, row in enumerate(rows[:10_000]):
cells = "".join(f"<td>{html.escape(str(c))}</td>" for c in row)
if i < 200:
ai_cell = (
f"<td><button class='row-ai-btn' id='row-btn-{i}' "
f"onclick='analyzeRow({i})' title='Analyse this row with AI'>⚡</button></td>"
)
expand = (
f"<tr class='row-expand-tr' id='row-expand-{i}' style='display:none'>"
f"<td colspan='{num_cols + 1}'>"
f"<span id='row-explain-{i}'></span>"
f"</td></tr>"
)
else:
ai_cell = "<td></td>"
expand = ""
trs += f"<tr>{cells}{ai_cell}</tr>{expand}"
truncated = f" (showing first 10 000 of {len(rows)})" if len(rows) > 10_000 else ""
return page(
label,
f"{back}<h2>{html.escape(label)}</h2>"
f"<p class='meta'>{len(rows)} rows{truncated}</p>"
f"{explain_bar}"
f"<div class='table-wrap'><table><thead><tr>{ths}</tr></thead>"
f"<tbody>{trs}</tbody></table></div>",
dataset_json=dataset_json,
)
SLUG_MAP = {s: (label, table) for s, label, table in DATASETS}
class Handler(BaseHTTPRequestHandler):
def log_message(self, fmt, *args):
pass
def do_GET(self):
path = self.path.split("?")[0].lstrip("/")
if path == "" or path == "index.html":
self._send(index_html())
elif path in SLUG_MAP:
label, table = SLUG_MAP[path]
self._send(table_html(path, label, table))
else:
self.send_response(404)
self.end_headers()
def do_POST(self):
path = self.path.split("?")[0].lstrip("/")
if path != "explain":
self.send_response(404)
self.end_headers()
return
length = int(self.headers.get("Content-Length", 0))
headers_sent = False
try:
body = json.loads(self.rfile.read(length))
api_key = body.get("key", "")
mode = body.get("mode", "dataset")
dataset = body.get("dataset", {})
if not api_key:
raise ValueError("missing key")
label = dataset.get("label", "")
headers = dataset.get("headers", [])
if mode == "row":
row = dataset.get("row", [])
row_desc = ", ".join(f"{h}: {v}" for h, v in zip(headers, row))
system_prompt = (
"You are a chess analytics expert. Analyze a single data entry from a chess analytics dataset. "
"Be specific and insightful — 3-4 sentences. "
"If the entry involves a chess opening (ECO code or opening name present), explain the opening's "
"strategic ideas, strengths and weaknesses, and why players choose it. "
"For player data, explain what the stats reveal about their playing style. "
"For other types, explain what makes this entry notable."
)
user_prompt = (
f"Dataset: \"{label}\"\n"
f"Columns: {', '.join(headers)}\n\n"
f"Entry to analyze: {row_desc}\n\n"
"Provide a detailed, chess-specific analysis of this entry."
)
max_tokens = 300
else:
sample = dataset.get("sample", [])
rows_text = "\n".join(
f"Row {i + 1}: " + ", ".join(f"{h}: {v}" for h, v in zip(headers, row))
for i, row in enumerate(sample)
)
system_prompt = (
"You are a chess analytics expert. Analyze the top 10 entries from a chess analytics dataset. "
"For each entry write 1-2 specific sentences about what is notable. "
"If entries are chess openings (ECO codes or opening names present), name each opening properly "
"and explain its strategic character and why it ranks here. "
"Format your response as 'Row N: [analysis]' for each row."
)
user_prompt = (
f"Dataset: \"{label}\" ({dataset.get('total_rows', 0)} total rows)\n"
f"Columns: {', '.join(headers)}\n\n"
f"Top 10 entries:\n{rows_text}\n\n"
"Analyze each entry."
)
max_tokens = 512
payload = json.dumps({
"model": "meta/llama-3.3-70b-instruct",
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
"max_tokens": max_tokens,
"temperature": 0.4,
"stream": True,
}).encode("utf-8")
req = urllib.request.Request(
"https://integrate.api.nvidia.com/v1/chat/completions",
data=payload,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
method="POST",
)
try:
upstream = urllib.request.urlopen(req, timeout=110)
except urllib.error.HTTPError as he:
if he.code == 429:
retry_after = he.headers.get("Retry-After", "10")
resp_body = json.dumps({"error": f"Rate limited — retry in {retry_after}s"}).encode("utf-8")
self.send_response(429)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(resp_body)))
self.send_header("Retry-After", str(retry_after))
self.end_headers()
try:
self.wfile.write(resp_body)
except BrokenPipeError:
pass
return
raise RuntimeError(f"NIM API error {he.code}: {he.reason}")
# Relay NIM's SSE stream to the browser as our own SSE.
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Cache-Control", "no-cache")
self.send_header("X-Accel-Buffering", "no")
self.end_headers()
headers_sent = True
for raw in upstream:
line = raw.decode("utf-8", "replace").strip()
if not line.startswith("data:"):
continue
data = line[5:].strip()
if data == "[DONE]":
break
try:
obj = json.loads(data)
except json.JSONDecodeError:
continue
piece = obj.get("choices", [{}])[0].get("delta", {}).get("content", "")
if piece and not self._emit({"delta": piece}):
return
self._emit({"done": True})
except Exception as e:
if not headers_sent:
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Cache-Control", "no-cache")
self.end_headers()
self._emit({"error": str(e)})
def _emit(self, obj: dict) -> bool:
try:
self.wfile.write(("data: " + json.dumps(obj) + "\n\n").encode("utf-8"))
self.wfile.flush()
return True
except (BrokenPipeError, ConnectionResetError):
return False
def _send(self, body: str):
data = body.encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
try:
self.wfile.write(data)
except BrokenPipeError:
pass
def _send_json(self, obj: dict):
data = json.dumps(obj).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
try:
self.wfile.write(data)
except BrokenPipeError:
pass
if __name__ == "__main__":
server = ThreadingHTTPServer(("0.0.0.0", PORT), Handler)
print(f"Listening on :{PORT} DB={DB_HOST}:{DB_PORT}/{DB_NAME} JDBC_URL={_url!r}", flush=True)
server.serve_forever()
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: spark-analytics-webview
labels:
app.kubernetes.io/name: spark-analytics
app.kubernetes.io/part-of: nowchess
spec:
replicas: 0
strategy:
type: Recreate
selector:
matchLabels:
app: spark-analytics-webview
template:
metadata:
labels:
app: spark-analytics-webview
app.kubernetes.io/name: spark-analytics
app.kubernetes.io/part-of: nowchess
spec:
securityContext:
runAsNonRoot: true
runAsUser: 65534
fsGroup: 65534
containers:
- name: webview
image: python:3.12-slim
command: ["sh", "-c", "pip install psycopg2-binary --quiet --no-cache-dir --target=/tmp/pkg && PYTHONPATH=/tmp/pkg python /scripts/serve.py"]
ports:
- containerPort: 8080
env:
- name: PORT
value: "8080"
- name: NOWCHESS_JDBC_URL
valueFrom:
secretKeyRef:
name: ncs-db-secrets
key: STORE_DB_URL
- name: NOWCHESS_DB_USER
valueFrom:
secretKeyRef:
name: ncs-db-secrets
key: STORE_DB_USER
- name: NOWCHESS_DB_PASS
valueFrom:
secretKeyRef:
name: ncs-db-secrets
key: STORE_DB_PASSWORD
volumeMounts:
- name: scripts
mountPath: /scripts
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
resources:
requests:
cpu: 10m
memory: 64Mi
limits:
cpu: 200m
memory: 256Mi
volumes:
- name: scripts
configMap:
name: spark-analytics-webview
defaultMode: 0755
---
apiVersion: v1
kind: Service
metadata:
name: spark-analytics-webview
labels:
app.kubernetes.io/name: spark-analytics
app.kubernetes.io/part-of: nowchess
spec:
selector:
app: spark-analytics-webview
ports:
- name: http
port: 8080
targetPort: 8080
@@ -26,14 +26,22 @@ patches:
- --master
- spark://spark-master:7077
- --conf
- spark.executor.instances=2
- spark.executor.instances=1
- --conf
- spark.executor.memory=1g
- spark.executor.cores=1
- --conf
- spark.executor.memory=512m
- --conf
- spark.driver.memory=512m
- --conf
- spark.driver.host=spark-opening-book-driver
- --conf
- spark.driver.port=44167
- --conf
- spark.blockManager.port=44777
- --class
- de.nowchess.analytics.OpeningBookJob
- local:///app/analytics.jar
- file:///app/analytics.jar
- /spark-output/opening-book
- "10"
target:
@@ -56,14 +64,22 @@ patches:
- --master
- spark://spark-master:7077
- --conf
- spark.executor.instances=2
- spark.executor.instances=1
- --conf
- spark.executor.memory=1g
- spark.executor.cores=1
- --conf
- spark.executor.memory=512m
- --conf
- spark.driver.memory=512m
- --conf
- spark.driver.host=spark-player-stats-driver
- --conf
- spark.driver.port=44167
- --conf
- spark.blockManager.port=44777
- --class
- de.nowchess.analytics.PlayerStatsJob
- local:///app/analytics.jar
- file:///app/analytics.jar
- /spark-output/player-stats
target:
kind: CronJob
@@ -85,14 +101,22 @@ patches:
- --master
- spark://spark-master:7077
- --conf
- spark.executor.instances=2
- spark.executor.instances=1
- --conf
- spark.executor.memory=1g
- spark.executor.cores=1
- --conf
- spark.executor.memory=512m
- --conf
- spark.driver.memory=512m
- --conf
- spark.driver.host=spark-player-clustering-driver
- --conf
- spark.driver.port=44167
- --conf
- spark.blockManager.port=44777
- --class
- de.nowchess.analytics.PlayerClusteringJob
- local:///app/analytics.jar
- file:///app/analytics.jar
- /spark-output/player-clusters
- "4"
target:
@@ -115,14 +139,22 @@ patches:
- --master
- spark://spark-master:7077
- --conf
- spark.executor.instances=2
- spark.executor.instances=1
- --conf
- spark.executor.memory=1g
- spark.executor.cores=1
- --conf
- spark.executor.memory=512m
- --conf
- spark.driver.memory=512m
- --conf
- spark.driver.host=spark-player-graph-driver
- --conf
- spark.driver.port=44167
- --conf
- spark.blockManager.port=44777
- --class
- de.nowchess.analytics.PlayerGraphJob
- local:///app/analytics.jar
- file:///app/analytics.jar
- /spark-output/player-graph
target:
kind: CronJob
@@ -144,9 +176,17 @@ patches:
- --conf
- spark.executor.instances=1
- --conf
- spark.executor.cores=1
- --conf
- spark.executor.memory=512m
- --conf
- spark.driver.memory=512m
- --conf
- spark.driver.host=spark-live-dashboard-driver
- --conf
- spark.driver.port=37007
- --conf
- spark.blockManager.port=37777
- --class
- de.nowchess.analytics.LiveDashboardJob
- local:///app/analytics.jar
+429
View File
@@ -0,0 +1,429 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: nowchess-staging
resources:
- ../base
- webview-ingress.yaml
images:
- name: ghcr.io/now-chess/now-chess-systems/analytics
newTag: 0.7.0
patches:
- patch: |-
apiVersion: batch/v1
kind: CronJob
metadata:
name: spark-opening-book
spec:
jobTemplate:
spec:
template:
spec:
containers:
- name: spark-driver
command:
- /opt/spark/bin/spark-submit
- --master
- spark://spark-master:7077
- --conf
- spark.executor.instances=1
- --conf
- spark.executor.cores=1
- --conf
- spark.executor.memory=512m
- --conf
- spark.driver.memory=512m
- --conf
- spark.driver.host=spark-opening-book-driver
- --conf
- spark.driver.port=44167
- --conf
- spark.blockManager.port=44777
- --class
- de.nowchess.analytics.OpeningBookJob
- file:///app/analytics.jar
- /spark-output/opening-book
- "10"
env:
- name: NOWCHESS_PGN_PATH
value: "https://database.lichess.org/standard/lichess_db_standard_rated_2013-01.pgn.zst"
- name: NOWCHESS_PGN_CACHE_DIR
value: "/spark-output/.pgn-cache"
target:
kind: CronJob
name: spark-opening-book
- patch: |-
apiVersion: batch/v1
kind: CronJob
metadata:
name: spark-player-stats
spec:
jobTemplate:
spec:
template:
spec:
containers:
- name: spark-driver
command:
- /opt/spark/bin/spark-submit
- --master
- spark://spark-master:7077
- --conf
- spark.executor.instances=1
- --conf
- spark.executor.cores=1
- --conf
- spark.executor.memory=512m
- --conf
- spark.driver.memory=512m
- --conf
- spark.driver.host=spark-player-stats-driver
- --conf
- spark.driver.port=44167
- --conf
- spark.blockManager.port=44777
- --class
- de.nowchess.analytics.PlayerStatsJob
- file:///app/analytics.jar
- /spark-output/player-stats
env:
- name: NOWCHESS_PGN_PATH
value: "https://database.lichess.org/standard/lichess_db_standard_rated_2013-01.pgn.zst"
- name: NOWCHESS_PGN_CACHE_DIR
value: "/spark-output/.pgn-cache"
target:
kind: CronJob
name: spark-player-stats
- patch: |-
apiVersion: batch/v1
kind: CronJob
metadata:
name: spark-player-clustering
spec:
jobTemplate:
spec:
template:
spec:
containers:
- name: spark-driver
command:
- /opt/spark/bin/spark-submit
- --master
- spark://spark-master:7077
- --conf
- spark.executor.instances=1
- --conf
- spark.executor.cores=1
- --conf
- spark.executor.memory=512m
- --conf
- spark.driver.memory=512m
- --conf
- spark.driver.host=spark-player-clustering-driver
- --conf
- spark.driver.port=44167
- --conf
- spark.blockManager.port=44777
- --class
- de.nowchess.analytics.PlayerClusteringJob
- file:///app/analytics.jar
- /spark-output/player-clusters
- "4"
env:
- name: NOWCHESS_PGN_PATH
value: "https://database.lichess.org/standard/lichess_db_standard_rated_2013-01.pgn.zst"
- name: NOWCHESS_PGN_CACHE_DIR
value: "/spark-output/.pgn-cache"
target:
kind: CronJob
name: spark-player-clustering
- patch: |-
apiVersion: batch/v1
kind: CronJob
metadata:
name: spark-player-graph
spec:
jobTemplate:
spec:
template:
spec:
containers:
- name: spark-driver
command:
- /opt/spark/bin/spark-submit
- --master
- spark://spark-master:7077
- --conf
- spark.executor.instances=1
- --conf
- spark.executor.cores=1
- --conf
- spark.executor.memory=512m
- --conf
- spark.driver.memory=512m
- --conf
- spark.driver.host=spark-player-graph-driver
- --conf
- spark.driver.port=44167
- --conf
- spark.blockManager.port=44777
- --class
- de.nowchess.analytics.PlayerGraphJob
- file:///app/analytics.jar
- /spark-output/player-graph
env:
- name: NOWCHESS_PGN_PATH
value: "https://database.lichess.org/standard/lichess_db_standard_rated_2013-01.pgn.zst"
- name: NOWCHESS_PGN_CACHE_DIR
value: "/spark-output/.pgn-cache"
target:
kind: CronJob
name: spark-player-graph
- patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: spark-live-dashboard
spec:
template:
spec:
containers:
- name: spark-driver
command:
- /opt/spark/bin/spark-submit
- --master
- spark://spark-master:7077
- --conf
- spark.executor.instances=1
- --conf
- spark.executor.cores=1
- --conf
- spark.executor.memory=512m
- --conf
- spark.driver.memory=512m
- --conf
- spark.driver.host=spark-live-dashboard-driver
- --conf
- spark.driver.port=37007
- --conf
- spark.blockManager.port=37777
- --class
- de.nowchess.analytics.LiveDashboardJob
- local:///app/analytics.jar
- /spark-checkpoint
target:
kind: Deployment
name: spark-live-dashboard
- patch: |-
apiVersion: batch/v1
kind: CronJob
metadata:
name: spark-game-length
spec:
jobTemplate:
spec:
template:
spec:
containers:
- name: spark-driver
env:
- name: NOWCHESS_PGN_PATH
value: "https://database.lichess.org/standard/lichess_db_standard_rated_2013-01.pgn.zst"
- name: NOWCHESS_PGN_CACHE_DIR
value: "/spark-output/.pgn-cache"
target:
kind: CronJob
name: spark-game-length
- patch: |-
apiVersion: batch/v1
kind: CronJob
metadata:
name: spark-color-advantage
spec:
jobTemplate:
spec:
template:
spec:
containers:
- name: spark-driver
env:
- name: NOWCHESS_PGN_PATH
value: "https://database.lichess.org/standard/lichess_db_standard_rated_2013-01.pgn.zst"
- name: NOWCHESS_PGN_CACHE_DIR
value: "/spark-output/.pgn-cache"
target:
kind: CronJob
name: spark-color-advantage
- patch: |-
apiVersion: batch/v1
kind: CronJob
metadata:
name: spark-elo-distribution
spec:
jobTemplate:
spec:
template:
spec:
containers:
- name: spark-driver
env:
- name: NOWCHESS_PGN_PATH
value: "https://database.lichess.org/standard/lichess_db_standard_rated_2013-01.pgn.zst"
- name: NOWCHESS_PGN_CACHE_DIR
value: "/spark-output/.pgn-cache"
target:
kind: CronJob
name: spark-elo-distribution
- patch: |-
apiVersion: batch/v1
kind: CronJob
metadata:
name: spark-time-control
spec:
jobTemplate:
spec:
template:
spec:
containers:
- name: spark-driver
env:
- name: NOWCHESS_PGN_PATH
value: "https://database.lichess.org/standard/lichess_db_standard_rated_2013-01.pgn.zst"
- name: NOWCHESS_PGN_CACHE_DIR
value: "/spark-output/.pgn-cache"
target:
kind: CronJob
name: spark-time-control
- patch: |-
apiVersion: batch/v1
kind: CronJob
metadata:
name: spark-daily-activity
spec:
jobTemplate:
spec:
template:
spec:
containers:
- name: spark-driver
env:
- name: NOWCHESS_PGN_PATH
value: "https://database.lichess.org/standard/lichess_db_standard_rated_2013-01.pgn.zst"
- name: NOWCHESS_PGN_CACHE_DIR
value: "/spark-output/.pgn-cache"
target:
kind: CronJob
name: spark-daily-activity
- patch: |-
apiVersion: batch/v1
kind: CronJob
metadata:
name: spark-rating-mismatch
spec:
jobTemplate:
spec:
template:
spec:
containers:
- name: spark-driver
env:
- name: NOWCHESS_PGN_PATH
value: "https://database.lichess.org/standard/lichess_db_standard_rated_2013-01.pgn.zst"
- name: NOWCHESS_PGN_CACHE_DIR
value: "/spark-output/.pgn-cache"
target:
kind: CronJob
name: spark-rating-mismatch
- patch: |-
apiVersion: batch/v1
kind: CronJob
metadata:
name: spark-termination-stats
spec:
jobTemplate:
spec:
template:
spec:
containers:
- name: spark-driver
env:
- name: NOWCHESS_PGN_PATH
value: "https://database.lichess.org/standard/lichess_db_standard_rated_2013-01.pgn.zst"
- name: NOWCHESS_PGN_CACHE_DIR
value: "/spark-output/.pgn-cache"
target:
kind: CronJob
name: spark-termination-stats
# Eval-annotated dump required for per-move accuracy; the default 2013 dump
# carries no [%eval] comments — point this at an evals dump for real output.
- patch: |-
apiVersion: batch/v1
kind: CronJob
metadata:
name: spark-accuracy-blunder
spec:
jobTemplate:
spec:
template:
spec:
containers:
- name: spark-driver
env:
- name: NOWCHESS_PGN_PATH
value: "https://database.lichess.org/standard/lichess_db_standard_rated_2013-01.pgn.zst"
- name: NOWCHESS_PGN_CACHE_DIR
value: "/spark-output/.pgn-cache"
target:
kind: CronJob
name: spark-accuracy-blunder
# Clock-annotated dump required; the default 2013 dump carries no [%clk]
# comments — point this at a dump with clocks for real output.
- patch: |-
apiVersion: batch/v1
kind: CronJob
metadata:
name: spark-clock-pressure
spec:
jobTemplate:
spec:
template:
spec:
containers:
- name: spark-driver
env:
- name: NOWCHESS_PGN_PATH
value: "https://database.lichess.org/standard/lichess_db_standard_rated_2013-01.pgn.zst"
- name: NOWCHESS_PGN_CACHE_DIR
value: "/spark-output/.pgn-cache"
target:
kind: CronJob
name: spark-clock-pressure
- patch: |-
apiVersion: batch/v1
kind: CronJob
metadata:
name: spark-smurf-anomaly
spec:
jobTemplate:
spec:
template:
spec:
containers:
- name: spark-driver
env:
- name: NOWCHESS_PGN_PATH
value: "https://database.lichess.org/standard/lichess_db_standard_rated_2013-01.pgn.zst"
- name: NOWCHESS_PGN_CACHE_DIR
value: "/spark-output/.pgn-cache"
target:
kind: CronJob
name: spark-smurf-anomaly
- patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: spark-analytics-webview
spec:
replicas: 1
target:
kind: Deployment
name: spark-analytics-webview
+32
View File
@@ -0,0 +1,32 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: spark-analytics-webview-ingress
labels:
app.kubernetes.io/name: spark-analytics
app.kubernetes.io/part-of: nowchess
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
# AI /explain streams Server-Sent Events — disable response buffering so
# chunks flush to the client immediately, and allow gaps between chunks.
nginx.ingress.kubernetes.io/proxy-buffering: "off"
nginx.ingress.kubernetes.io/proxy-read-timeout: "120"
spec:
ingressClassName: nginx
rules:
- host: spark-results-st.nowchess.janis-eccarius.de
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: spark-analytics-webview
port:
number: 8080
tls:
- hosts:
- spark-results-st.nowchess.janis-eccarius.de
secretName: spark-analytics-webview-staging-cert
+1 -1
View File
@@ -6,7 +6,7 @@ metadata:
app.kubernetes.io/name: spark-history-server
app.kubernetes.io/part-of: nowchess
spec:
replicas: 1
replicas: 0
selector:
matchLabels:
app.kubernetes.io/name: spark-history-server
+3 -3
View File
@@ -32,11 +32,11 @@ spec:
containerPort: 8080
resources:
requests:
cpu: 200m
cpu: 100m
memory: 256Mi
limits:
cpu: "1"
memory: 2Gi
cpu: 500m
memory: 512Mi
volumeMounts:
- name: spark-events
mountPath: /spark-events
+10 -5
View File
@@ -6,7 +6,7 @@ metadata:
app.kubernetes.io/name: spark-worker
app.kubernetes.io/part-of: nowchess
spec:
replicas: 2
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: spark-worker
@@ -25,7 +25,7 @@ spec:
- spark://spark-master:7077
env:
- name: SPARK_WORKER_CORES
value: "1"
value: "2"
- name: SPARK_WORKER_MEMORY
value: "512m"
ports:
@@ -33,14 +33,19 @@ spec:
containerPort: 8081
resources:
requests:
cpu: 200m
cpu: 100m
memory: 256Mi
limits:
cpu: "1"
memory: 2Gi
cpu: 500m
memory: 1Gi
volumeMounts:
- name: spark-events
mountPath: /spark-events
- name: spark-output
mountPath: /spark-output
volumes:
- name: spark-events
emptyDir: {}
- name: spark-output
persistentVolumeClaim:
claimName: spark-analytics-output
@@ -3,3 +3,24 @@ kind: Kustomization
namespace: nowchess
resources:
- ../base
patches:
- patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: spark-master
spec:
replicas: 0
target:
kind: Deployment
name: spark-master
- patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: spark-worker
spec:
replicas: 0
target:
kind: Deployment
name: spark-worker
+62
View File
@@ -0,0 +1,62 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: nowchess-staging
resources:
- ../base
patches:
- patch: |-
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: spark-master-ingress
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
spec:
rules:
- host: spark-st.nowchess.janis-eccarius.de
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: spark-master
port:
number: 8080
tls:
- hosts:
- spark-st.nowchess.janis-eccarius.de
secretName: spark-cluster-staging-cert
target:
kind: Ingress
name: spark-master-ingress
- patch: |-
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: spark-history-ingress
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
spec:
rules:
- host: spark-history-st.nowchess.janis-eccarius.de
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: spark-history-server-svc
port:
number: 18080
tls:
- hosts:
- spark-history-st.nowchess.janis-eccarius.de
secretName: spark-history-staging-cert
target:
kind: Ingress
name: spark-history-ingress