Files
chess/internal/server/webserver/chess-client-web/index.html
T
lixen e1fa5242e3
test / test (push) Canceled after 0s
test / e2e (push) Canceled after 0s
v0.15.0 password recovery, phone layout, CLI pager, embed policy fixes
2026-10-19 18:15:31 -04:00

372 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="dark">
<title>Chess</title>
<link rel="stylesheet" href="style.css">
</head>
<body data-mode="live">
<div class="app">
<header class="topbar">
<div class="brand" aria-label="Chess">
<svg class="brand-mark" viewBox="41 34 430 430" aria-hidden="true" focusable="false">
<defs>
<linearGradient id="brand-piece" x1="20%" y1="0%" x2="80%" y2="100%">
<stop offset="0%" stop-color="#C8CDD6"/>
<stop offset="45%" stop-color="#8B929E"/>
<stop offset="100%" stop-color="#4F545D"/>
</linearGradient>
</defs>
<g fill="url(#brand-piece)" stroke="#14161A" stroke-width="4" stroke-linejoin="round">
<path d="M136 450Q256 465 376 450L346 410Q256 420 166 410Z"/>
<path d="M166 410Q256 420 346 410L316 370Q256 380 196 370Z"/>
<path d="M196 370Q256 380 316 370C300 290 286 230 282 205H230C226 230 212 290 196 370Z"/>
</g>
<path d="M330 90Q265 96 230 120Q265 144 335 150Z" fill="#111318"/>
<g stroke="#F0F2F5" stroke-width="6" stroke-linecap="round">
<path d="M263.17 85V155M299.33 85V155"/>
</g>
<g fill="url(#brand-piece)" stroke="#14161A" stroke-width="4" stroke-linejoin="round">
<path d="M226 200C150 180 150 70 256 40C290 50 315 65 330 90Q265 96 230 120Q265 144 335 150C335 170 315 190 286 200Z"/>
<rect x="216" y="195" width="80" height="16" rx="8"/>
</g>
</svg>
<span class="brand-name">Chess</span>
</div>
<div class="health" aria-label="Server status">
<span class="dot" id="server-dot" data-status="unknown" title="Server: unknown" role="img" aria-label="Server: unknown"></span>
<span class="dot" id="storage-dot" data-status="unknown" title="Storage: unknown" role="img" aria-label="Storage: unknown"></span>
</div>
<div class="topbar-spacer"></div>
<button id="games-btn" class="btn btn-ghost btn-sm" type="button" title="Stored games">
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h10" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<span>My games</span>
</button>
<button id="auth-btn" class="btn btn-ghost btn-sm" type="button" data-status="anonymous" title="Sign in or register">
<span class="dot" id="auth-dot" data-status="anonymous" aria-hidden="true"></span>
<span id="auth-label">Sign in</span>
</button>
</header>
<main class="workspace">
<section class="board-area" aria-label="Board">
<div class="board-wrapper">
<div class="coordinates files" aria-hidden="true"></div>
<div class="coordinates ranks" aria-hidden="true"></div>
<div id="board" role="grid" aria-label="Chess board"></div>
</div>
<div class="toast-host"><div id="toast" class="toast" role="status" aria-live="polite"></div></div>
</section>
<aside class="panel" aria-label="Game panel">
<!-- Live game -->
<div class="pane pane-live" id="pane-live">
<div class="turn-strip" id="turn-strip">
<span class="dot turn-dot" id="turn-dot" data-status="idle" aria-hidden="true"></span>
<span class="turn-text" id="turn-text">No game</span>
</div>
<div class="controls" role="group" aria-label="Game controls">
<button id="new-game-btn" class="btn btn-primary" type="button" title="New game">New</button>
<button id="undo-btn" class="btn btn-secondary" type="button" disabled>Undo</button>
<button id="draw-btn" class="btn btn-secondary" type="button" title="Offer a draw" disabled>Draw</button>
<button id="resign-btn" class="btn btn-secondary" type="button" title="Resign this game" disabled>Resign</button>
</div>
<div class="moves" id="live-moves-box">
<div class="move-grid" id="live-moves" aria-label="Moves"></div>
<p class="moves-empty" id="live-moves-empty">Start a game to play Stockfish, or open <strong>My games</strong> to replay one.</p>
</div>
<div class="toolbar" role="group" aria-label="Export">
<button id="copy-fen-btn" class="btn btn-tool" type="button" title="Copy the current position as FEN" disabled>
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2" fill="none" stroke="currentColor" stroke-width="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" fill="none" stroke="currentColor" stroke-width="2"/></svg>
<span>FEN</span>
</button>
<button id="copy-pgn-btn" class="btn btn-tool" type="button" title="Copy the game as PGN" disabled>
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2" fill="none" stroke="currentColor" stroke-width="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" fill="none" stroke="currentColor" stroke-width="2"/></svg>
<span>PGN</span>
</button>
<button id="replay-live-btn" class="btn btn-tool" type="button" title="Replay this game from the start" disabled>
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z" fill="currentColor"/></svg>
<span>Replay</span>
</button>
</div>
</div>
<!-- Replay -->
<div class="pane pane-replay" id="pane-replay">
<div class="replay-head">
<div class="replay-title-row">
<span class="badge" id="replay-badge">Replay</span>
<span class="replay-result" id="replay-result"></span>
<span class="replay-ply" id="replay-ply" aria-hidden="true"></span>
</div>
<div class="replay-players" id="replay-players"></div>
<div class="replay-meta" id="replay-meta"></div>
</div>
<div class="moves" id="replay-moves-box">
<div class="move-grid move-grid-replay" id="replay-moves" aria-label="Moves"></div>
<p class="moves-empty" id="replay-moves-empty">Loading…</p>
</div>
<div class="replay-nav" role="group" aria-label="Replay controls">
<button id="replay-first" class="btn btn-nav" type="button" title="First position (Home)" aria-label="First position">
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 5v14M18 5l-9 7 9 7z" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg>
</button>
<button id="replay-prev" class="btn btn-nav" type="button" title="Previous move (←)" aria-label="Previous move">
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M16 5l-9 7 9 7z" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg>
</button>
<button id="replay-play" class="btn btn-nav btn-play" type="button" title="Autoplay (Space)" aria-label="Autoplay" aria-pressed="false">
<svg class="icon icon-play" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z" fill="currentColor"/></svg>
<svg class="icon icon-pause" viewBox="0 0 24 24" aria-hidden="true"><path d="M7 5h4v14H7zM13 5h4v14h-4z" fill="currentColor"/></svg>
</button>
<button id="replay-next" class="btn btn-nav" type="button" title="Next move (→)" aria-label="Next move">
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5l9 7-9 7z" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg>
</button>
<button id="replay-last" class="btn btn-nav" type="button" title="Last position (End)" aria-label="Last position">
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M18 5v14M6 5l9 7-9 7z" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg>
</button>
<label class="speed" title="Autoplay speed">
<span class="visually-hidden">Autoplay speed</span>
<select id="replay-speed">
<option value="2000">0.5×</option>
<option value="1000" selected>1×</option>
<option value="500">2×</option>
<option value="250">4×</option>
</select>
</label>
<button id="replay-flip" class="btn btn-nav" type="button" title="Flip the board (F)" aria-label="Flip the board" aria-pressed="false">
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M7 4v12M7 16l-3-3M7 16l3-3M17 20V8M17 8l-3 3M17 8l3 3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
<div class="toolbar" role="group" aria-label="Export at this position">
<button id="replay-copy-fen" class="btn btn-tool" type="button" title="Copy the FEN of this position">
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2" fill="none" stroke="currentColor" stroke-width="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" fill="none" stroke="currentColor" stroke-width="2"/></svg>
<span>FEN</span>
</button>
<button id="replay-copy-pgn" class="btn btn-tool" type="button" title="Copy the PGN up to this position">
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2" fill="none" stroke="currentColor" stroke-width="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" fill="none" stroke="currentColor" stroke-width="2"/></svg>
<span>PGN</span>
</button>
<button id="replay-download" class="btn btn-tool" type="button" title="Download the PGN up to this position">
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4v11M7 10l5 5 5-5M5 19h14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span>Save</span>
</button>
<button id="replay-link" class="btn btn-tool" type="button" title="Copy a link to this position">
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1.5 1.5M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1.5-1.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<span>Link</span>
</button>
</div>
<button id="replay-exit" class="btn btn-secondary btn-block" type="button">Back to live game</button>
<div id="ply-announcer" class="visually-hidden" aria-live="polite" aria-atomic="true"></div>
</div>
</aside>
</main>
</div>
<!-- My games drawer -->
<div id="games-overlay" class="modal-overlay">
<div class="modal games-modal" role="dialog" aria-modal="true" aria-labelledby="games-title">
<div class="modal-head">
<h2 id="games-title">My games</h2>
<button id="games-close" class="btn btn-icon" type="button" aria-label="Close">
<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
</button>
</div>
<div class="games-filters" id="games-filters">
<div class="segmented" role="group" aria-label="Status">
<button type="button" class="seg active" data-filter="status" data-value="">All</button>
<button type="button" class="seg" data-filter="status" data-value="ongoing">Ongoing</button>
<button type="button" class="seg" data-filter="status" data-value="finished">Finished</button>
</div>
<div class="segmented" role="group" aria-label="Your colour">
<button type="button" class="seg active" data-filter="color" data-value="">Any</button>
<button type="button" class="seg" data-filter="color" data-value="white">White</button>
<button type="button" class="seg" data-filter="color" data-value="black">Black</button>
</div>
</div>
<div class="games-body">
<section class="games-section" id="account-games-section">
<h3 class="games-heading" id="account-games-heading">Your games</h3>
<p class="games-note" id="games-signin-note">Sign in to keep your games and replay them here. Games played anonymously are kept for 24 hours.</p>
<ul class="games-list" id="games-list"></ul>
<p class="games-status" id="games-status" role="status"></p>
<button id="games-more" class="btn btn-secondary btn-block" type="button">Load more</button>
</section>
<section class="games-section" id="local-games-section">
<h3 class="games-heading">On this device</h3>
<p class="games-note">Games played here without signing in; the server keeps them for 24 hours after the last move.</p>
<ul class="games-list" id="local-games-list"></ul>
</section>
</div>
</div>
</div>
<!-- Auth Modal -->
<div id="auth-modal-overlay" class="modal-overlay">
<div class="modal auth-modal" role="dialog" aria-modal="true" aria-labelledby="auth-title">
<h2 id="auth-title" class="visually-hidden">Account</h2>
<div class="auth-tabs" role="tablist">
<button class="auth-tab active" type="button" role="tab" data-tab="login" aria-selected="true">Sign in</button>
<button class="auth-tab" type="button" role="tab" data-tab="register" aria-selected="false">Register</button>
</div>
<div id="auth-modal-message" class="modal-message" role="status" aria-live="polite"></div>
<div id="login-form" class="auth-form">
<div class="form-group">
<label for="login-identifier">Username or email</label>
<input type="text" id="login-identifier" autocomplete="username">
</div>
<div class="form-group">
<label for="login-password">Password</label>
<input type="password" id="login-password" autocomplete="current-password">
</div>
<div class="modal-buttons">
<button id="login-submit-btn" class="btn btn-primary" type="button">Sign in</button>
<button id="auth-cancel-btn" class="btn btn-secondary" type="button">Cancel</button>
</div>
<p class="auth-aside hidden" id="forgot-row"><button id="forgot-btn" class="link-btn" type="button">Forgot password?</button></p>
</div>
<div id="recover-form" class="auth-form hidden">
<p class="auth-help">Enter your username or email. If the account has an email address, a link to choose a new password is sent to it; the link works for an hour.</p>
<div class="form-group">
<label for="recover-identifier">Username or email</label>
<input type="text" id="recover-identifier" autocomplete="username">
</div>
<div class="modal-buttons">
<button id="recover-submit-btn" class="btn btn-primary" type="button">Send reset link</button>
<button id="recover-back-btn" class="btn btn-secondary" type="button">Back</button>
</div>
</div>
<div id="register-form" class="auth-form hidden">
<div class="form-group">
<label for="register-username">Username</label>
<input type="text" id="register-username" autocomplete="username">
</div>
<div class="form-group">
<label for="register-email">Email (optional)</label>
<input type="email" id="register-email" autocomplete="email">
</div>
<div class="form-group">
<label for="register-password">Password (8+ characters, a letter and a number)</label>
<input type="password" id="register-password" autocomplete="new-password">
</div>
<div class="modal-buttons">
<button id="register-submit-btn" class="btn btn-primary" type="button">Register</button>
<button id="auth-cancel-btn-2" class="btn btn-secondary" type="button">Cancel</button>
</div>
</div>
</div>
</div>
<!-- Reset Modal: opened by the link in the recovery email (?reset=<token>) -->
<div id="reset-overlay" class="modal-overlay">
<div class="modal auth-modal" role="dialog" aria-modal="true" aria-labelledby="reset-title">
<h2 id="reset-title">Choose a new password</h2>
<div id="reset-modal-message" class="modal-message" role="status" aria-live="polite"></div>
<div class="form-group">
<label for="reset-password">New password (8+ characters, a letter and a number)</label>
<input type="password" id="reset-password" autocomplete="new-password">
</div>
<div class="form-group">
<label for="reset-password-2">Repeat the password</label>
<input type="password" id="reset-password-2" autocomplete="new-password">
</div>
<div class="modal-buttons">
<button id="reset-submit-btn" class="btn btn-primary" type="button">Set password</button>
<button id="reset-cancel-btn" class="btn btn-secondary" type="button">Cancel</button>
</div>
</div>
</div>
<!-- Account Modal: shown instead of the login modal while signed in -->
<div id="account-overlay" class="modal-overlay">
<div class="modal account-modal" role="dialog" aria-modal="true" aria-labelledby="account-title">
<h2 id="account-title">Account</h2>
<div id="account-modal-message" class="modal-message" role="status" aria-live="polite"></div>
<p class="account-name">Signed in as <strong id="account-username"></strong></p>
<div class="modal-buttons">
<button id="logout-btn" class="btn btn-primary" type="button">Sign out</button>
<button id="account-cancel-btn" class="btn btn-secondary" type="button">Close</button>
</div>
<details class="danger-zone" id="delete-account">
<summary>Delete account</summary>
<p>Deletes the account and signs you out. Your name and claim are removed
from every stored game; games nobody else plays are deleted 24 hours
after their last move. This cannot be undone.</p>
<div class="form-group account-form">
<label for="delete-password">Password</label>
<input type="password" id="delete-password" autocomplete="current-password">
</div>
<div class="modal-buttons">
<button id="delete-account-btn" class="btn btn-danger" type="button">Delete account</button>
</div>
</details>
</div>
</div>
<!-- New Game Modal -->
<div id="modal-overlay" class="modal-overlay">
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="new-game-title">
<h2 id="new-game-title">New game</h2>
<div id="new-game-modal-message" class="modal-message" role="status" aria-live="polite"></div>
<div class="form-group form-row">
<span class="group-label" id="player-color-label">Your colour</span>
<div class="radio-group" role="radiogroup" aria-labelledby="player-color-label">
<label><input type="radio" name="player-color" value="white" checked><span>White</span></label>
<label><input type="radio" name="player-color" value="black"><span>Black</span></label>
</div>
</div>
<div class="form-group">
<label for="computer-level">Computer level: <span id="level-value">10</span></label>
<input type="range" id="computer-level" min="0" max="20" value="10">
</div>
<div class="form-group">
<label for="search-time">Search time: <span id="time-value">1000</span> ms</label>
<input type="range" id="search-time" min="100" max="10000" step="100" value="1000">
</div>
<div class="form-group">
<label for="starting-fen">Starting position (FEN)</label>
<textarea id="starting-fen" class="fen-input" rows="2" spellcheck="false"
placeholder="Enter FEN notation">rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1</textarea>
</div>
<div class="modal-buttons">
<button id="start-game-btn" class="btn btn-primary" type="button">Start game</button>
<button id="cancel-btn" class="btn btn-secondary" type="button">Cancel</button>
</div>
</div>
</div>
<!-- Resign Confirmation -->
<div id="resign-overlay" class="modal-overlay">
<div class="modal confirm-modal" role="dialog" aria-modal="true" aria-labelledby="resign-title">
<h2 id="resign-title">Resign this game?</h2>
<div class="modal-buttons">
<button id="resign-confirm-btn" class="btn btn-primary" type="button">Resign</button>
<button id="resign-cancel-btn" class="btn btn-secondary" type="button">Cancel</button>
</div>
</div>
</div>
<!-- Promotion Modal -->
<div id="promotion-overlay" class="modal-overlay">
<div class="modal promotion-modal" role="dialog" aria-modal="true" aria-labelledby="promotion-title">
<h2 id="promotion-title">Promote to</h2>
<div class="promotion-choices">
<button class="promotion-choice" type="button" data-piece="q" title="Queen (Q)" aria-label="Queen">♛</button>
<button class="promotion-choice" type="button" data-piece="r" title="Rook (R)" aria-label="Rook">♜</button>
<button class="promotion-choice" type="button" data-piece="b" title="Bishop (B)" aria-label="Bishop">♝</button>
<button class="promotion-choice" type="button" data-piece="n" title="Knight (N)" aria-label="Knight">♞</button>
</div>
<div class="modal-buttons">
<button id="promotion-cancel-btn" class="btn btn-secondary" type="button">Cancel</button>
</div>
</div>
</div>
<script src="app.js"></script>
</body>
</html>