Skip to content

feat: add free Football VM Prophet dashboard#1

Open
Thugney wants to merge 1 commit into
mainfrom
feature/football-vm-prophet
Open

feat: add free Football VM Prophet dashboard#1
Thugney wants to merge 1 commit into
mainfrom
feature/football-vm-prophet

Conversation

@Thugney

@Thugney Thugney commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds Sports/FootballVMProphet, a free local FIFA World Cup 2026 dashboard.
  • Uses a local Node server and HTML/JS frontend with no paid APIs and no npm runtime dependencies.
  • Includes fixture filters, due/upcoming/past/live/stale views, manual odds CSV import, implied probability/movement, and data-health warnings.

Free data model

  • Local fixture JSON is the source of truth.
  • Optional FREE_LIVE_ENDPOINT can be added later for free live score data.
  • Manual odds CSV is used for betting-board evidence without paid odds APIs.
  • Broken/stale/missing sources are shown in the dashboard instead of hidden.

Validation

  • npm test passed with Node v20.20.2.
  • Local server smoke test passed:
    • /api/health returns data-health JSON.
    • /api/dashboard returns 5 seed matches and 3 odds summary rows.
    • / serves the dashboard HTML.

Limitations

  • Included World Cup fixture file is seed data because final official fixtures are not complete yet.
  • No paid API, no automated betting, no guaranteed predictions.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Football VM Prophet, a local dashboard for FIFA World Cup 2026 fixtures and live match tracking
    • Integrated live match status updates with health monitoring
    • Implemented match filtering by team, stage, venue, and search criteria
    • Added manual odds tracking with implied probability calculations
    • Deployed responsive web interface with dark theme and mobile support
  • Documentation

    • Complete setup and configuration guide
    • Data model and API specifications
    • Free data source operational guidelines

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This pull request introduces Football VM Prophet, a local command-line dashboard for FIFA World Cup 2026 fixtures, live-score health, match filtering, and manual odds tracking. The project includes complete documentation, seed fixture data, backend server with data loaders, and a responsive web UI.

Changes

Football VM Prophet Dashboard

Layer / File(s) Summary
Documentation & Project Configuration
package.json, README.md, docs/Data-Model.md, docs/Free-Source-Notes.md
Package manifest, comprehensive README with scope/setup/architecture, data model documentation for fixtures/live endpoint/odds CSV, and operational notes on free-data principles and manual workflow.
Seed Fixture & Configuration Data
data/fixtures.worldcup2026.json, data/watchlist.json
World Cup 2026 tournament metadata and seed fixture matches (five scheduled with TBD teams and UTC kickoff times); watchlist config with enabled markets (1X2/OverUnder/BTTS) and alert timing.
Backend Data Loading & API Server
server/sources/local-fixtures.js, server/sources/manual-odds.js, server/sources/free-live-source.js, server/server.js
Three modular data loaders (fixtures with field validation and seed warnings; odds with CSV parsing and implied-probability computation; free live with cached curl fetch and fallback logic) integrated into an HTTP server exposing /api/dashboard and /api/health, serving static assets with path-traversal guards, and supporting startup self-test validation.
Frontend Dashboard UI
web/index.html, web/styles.css, web/app.js
HTML structure with top bar, filter panel (view/team/stage/venue/search), and content sections for health, stats, match cards, and odds board; dark-theme CSS with responsive layout; JavaScript that fetches dashboard API, maintains filter state, computes live/upcoming/stale counts, renders dynamic match list with scores/metadata/countdowns, and displays odds progression with movement styling.

🎯 3 (Moderate) | ⏱️ ~25 minutes


🐰 A dashboard springs forth with fixtures bright,
World Cup matches streaming through the night,
Odds and scores in careful array,
Built from scratch to light the way! ⚽✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add free Football VM Prophet dashboard' accurately captures the main change: introducing a new free local dashboard application for FIFA World Cup 2026, which aligns with the PR's primary objective.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/football-vm-prophet

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (2)
Sports/FootballVMProphet/data/watchlist.json (1)

1-7: Verify watchlist.json integration: file exists but is not loaded by the backend.

The watchlist.json file is included in the project and listed in the README file layout (line 104). However, the buildDashboard() function in server.js does not load or use this configuration. Either:

  1. This is intended as future work: Watchlist.json is a placeholder for a v2 feature and should be documented as such, or removed from the current PR.
  2. Integration is incomplete: Watchlist.json should be loaded and its teams and markets used to drive backend filtering/alerts, but the code is missing.

This needs clarification to avoid confusion about whether watchlist configuration is actively used.

