chore: automate release checks
This commit is contained in:
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ -f package.json && -f scripts/release-sync.cjs && -f scripts/release-verify.cjs ]]; then
|
||||
npm run --silent release:sync >/dev/null
|
||||
if [[ -d public ]]; then
|
||||
while IFS= read -r -d '' file; do
|
||||
git add -- "$file"
|
||||
done < <(find public -type f -name '*.html' -print0 2>/dev/null)
|
||||
fi
|
||||
npm run --silent release:verify
|
||||
fi
|
||||
@@ -30,3 +30,10 @@ helldivers/
|
||||
|
||||
## Sicherheit
|
||||
- Niemals `data/helldivers.db` committen
|
||||
|
||||
## Release-Automation
|
||||
- **Version ist Single Source of Truth:** `package.json`
|
||||
- **Cache-Busting nie manuell pflegen** – stattdessen `npm run release:sync`
|
||||
- **Vor jedem Commit zusätzlich:** `npm run release:verify`
|
||||
- Repo-Hook: `.githooks/pre-commit` führt `release:sync` und `release:verify` automatisch aus
|
||||
- Für Versionssprünge: `npm run release:bump:patch`, `release:bump:minor` oder `release:bump:major`
|
||||
|
||||
+6
-1
@@ -1,4 +1,9 @@
|
||||
# Changelog – helldivers-trainer
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
- Release-Automation ergänzt: `npm run release:sync`, `npm run release:verify`, `.githooks/pre-commit` und versionsbasiertes Cache-Busting aus `package.json` – helldivers-trainer
|
||||
|
||||
## [2.1.3] – 2026-03-31
|
||||
|
||||
|
||||
@@ -40,3 +40,11 @@ Falls Funktionen, API-Routen, WebSocket-Message-Types oder State-Variablen **hin
|
||||
## Changelog
|
||||
- Nach jeder Änderung `CHANGELOG.md` aktualisieren
|
||||
- Format: `## [Unreleased]` für laufende Änderungen
|
||||
|
||||
## Release Automation
|
||||
|
||||
- Version source of truth: `package.json`
|
||||
- Never update `?v=...` asset parameters manually; use `npm run release:sync`
|
||||
- Run `npm run release:verify` before commit whenever frontend/server/version files changed
|
||||
- Repo hook: `.githooks/pre-commit` runs release sync + verification automatically
|
||||
- Use `npm run release:bump:patch`, `release:bump:minor`, or `release:bump:major` for version bumps
|
||||
|
||||
@@ -132,3 +132,9 @@ state = {
|
||||
|
||||
### WebSocket Message Types (Server → Client)
|
||||
`lobby_update`, `challenge_received`, `challenge_declined`, `match_start`, `round_start`, `round_result`, `match_end`, `opponent_progress`, `error`
|
||||
|
||||
## Release Automation
|
||||
- Version source of truth: `package.json`
|
||||
- `scripts/release-sync.cjs`: synchronisiert lokale CSS/JS-Referenzen in HTML-Dateien auf die aktuelle Paketversion
|
||||
- `scripts/release-verify.cjs`: prüft Version-Parameter sowie Pflichtdateien wie `CHANGELOG.md` und `PROJECT_MAP.md` vor Commits
|
||||
- `.githooks/pre-commit`: führt Release-Sync und Verify automatisch aus
|
||||
|
||||
+6
-1
@@ -5,7 +5,12 @@
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "node --watch server.js"
|
||||
"dev": "node --watch server.js",
|
||||
"release:sync": "node scripts/release-sync.cjs",
|
||||
"release:verify": "node scripts/release-verify.cjs",
|
||||
"release:bump:patch": "npm version patch --no-git-tag-version && npm run release:sync",
|
||||
"release:bump:minor": "npm version minor --no-git-tag-version && npm run release:sync",
|
||||
"release:bump:major": "npm version major --no-git-tag-version && npm run release:sync"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.3",
|
||||
|
||||
+230
-112
@@ -7,7 +7,7 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Exo+2:wght@400;600;700&family=Rajdhani:wght@600;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
<link rel="stylesheet" href="styles.css?v=1.0.0">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -55,24 +55,47 @@
|
||||
|
||||
<!-- ── LOGIN ─────────────────────────────────────────────────── -->
|
||||
<div id="view-login" class="view view-centered">
|
||||
<div class="login-box">
|
||||
<div class="login-header">
|
||||
<span class="login-logo">⚡</span>
|
||||
<h1>HELLDIVERS 2</h1>
|
||||
<p class="login-sub">STRATAGEM TRAINER — SUPER EARTH AUTHORIZED</p>
|
||||
<div class="login-shell">
|
||||
<section class="login-intel card">
|
||||
<div class="eyebrow">Super Earth Command</div>
|
||||
<h1 class="login-intel-title">Deploy faster. Input cleaner. Win harder.</h1>
|
||||
<p class="login-intel-copy">
|
||||
Train every stratagem like a live-fire drill, chase leaderboard position, and challenge active Helldivers in the arena.
|
||||
</p>
|
||||
<div class="login-intel-grid">
|
||||
<div class="intel-stat">
|
||||
<span class="intel-stat-value">4</span>
|
||||
<span class="intel-stat-label">Training Modes</span>
|
||||
</div>
|
||||
<div class="intel-stat">
|
||||
<span class="intel-stat-value">1v1</span>
|
||||
<span class="intel-stat-label">Arena Duels</span>
|
||||
</div>
|
||||
<div class="intel-stat">
|
||||
<span class="intel-stat-value">ELO</span>
|
||||
<span class="intel-stat-label">Rank Tracking</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="login-box">
|
||||
<div class="login-header">
|
||||
<span class="login-logo">⚡</span>
|
||||
<h1>HELLDIVERS 2</h1>
|
||||
<p class="login-sub">STRATAGEM TRAINER — SUPER EARTH AUTHORIZED</p>
|
||||
</div>
|
||||
<form id="login-form" class="login-form" autocomplete="off">
|
||||
<div class="field">
|
||||
<label for="login-username">Helldiver ID</label>
|
||||
<input id="login-username" type="text" placeholder="Username" autocomplete="username" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="login-password">Access Code</label>
|
||||
<input id="login-password" type="password" placeholder="Password" autocomplete="current-password" required>
|
||||
</div>
|
||||
<p id="login-error" class="error hidden"></p>
|
||||
<button type="submit" class="btn btn-accent btn-full">AUTHENTICATE</button>
|
||||
</form>
|
||||
</div>
|
||||
<form id="login-form" class="login-form" autocomplete="off">
|
||||
<div class="field">
|
||||
<label for="login-username">Helldiver ID</label>
|
||||
<input id="login-username" type="text" placeholder="Username" autocomplete="username" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="login-password">Access Code</label>
|
||||
<input id="login-password" type="password" placeholder="Password" autocomplete="current-password" required>
|
||||
</div>
|
||||
<p id="login-error" class="error hidden"></p>
|
||||
<button type="submit" class="btn btn-accent btn-full">AUTHENTICATE</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -105,6 +128,28 @@
|
||||
<!-- ── DASHBOARD ─────────────────────────────────────────────── -->
|
||||
<div id="view-dashboard" class="view hidden">
|
||||
<div class="dashboard-grid">
|
||||
<div class="card dashboard-briefing">
|
||||
<div class="briefing-copy">
|
||||
<div class="eyebrow">Mission Briefing</div>
|
||||
<h2 class="briefing-title">Your command deck for practice, duels, and rank progress.</h2>
|
||||
<p class="briefing-text" id="dash-status-line">Loading tactical status...</p>
|
||||
<div class="briefing-pills">
|
||||
<span class="status-pill">
|
||||
<span class="status-pill-label">Online</span>
|
||||
<strong id="dash-online-count">0</strong>
|
||||
</span>
|
||||
<span class="status-pill">
|
||||
<span class="status-pill-label">Daily</span>
|
||||
<strong id="dash-daily-focus">Pending</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="briefing-actions">
|
||||
<button class="btn btn-accent" id="btn-briefing-practice">⚡ Quick Start</button>
|
||||
<button class="btn btn-muted" id="btn-briefing-lobby">⚔ Open Arena</button>
|
||||
<button class="btn btn-muted" id="btn-briefing-leaderboard">Hall of Heroes</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hero card -->
|
||||
<div class="card card-hero dashboard-hero">
|
||||
@@ -162,14 +207,16 @@
|
||||
<!-- Recent sessions -->
|
||||
<div class="card">
|
||||
<h3 class="card-title">Recent Sessions</h3>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr><th>Stratagem</th><th>Mode</th><th>Score</th><th>Time</th></tr>
|
||||
</thead>
|
||||
<tbody id="dash-recent">
|
||||
<tr><td colspan="4" class="muted">No sessions yet</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr><th>Stratagem</th><th>Mode</th><th>Score</th><th>Time</th></tr>
|
||||
</thead>
|
||||
<tbody id="dash-recent">
|
||||
<tr><td colspan="4" class="muted">No sessions yet</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -234,72 +281,80 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stratagem-display card">
|
||||
<!-- Stratagem icon -->
|
||||
<div class="stratagem-icon-wrap">
|
||||
<img id="practice-icon" class="stratagem-icon-lg" src="" alt="" style="display:none">
|
||||
<div id="practice-icon-fallback" class="stratagem-icon-fallback">⚡</div>
|
||||
<div class="practice-focus-shell">
|
||||
<div class="practice-focus-meta">
|
||||
<span class="focus-chip">Live Drill</span>
|
||||
<span class="focus-chip focus-chip-muted">Stay on target</span>
|
||||
</div>
|
||||
<div class="stratagem-category" id="practice-category"></div>
|
||||
<div class="stratagem-name" id="practice-name"></div>
|
||||
<div class="arrow-sequence" id="practice-sequence"></div>
|
||||
<div class="practice-hint">Arrow Keys or D-Pad · <kbd>Esc</kbd> to stop</div>
|
||||
</div>
|
||||
<div id="practice-feedback" class="gameplay-feedback hidden" aria-live="polite"></div>
|
||||
|
||||
<!-- Upcoming queue -->
|
||||
<div class="stratagem-queue" id="practice-queue"></div>
|
||||
<div class="stratagem-display card">
|
||||
<!-- Stratagem icon -->
|
||||
<div class="stratagem-icon-wrap">
|
||||
<img id="practice-icon" class="stratagem-icon-lg" src="" alt="" style="display:none">
|
||||
<div id="practice-icon-fallback" class="stratagem-icon-fallback">⚡</div>
|
||||
</div>
|
||||
<div class="stratagem-category" id="practice-category"></div>
|
||||
<div class="stratagem-name" id="practice-name"></div>
|
||||
<div class="arrow-sequence arrow-sequence-hero" id="practice-sequence"></div>
|
||||
<div class="practice-hint">Arrow Keys or D-Pad · <kbd>Esc</kbd> to stop</div>
|
||||
</div>
|
||||
|
||||
<div class="practice-hud">
|
||||
<!-- Timer ring / lives / elapsed -->
|
||||
<div class="hud-item" id="hud-timer-wrap">
|
||||
<div class="hud-label" id="hud-timer-label">TIME</div>
|
||||
<div class="timer-ring-wrap">
|
||||
<svg class="timer-ring-svg" viewBox="0 0 80 80">
|
||||
<circle class="timer-ring-bg" cx="40" cy="40" r="35"/>
|
||||
<circle class="timer-ring-fill" id="timer-ring-fill" cx="40" cy="40" r="35"
|
||||
stroke-dasharray="219.9" stroke-dashoffset="0"/>
|
||||
</svg>
|
||||
<div class="timer-ring-val" id="practice-timer">30</div>
|
||||
<!-- Upcoming queue -->
|
||||
<div class="stratagem-queue" id="practice-queue"></div>
|
||||
|
||||
<div class="practice-hud">
|
||||
<!-- Timer ring / lives / elapsed -->
|
||||
<div class="hud-item" id="hud-timer-wrap">
|
||||
<div class="hud-label" id="hud-timer-label">TIME</div>
|
||||
<div class="timer-ring-wrap">
|
||||
<svg class="timer-ring-svg" viewBox="0 0 80 80">
|
||||
<circle class="timer-ring-bg" cx="40" cy="40" r="35"/>
|
||||
<circle class="timer-ring-fill" id="timer-ring-fill" cx="40" cy="40" r="35"
|
||||
stroke-dasharray="219.9" stroke-dashoffset="0"/>
|
||||
</svg>
|
||||
<div class="timer-ring-val" id="practice-timer">30</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lives (endless mode) -->
|
||||
<div class="hud-item hidden" id="hud-lives-wrap">
|
||||
<div class="hud-label">LIVES</div>
|
||||
<div class="lives-display" id="practice-lives">
|
||||
<span class="life-icon">❤</span>
|
||||
<span class="life-icon">❤</span>
|
||||
<span class="life-icon">❤</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hud-item">
|
||||
<div class="hud-label">SCORE</div>
|
||||
<div class="hud-value" id="practice-score">0</div>
|
||||
</div>
|
||||
<div class="hud-item" id="hud-streak-item">
|
||||
<div class="hud-label">STREAK</div>
|
||||
<div class="hud-value accent" id="practice-streak">0</div>
|
||||
<div class="combo-badge hidden" id="practice-combo">×1.0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lives (endless mode) -->
|
||||
<div class="hud-item hidden" id="hud-lives-wrap">
|
||||
<div class="hud-label">LIVES</div>
|
||||
<div class="lives-display" id="practice-lives">
|
||||
<span class="life-icon">❤</span>
|
||||
<span class="life-icon">❤</span>
|
||||
<span class="life-icon">❤</span>
|
||||
<!-- D-Pad -->
|
||||
<div class="dpad" id="practice-dpad">
|
||||
<div class="dpad-row">
|
||||
<button class="dpad-btn" data-dir="up" aria-label="Arrow up"><span class="dir-glyph dir-up" aria-hidden="true"><svg viewBox="0 0 64 64" focusable="false"><path class="dir-line" d="M32 50 L32 18" /><path class="dir-head" d="M32 12 L20 24 H28 V50 H36 V24 H44 Z" /></svg></span></button>
|
||||
</div>
|
||||
<div class="dpad-row">
|
||||
<button class="dpad-btn" data-dir="left" aria-label="Arrow left"><span class="dir-glyph dir-left" aria-hidden="true"><svg viewBox="0 0 64 64" focusable="false"><path class="dir-line" d="M32 50 L32 18" /><path class="dir-head" d="M32 12 L20 24 H28 V50 H36 V24 H44 Z" /></svg></span></button>
|
||||
<div class="dpad-center"></div>
|
||||
<button class="dpad-btn" data-dir="right" aria-label="Arrow right"><span class="dir-glyph dir-right" aria-hidden="true"><svg viewBox="0 0 64 64" focusable="false"><path class="dir-line" d="M32 50 L32 18" /><path class="dir-head" d="M32 12 L20 24 H28 V50 H36 V24 H44 Z" /></svg></span></button>
|
||||
</div>
|
||||
<div class="dpad-row">
|
||||
<button class="dpad-btn" data-dir="down" aria-label="Arrow down"><span class="dir-glyph dir-down" aria-hidden="true"><svg viewBox="0 0 64 64" focusable="false"><path class="dir-line" d="M32 50 L32 18" /><path class="dir-head" d="M32 12 L20 24 H28 V50 H36 V24 H44 Z" /></svg></span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hud-item">
|
||||
<div class="hud-label">SCORE</div>
|
||||
<div class="hud-value" id="practice-score">0</div>
|
||||
</div>
|
||||
<div class="hud-item" id="hud-streak-item">
|
||||
<div class="hud-label">STREAK</div>
|
||||
<div class="hud-value accent" id="practice-streak">0</div>
|
||||
<div class="combo-badge hidden" id="practice-combo">×1.0</div>
|
||||
</div>
|
||||
<button class="btn btn-muted btn-sm" id="btn-stop-practice">■ Stop Training</button>
|
||||
</div>
|
||||
|
||||
<!-- D-Pad -->
|
||||
<div class="dpad" id="practice-dpad">
|
||||
<div class="dpad-row">
|
||||
<button class="dpad-btn" data-dir="up" aria-label="Arrow up">↑</button>
|
||||
</div>
|
||||
<div class="dpad-row">
|
||||
<button class="dpad-btn" data-dir="left" aria-label="Arrow left">←</button>
|
||||
<div class="dpad-center"></div>
|
||||
<button class="dpad-btn" data-dir="right" aria-label="Arrow right">→</button>
|
||||
</div>
|
||||
<div class="dpad-row">
|
||||
<button class="dpad-btn" data-dir="down" aria-label="Arrow down">↓</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-muted btn-sm" id="btn-stop-practice">■ Stop Training</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -406,27 +461,38 @@
|
||||
<div style="text-align:center;margin-bottom:8px">
|
||||
<img id="match-icon" class="stratagem-icon-md" src="" alt="" style="display:none">
|
||||
</div>
|
||||
<div class="match-sequences">
|
||||
<div class="match-seq-col">
|
||||
<div class="match-seq-label">YOU</div>
|
||||
<div class="arrow-sequence" id="match-me-sequence"></div>
|
||||
<div class="match-focus-shell">
|
||||
<div id="match-feedback" class="gameplay-feedback hidden" aria-live="polite"></div>
|
||||
<div class="match-sequences">
|
||||
<div class="match-seq-col">
|
||||
<div class="match-seq-label">YOU</div>
|
||||
<div class="duel-progress">
|
||||
<div class="duel-progress-bar"><div class="duel-progress-fill" id="match-me-progress-fill" style="width:0%"></div></div>
|
||||
<div class="duel-progress-text" id="match-me-progress-text">0 / 0</div>
|
||||
</div>
|
||||
<div class="arrow-sequence arrow-sequence-hero arrow-sequence-duel" id="match-me-sequence"></div>
|
||||
</div>
|
||||
<div class="match-seq-col">
|
||||
<div class="match-seq-label">OPPONENT</div>
|
||||
<div class="duel-progress">
|
||||
<div class="duel-progress-bar"><div class="duel-progress-fill duel-progress-fill-opp" id="match-opp-progress-fill" style="width:0%"></div></div>
|
||||
<div class="duel-progress-text" id="match-opp-progress-text">0 / 0</div>
|
||||
</div>
|
||||
<div class="arrow-sequence arrow-sequence-hero arrow-sequence-duel" id="match-opp-sequence"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="match-seq-col">
|
||||
<div class="match-seq-label">OPPONENT</div>
|
||||
<div class="arrow-sequence" id="match-opp-sequence"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dpad" id="match-dpad">
|
||||
<div class="dpad-row">
|
||||
<button class="dpad-btn" data-dir="up" aria-label="Arrow up">↑</button>
|
||||
</div>
|
||||
<div class="dpad-row">
|
||||
<button class="dpad-btn" data-dir="left" aria-label="Arrow left">←</button>
|
||||
<div class="dpad-center"></div>
|
||||
<button class="dpad-btn" data-dir="right" aria-label="Arrow right">→</button>
|
||||
</div>
|
||||
<div class="dpad-row">
|
||||
<button class="dpad-btn" data-dir="down" aria-label="Arrow down">↓</button>
|
||||
<div class="dpad" id="match-dpad">
|
||||
<div class="dpad-row">
|
||||
<button class="dpad-btn" data-dir="up" aria-label="Arrow up"><span class="dir-glyph dir-up" aria-hidden="true"><svg viewBox="0 0 64 64" focusable="false"><path class="dir-line" d="M32 50 L32 18" /><path class="dir-head" d="M32 12 L20 24 H28 V50 H36 V24 H44 Z" /></svg></span></button>
|
||||
</div>
|
||||
<div class="dpad-row">
|
||||
<button class="dpad-btn" data-dir="left" aria-label="Arrow left"><span class="dir-glyph dir-left" aria-hidden="true"><svg viewBox="0 0 64 64" focusable="false"><path class="dir-line" d="M32 50 L32 18" /><path class="dir-head" d="M32 12 L20 24 H28 V50 H36 V24 H44 Z" /></svg></span></button>
|
||||
<div class="dpad-center"></div>
|
||||
<button class="dpad-btn" data-dir="right" aria-label="Arrow right"><span class="dir-glyph dir-right" aria-hidden="true"><svg viewBox="0 0 64 64" focusable="false"><path class="dir-line" d="M32 50 L32 18" /><path class="dir-head" d="M32 12 L20 24 H28 V50 H36 V24 H44 Z" /></svg></span></button>
|
||||
</div>
|
||||
<div class="dpad-row">
|
||||
<button class="dpad-btn" data-dir="down" aria-label="Arrow down"><span class="dir-glyph dir-down" aria-hidden="true"><svg viewBox="0 0 64 64" focusable="false"><path class="dir-line" d="M32 50 L32 18" /><path class="dir-head" d="M32 12 L20 24 H28 V50 H36 V24 H44 Z" /></svg></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -451,14 +517,16 @@
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<table class="data-table">
|
||||
<thead id="leaderboard-thead">
|
||||
<tr><th>#</th><th>Helldiver</th><th>Rank</th><th>Total Score</th><th>Sessions</th><th>Match W/Total</th></tr>
|
||||
</thead>
|
||||
<tbody id="leaderboard-table-body">
|
||||
<tr><td colspan="6" class="muted">Loading...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<thead id="leaderboard-thead">
|
||||
<tr><th>#</th><th>Helldiver</th><th>Rank</th><th>Total Score</th><th>Sessions</th><th>Match W/Total</th></tr>
|
||||
</thead>
|
||||
<tbody id="leaderboard-table-body">
|
||||
<tr><td colspan="6" class="muted">Loading...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -466,6 +534,29 @@
|
||||
<div id="view-admin" class="view hidden">
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">ADMIN PANEL</h2>
|
||||
<p class="page-sub">User control, password operations, role management, and live system activity</p>
|
||||
</div>
|
||||
<div class="admin-overview" id="admin-overview">
|
||||
<div class="card admin-stat-card">
|
||||
<div class="card-title">Helldivers</div>
|
||||
<div class="admin-stat-value" id="admin-total-users">—</div>
|
||||
<div class="admin-stat-meta">Registered accounts</div>
|
||||
</div>
|
||||
<div class="card admin-stat-card">
|
||||
<div class="card-title">Admins</div>
|
||||
<div class="admin-stat-value" id="admin-total-admins">—</div>
|
||||
<div class="admin-stat-meta">Users with command access</div>
|
||||
</div>
|
||||
<div class="card admin-stat-card">
|
||||
<div class="card-title">Temp Passwords</div>
|
||||
<div class="admin-stat-value" id="admin-temp-passwords">—</div>
|
||||
<div class="admin-stat-meta">Accounts requiring password change</div>
|
||||
</div>
|
||||
<div class="card admin-stat-card">
|
||||
<div class="card-title">Practice Sessions</div>
|
||||
<div class="admin-stat-value" id="admin-practice-sessions">—</div>
|
||||
<div class="admin-stat-meta">Total runs recorded</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-layout">
|
||||
<div class="card">
|
||||
@@ -484,12 +575,35 @@
|
||||
<p id="admin-error" class="error hidden"></p>
|
||||
<button class="btn btn-accent" id="btn-create-user">Create User</button>
|
||||
<div id="new-pw-display" class="pw-display hidden"></div>
|
||||
<div class="divider"></div>
|
||||
<div class="admin-spotlight">
|
||||
<div class="admin-spotlight-label">Top performer</div>
|
||||
<div class="admin-spotlight-value" id="admin-top-user">—</div>
|
||||
<div class="admin-spotlight-meta" id="admin-top-user-meta">Loading tactical data...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3 class="card-title">Active Helldivers</h3>
|
||||
<div class="admin-users-header">
|
||||
<h3 class="card-title">User Management</h3>
|
||||
<input id="admin-user-search" class="admin-search" type="text" placeholder="Search user">
|
||||
</div>
|
||||
<div id="admin-users" class="admin-user-list">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="admin-activity-grid">
|
||||
<div class="card">
|
||||
<h3 class="card-title">Recent Practice Activity</h3>
|
||||
<div id="admin-recent-practice" class="admin-activity-list">
|
||||
<span class="muted">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3 class="card-title">Recent Matches</h3>
|
||||
<div id="admin-recent-matches" class="admin-activity-list">
|
||||
<span class="muted">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── PRACTICE SETTINGS MODAL ──────────────────────────────── -->
|
||||
@@ -566,6 +680,9 @@
|
||||
</div>
|
||||
<div class="summary-grid" id="summary-grid"></div>
|
||||
<div style="height:1px;background:var(--border);margin:16px 0"></div>
|
||||
<h4 style="font-family:var(--font-heading);font-size:0.85rem;letter-spacing:.06em;color:var(--text-muted);margin-bottom:8px">PERFORMANCE ANALYSIS</h4>
|
||||
<div id="summary-insights" class="summary-insights"></div>
|
||||
<div style="height:1px;background:var(--border);margin:16px 0"></div>
|
||||
<h4 style="font-family:var(--font-heading);font-size:0.85rem;letter-spacing:.06em;color:var(--text-muted);margin-bottom:8px">TOP STRATAGEMS</h4>
|
||||
<div id="summary-top-stratagems"></div>
|
||||
<div class="modal-actions">
|
||||
@@ -577,10 +694,11 @@
|
||||
|
||||
<!-- ── Danger vignette (≤5s) ──────────────────────────────────── -->
|
||||
<div id="danger-vignette" class="danger-vignette hidden"></div>
|
||||
<div id="gameplay-countdown" class="gameplay-countdown hidden" aria-live="assertive"></div>
|
||||
|
||||
<!-- Toast notifications -->
|
||||
<div id="toast-container"></div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
<script src="app.js?v=1.0.0"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const repoRoot = process.cwd();
|
||||
const pkgPath = path.join(repoRoot, 'package.json');
|
||||
const publicDir = path.join(repoRoot, 'public');
|
||||
if (!fs.existsSync(pkgPath) || !fs.existsSync(publicDir)) process.exit(0);
|
||||
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
||||
const version = pkg.version;
|
||||
if (!version) {
|
||||
console.error('package.json version missing');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function walkHtmlFiles(dir) {
|
||||
const out = [];
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) out.push(...walkHtmlFiles(full));
|
||||
else if (entry.isFile() && entry.name.endsWith('.html')) out.push(full);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function isLocalAsset(rawUrl) {
|
||||
return !/^(?:[a-z]+:|\/\/|#)/i.test(rawUrl);
|
||||
}
|
||||
|
||||
function withVersion(rawUrl) {
|
||||
const [baseWithQuery, hash = ''] = rawUrl.split('#');
|
||||
const [pathname, query = ''] = baseWithQuery.split('?');
|
||||
const params = new URLSearchParams(query);
|
||||
params.set('v', version);
|
||||
const qs = params.toString();
|
||||
return pathname + (qs ? '?' + qs : '') + (hash ? '#' + hash : '');
|
||||
}
|
||||
|
||||
const htmlFiles = walkHtmlFiles(publicDir);
|
||||
let changed = 0;
|
||||
for (const file of htmlFiles) {
|
||||
let content = fs.readFileSync(file, 'utf8');
|
||||
const next = content
|
||||
.replace(/(<link\b[^>]*href=["'])([^"']+\.css(?:\?[^"']*)?)(["'][^>]*>)/gi, (m, p1, url, p3) => {
|
||||
return isLocalAsset(url) ? p1 + withVersion(url) + p3 : m;
|
||||
})
|
||||
.replace(/(<script\b(?![^>]*type=["']application\/json["'])[^>]*\bsrc=["'])([^"']+\.js(?:\?[^"']*)?)(["'][^>]*>\s*<\/script>)/gi, (m, p1, url, p3) => {
|
||||
return isLocalAsset(url) ? p1 + withVersion(url) + p3 : m;
|
||||
});
|
||||
if (next !== content) {
|
||||
fs.writeFileSync(file, next);
|
||||
changed += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed > 0) console.log('Synced versioned assets in ' + changed + ' HTML file(s).');
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const cp = require('child_process');
|
||||
|
||||
const repoRoot = process.cwd();
|
||||
const pkgPath = path.join(repoRoot, 'package.json');
|
||||
const publicDir = path.join(repoRoot, 'public');
|
||||
if (!fs.existsSync(pkgPath) || !fs.existsSync(publicDir)) process.exit(0);
|
||||
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
||||
const version = pkg.version;
|
||||
const failures = [];
|
||||
|
||||
function git(args) {
|
||||
return cp.execFileSync('git', args, { cwd: repoRoot, encoding: 'utf8' }).trim();
|
||||
}
|
||||
|
||||
function walkHtmlFiles(dir) {
|
||||
const out = [];
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) out.push(...walkHtmlFiles(full));
|
||||
else if (entry.isFile() && entry.name.endsWith('.html')) out.push(full);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function isLocalAsset(rawUrl) {
|
||||
return !/^(?:[a-z]+:|\/\/|#)/i.test(rawUrl);
|
||||
}
|
||||
|
||||
for (const file of walkHtmlFiles(publicDir)) {
|
||||
const rel = path.relative(repoRoot, file);
|
||||
const content = fs.readFileSync(file, 'utf8');
|
||||
for (const match of content.matchAll(/<link\b[^>]*href=["']([^"']+\.css(?:\?[^"']*)?)["']/gi)) {
|
||||
const url = match[1];
|
||||
if (isLocalAsset(url) && !new URLSearchParams((url.split('?')[1] || '')).get('v')?.includes(version)) {
|
||||
failures.push(rel + ': stylesheet version does not match package.json');
|
||||
}
|
||||
}
|
||||
for (const match of content.matchAll(/<script\b(?![^>]*type=["']application\/json["'])[^>]*\bsrc=["']([^"']+\.js(?:\?[^"']*)?)["']/gi)) {
|
||||
const url = match[1];
|
||||
if (isLocalAsset(url) && !new URLSearchParams((url.split('?')[1] || '')).get('v')?.includes(version)) {
|
||||
failures.push(rel + ': script version does not match package.json');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const staged = git(['diff', '--cached', '--name-only', '--diff-filter=ACMR']).split(/\n+/).filter(Boolean);
|
||||
const codeTouched = staged.some(file => /^(public\/.*\.(html|css|js)|server\.js|package\.json|package-lock\.json)$/.test(file));
|
||||
const projectMapTouched = staged.includes('PROJECT_MAP.md');
|
||||
const changelogTouched = staged.includes('CHANGELOG.md');
|
||||
if (codeTouched && !changelogTouched) failures.push('CHANGELOG.md must be staged when app code, assets, or version files change.');
|
||||
if (codeTouched && fs.existsSync(path.join(repoRoot, 'PROJECT_MAP.md')) && !projectMapTouched) {
|
||||
failures.push('PROJECT_MAP.md must be staged when app code or frontend state/routes change.');
|
||||
}
|
||||
|
||||
const stagedPackageDiff = staged.includes('package.json')
|
||||
? git(['diff', '--cached', '--', 'package.json'])
|
||||
: '';
|
||||
if (stagedPackageDiff.includes('"version"') && fs.existsSync(path.join(repoRoot, 'CHANGELOG.md'))) {
|
||||
const changelog = fs.readFileSync(path.join(repoRoot, 'CHANGELOG.md'), 'utf8');
|
||||
if (!changelog.includes('## [' + version + ']') && !changelog.includes('## [Unreleased]')) {
|
||||
failures.push('CHANGELOG.md must mention the current package.json version or contain an [Unreleased] section.');
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length) {
|
||||
console.error('Release verification failed:');
|
||||
for (const failure of failures) console.error('- ' + failure);
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user