Can you clarify the intended role of watchlist.json in this PR? If it's not yet integrated into the backend, I'd recommend either removing it or documenting it as a future feature placeholder.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sports/FootballVMProphet/data/watchlist.json` around lines 1 - 7, Decide
whether watchlist.json is a placeholder or required: if placeholder, remove the
file or add a clear note in the README and the top of watchlist.json stating
"placeholder / v2 feature - not used by backend"; if it should be active, update
server.js to load watchlist.json inside buildDashboard() (or a helper called by
buildDashboard), parse the "teams" and "markets" arrays and apply them to the
backend filtering/alert logic so buildDashboard uses those filters when
constructing dashboard data; reference watchlist.json and the buildDashboard()
function when making the change.
Sports/FootballVMProphet/README.md (1)

19-23: 💤 Low value

Reduce repetitive "It does not" sentence starters.

Lines 19–23 use parallel construction with "It does not..." four times in a row. While intentional, this reads as repetitive. Consider rewording one or two lines for variety.

💡 Example rewording
- - It does not place bets.
- - It does not guarantee predictions.
- - It does not hide stale data behind a "live" label.
+ - Bets are not placed automatically.
+ - Predictions are not guaranteed.
- - It does not require paid APIs.
- - It does not include official final World Cup 2026 fixture data before the draw/schedule is complete. The included fixture file is a starter/seed structure that you must update when official matches are available.
+ - No paid APIs required.
+ - Official World Cup 2026 fixture data is not included; the seed file must be replaced when the schedule is confirmed.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sports/FootballVMProphet/README.md` around lines 19 - 23, The README contains
four consecutive lines starting with the repeated phrase "It does not" making
the copy feel redundant; revise one or two of those lines (the lines containing
the repeated "It does not" sentences) to vary sentence starters and rhythm while
preserving the same meaning—for example, convert one to a positive-framed clause
or use a different verb phrase ("Does not place bets" → "No betting is
performed" or "Predictions are not guaranteed" → "Predictions are informational
only"); update the sentences in-place so the bullet list still conveys the same
constraints but with less repetitive phrasing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sports/FootballVMProphet/server/server.js`:
- Around line 87-89: The current string-prefix guard for filePath (built from
rootDir and safePath) is insecure; resolve the real paths and use path.relative
to ensure the requested file is inside the web root: compute const webRootReal =
await fs.promises.realpath(path.resolve(rootDir, 'web')) and const fileReal =
await fs.promises.realpath(path.resolve(rootDir, 'web', `.${safePath}`)), then
get rel = path.relative(webRootReal, fileReal) and reject the request (call
textResponse(res, 403, ... 'Forbidden')) if rel.startsWith('..') or
path.isAbsolute(rel); update the code paths that reference
filePath/safePath/rootDir to use these checks so symlinks and sibling-directory
tricks cannot escape the web root.

In `@Sports/FootballVMProphet/server/sources/free-live-source.js`:
- Around line 39-50: fetchFreeLive currently blocks the server using
execFileSync('curl', ...) and passes endpoint unsanitized to curl; change it to
an asynchronous, non-blocking fetch with proper URL validation/whitelist and a
safe argument separator instead of passing raw endpoint to a shell command.
Specifically: replace the synchronous execFileSync call in fetchFreeLive with an
async HTTP client (fetch/axios or execFile with callbacks/promises) so requests
are served from cache immediately; implement a background refresher that updates
the cache on a TTL interval and on startup (refer to fetchFreeLive,
writeFileSync usage, health.ok and health.fetchedAt updates), validate
FREE_LIVE_ENDPOINT with URL parsing/whitelist before any network call and never
pass an untrusted string as a CLI argument (if you must keep curl, include '--'
and an explicit validation step), and ensure buildDashboard() and /api/health
serve cached payload and reflect TTL/health state updated by the background
task.
- Line 40: Validate the endpoint's scheme and terminate curl options: before
calling execFileSync, construct a URL from the FREE_LIVE_ENDPOINT/endpoint
variable using the URL constructor, and if url.protocol is not 'http:' or
'https:' throw an error so the existing try/catch handles invalid endpoints;
then call execFileSync('curl',
['--fail','--silent','--show-error','--max-time','10','--', endpoint], {
encoding: 'utf8' }) (note the added '--' before endpoint) so values beginning
with '-' cannot be interpreted as curl flags.

In `@Sports/FootballVMProphet/server/sources/local-fixtures.js`:
- Around line 18-22: The current validation only checks presence of
match.matchId and pushes a warning to health.warnings; instead detect duplicate
matchId values in parsed.matches and treat them as errors: iterate
parsed.matches, maintain a seenIds Set, and when encountering a match.matchId
that is already in seenIds push a descriptive error to health.errors (not
health.warnings) referencing the duplicate matchId and the offending item (use
match.matchId or index) so the file is rejected; keep the existing presence
checks but move any duplicate-detection logic into the same loop that inspects
parsed.matches (so mergeLive() and web/app.js won’t later collapse/choose the
wrong fixture).

In `@Sports/FootballVMProphet/server/sources/manual-odds.js`:
- Around line 47-54: The CSV header is not validated before building rows
(headers variable used in the rows mapping), so typos/missing fields produce bad
data; update the loader that calls parseCsvLine and builds rows to assert that
headers includes the required fields
['matchId','snapshotTime','bookmaker','market','selection','odds'] (or
throw/return an error) before mapping lines to rows, and ensure any missing
header causes an immediate failure (not silent empty values), so downstream uses
of rows, impliedProbability, and dashboard.oddsSummary only receive validated
data.
- Around line 48-79: The parsing pipeline (parseCsvLine -> rows -> grouped ->
summary) does not validate required fields so malformed lines produce bad
buckets and NaN movements; update the rows construction to validate each parsed
row before grouping: after building row and converting row.odds and
row.snapshotTime, check that row.matchId, row.market, row.selection and a valid
Date(snapshotTime) exist and that Number.isFinite(row.odds); for any rejected
row increment or push a health warning (use the module's existing
logger/health-reporting mechanism or an array named e.g. manualOddsWarnings)
with the original line and reason, and skip adding that row to rows so
grouped/summary only processes valid entries; ensure
impliedProbability(row.odds) is only called for validated numeric odds.

In `@Sports/FootballVMProphet/web/app.js`:
- Around line 44-47: isToday currently compares dates using the viewer's local
timezone which can mismatch the UI that formats kickoff times with
dashboard.timezone; update isToday to interpret the input value in
dashboard.timezone (use the same timezone conversion helper used elsewhere in
the app) and compare the date portions in that timezone instead of using
date.toDateString(); locate and modify the isToday function and any call sites
(e.g., the check used around line 81) to use the timezone-aware date result so
"today" aligns with dashboard.timezone.
- Around line 32-41: The countdown function can receive an Invalid Date from
asDate (e.g., asDate('bad-value')), so update countdown to guard against that by
checking date validity (e.g., isNaN(date.getTime()) or date.toString() ===
'Invalid Date') right after calling asDate; if invalid, return 'Unknown' and
only compute ms, hours, days, remHours and minutes when date is valid—adjust
logic in the countdown function to perform this validation before any time
arithmetic.
- Around line 164-180: The loadDashboard function does not check response.ok or
handle JSON parsing errors and the refresh button calls loadDashboard without
handling rejections; update loadDashboard to check response.ok and throw a
descriptive error when the fetch fails, wrap the response.json() in try/catch to
handle bad payloads, and ensure on any error you set els.healthBanner.className
to 'health-banner broken' and set els.healthBanner.textContent to a clear
message; also change the els.refreshButton click handler to call
loadDashboard().catch(...) (or otherwise handle the returned promise) so manual
refresh failures are caught and invoke the same banner update path; reference
loadDashboard, els.refreshButton, els.healthBanner, render, and renderMatches
when applying the fix.

In `@Sports/FootballVMProphet/web/styles.css`:
- Line 14: The font-family declaration on the body rule uses an unquoted
multi-word family name; update the font-family in the body selector (the body {
... font-family: ... } rule) to quote "Segoe UI" (use double quotes) so it
satisfies the font-family-name-quotes Stylelint rule, e.g. change Segoe UI to
"Segoe UI" while keeping the rest of the stack unchanged.

---

Nitpick comments:
In `@Sports/FootballVMProphet/data/watchlist.json`:
- Around line 1-7: Decide whether watchlist.json is a placeholder or required:
if placeholder, remove the file or add a clear note in the README and the top of
watchlist.json stating "placeholder / v2 feature - not used by backend"; if it
should be active, update server.js to load watchlist.json inside
buildDashboard() (or a helper called by buildDashboard), parse the "teams" and
"markets" arrays and apply them to the backend filtering/alert logic so
buildDashboard uses those filters when constructing dashboard data; reference
watchlist.json and the buildDashboard() function when making the change.

In `@Sports/FootballVMProphet/README.md`:
- Around line 19-23: The README contains four consecutive lines starting with
the repeated phrase "It does not" making the copy feel redundant; revise one or
two of those lines (the lines containing the repeated "It does not" sentences)
to vary sentence starters and rhythm while preserving the same meaning—for
example, convert one to a positive-framed clause or use a different verb phrase
("Does not place bets" → "No betting is performed" or "Predictions are not
guaranteed" → "Predictions are informational only"); update the sentences
in-place so the bullet list still conveys the same constraints but with less
repetitive phrasing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ea849b13-326e-4a90-8210-a3a7dd9fc5bc

📥 Commits

Reviewing files that changed from the base of the PR and between 8420dc7 and 1f88083.

⛔ Files ignored due to path filters (1)
  • Sports/FootballVMProphet/data/odds.manual.example.csv is excluded by !**/*.csv
📒 Files selected for processing (13)
  • Sports/FootballVMProphet/README.md
  • Sports/FootballVMProphet/data/fixtures.worldcup2026.json
  • Sports/FootballVMProphet/data/watchlist.json
  • Sports/FootballVMProphet/docs/Data-Model.md
  • Sports/FootballVMProphet/docs/Free-Source-Notes.md
  • Sports/FootballVMProphet/package.json
  • Sports/FootballVMProphet/server/server.js
  • Sports/FootballVMProphet/server/sources/free-live-source.js
  • Sports/FootballVMProphet/server/sources/local-fixtures.js
  • Sports/FootballVMProphet/server/sources/manual-odds.js
  • Sports/FootballVMProphet/web/app.js
  • Sports/FootballVMProphet/web/index.html
  • Sports/FootballVMProphet/web/styles.css

Comment on lines +87 to +89
const filePath = path.resolve(rootDir, 'web', `.${safePath}`);
if (!filePath.startsWith(path.resolve(rootDir, 'web'))) {
textResponse(res, 403, 'text/plain; charset=utf-8', 'Forbidden');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

The static-file traversal guard is bypassable.

This startsWith() check is string-based, so sibling paths like <root>/web-evil/... still pass because the string begins with <root>/web. It also does not protect against symlinks inside web/ that point outside the directory. That makes the static handler capable of serving files outside Sports/FootballVMProphet/web. Use path.relative() against a realpath’d web root/file instead of raw string prefix checks.

Suggested fix
-import { existsSync } from 'node:fs';
+import { existsSync, realpathSync } from 'node:fs';
@@
 async function serveStatic(req, res) {
   const urlPath = new URL(req.url, `http://localhost:${port}`).pathname;
   const safePath = urlPath === '/' ? '/index.html' : urlPath;
-  const filePath = path.resolve(rootDir, 'web', `.${safePath}`);
-  if (!filePath.startsWith(path.resolve(rootDir, 'web'))) {
-    textResponse(res, 403, 'text/plain; charset=utf-8', 'Forbidden');
-    return;
-  }
-  if (!existsSync(filePath)) {
+  const webRoot = realpathSync(path.resolve(rootDir, 'web'));
+  const filePath = path.resolve(webRoot, `.${safePath}`);
+  if (!existsSync(filePath)) {
     textResponse(res, 404, 'text/plain; charset=utf-8', 'Not found');
     return;
   }
-  const ext = path.extname(filePath).toLowerCase();
+  const realFilePath = realpathSync(filePath);
+  const relativePath = path.relative(webRoot, realFilePath);
+  if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
+    textResponse(res, 403, 'text/plain; charset=utf-8', 'Forbidden');
+    return;
+  }
+  const ext = path.extname(realFilePath).toLowerCase();
@@
-  const body = await readFile(filePath);
+  const body = await readFile(realFilePath);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sports/FootballVMProphet/server/server.js` around lines 87 - 89, The current
string-prefix guard for filePath (built from rootDir and safePath) is insecure;
resolve the real paths and use path.relative to ensure the requested file is
inside the web root: compute const webRootReal = await
fs.promises.realpath(path.resolve(rootDir, 'web')) and const fileReal = await
fs.promises.realpath(path.resolve(rootDir, 'web', `.${safePath}`)), then get rel
= path.relative(webRootReal, fileReal) and reject the request (call
textResponse(res, 403, ... 'Forbidden')) if rel.startsWith('..') or
path.isAbsolute(rel); update the code paths that reference
filePath/safePath/rootDir to use these checks so symlinks and sibling-directory
tricks cannot escape the web root.

Comment on lines +39 to +50
try {
const raw = execFileSync('curl', ['--fail', '--silent', '--show-error', '--max-time', '10', endpoint], { encoding: 'utf8' });
const parsed = JSON.parse(raw);
const payload = {
matches: Array.isArray(parsed.matches) ? parsed.matches : [],
fetchedAt: new Date().toISOString(),
source: endpoint
};
writeFileSync(cachePath, JSON.stringify(payload, null, 2), 'utf8');
health.ok = true;
health.fetchedAt = payload.fetchedAt;
return { ...payload, ok: true, health };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n[1] fetchFreeLive implementation:\n'
sed -n '39,50p' Sports/FootballVMProphet/server/sources/free-live-source.js

printf '\n[2] request handlers calling buildDashboard():\n'
sed -n '107,119p' Sports/FootballVMProphet/server/server.js

printf '\n[3] buildDashboard calling fetchFreeLive():\n'
sed -n '57,61p' Sports/FootballVMProphet/server/server.js

Repository: Thugney/eriteach-scripts

Length of output: 1345


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== free-live-source.js (full relevant context) ==="
nl -ba Sports/FootballVMProphet/server/sources/free-live-source.js | sed -n '1,200p'

echo
echo "=== server.js (full relevant context for handlers/buildDashboard) ==="
nl -ba Sports/FootballVMProphet/server/server.js | sed -n '1,220p'

echo
echo "=== Grep for FREE_LIVE_ENDPOINT / cache usage ==="
rg -n --hidden --no-ignore-vcs "FREE_LIVE|endpoint|cachePath|cache|fetchedAt|curl" Sports/FootballVMProphet/server/sources/free-live-source.js Sports/FootballVMProphet/server/server.js

Repository: Thugney/eriteach-scripts

Length of output: 181


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== File sizes ==="
wc -l Sports/FootballVMProphet/server/sources/free-live-source.js
wc -l Sports/FootballVMProphet/server/server.js

echo
echo "=== free-live-source.js: lines 1-120 ==="
cat -n Sports/FootballVMProphet/server/sources/free-live-source.js | sed -n '1,120p'

echo
echo "=== free-live-source.js: lines 120-end (last 80) ==="
tail -n 80 Sports/FootballVMProphet/server/sources/free-live-source.js

echo
echo "=== server.js: lines 1-220 ==="
cat -n Sports/FootballVMProphet/server/server.js | sed -n '1,220p'

echo
echo "=== server.js: any other references to buildDashboard / /api/dashboard / /api/health ==="
rg -n "buildDashboard\\(|/api/dashboard|/api/health|fetchFreeLive\\(" Sports/FootballVMProphet/server/server.js

echo
echo "=== free-live-source.js: locate FREE_LIVE_ENDPOINT / endpoint / cachePath definitions ==="
rg -n "FREE_LIVE_ENDPOINT|endpoint|cachePath|writeFileSync\\(|execFileSync\\('curl'|fetchFreeLive\\(" Sports/FootballVMProphet/server/sources/free-live-source.js

Repository: Thugney/eriteach-scripts

Length of output: 12985


Stop blocking /api/dashboard and /api/health with synchronous curl, and harden FREE_LIVE_ENDPOINT usage

  • Availability/Stability: buildDashboard() calls fetchFreeLive(), which uses execFileSync('curl', ... '--max-time', '10', endpoint); both /api/dashboard and /api/health hit buildDashboard() per request, so one slow/free endpoint can stall the server thread up to 10s. Sports/FootballVMProphet/server/sources/free-live-source.js:39-61, Sports/FootballVMProphet/server/server.js:57-82,107-118
  • Security: endpoint is passed to curl without a -- argument terminator, so a misconfigured FREE_LIVE_ENDPOINT starting with - can be interpreted as curl options (option-injection). Add -- and validate/whitelist the URL format. Sports/FootballVMProphet/server/sources/free-live-source.js:39-50 [security]

Move the fetch off the request path (background refresh) and serve cached data with a TTL (current cache is only “stale” fallback, not TTL-based).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sports/FootballVMProphet/server/sources/free-live-source.js` around lines 39
- 50, fetchFreeLive currently blocks the server using execFileSync('curl', ...)
and passes endpoint unsanitized to curl; change it to an asynchronous,
non-blocking fetch with proper URL validation/whitelist and a safe argument
separator instead of passing raw endpoint to a shell command. Specifically:
replace the synchronous execFileSync call in fetchFreeLive with an async HTTP
client (fetch/axios or execFile with callbacks/promises) so requests are served
from cache immediately; implement a background refresher that updates the cache
on a TTL interval and on startup (refer to fetchFreeLive, writeFileSync usage,
health.ok and health.fetchedAt updates), validate FREE_LIVE_ENDPOINT with URL
parsing/whitelist before any network call and never pass an untrusted string as
a CLI argument (if you must keep curl, include '--' and an explicit validation
step), and ensure buildDashboard() and /api/health serve cached payload and
reflect TTL/health state updated by the background task.

}

try {
const raw = execFileSync('curl', ['--fail', '--silent', '--show-error', '--max-time', '10', endpoint], { encoding: 'utf8' });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\nCurrent curl invocation:\n'
sed -n '39,41p' Sports/FootballVMProphet/server/sources/free-live-source.js

Repository: Thugney/eriteach-scripts

Length of output: 268


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="Sports/FootballVMProphet/server/sources/free-live-source.js"

echo "---- file context (1-120) ----"
sed -n '1,120p' "$FILE"

echo "---- find FREE_LIVE_ENDPOINT usage in repo ----"
rg -n "FREE_LIVE_ENDPOINT" -S .

echo "---- find curl / execFileSync usage in repo ----"
rg -n "execFileSync\\('curl'|\\bcurl\\b" -S Sports/FootballVMProphet/server/sources/free-live-source.js Sports/FootballVMProphet/server/sources

echo "---- exact lines around invocation (30-60) ----"
sed -n '30,60p' "$FILE"

Repository: Thugney/eriteach-scripts

Length of output: 5673


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE1="Sports/FootballVMProphet/server/server.js"
FILE2="Sports/FootballVMProphet/docs/Data-Model.md"
FILE3="Sports/FootballVMProphet/README.md"

echo "---- server.js context around FREE_LIVE_ENDPOINT (120-190) ----"
sed -n '120,190p' "$FILE1"

echo "---- Data-Model.md FREE_LIVE_ENDPOINT (1-80) ----"
sed -n '1,80p' "$FILE2"

echo "---- README.md FREE_LIVE_ENDPOINT (35-110) ----"
sed -n '35,110p' "$FILE3"

Repository: Thugney/eriteach-scripts

Length of output: 4054


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "fetchFreeLive\\(" -S Sports/FootballVMProphet/server

Repository: Thugney/eriteach-scripts

Length of output: 256


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="Sports/FootballVMProphet/server/server.js"
sed -n '1,120p' "$FILE"

Repository: Thugney/eriteach-scripts

Length of output: 3953


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="Sports/FootballVMProphet/server/server.js"
sed -n '120,220p' "$FILE"

Repository: Thugney/eriteach-scripts

Length of output: 1085


Terminate curl options before the endpoint and restrict the scheme.

FREE_LIVE_ENDPOINT is passed as the final curl argument without a --, so a value starting with - can be interpreted as additional curl flags (option injection). Also, there’s no validation that the endpoint is only http/https, so non-web schemes could be fetched (SSRF/LFR-style risk). Update Sports/FootballVMProphet/server/sources/free-live-source.js so curl receives -- before the URL and reject non-http(s) protocols, while keeping invalid-endpoint handling inside the existing try/catch behavior to preserve the current cache/fallback flow.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sports/FootballVMProphet/server/sources/free-live-source.js` at line 40,
Validate the endpoint's scheme and terminate curl options: before calling
execFileSync, construct a URL from the FREE_LIVE_ENDPOINT/endpoint variable
using the URL constructor, and if url.protocol is not 'http:' or 'https:' throw
an error so the existing try/catch handles invalid endpoints; then call
execFileSync('curl', ['--fail','--silent','--show-error','--max-time','10','--',
endpoint], { encoding: 'utf8' }) (note the added '--' before endpoint) so values
beginning with '-' cannot be interpreted as curl flags.

Comment on lines +18 to +22
for (const match of parsed.matches) {
if (!match.matchId || !match.kickoffUtc || !match.homeTeam || !match.awayTeam) {
health.warnings.push(`Fixture ${match.matchId || 'unknown'} is missing required fields.`);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Enforce unique matchId values here, not just presence.

matchId is the join key for both live data and odds, but this validation only checks that it exists. If the fixture file contains duplicates, mergeLive() in Sports/FootballVMProphet/server/server.js collapses live rows into a Map by matchId, and web/app.js resolves odds rows back to the first matching fixture, so the dashboard can silently show the wrong teams/scores for one of the duplicates. This should be treated as invalid fixture data instead of a warning-only condition.

Suggested fix
   try {
     const raw = readFileSync(fixturePath, 'utf8');
     const parsed = JSON.parse(raw);
     if (!Array.isArray(parsed.matches)) {
       throw new Error('fixtures.worldcup2026.json must contain a matches array');
     }
+    const seenMatchIds = new Set();
     for (const match of parsed.matches) {
       if (!match.matchId || !match.kickoffUtc || !match.homeTeam || !match.awayTeam) {
         health.warnings.push(`Fixture ${match.matchId || 'unknown'} is missing required fields.`);
       }
+      if (match.matchId) {
+        if (seenMatchIds.has(match.matchId)) {
+          throw new Error(`Duplicate fixture matchId: ${match.matchId}`);
+        }
+        seenMatchIds.add(match.matchId);
+      }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sports/FootballVMProphet/server/sources/local-fixtures.js` around lines 18 -
22, The current validation only checks presence of match.matchId and pushes a
warning to health.warnings; instead detect duplicate matchId values in
parsed.matches and treat them as errors: iterate parsed.matches, maintain a
seenIds Set, and when encountering a match.matchId that is already in seenIds
push a descriptive error to health.errors (not health.warnings) referencing the
duplicate matchId and the offending item (use match.matchId or index) so the
file is rejected; keep the existing presence checks but move any
duplicate-detection logic into the same loop that inspects parsed.matches (so
mergeLive() and web/app.js won’t later collapse/choose the wrong fixture).

Comment on lines +47 to +54
const headers = parseCsvLine(lines.shift());
const rows = lines.map((line) => {
const values = parseCsvLine(line);
const row = Object.fromEntries(headers.map((header, index) => [header, values[index] || '']));
row.odds = Number(row.odds);
row.impliedProbability = impliedProbability(row.odds);
return row;
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate the CSV header before building rows.

This trusts whatever header row is present, but matchId, snapshotTime, bookmaker, market, selection, and odds are required by the documented CSV contract. A typo like matchID or an incomplete header silently produces empty keys, NaN odds, and merged summaries under '||', which then flow straight into dashboard.oddsSummary and the UI. Fail fast on missing headers instead of treating the file as loaded successfully.

Suggested fix
     const raw = readFileSync(sourcePath, 'utf8').trim();
     if (!raw) return { rows: [], summary: [], health };
     const lines = raw.split(/\r?\n/).filter(Boolean);
     const headers = parseCsvLine(lines.shift());
+    const requiredHeaders = ['matchId', 'snapshotTime', 'bookmaker', 'market', 'selection', 'odds'];
+    const missingHeaders = requiredHeaders.filter((header) => !headers.includes(header));
+    if (missingHeaders.length) {
+      throw new Error(`Manual odds CSV is missing required headers: ${missingHeaders.join(', ')}`);
+    }
     const rows = lines.map((line) => {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sports/FootballVMProphet/server/sources/manual-odds.js` around lines 47 - 54,
The CSV header is not validated before building rows (headers variable used in
the rows mapping), so typos/missing fields produce bad data; update the loader
that calls parseCsvLine and builds rows to assert that headers includes the
required fields
['matchId','snapshotTime','bookmaker','market','selection','odds'] (or
throw/return an error) before mapping lines to rows, and ensure any missing
header causes an immediate failure (not silent empty values), so downstream uses
of rows, impliedProbability, and dashboard.oddsSummary only receive validated
data.

Comment on lines +32 to +41
function countdown(value) {
const date = asDate(value);
if (!date) return 'Unknown';
const ms = date.getTime() - Date.now();
if (ms < 0) return 'Started / past';
const hours = Math.floor(ms / 36e5);
const days = Math.floor(hours / 24);
const remHours = hours % 24;
const minutes = Math.floor((ms % 36e5) / 60000);
return `${days}d ${remHours}h ${minutes}m`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard countdown() against invalid timestamps.

asDate('bad-value') returns an Invalid Date object, so this path currently renders NaNd NaNh NaNm instead of a safe fallback.

Suggested fix
 function countdown(value) {
   const date = asDate(value);
-  if (!date) return 'Unknown';
+  if (!date || Number.isNaN(date.getTime())) return 'Unknown';
   const ms = date.getTime() - Date.now();
   if (ms < 0) return 'Started / past';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function countdown(value) {
const date = asDate(value);
if (!date) return 'Unknown';
const ms = date.getTime() - Date.now();
if (ms < 0) return 'Started / past';
const hours = Math.floor(ms / 36e5);
const days = Math.floor(hours / 24);
const remHours = hours % 24;
const minutes = Math.floor((ms % 36e5) / 60000);
return `${days}d ${remHours}h ${minutes}m`;
function countdown(value) {
const date = asDate(value);
if (!date || Number.isNaN(date.getTime())) return 'Unknown';
const ms = date.getTime() - Date.now();
if (ms < 0) return 'Started / past';
const hours = Math.floor(ms / 36e5);
const days = Math.floor(hours / 24);
const remHours = hours % 24;
const minutes = Math.floor((ms % 36e5) / 60000);
return `${days}d ${remHours}h ${minutes}m`;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sports/FootballVMProphet/web/app.js` around lines 32 - 41, The countdown
function can receive an Invalid Date from asDate (e.g., asDate('bad-value')), so
update countdown to guard against that by checking date validity (e.g.,
isNaN(date.getTime()) or date.toString() === 'Invalid Date') right after calling
asDate; if invalid, return 'Unknown' and only compute ms, hours, days, remHours
and minutes when date is valid—adjust logic in the countdown function to perform
this validation before any time arithmetic.

Comment on lines +44 to +47
function isToday(value) {
const date = asDate(value);
const now = new Date();
return date && date.toDateString() === now.toDateString();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use dashboard.timezone for the “Today” filter too.

The UI formats kickoff times with dashboard.timezone, but isToday() compares toDateString() in the viewer’s local timezone. For matches near midnight, Line 81 can file a match under the wrong day relative to the date shown on screen.

Suggested fix
-function isToday(value) {
+function isToday(value, timezone) {
   const date = asDate(value);
-  const now = new Date();
-  return date && date.toDateString() === now.toDateString();
+  if (!date || Number.isNaN(date.getTime())) return false;
+  const fmt = new Intl.DateTimeFormat('en-CA', {
+    timeZone: timezone,
+    year: 'numeric',
+    month: '2-digit',
+    day: '2-digit'
+  });
+  return fmt.format(date) === fmt.format(new Date());
 }
@@
-    if (view === 'today' && !isToday(match.kickoffUtc)) return false;
+    if (view === 'today' && !isToday(match.kickoffUtc, dashboard.timezone)) return false;

Also applies to: 81-81

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sports/FootballVMProphet/web/app.js` around lines 44 - 47, isToday currently
compares dates using the viewer's local timezone which can mismatch the UI that
formats kickoff times with dashboard.timezone; update isToday to interpret the
input value in dashboard.timezone (use the same timezone conversion helper used
elsewhere in the app) and compare the date portions in that timezone instead of
using date.toDateString(); locate and modify the isToday function and any call
sites (e.g., the check used around line 81) to use the timezone-aware date
result so "today" aligns with dashboard.timezone.

Comment on lines +116 to +133
els.matches.innerHTML = matches.map((match) => {
const score = Number.isFinite(match.homeScore) && Number.isFinite(match.awayScore)
? `${match.homeScore} - ${match.awayScore}`
: 'vs';
const dataTag = match.dataStatus === 'live' ? 'LIVE DATA' : String(match.dataStatus || 'unknown').toUpperCase();
return `<article class="match-card ${statusClass(match.status)}">
<div class="match-main">
<div class="teams"><span>${match.homeTeam}</span><b>${score}</b><span>${match.awayTeam}</span></div>
<div class="meta">${match.stage}${match.group ? ` / ${match.group}` : ''} · ${match.venue || 'Venue TBD'}${match.city ? `, ${match.city}` : ''}</div>
<div class="meta">Kickoff: ${fmtDate(match.kickoffUtc, dashboard.timezone)} · Countdown: ${countdown(match.kickoffUtc)}</div>
</div>
<div class="badges">
<span class="badge ${statusClass(match.status)}">${match.status}</span>
<span class="badge data">${dataTag}</span>
${match.minute ? `<span class="badge">${match.minute}'</span>` : ''}
</div>
</article>`;
}).join('');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Stop injecting fixture/odds fields into innerHTML unsafely.

match.homeTeam, match.venue, row.market, row.selection, and similar fields come from local fixture JSON, manual CSV, and the optional live-source pipeline. Rendering them straight into template strings makes the dashboard vulnerable to DOM XSS if any source contains markup.

Also applies to: 142-154

Comment on lines +164 to +180
async function loadDashboard() {
els.healthBanner.textContent = 'Loading...';
const response = await fetch('/api/dashboard', { cache: 'no-store' });
dashboard = await response.json();
render();
}

['change', 'input'].forEach((eventName) => {
[els.viewFilter, els.teamFilter, els.stageFilter, els.venueFilter, els.searchFilter].forEach((el) => {
el.addEventListener(eventName, () => dashboard && renderMatches());
});
});
els.refreshButton.addEventListener('click', loadDashboard);
loadDashboard().catch((error) => {
els.healthBanner.className = 'health-banner broken';
els.healthBanner.textContent = `APP BROKEN: ${error.message}`;
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle failed refreshes explicitly instead of leaving an unhandled rejection.

loadDashboard() never checks response.ok, and the refresh button calls it without a .catch(...). A 500/404 or bad payload during manual refresh leaves the banner on Loading... and pushes the failure into an unhandled promise rejection path.

Suggested fix
+function showLoadError(error) {
+  els.healthBanner.className = 'health-banner broken';
+  els.healthBanner.textContent = `APP BROKEN: ${error.message}`;
+}
+
 async function loadDashboard() {
   els.healthBanner.textContent = 'Loading...';
   const response = await fetch('/api/dashboard', { cache: 'no-store' });
+  if (!response.ok) {
+    throw new Error(`Dashboard request failed (${response.status})`);
+  }
   dashboard = await response.json();
   render();
 }
@@
-els.refreshButton.addEventListener('click', loadDashboard);
-loadDashboard().catch((error) => {
-  els.healthBanner.className = 'health-banner broken';
-  els.healthBanner.textContent = `APP BROKEN: ${error.message}`;
-});
+els.refreshButton.addEventListener('click', () => loadDashboard().catch(showLoadError));
+loadDashboard().catch(showLoadError);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function loadDashboard() {
els.healthBanner.textContent = 'Loading...';
const response = await fetch('/api/dashboard', { cache: 'no-store' });
dashboard = await response.json();
render();
}
['change', 'input'].forEach((eventName) => {
[els.viewFilter, els.teamFilter, els.stageFilter, els.venueFilter, els.searchFilter].forEach((el) => {
el.addEventListener(eventName, () => dashboard && renderMatches());
});
});
els.refreshButton.addEventListener('click', loadDashboard);
loadDashboard().catch((error) => {
els.healthBanner.className = 'health-banner broken';
els.healthBanner.textContent = `APP BROKEN: ${error.message}`;
});
function showLoadError(error) {
els.healthBanner.className = 'health-banner broken';
els.healthBanner.textContent = `APP BROKEN: ${error.message}`;
}
async function loadDashboard() {
els.healthBanner.textContent = 'Loading...';
const response = await fetch('/api/dashboard', { cache: 'no-store' });
if (!response.ok) {
throw new Error(`Dashboard request failed (${response.status})`);
}
dashboard = await response.json();
render();
}
['change', 'input'].forEach((eventName) => {
[els.viewFilter, els.teamFilter, els.stageFilter, els.venueFilter, els.searchFilter].forEach((el) => {
el.addEventListener(eventName, () => dashboard && renderMatches());
});
});
els.refreshButton.addEventListener('click', () => loadDashboard().catch(showLoadError));
loadDashboard().catch(showLoadError);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sports/FootballVMProphet/web/app.js` around lines 164 - 180, The
loadDashboard function does not check response.ok or handle JSON parsing errors
and the refresh button calls loadDashboard without handling rejections; update
loadDashboard to check response.ok and throw a descriptive error when the fetch
fails, wrap the response.json() in try/catch to handle bad payloads, and ensure
on any error you set els.healthBanner.className to 'health-banner broken' and
set els.healthBanner.textContent to a clear message; also change the
els.refreshButton click handler to call loadDashboard().catch(...) (or otherwise
handle the returned promise) so manual refresh failures are caught and invoke
the same banner update path; reference loadDashboard, els.refreshButton,
els.healthBanner, render, and renderMatches when applying the fix.

--blue: #4dabf7;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--text); font-family: Inter, Segoe UI, Arial, sans-serif; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Quote "Segoe UI" to satisfy the configured Stylelint rule.

This declaration currently violates font-family-name-quotes, so the stylesheet will keep failing lint until the family name is quoted.

Suggested fix
-body { margin: 0; background: var(--bg); color: var(--text); font-family: Inter, Segoe UI, Arial, sans-serif; }
+body { margin: 0; background: var(--bg); color: var(--text); font-family: Inter, "Segoe UI", Arial, sans-serif; }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
body { margin: 0; background: var(--bg); color: var(--text); font-family: Inter, Segoe UI, Arial, sans-serif; }
body { margin: 0; background: var(--bg); color: var(--text); font-family: Inter, "Segoe UI", Arial, sans-serif; }
🧰 Tools
🪛 Stylelint (17.12.0)

[error] 14-14: Expected quotes around "Segoe UI" (font-family-name-quotes)

(font-family-name-quotes)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sports/FootballVMProphet/web/styles.css` at line 14, The font-family
declaration on the body rule uses an unquoted multi-word family name; update the
font-family in the body selector (the body { ... font-family: ... } rule) to
quote "Segoe UI" (use double quotes) so it satisfies the font-family-name-quotes
Stylelint rule, e.g. change Segoe UI to "Segoe UI" while keeping the rest of the
stack unchanged.

Source: Linters/SAST tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant