Compare commits
61 Commits
e7e65d0b3f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f8127641d | |||
| e791425b2b | |||
| 9b25768a3f | |||
| 69bcf467bc | |||
| 22a6fa02fd | |||
| b2b19f6e8b | |||
| 5e8f2ba695 | |||
| 8eeb359a08 | |||
| 9f98168934 | |||
| e1bfeb2b45 | |||
| c84aa750ae | |||
| f5570cea25 | |||
| 1f67ba46fb | |||
| 7649ba2fb9 | |||
| a6e007762d | |||
| 2dc41cc758 | |||
| d75234b15f | |||
| e5d755980e | |||
| dab6a517e3 | |||
| 9444b9b64f | |||
| de962eb8a0 | |||
| 39ef9f039f | |||
| 62ac188c18 | |||
| 489f6a7728 | |||
| a09b341be7 | |||
| 89f0858bce | |||
| a3d3bd18a9 | |||
| df1d7cbd63 | |||
| 5f9dc9ba7b | |||
| e0815fc279 | |||
| 9b46b9f052 | |||
| 198109e14e | |||
| 39c563ff37 | |||
| e6677df9ae | |||
| 795764c359 | |||
| d134947b14 | |||
| 938251ad40 | |||
| 351f0bf1fe | |||
| c3818e6417 | |||
| 3bf1e93543 | |||
| 224300c1c3 | |||
| 5582df99b7 | |||
| 0da74c6484 | |||
| f11afb3600 | |||
| 73ba5f4d3c | |||
| d5db7d7ea4 | |||
| 15528c2062 | |||
| 2c5f276f53 | |||
| 7330563ae6 | |||
| a90b1b6d8a | |||
| 7d0be5baa5 | |||
| 2d5a4adf99 | |||
| 5a287be786 | |||
| c00e2369a8 | |||
| ec87a57712 | |||
| ac1e20603e | |||
| 0b4f8f07a1 | |||
| a095f2d4fa | |||
| 1d71956ec9 | |||
| e1ef2b8255 | |||
| 041d97378a |
96
README.md
96
README.md
@@ -6,14 +6,14 @@ Application web mobile-first pour téléphone et tablette, pensée comme applica
|
||||
|
||||
- configure une rencontre `Twice` ou `Time`
|
||||
- sépare l'application en pages dédiées : configuration, phase chrono, phase cube
|
||||
- permet de définir librement le temps de block et le temps par coup
|
||||
- permet de définir librement le temps de partie et le temps par coup
|
||||
- suit les quotas `FAST`, `FREEZE` et `MASTERS`
|
||||
- orchestre la phase cube avec désignation du cube, capture des temps et préparation du block suivant
|
||||
- orchestre la phase cube avec désignation du cube, capture des temps et préparation de la partie suivante
|
||||
- applique la logique du double coup V2 en `Twice`
|
||||
- applique les ajustements `bloc -` et `bloc +` en `Time` avec plafond de 120 s pris en compte
|
||||
- conserve un historique local dans le navigateur
|
||||
- propose une page chrono pensée pour le téléphone avec deux grandes zones tactiles, une par joueur
|
||||
- ouvre automatiquement la page cube dès que la phase chess du block est terminée
|
||||
- ouvre automatiquement la page cube dès que la phase chess de la partie est terminée
|
||||
|
||||
## Hypothèse de produit
|
||||
|
||||
@@ -28,6 +28,94 @@ docker compose up -d --build
|
||||
|
||||
L'application est ensuite disponible sur `http://localhost:8080`.
|
||||
|
||||
## Déploiement dans un LXC Proxmox
|
||||
|
||||
Deux scripts Bash permettent de créer un conteneur LXC Debian sur Proxmox puis de le mettre à jour depuis Git.
|
||||
|
||||
Prérequis sur la machine qui lance les scripts :
|
||||
|
||||
- en mode distant : `ssh` et `sshpass`
|
||||
- en mode local sur l'hôte Proxmox : aucun paquet supplémentaire n'est installé sur Proxmox
|
||||
|
||||
Le déploiement dans le LXC n'utilise pas Docker. Le script installe `nginx`, `git` et `rsync` dans le conteneur, clone le dépôt principal, synchronise aussi le projet d'Ethan, puis publie uniquement les fichiers web.
|
||||
|
||||
### Installer un nouveau LXC
|
||||
|
||||
```bash
|
||||
./scripts/install-proxmox-lxc.sh \
|
||||
--proxmox-host 10.0.0.2 \
|
||||
--proxmox-user root@pam \
|
||||
--proxmox-password 'secret'
|
||||
```
|
||||
|
||||
Version "curl | bash" :
|
||||
|
||||
```bash
|
||||
bash -c "$(curl -fsSL https://git.jeannerot.fr/christophe/chesscubing/raw/branch/main/install-chesscubing-proxmox.sh)"
|
||||
```
|
||||
|
||||
Cette version pose les questions nécessaires si les variables d'environnement ne sont pas déjà définies.
|
||||
|
||||
Si elle est lancée directement sur l'hôte Proxmox, elle passe automatiquement en mode local :
|
||||
|
||||
- elle ne demande ni serveur, ni login, ni mot de passe SSH
|
||||
- elle n'installe rien sur l'hôte Proxmox
|
||||
- elle crée uniquement le LXC puis installe les dépendances dans ce LXC
|
||||
|
||||
Valeurs par défaut utiles :
|
||||
|
||||
- LXC nommé `chesscubing-web`
|
||||
- IP du LXC en `dhcp`
|
||||
- branche Git `main`
|
||||
- dépôt `https://git.jeannerot.fr/christophe/chesscubing.git`
|
||||
- dépôt Ethan `https://git.jeannerot.fr/Mineloulou/Chesscubing.git`
|
||||
|
||||
Options utiles si besoin :
|
||||
|
||||
- `--ctid 120`
|
||||
- `--lxc-ip 192.168.1.50/24 --gateway 192.168.1.1`
|
||||
- `--template-storage local`
|
||||
- `--rootfs-storage local-lvm`
|
||||
- `--branch main`
|
||||
- `--ethan-branch main`
|
||||
|
||||
À la fin, le script affiche :
|
||||
|
||||
- le `CTID`
|
||||
- le mot de passe `root` du LXC
|
||||
- l'URL probable du site
|
||||
|
||||
### Mettre à jour depuis Git
|
||||
|
||||
```bash
|
||||
./scripts/update-proxmox-lxc.sh \
|
||||
--proxmox-host 10.0.0.2 \
|
||||
--proxmox-user root@pam \
|
||||
--proxmox-password 'secret' \
|
||||
--ctid 120
|
||||
```
|
||||
|
||||
Version "curl | bash" :
|
||||
|
||||
```bash
|
||||
bash -c "$(curl -fsSL https://git.jeannerot.fr/christophe/chesscubing/raw/branch/main/update-chesscubing-proxmox.sh)"
|
||||
```
|
||||
|
||||
Sur l'hôte Proxmox, cette commande met à jour le LXC local sans passer par SSH.
|
||||
Par défaut, elle cible le conteneur `chesscubing-web` sans demander le `CTID`.
|
||||
|
||||
On peut aussi cibler le conteneur par nom si on n'a pas le `CTID` :
|
||||
|
||||
```bash
|
||||
./scripts/update-proxmox-lxc.sh \
|
||||
--proxmox-host 10.0.0.2 \
|
||||
--proxmox-user root@pam \
|
||||
--proxmox-password 'secret' \
|
||||
--hostname chesscubing-web
|
||||
```
|
||||
|
||||
Le script de mise à jour exécute un `git pull --ff-only` pour le dépôt principal et le dépôt d'Ethan dans le conteneur, puis republie les fichiers statiques via `nginx`, y compris la route `/ethan/`.
|
||||
|
||||
## Fichiers clés
|
||||
|
||||
- `index.html` : page d'accueil du site
|
||||
@@ -38,3 +126,5 @@ L'application est ensuite disponible sur `http://localhost:8080`.
|
||||
- `styles.css` : design mobile/tablette
|
||||
- `app.js` : logique de match et arbitrage
|
||||
- `docker-compose.yml` + `Dockerfile` : exécution locale
|
||||
- `scripts/install-proxmox-lxc.sh` : création et déploiement d'un LXC Proxmox
|
||||
- `scripts/update-proxmox-lxc.sh` : mise à jour d'un LXC existant depuis Git
|
||||
|
||||
113
application.html
113
application.html
@@ -2,14 +2,61 @@
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#140700" />
|
||||
<meta name="application-name" content="ChessCubing Arena" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="ChessCubing" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Application officielle de match pour ChessCubing Arena sur téléphone et tablette."
|
||||
/>
|
||||
<title>ChessCubing Arena | Application</title>
|
||||
<link rel="icon" type="image/png" href="logo.png" />
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="shortcut icon" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" href="logo.png" />
|
||||
<link rel="manifest" href="site.webmanifest" />
|
||||
<script>
|
||||
(() => {
|
||||
const assetTokenStorageKey = "chesscubing-arena-asset-token";
|
||||
const pageUrl = new URL(window.location.href);
|
||||
const refreshToken = pageUrl.searchParams.get("refresh");
|
||||
if (refreshToken) {
|
||||
try {
|
||||
window.sessionStorage.setItem(assetTokenStorageKey, refreshToken);
|
||||
} catch {
|
||||
// Ignore storage failures in restricted browsers.
|
||||
}
|
||||
pageUrl.searchParams.delete("refresh");
|
||||
window.history.replaceState(null, "", pageUrl.toString());
|
||||
}
|
||||
|
||||
let assetToken = "";
|
||||
try {
|
||||
assetToken = window.sessionStorage.getItem(assetTokenStorageKey) || "";
|
||||
} catch {
|
||||
assetToken = "";
|
||||
}
|
||||
|
||||
window.__CHESSCUBING_ASSET_TOKEN__ = assetToken;
|
||||
window.__CHESSCUBING_ASSET_URL__ = (path) => {
|
||||
if (!assetToken) {
|
||||
return path;
|
||||
}
|
||||
|
||||
const assetUrl = new URL(path, window.location.href);
|
||||
assetUrl.searchParams.set("v", assetToken);
|
||||
return `${assetUrl.pathname}${assetUrl.search}${assetUrl.hash}`;
|
||||
};
|
||||
|
||||
const stylesheet = document.createElement("link");
|
||||
stylesheet.rel = "stylesheet";
|
||||
stylesheet.href = window.__CHESSCUBING_ASSET_URL__("styles.css");
|
||||
document.head.append(stylesheet);
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body data-page="setup">
|
||||
<div class="ambient ambient-left"></div>
|
||||
@@ -24,11 +71,6 @@
|
||||
</a>
|
||||
<p class="eyebrow">Application officielle de match</p>
|
||||
<h1>ChessCubing Arena</h1>
|
||||
<p class="lead">
|
||||
Une version mobile et tablette organisée par phases : configuration,
|
||||
page chrono ultra lisible, puis page cube dédiée qui s'ouvre
|
||||
automatiquement à la fin de la phase chess.
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<a class="button ghost" href="index.html">Accueil du site</a>
|
||||
<a class="button secondary" href="reglement.html">Consulter le règlement</a>
|
||||
@@ -42,7 +84,7 @@
|
||||
<li>Configurer la rencontre</li>
|
||||
<li>Passer à la page chrono</li>
|
||||
<li>Basculer automatiquement sur la page cube</li>
|
||||
<li>Revenir sur la page chrono pour le block suivant</li>
|
||||
<li>Revenir sur la page chrono pour le Block suivant</li>
|
||||
</ol>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -61,7 +103,16 @@
|
||||
</div>
|
||||
|
||||
<form id="setupForm" class="setup-form">
|
||||
<label class="field span-2">
|
||||
<label class="option-card competition-option span-2">
|
||||
<input id="competitionMode" name="competitionMode" type="checkbox" />
|
||||
<strong>Mode compétition</strong>
|
||||
<span>
|
||||
Affiche le nom de la rencontre, l'arbitre, le club ou l'événement
|
||||
et les notes d'organisation.
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="field span-2" id="matchLabelField" data-competition-field hidden>
|
||||
<span>Nom de la rencontre</span>
|
||||
<input
|
||||
name="matchLabel"
|
||||
@@ -78,7 +129,7 @@
|
||||
<input type="radio" name="mode" value="twice" checked />
|
||||
<strong>ChessCubing Twice</strong>
|
||||
<span>
|
||||
Le gagnant du cube ouvre le block suivant et peut obtenir un
|
||||
Le gagnant du cube ouvre la partie suivante et peut obtenir un
|
||||
double coup V2.
|
||||
</span>
|
||||
</label>
|
||||
@@ -86,7 +137,7 @@
|
||||
<input type="radio" name="mode" value="time" />
|
||||
<strong>ChessCubing Time</strong>
|
||||
<span>
|
||||
Même structure de blocks, avec chronos cumulés et alternance
|
||||
Même structure de Blocks, avec chronos cumulés et alternance
|
||||
bloc - / bloc +.
|
||||
</span>
|
||||
</label>
|
||||
@@ -94,7 +145,7 @@
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="field span-2">
|
||||
<legend>Cadence du block</legend>
|
||||
<legend>Cadence du match</legend>
|
||||
<div class="option-grid preset-grid">
|
||||
<label class="option-card">
|
||||
<input type="radio" name="preset" value="fast" checked />
|
||||
@@ -118,7 +169,7 @@
|
||||
<legend>Temps personnalisés</legend>
|
||||
<div class="timing-grid">
|
||||
<label class="field">
|
||||
<span>Temps block (secondes)</span>
|
||||
<span id="blockSecondsLabel">Temps partie (secondes)</span>
|
||||
<input
|
||||
name="blockSeconds"
|
||||
type="number"
|
||||
@@ -128,7 +179,18 @@
|
||||
value="180"
|
||||
/>
|
||||
</label>
|
||||
<label class="field">
|
||||
<label class="field" id="timeInitialField" hidden>
|
||||
<span>Temps de chaque joueur (minutes)</span>
|
||||
<input
|
||||
name="timeInitialMinutes"
|
||||
type="number"
|
||||
min="1"
|
||||
max="180"
|
||||
step="1"
|
||||
value="10"
|
||||
/>
|
||||
</label>
|
||||
<label class="field" id="moveSecondsField">
|
||||
<span>Temps coup (secondes)</span>
|
||||
<input
|
||||
name="moveSeconds"
|
||||
@@ -152,7 +214,7 @@
|
||||
<input name="blackName" type="text" maxlength="40" placeholder="Noir" value="Noir" />
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<label class="field" id="arbiterField" data-competition-field hidden>
|
||||
<span>Arbitre</span>
|
||||
<input
|
||||
name="arbiterName"
|
||||
@@ -162,7 +224,7 @@
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<label class="field" id="eventField" data-competition-field hidden>
|
||||
<span>Club / événement</span>
|
||||
<input
|
||||
name="eventName"
|
||||
@@ -172,7 +234,7 @@
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="field span-2">
|
||||
<label class="field span-2" id="notesField" data-competition-field hidden>
|
||||
<span>Notes</span>
|
||||
<textarea
|
||||
name="notes"
|
||||
@@ -214,7 +276,7 @@
|
||||
<p>
|
||||
Chaque joueur dispose d'une grande zone tactile pour signaler la
|
||||
fin de son coup, puis l'app ouvre automatiquement la phase cube
|
||||
quand le block chess est terminé.
|
||||
quand le Block d'échecs est terminé.
|
||||
</p>
|
||||
</article>
|
||||
<article class="rule-card">
|
||||
@@ -243,8 +305,19 @@
|
||||
</div>
|
||||
</aside>
|
||||
</main>
|
||||
|
||||
<div class="setup-refresh-footer">
|
||||
<button class="refresh-link-button" id="refreshAppButton" type="button">
|
||||
Rafraîchir l'app
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="app.js"></script>
|
||||
<script>
|
||||
const appScript = document.createElement("script");
|
||||
appScript.type = "module";
|
||||
appScript.src = window.__CHESSCUBING_ASSET_URL__("app.js");
|
||||
document.body.append(appScript);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
74
chrono.html
74
chrono.html
@@ -2,17 +2,64 @@
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#140700" />
|
||||
<meta name="application-name" content="ChessCubing Arena" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="ChessCubing" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Phase chrono de ChessCubing Arena avec gros boutons face-à-face."
|
||||
/>
|
||||
<title>ChessCubing Arena | Phase Chrono</title>
|
||||
<link rel="icon" type="image/png" href="logo.png" />
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="shortcut icon" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" href="logo.png" />
|
||||
<link rel="manifest" href="site.webmanifest" />
|
||||
<script>
|
||||
(() => {
|
||||
const assetTokenStorageKey = "chesscubing-arena-asset-token";
|
||||
const pageUrl = new URL(window.location.href);
|
||||
const refreshToken = pageUrl.searchParams.get("refresh");
|
||||
if (refreshToken) {
|
||||
try {
|
||||
window.sessionStorage.setItem(assetTokenStorageKey, refreshToken);
|
||||
} catch {
|
||||
// Ignore storage failures in restricted browsers.
|
||||
}
|
||||
pageUrl.searchParams.delete("refresh");
|
||||
window.history.replaceState(null, "", pageUrl.toString());
|
||||
}
|
||||
|
||||
let assetToken = "";
|
||||
try {
|
||||
assetToken = window.sessionStorage.getItem(assetTokenStorageKey) || "";
|
||||
} catch {
|
||||
assetToken = "";
|
||||
}
|
||||
|
||||
window.__CHESSCUBING_ASSET_TOKEN__ = assetToken;
|
||||
window.__CHESSCUBING_ASSET_URL__ = (path) => {
|
||||
if (!assetToken) {
|
||||
return path;
|
||||
}
|
||||
|
||||
const assetUrl = new URL(path, window.location.href);
|
||||
assetUrl.searchParams.set("v", assetToken);
|
||||
return `${assetUrl.pathname}${assetUrl.search}${assetUrl.hash}`;
|
||||
};
|
||||
|
||||
const stylesheet = document.createElement("link");
|
||||
stylesheet.rel = "stylesheet";
|
||||
stylesheet.href = window.__CHESSCUBING_ASSET_URL__("styles.css");
|
||||
document.head.append(stylesheet);
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body data-page="chrono" class="phase-body">
|
||||
<main class="phase-shell">
|
||||
<main class="phase-shell chrono-stage">
|
||||
<header class="phase-header">
|
||||
<a class="brand-link brand-link-logo" href="application.html" aria-label="Retour à l'application">
|
||||
<img class="brand-link-icon" src="logo.png" alt="Icône ChessCubing" />
|
||||
@@ -30,10 +77,10 @@
|
||||
|
||||
<section class="status-strip">
|
||||
<article class="status-card">
|
||||
<span>Temps block</span>
|
||||
<span id="blockTimerLabel">Temps Block</span>
|
||||
<strong id="blockTimer">03:00</strong>
|
||||
</article>
|
||||
<article class="status-card">
|
||||
<article class="status-card" id="moveTimerCard">
|
||||
<span>Temps coup</span>
|
||||
<strong id="moveTimer">00:20</strong>
|
||||
</article>
|
||||
@@ -53,7 +100,7 @@
|
||||
</div>
|
||||
<div class="zone-stats">
|
||||
<strong id="blackMovesChrono">0 / 6</strong>
|
||||
<span id="blackClockChrono"></span>
|
||||
<span class="player-clock" id="blackClockChrono"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -67,13 +114,13 @@
|
||||
|
||||
<article class="phase-spine">
|
||||
<div class="spine-card">
|
||||
<p class="micro-label" id="spineLabel">État du block</p>
|
||||
<p class="micro-label" id="spineLabel">État du Block</p>
|
||||
<strong id="spineHeadline">Prêt à démarrer</strong>
|
||||
<p id="spineText"></p>
|
||||
</div>
|
||||
|
||||
<button class="button primary spine-button" id="primaryChronoButton" type="button">
|
||||
Démarrer le block
|
||||
Démarrer le Block
|
||||
</button>
|
||||
</article>
|
||||
|
||||
@@ -86,7 +133,7 @@
|
||||
</div>
|
||||
<div class="zone-stats">
|
||||
<strong id="whiteMovesChrono">0 / 6</strong>
|
||||
<span id="whiteClockChrono"></span>
|
||||
<span class="player-clock" id="whiteClockChrono"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -142,6 +189,11 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script type="module" src="app.js"></script>
|
||||
<script>
|
||||
const appScript = document.createElement("script");
|
||||
appScript.type = "module";
|
||||
appScript.src = window.__CHESSCUBING_ASSET_URL__("app.js");
|
||||
document.body.append(appScript);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
116
cube.html
116
cube.html
@@ -2,14 +2,61 @@
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#140700" />
|
||||
<meta name="application-name" content="ChessCubing Arena" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="ChessCubing" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Phase cube de ChessCubing Arena avec gros boutons de fin."
|
||||
/>
|
||||
<title>ChessCubing Arena | Phase Cube</title>
|
||||
<link rel="icon" type="image/png" href="logo.png" />
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="shortcut icon" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" href="logo.png" />
|
||||
<link rel="manifest" href="site.webmanifest" />
|
||||
<script>
|
||||
(() => {
|
||||
const assetTokenStorageKey = "chesscubing-arena-asset-token";
|
||||
const pageUrl = new URL(window.location.href);
|
||||
const refreshToken = pageUrl.searchParams.get("refresh");
|
||||
if (refreshToken) {
|
||||
try {
|
||||
window.sessionStorage.setItem(assetTokenStorageKey, refreshToken);
|
||||
} catch {
|
||||
// Ignore storage failures in restricted browsers.
|
||||
}
|
||||
pageUrl.searchParams.delete("refresh");
|
||||
window.history.replaceState(null, "", pageUrl.toString());
|
||||
}
|
||||
|
||||
let assetToken = "";
|
||||
try {
|
||||
assetToken = window.sessionStorage.getItem(assetTokenStorageKey) || "";
|
||||
} catch {
|
||||
assetToken = "";
|
||||
}
|
||||
|
||||
window.__CHESSCUBING_ASSET_TOKEN__ = assetToken;
|
||||
window.__CHESSCUBING_ASSET_URL__ = (path) => {
|
||||
if (!assetToken) {
|
||||
return path;
|
||||
}
|
||||
|
||||
const assetUrl = new URL(path, window.location.href);
|
||||
assetUrl.searchParams.set("v", assetToken);
|
||||
return `${assetUrl.pathname}${assetUrl.search}${assetUrl.hash}`;
|
||||
};
|
||||
|
||||
const stylesheet = document.createElement("link");
|
||||
stylesheet.rel = "stylesheet";
|
||||
stylesheet.href = window.__CHESSCUBING_ASSET_URL__("styles.css");
|
||||
document.head.append(stylesheet);
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body data-page="cube" class="phase-body">
|
||||
<main class="phase-shell cube-shell">
|
||||
@@ -30,7 +77,7 @@
|
||||
|
||||
<section class="status-strip">
|
||||
<article class="status-card">
|
||||
<span>Block</span>
|
||||
<span id="cubeBlockLabelText">Block</span>
|
||||
<strong id="cubeBlockLabel">1</strong>
|
||||
</article>
|
||||
<article class="status-card">
|
||||
@@ -126,6 +173,65 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script type="module" src="app.js"></script>
|
||||
<section class="modal hidden" id="cubeResultModal" aria-hidden="true">
|
||||
<div class="modal-backdrop" data-close-cube-result-modal="true"></div>
|
||||
<div class="modal-card result-modal-card">
|
||||
<div class="modal-head">
|
||||
<div>
|
||||
<p class="eyebrow">Fin de phase cube</p>
|
||||
<h2 id="cubeResultModalTitle">Résumé du cube</h2>
|
||||
</div>
|
||||
<button class="button ghost small" id="closeCubeResultButton" type="button">
|
||||
Fermer
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="section-copy" id="cubeResultSummary"></p>
|
||||
|
||||
<div class="cube-result-overview">
|
||||
<article class="result-pill-card">
|
||||
<span>Vainqueur cube</span>
|
||||
<strong id="cubeResultWinner">--</strong>
|
||||
</article>
|
||||
<article class="result-pill-card">
|
||||
<span>Suite</span>
|
||||
<strong id="cubeResultOutcome">--</strong>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="cube-result-player-grid">
|
||||
<article class="cube-result-player-card">
|
||||
<span class="seat-tag light-seat">Blanc</span>
|
||||
<strong id="cubeResultWhiteName">Blanc</strong>
|
||||
<span id="cubeResultWhiteTime">Temps cube --</span>
|
||||
<span id="cubeResultWhiteDetail">Résultat --</span>
|
||||
<span id="cubeResultWhiteClock">Chrono après --</span>
|
||||
</article>
|
||||
<article class="cube-result-player-card">
|
||||
<span class="seat-tag dark-seat">Noir</span>
|
||||
<strong id="cubeResultBlackName">Noir</strong>
|
||||
<span id="cubeResultBlackTime">Temps cube --</span>
|
||||
<span id="cubeResultBlackDetail">Résultat --</span>
|
||||
<span id="cubeResultBlackClock">Chrono après --</span>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="modal-actions">
|
||||
<button class="button primary" id="cubeResultActionButton" type="button">
|
||||
Appliquer et ouvrir la page chrono
|
||||
</button>
|
||||
<button class="button ghost" id="cubeResultDismissButton" type="button">
|
||||
Revenir à la phase cube
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
const appScript = document.createElement("script");
|
||||
appScript.type = "module";
|
||||
appScript.src = window.__CHESSCUBING_ASSET_URL__("app.js");
|
||||
document.body.append(appScript);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
4
ethan/.gitignore
vendored
Normal file
4
ethan/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
dist
|
||||
.DS_Store
|
||||
.codex
|
||||
1835
ethan/ChessClock.html
Normal file
1835
ethan/ChessClock.html
Normal file
File diff suppressed because one or more lines are too long
29
ethan/README.md
Normal file
29
ethan/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Chess Clock
|
||||
|
||||
Projet React/Vite reconstruit a partir d'un ancien fichier HTML monolithique.
|
||||
|
||||
## Scripts
|
||||
|
||||
- `npm install`
|
||||
- `npm run dev`
|
||||
- `npm run build`
|
||||
- `npm run preview`
|
||||
|
||||
## Ouverture
|
||||
|
||||
- Double-clic sur `index.html` : mode autonome direct dans le navigateur via CDN React/Babel
|
||||
- Serveur Vite : le meme `index.html` bascule automatiquement sur l'entree `src/main.jsx`
|
||||
|
||||
## Structure
|
||||
|
||||
- `index.html` : shell HTML minimal
|
||||
- `src/main.jsx` : point d'entree React
|
||||
- `src/App.jsx` : wrapper d'application
|
||||
- `src/features/chess-clock/ChessClockApp.jsx` : logique de l'horloge et ecrans associes
|
||||
- `src/features/chess-clock/ChessClockStandalone.jsx` : entree autonome pour ouverture directe du fichier HTML
|
||||
- `src/features/chess-clock/chessClock.css` : styles extraits du HTML initial
|
||||
- `legacy/ChessClock.legacy.html` : archive du fichier HTML d'origine
|
||||
|
||||
## Notes
|
||||
|
||||
Le nettoyage a separe l'enveloppe HTML, les styles et la logique React sans changer volontairement le comportement metier. Le composant principal reste encore volumineux, mais il est maintenant place dans une structure de projet standard et pret a etre decoupe plus finement si besoin.
|
||||
1835
ethan/index.html
Normal file
1835
ethan/index.html
Normal file
File diff suppressed because one or more lines are too long
2345
ethan/legacy/ChessClock.legacy.html
Normal file
2345
ethan/legacy/ChessClock.legacy.html
Normal file
File diff suppressed because one or more lines are too long
19
ethan/package.json
Normal file
19
ethan/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "chess-clock",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "19.1.1",
|
||||
"react-dom": "19.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "5.0.2",
|
||||
"vite": "7.1.5"
|
||||
}
|
||||
}
|
||||
5
ethan/src/App.jsx
Normal file
5
ethan/src/App.jsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import ChessClockApp from "./features/chess-clock/ChessClockApp.jsx";
|
||||
|
||||
export default function App() {
|
||||
return <ChessClockApp />;
|
||||
}
|
||||
1785
ethan/src/features/chess-clock/ChessClockApp.jsx
Normal file
1785
ethan/src/features/chess-clock/ChessClockApp.jsx
Normal file
File diff suppressed because one or more lines are too long
1793
ethan/src/features/chess-clock/ChessClockStandalone.jsx
Normal file
1793
ethan/src/features/chess-clock/ChessClockStandalone.jsx
Normal file
File diff suppressed because one or more lines are too long
536
ethan/src/features/chess-clock/chessClock.css
Normal file
536
ethan/src/features/chess-clock/chessClock.css
Normal file
@@ -0,0 +1,536 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Oxanium:wght@400;600;700;800&display=swap');
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--bg:#060a0e;--s1:#0d1218;--s2:#131b24;--s3:#1c2632;--bd:#263040;
|
||||
--gold:#e8b84b;--g2:#ffd97a;--g3:#7a5c1e;
|
||||
--red:#f04040;--grn:#38e878;--blu:#4b9ee8;--pur:#a855f7;
|
||||
--txt:#c8d8e8;--dim:#3e5060;
|
||||
}
|
||||
html,body,#root{width:100%;height:100%;overflow:hidden;background:var(--bg);
|
||||
-webkit-tap-highlight-color:transparent;user-select:none;-webkit-user-select:none}
|
||||
.app{width:100vw;height:100dvh;display:flex;flex-direction:column;
|
||||
font-family:'Oxanium',sans-serif;background:var(--bg);position:relative;overflow:hidden;
|
||||
animation:app-in .5s cubic-bezier(.16,1,.3,1) both}
|
||||
@keyframes app-in{from{opacity:0;transform:scale(.97)}to{opacity:1;transform:scale(1)}}
|
||||
.app::before{content:'';position:fixed;inset:-20%;width:140%;height:140%;
|
||||
background:radial-gradient(ellipse 60% 40% at 30% 10%,rgba(232,184,75,.07) 0%,transparent 70%),
|
||||
radial-gradient(ellipse 50% 50% at 70% 90%,rgba(75,158,232,.05) 0%,transparent 70%);
|
||||
pointer-events:none;z-index:0;animation:mesh 14s ease-in-out infinite alternate}
|
||||
@keyframes mesh{from{transform:translate(0,0)}to{transform:translate(2%,1%)}}
|
||||
.app::after{content:'';position:fixed;inset:0;
|
||||
background:repeating-linear-gradient(0deg,transparent,transparent 3px,rgba(0,0,0,.06) 3px,rgba(0,0,0,.06) 4px);
|
||||
pointer-events:none;z-index:999;opacity:.35}
|
||||
@keyframes shimmer{0%,100%{background-position:0%}50%{background-position:100%}}
|
||||
@keyframes cfg-in{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}}
|
||||
@keyframes fade-in{from{opacity:0}to{opacity:1}}
|
||||
@keyframes pop{0%{transform:scale(0);opacity:0}60%{transform:scale(1.2)}100%{transform:scale(1);opacity:1}}
|
||||
|
||||
/* CONFIG */
|
||||
.cfg{width:100%;height:100%;display:flex;flex-direction:column;position:relative;z-index:1;overflow:hidden;animation:cfg-in .45s cubic-bezier(.16,1,.3,1) both}
|
||||
.cfg-hd{padding:clamp(14px,3.5vh,24px) 20px 8px;text-align:center;flex-shrink:0}
|
||||
.cfg-logo{font-family:'Bebas Neue',sans-serif;font-size:clamp(28px,7.5vw,44px);letter-spacing:8px;
|
||||
background:linear-gradient(135deg,var(--g2) 0%,var(--gold) 50%,#b8882b 100%);
|
||||
-webkit-background-clip:text;background-clip:text;color:transparent;line-height:1;
|
||||
background-size:200%;animation:shimmer 4s ease-in-out infinite}
|
||||
.cfg-sub{font-size:9px;letter-spacing:6px;color:var(--dim);text-transform:uppercase;margin-top:3px}
|
||||
.cfg-hd-row{display:flex;align-items:center;justify-content:center;gap:10px;margin-top:8px;flex-wrap:wrap}
|
||||
.icon-btn{display:flex;align-items:center;gap:5px;background:var(--s1);border:1px solid var(--bd);
|
||||
border-radius:10px;padding:5px 10px;cursor:pointer;transition:all .15s}
|
||||
.icon-btn:active{background:var(--s2);border-color:var(--gold)}
|
||||
.icon-btn-lbl{font-size:9px;letter-spacing:2px;color:var(--dim);text-transform:uppercase;font-weight:700}
|
||||
.cfg-body{flex:1;overflow-y:auto;overflow-x:hidden;padding:8px 14px 0;-webkit-overflow-scrolling:touch}
|
||||
.cfg-body::-webkit-scrollbar{display:none}
|
||||
.cfg-section{font-size:9px;letter-spacing:4px;color:var(--dim);text-transform:uppercase;margin:12px 0 6px 2px}
|
||||
.cfg-card{background:var(--s1);border:1px solid var(--bd);border-radius:14px;overflow:hidden;margin-bottom:8px;box-shadow:0 4px 24px rgba(0,0,0,.3)}
|
||||
.cfg-row{display:flex;align-items:center;justify-content:space-between;
|
||||
padding:11px 14px;border-bottom:1px solid rgba(255,255,255,.04);cursor:pointer;
|
||||
position:relative;overflow:hidden;transition:background .2s}
|
||||
.cfg-row:last-child{border-bottom:none}
|
||||
.cfg-row:active{background:rgba(232,184,75,.04)}
|
||||
.cfg-row.sel{background:linear-gradient(90deg,rgba(232,184,75,.09),rgba(232,184,75,.03));border-left:2px solid var(--gold)}
|
||||
.cfg-row-lbl{font-weight:600;font-size:clamp(12px,3.5vw,15px);color:var(--txt);transition:color .2s}
|
||||
.cfg-row.sel .cfg-row-lbl{color:var(--g2)}
|
||||
.cfg-chk{color:var(--gold);font-size:15px;opacity:0;transform:scale(0) rotate(-45deg);transition:opacity .25s,transform .35s cubic-bezier(.34,1.56,.64,1)}
|
||||
.cfg-row.sel .cfg-chk{opacity:1;transform:scale(1) rotate(0deg)}
|
||||
.cfg-stepper{display:flex;align-items:center;justify-content:space-between;padding:10px 14px}
|
||||
.cfg-s-lbl{font-weight:600;font-size:clamp(12px,3.3vw,14px);color:var(--txt)}
|
||||
.cfg-s-val{font-family:'Bebas Neue',sans-serif;font-size:clamp(18px,5vw,26px);color:var(--gold);letter-spacing:2px;min-width:36px;text-align:center}
|
||||
.cfg-btns{display:flex;gap:5px}
|
||||
.cfg-btn{width:32px;height:32px;border-radius:9px;background:var(--s2);border:1px solid var(--bd);color:var(--gold);font-size:18px;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:background .15s,transform .15s}
|
||||
.cfg-btn:active{background:var(--s3);transform:scale(.88)}
|
||||
.cfg-ft{padding:8px 14px clamp(14px,3.5vh,24px);flex-shrink:0}
|
||||
.start-btn{width:100%;padding:clamp(14px,3.8vw,18px);border:none;border-radius:14px;font-family:'Bebas Neue',sans-serif;font-size:clamp(16px,4.5vw,20px);letter-spacing:7px;color:var(--bg);background:linear-gradient(110deg,var(--g3),var(--g2),var(--gold),var(--g2),var(--g3));background-size:250%;cursor:pointer;box-shadow:0 4px 28px rgba(232,184,75,.5),inset 0 1px 0 rgba(255,255,255,.2);text-transform:uppercase;transition:transform .18s cubic-bezier(.34,1.56,.64,1);animation:shimmer 3s ease-in-out infinite}
|
||||
.start-btn:active{transform:scale(.96)}
|
||||
.cfg-custom{display:flex;align-items:center;justify-content:space-between;background:var(--s1);border:1px dashed var(--g3);border-radius:12px;padding:11px 16px;margin-bottom:8px;cursor:pointer;transition:background .15s,border-color .15s}
|
||||
.cfg-custom:active{background:var(--s2);border-color:var(--gold)}
|
||||
.cfg-custom.c-on{border-style:solid;border-color:var(--gold);background:rgba(232,184,75,.06)}
|
||||
.cfg-c-main{font-family:'Bebas Neue',sans-serif;font-size:clamp(14px,3.8vw,18px);letter-spacing:3px;color:var(--gold)}
|
||||
.cfg-c-detail{font-size:9px;letter-spacing:1px;color:var(--dim);margin-top:2px}
|
||||
.cfg-c-arrow{font-size:17px;color:var(--dim)}
|
||||
.toggle-row{display:flex;align-items:center;justify-content:space-between;padding:11px 14px;border-bottom:1px solid rgba(255,255,255,.04)}
|
||||
.toggle-row:last-child{border-bottom:none}
|
||||
.toggle-lbl{display:flex;flex-direction:column;gap:2px}
|
||||
.toggle-name{font-weight:600;font-size:clamp(11px,3.2vw,13px);color:var(--txt)}
|
||||
.toggle-desc{font-size:9px;letter-spacing:1px;color:var(--dim)}
|
||||
.toggle{width:40px;height:22px;border-radius:11px;background:var(--s3);border:1px solid var(--bd);position:relative;cursor:pointer;transition:background .25s,border-color .25s;flex-shrink:0}
|
||||
.toggle.on{background:var(--grn);border-color:var(--grn)}
|
||||
.toggle-knob{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:var(--dim);transition:all .25s cubic-bezier(.34,1.56,.64,1)}
|
||||
.toggle.on .toggle-knob{left:20px;background:#fff;box-shadow:0 0 8px rgba(56,232,120,.6)}
|
||||
|
||||
/* MODAL */
|
||||
.modal-bg{position:fixed;inset:0;z-index:300;background:rgba(0,0,0,.75);backdrop-filter:blur(6px);display:flex;align-items:flex-end;justify-content:center;animation:fade-in .2s ease-out}
|
||||
.modal{width:100%;max-width:480px;background:var(--s1);border-radius:22px 22px 0 0;border-top:1px solid var(--bd);padding:0 0 clamp(18px,4.5vh,32px);animation:modal-in .28s cubic-bezier(.34,1.4,.64,1)}
|
||||
@keyframes modal-in{from{transform:translateY(100%)}to{transform:translateY(0)}}
|
||||
.modal-handle{width:40px;height:4px;background:var(--bd);border-radius:2px;margin:12px auto 0}
|
||||
.modal-title{font-family:'Bebas Neue',sans-serif;font-size:clamp(16px,4.5vw,20px);letter-spacing:5px;color:var(--gold);text-align:center;padding:12px 18px 3px}
|
||||
.modal-sub{font-size:9px;letter-spacing:3px;color:var(--dim);text-transform:uppercase;text-align:center;margin-bottom:14px}
|
||||
.modal-fields{display:flex;gap:8px;padding:0 14px;margin-bottom:14px}
|
||||
.modal-field{flex:1;display:flex;flex-direction:column;gap:5px}
|
||||
.modal-f-lbl{font-size:9px;letter-spacing:3px;color:var(--dim);text-transform:uppercase;padding-left:2px}
|
||||
.modal-spin{display:flex;align-items:center;background:var(--s2);border:1px solid var(--bd);border-radius:11px;overflow:hidden}
|
||||
.modal-sb{width:38px;height:46px;display:flex;align-items:center;justify-content:center;font-size:19px;color:var(--gold);cursor:pointer;flex-shrink:0;transition:background .12s}
|
||||
.modal-sb:active{background:var(--s3)}
|
||||
.modal-sv{flex:1;text-align:center;font-family:'Bebas Neue',sans-serif;font-size:clamp(19px,5vw,24px);color:var(--txt);letter-spacing:2px}
|
||||
.modal-su{font-size:8px;letter-spacing:2px;color:var(--dim);text-align:center;margin-top:2px}
|
||||
.modal-prev{margin:0 14px 12px;background:var(--s2);border:1px solid var(--bd);border-radius:11px;padding:9px 13px;display:flex;align-items:center;justify-content:space-between}
|
||||
.modal-prev-lbl{font-size:9px;letter-spacing:3px;color:var(--dim);text-transform:uppercase}
|
||||
.modal-prev-val{font-family:'Bebas Neue',sans-serif;font-size:clamp(14px,3.8vw,18px);color:var(--gold);letter-spacing:2px}
|
||||
.modal-actions{display:flex;flex-direction:column;gap:7px;padding:0 14px}
|
||||
.modal-ok{width:100%;padding:12px;border:none;border-radius:11px;font-family:'Bebas Neue',sans-serif;font-size:13px;letter-spacing:4px;color:var(--bg);background:linear-gradient(135deg,var(--g2),var(--gold));cursor:pointer;box-shadow:0 4px 20px rgba(232,184,75,.4);transition:filter .15s}
|
||||
.modal-ok:active{filter:brightness(.88)}
|
||||
.modal-cancel{width:100%;padding:9px;border:none;background:transparent;font-size:10px;letter-spacing:3px;color:var(--dim);cursor:pointer;text-transform:uppercase}
|
||||
|
||||
/* GAME */
|
||||
.game{width:100%;height:100%;display:flex;flex-direction:column;position:relative;z-index:1}
|
||||
.panel{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;position:relative;cursor:pointer;gap:clamp(5px,1.5vh,10px);overflow:hidden;transition:opacity .3s,filter .3s}
|
||||
.panel.flip{transform:rotate(180deg)}
|
||||
.panel.on{opacity:1}
|
||||
.panel.on::before{content:'';position:absolute;inset:0;background:linear-gradient(135deg,rgba(232,184,75,.10) 0%,rgba(232,184,75,.02) 40%,transparent 70%);pointer-events:none;animation:sweep 2.4s ease-in-out infinite alternate}
|
||||
@keyframes sweep{from{opacity:.6}to{opacity:1}}
|
||||
.panel.off{opacity:.38;filter:brightness(.7)}
|
||||
.panel.dng{animation:dng .55s ease-in-out infinite}
|
||||
@keyframes dng{0%,100%{background:transparent}30%{background:rgba(240,64,64,.16)}60%{background:rgba(240,64,64,.06)}}
|
||||
.panel.pf{animation:pfx .25s ease-in-out 3}
|
||||
@keyframes pfx{0%,100%{background:transparent}50%{background:rgba(168,85,247,.22)}}
|
||||
.p-ripple{position:absolute;width:200px;height:200px;border-radius:50%;background:radial-gradient(circle,rgba(232,184,75,.22) 0%,transparent 70%);transform:scale(0);pointer-events:none;animation:ripple .55s cubic-bezier(.2,.8,.3,1) forwards}
|
||||
@keyframes ripple{from{transform:scale(0);opacity:1}to{transform:scale(4);opacity:0}}
|
||||
.p-label{font-size:9px;letter-spacing:6px;color:var(--dim);text-transform:uppercase;font-weight:700}
|
||||
.clock{font-family:'Bebas Neue',sans-serif;font-size:clamp(58px,16vw,106px);letter-spacing:-3px;color:var(--gold);line-height:1;text-shadow:0 0 30px rgba(232,184,75,.45),0 0 70px rgba(232,184,75,.15);position:relative;z-index:1;transition:color .3s,text-shadow .3s}
|
||||
.clock.dng{color:var(--red);text-shadow:0 0 20px rgba(240,64,64,.8),0 0 50px rgba(240,64,64,.4);animation:glitch 1.4s steps(1) infinite}
|
||||
@keyframes glitch{0%,89%,100%{transform:translate(0,0)}90%{transform:translate(3px,0)}92%{transform:translate(-3px,0)}94%{transform:translate(0,0)}}
|
||||
.sonar{width:9px;height:9px;border-radius:50%;background:var(--gold);box-shadow:0 0 0 0 rgba(232,184,75,.7);animation:sonar 1.4s ease-out infinite}
|
||||
@keyframes sonar{0%{box-shadow:0 0 0 0 rgba(232,184,75,.8);transform:scale(1)}50%{box-shadow:0 0 0 10px rgba(232,184,75,0);transform:scale(1.15)}100%{box-shadow:0 0 0 0 rgba(232,184,75,0);transform:scale(1)}}
|
||||
.dots{display:flex;gap:5px;flex-wrap:wrap;justify-content:center;max-width:190px}
|
||||
.dot{width:6px;height:6px;border-radius:50%;border:1px solid rgba(122,92,30,.6);background:transparent;transition:transform .3s cubic-bezier(.34,1.56,.64,1),background .25s,box-shadow .25s}
|
||||
.dot.f{background:var(--gold);border-color:var(--gold);box-shadow:0 0 6px rgba(232,184,75,.7);transform:scale(1.15);animation:dpop .35s cubic-bezier(.34,1.56,.64,1)}
|
||||
@keyframes dpop{0%{transform:scale(0);opacity:0}60%{transform:scale(1.4)}100%{transform:scale(1.15);opacity:1}}
|
||||
.dot.t{border-color:rgba(240,64,64,.5)}
|
||||
.dot.f.t{background:var(--red);border-color:var(--red);animation:tstrobe .45s ease-in-out infinite alternate}
|
||||
@keyframes tstrobe{from{box-shadow:0 0 4px rgba(240,64,64,.5);transform:scale(1.1)}to{box-shadow:0 0 14px rgba(240,64,64,1);transform:scale(1.3)}}
|
||||
.pen-badge{background:rgba(168,85,247,.18);border:1px solid rgba(168,85,247,.45);border-radius:18px;padding:2px 9px;font-family:'Bebas Neue',sans-serif;font-size:10px;letter-spacing:2px;color:var(--pur)}
|
||||
.mt-wrap{width:75%;max-width:160px;height:3px;background:var(--bd);border-radius:2px;overflow:hidden}
|
||||
.mt-bar{height:100%;border-radius:2px;transition:width .1s linear;background:linear-gradient(90deg,var(--grn),var(--gold))}
|
||||
.mt-bar.warn{background:linear-gradient(90deg,var(--gold),var(--red))}
|
||||
.mt-bar.crit{background:var(--red);animation:crit .3s steps(1) infinite}
|
||||
@keyframes crit{0%,100%{opacity:1}50%{opacity:.45}}
|
||||
.pen-bar{position:absolute;bottom:0;left:0;right:0;height:3px;background:linear-gradient(90deg,transparent,var(--pur),transparent);animation:pb-fl .3s ease-in-out 3}
|
||||
@keyframes pb-fl{0%,100%{opacity:0}50%{opacity:1}}
|
||||
.div{height:3px;background:linear-gradient(90deg,transparent 0%,rgba(122,92,30,.4) 10%,var(--gold) 30%,var(--g2) 50%,var(--gold) 70%,rgba(122,92,30,.4) 90%,transparent 100%);background-size:200%;position:relative;z-index:10;flex-shrink:0;box-shadow:0 0 18px rgba(232,184,75,.4);animation:dflow 3s linear infinite}
|
||||
@keyframes dflow{from{background-position:0%}to{background-position:200%}}
|
||||
.div-c{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);display:flex;align-items:center;justify-content:center;flex-direction:column;gap:3px;z-index:20;pointer-events:none}
|
||||
.div-badge{background:var(--bg);border:1.5px solid var(--gold);border-radius:20px;padding:3px 13px;font-family:'Bebas Neue',sans-serif;font-size:12px;letter-spacing:3px;color:var(--gold);white-space:nowrap;box-shadow:0 0 12px rgba(232,184,75,.25)}
|
||||
.div-sub{background:var(--bg);border:1px solid var(--bd);border-radius:10px;padding:1px 7px;font-size:8px;letter-spacing:2px;color:var(--dim);text-transform:uppercase;white-space:nowrap}
|
||||
.win-btn{position:absolute;top:50%;z-index:30;cursor:pointer;transform:translateY(-50%);transition:all .2s cubic-bezier(.34,1.56,.64,1)}
|
||||
.win-btn.wl{left:8px}
|
||||
.win-btn.wr{right:8px}
|
||||
.win-btn-inner{background:var(--s2);border:1.5px solid var(--bd);border-radius:10px;padding:5px 8px;display:flex;flex-direction:column;align-items:center;gap:2px;transition:all .2s;box-shadow:0 2px 10px rgba(0,0,0,.3)}
|
||||
.win-btn:active .win-btn-inner{transform:scale(.9)}
|
||||
.win-btn.pnd .win-btn-inner{background:rgba(232,184,75,.12);border-color:var(--gold);animation:wbp .5s ease-in-out infinite alternate}
|
||||
@keyframes wbp{from{box-shadow:0 0 8px rgba(232,184,75,.3)}to{box-shadow:0 0 22px rgba(232,184,75,.7)}}
|
||||
.win-btn-ico{font-size:12px;line-height:1}
|
||||
.win-btn-lbl{font-family:'Bebas Neue',sans-serif;font-size:8px;letter-spacing:2px;color:var(--dim);text-transform:uppercase;white-space:nowrap}
|
||||
.win-btn.pnd .win-btn-lbl{color:var(--gold)}
|
||||
.win-btn.flip2{transform:translateY(-50%) rotate(180deg)}
|
||||
.ctrls{position:fixed;bottom:0;left:0;right:0;display:flex;justify-content:center;gap:10px;padding:7px 14px clamp(10px,2.5vh,20px);z-index:100;background:linear-gradient(to top,rgba(6,10,14,.98) 55%,transparent)}
|
||||
.cbtn{font-family:'Oxanium',sans-serif;font-size:10px;font-weight:700;letter-spacing:2px;color:var(--dim);background:var(--s1);border:1px solid var(--bd);border-radius:10px;padding:8px 13px;cursor:pointer;text-transform:uppercase;transition:all .2s cubic-bezier(.34,1.56,.64,1)}
|
||||
.cbtn:active{color:var(--gold);border-color:var(--g3);transform:scale(.92)}
|
||||
|
||||
/* CS OVERLAY */
|
||||
.cso{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:clamp(5px,1.3vh,9px);z-index:50;overflow:hidden;animation:cso-in .35s cubic-bezier(.16,1,.3,1) both}
|
||||
@keyframes cso-in{from{opacity:0;transform:scale(.94) translateY(8px)}to{opacity:1;transform:scale(1) translateY(0)}}
|
||||
.cso-bg{position:absolute;inset:0;transition:background .4s}
|
||||
.cso-bg.idle{background:radial-gradient(ellipse 70% 70% at center,rgba(20,55,130,.98) 0%,rgba(6,10,14,.99) 70%)}
|
||||
.cso-bg.hold{animation:hbg .9s ease-in-out infinite alternate}
|
||||
@keyframes hbg{from{background:radial-gradient(ellipse 65% 65% at center,rgba(10,110,44,.98) 0%,rgba(6,10,14,.99) 68%)}to{background:radial-gradient(ellipse 88% 88% at center,rgba(14,140,56,.98) 0%,rgba(6,10,14,.99) 68%)}}
|
||||
.cso-bg.run{animation:rbg .4s ease-in-out infinite alternate}
|
||||
@keyframes rbg{from{background:radial-gradient(ellipse 70% 70% at center,rgba(140,20,20,.98) 0%,rgba(6,10,14,.99) 68%)}to{background:radial-gradient(ellipse 90% 90% at center,rgba(180,28,28,.98) 0%,rgba(6,10,14,.99) 68%)}}
|
||||
.cso-bg.stop{background:rgba(6,10,14,.98)}
|
||||
.cso-bg.win{animation:wbg .6s ease-in-out infinite alternate}
|
||||
@keyframes wbg{from{background:radial-gradient(ellipse 70% 70% at center,rgba(10,120,48,.98) 0%,rgba(6,10,14,.99) 68%)}to{background:radial-gradient(ellipse 95% 95% at center,rgba(16,160,64,.98) 0%,rgba(6,10,14,.99) 68%)}}
|
||||
.cso-bg.lose{background:rgba(6,10,14,.98)}
|
||||
.cso-lbl{font-family:'Bebas Neue',sans-serif;font-size:clamp(12px,3.2vw,15px);letter-spacing:6px;text-transform:uppercase;position:relative;z-index:1;font-weight:700}
|
||||
.cso-lbl.blu{color:#7ec8ff;text-shadow:0 0 24px rgba(100,180,255,1),0 0 48px rgba(75,158,232,.6)}
|
||||
.cso-lbl.grn{color:#7affc0;text-shadow:0 0 24px rgba(80,255,160,1),0 0 48px rgba(56,232,120,.6)}
|
||||
.cso-lbl.red{color:#ff8080;text-shadow:0 0 24px rgba(255,100,100,1),0 0 48px rgba(240,64,64,.7)}
|
||||
.cso-lbl.dim{color:rgba(200,216,232,.6)}
|
||||
.cso-t{font-family:'Bebas Neue',sans-serif;font-size:clamp(38px,10.5vw,70px);line-height:1;letter-spacing:-1px;position:relative;z-index:1;transition:color .25s,text-shadow .25s}
|
||||
.cso-t.idle{color:#90d4ff;text-shadow:0 0 40px rgba(100,190,255,1),0 0 80px rgba(75,158,232,.5)}
|
||||
.cso-t.hold{color:#80ffbe;text-shadow:0 0 50px rgba(80,255,160,1),0 0 100px rgba(56,232,120,.5);animation:hbreathe .5s ease-in-out infinite alternate}
|
||||
@keyframes hbreathe{from{transform:scale(.97);filter:brightness(.9)}to{transform:scale(1.05);filter:brightness(1.2)}}
|
||||
.cso-t.run{color:#ff9090;text-shadow:0 0 40px rgba(255,100,100,1),0 0 80px rgba(240,64,64,.6);animation:runpulse .35s ease-in-out infinite alternate}
|
||||
@keyframes runpulse{from{filter:brightness(1)}to{filter:brightness(1.3)}}
|
||||
.cso-t.stop{color:rgba(200,216,232,.5)}
|
||||
.cso-t.win{color:#80ffbe;text-shadow:0 0 60px rgba(80,255,160,1),0 0 120px rgba(56,232,120,.7);animation:wbreathe .5s ease-in-out infinite alternate}
|
||||
.cso-t.lose{color:rgba(200,216,232,.3)}
|
||||
@keyframes wbreathe{from{transform:scale(1);filter:brightness(1)}to{transform:scale(1.07);filter:brightness(1.25)}}
|
||||
.cso-hint{font-size:clamp(9px,2.2vw,11px);letter-spacing:3px;color:rgba(200,216,232,.75);text-transform:uppercase;position:relative;z-index:1;text-align:center;padding:0 10px;font-weight:600}
|
||||
.cso-win{font-family:'Bebas Neue',sans-serif;font-size:clamp(11px,3.2vw,15px);letter-spacing:6px;color:#80ffbe;text-shadow:0 0 30px rgba(80,255,160,1);position:relative;z-index:1;text-align:center}
|
||||
.cso-ring{position:relative;z-index:1;display:flex;align-items:center;justify-content:center}
|
||||
.cso-ring svg{position:absolute;top:0;left:0;overflow:visible}
|
||||
.rb{fill:none;stroke:rgba(255,255,255,.08);stroke-linecap:round}
|
||||
.rf{fill:none;stroke:#80ffbe;stroke-linecap:round;filter:drop-shadow(0 0 10px rgba(80,255,160,.9));stroke-dasharray:289;animation:rspin .7s linear infinite}
|
||||
@keyframes rspin{from{stroke-dashoffset:289}to{stroke-dashoffset:0}}
|
||||
.rf2{fill:none;stroke:rgba(80,255,160,.3);stroke-linecap:round;stroke-dasharray:60 229;animation:rspin2 1.4s linear infinite reverse}
|
||||
@keyframes rspin2{from{stroke-dashoffset:0}to{stroke-dashoffset:289}}
|
||||
|
||||
/* DOUBLE MOVE */
|
||||
.dbl{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:clamp(8px,2vh,14px);z-index:50;pointer-events:none;animation:dbl-in .4s cubic-bezier(.16,1,.3,1) both}
|
||||
@keyframes dbl-in{from{opacity:0;transform:scale(.92)}to{opacity:1;transform:scale(1)}}
|
||||
.dbl-bw{position:absolute;inset:0;background:rgba(3,18,10,.97)}
|
||||
.dbl-bl{position:absolute;inset:0;background:rgba(4,8,14,.97)}
|
||||
.dbl-card{position:relative;z-index:1;display:flex;flex-direction:column;align-items:center;gap:clamp(8px,2vh,13px);border-radius:20px;padding:clamp(16px,4vh,24px) clamp(20px,6vw,34px)}
|
||||
.dbl-card.win-card{background:rgba(56,232,120,.12);border:1.5px solid rgba(56,232,120,.5);box-shadow:0 0 40px rgba(56,232,120,.15),inset 0 1px 0 rgba(56,232,120,.2)}
|
||||
.dbl-card.lose-card{background:rgba(255,255,255,.06);border:1.5px solid rgba(255,255,255,.15);box-shadow:0 4px 24px rgba(0,0,0,.4)}
|
||||
.dbl-icon{font-size:clamp(28px,8vw,40px);line-height:1;filter:drop-shadow(0 0 16px rgba(56,232,120,.8))}
|
||||
.dbl-title{font-family:'Bebas Neue',sans-serif;font-size:clamp(24px,6.5vw,34px);letter-spacing:6px;color:#ffffff;text-transform:uppercase;text-align:center;text-shadow:0 0 30px rgba(56,232,120,.8),0 2px 0 rgba(0,0,0,.6)}
|
||||
.dbl-sub{font-size:11px;letter-spacing:2px;color:rgba(56,232,120,1);text-transform:uppercase;text-align:center;font-weight:700;text-shadow:0 1px 6px rgba(0,0,0,.7);background:rgba(56,232,120,.15);border:1px solid rgba(56,232,120,.4);border-radius:8px;padding:4px 10px}
|
||||
.dbl-pips{display:flex;gap:16px}
|
||||
.dbl-pip{width:20px;height:20px;border-radius:50%;background:var(--grn);box-shadow:0 0 16px rgba(56,232,120,.9),0 2px 6px rgba(0,0,0,.5);animation:psonar 1s ease-out infinite}
|
||||
.dbl-pip:nth-child(2){animation-delay:.5s}
|
||||
@keyframes psonar{0%{box-shadow:0 0 0 0 rgba(56,232,120,.9),0 2px 6px rgba(0,0,0,.5)}70%{box-shadow:0 0 0 14px rgba(56,232,120,0),0 2px 6px rgba(0,0,0,.5)}100%{box-shadow:0 0 0 0 rgba(56,232,120,0),0 2px 6px rgba(0,0,0,.5)}}
|
||||
.dbl-pip.used{background:rgba(255,255,255,.2);box-shadow:none;animation:none;transform:scale(.65);opacity:.35;transition:all .35s cubic-bezier(.34,1.56,.64,1)}
|
||||
.dbl-hint{font-size:12px;letter-spacing:3px;color:rgba(255,255,255,.85);text-transform:uppercase;font-weight:700;text-shadow:0 1px 6px rgba(0,0,0,.7)}
|
||||
.dbl-wait-ico{font-size:clamp(22px,6.5vw,32px);line-height:1;opacity:.6}
|
||||
.dbl-wait-title{font-family:'Bebas Neue',sans-serif;font-size:clamp(18px,5vw,26px);letter-spacing:5px;color:#ffffff;text-transform:uppercase;text-align:center;text-shadow:0 2px 10px rgba(0,0,0,.8);opacity:.9}
|
||||
.dbl-wait-sub{font-size:11px;letter-spacing:2px;color:rgba(200,216,232,.65);text-transform:uppercase;text-align:center;font-weight:600}
|
||||
.dbl-challenge{font-size:11px;letter-spacing:2px;color:#ffffff;text-transform:uppercase;text-align:center;padding:8px 16px;background:rgba(168,85,247,.4);border:1.5px solid rgba(168,85,247,.8);border-radius:11px;font-weight:700;text-shadow:0 1px 4px rgba(0,0,0,.6);box-shadow:0 0 20px rgba(168,85,247,.5);animation:wfade 1s ease-in-out infinite alternate}
|
||||
@keyframes wfade{from{opacity:.8}to{opacity:1}}
|
||||
/* FORBIDDEN MOVE */
|
||||
.forb{position:absolute;inset:0;z-index:60;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;background:rgba(240,64,64,.08);animation:fade-in .2s ease-out both}
|
||||
.forb-ico{font-size:clamp(28px,8vw,46px);animation:fshake .4s ease-in-out}
|
||||
@keyframes fshake{0%,100%{transform:rotate(0)}25%{transform:rotate(-9deg)}75%{transform:rotate(9deg)}}
|
||||
.forb-title{font-family:'Bebas Neue',sans-serif;font-size:clamp(14px,4vw,20px);letter-spacing:5px;color:var(--red);text-align:center}
|
||||
.forb-sub{font-size:8px;letter-spacing:2px;color:var(--dim);text-transform:uppercase;text-align:center;padding:0 14px}
|
||||
|
||||
/* TOURNAMENT */
|
||||
.trn{position:fixed;inset:0;z-index:250;display:flex;flex-direction:column;background:var(--bg);animation:cfg-in .35s cubic-bezier(.16,1,.3,1) both}
|
||||
.trn-hd{display:flex;align-items:center;justify-content:space-between;padding:clamp(14px,3.5vh,24px) 18px 10px;border-bottom:1px solid var(--bd);flex-shrink:0}
|
||||
.trn-title{font-family:'Bebas Neue',sans-serif;font-size:clamp(16px,4.5vw,22px);letter-spacing:5px;color:var(--gold)}
|
||||
.trn-close{width:34px;height:34px;border-radius:50%;background:var(--s2);border:1px solid var(--bd);color:var(--dim);font-size:15px;display:flex;align-items:center;justify-content:center;cursor:pointer}
|
||||
.trn-close:active{background:var(--s3)}
|
||||
.trn-body{flex:1;overflow-y:auto;padding:12px 14px 36px;-webkit-overflow-scrolling:touch}
|
||||
.trn-body::-webkit-scrollbar{display:none}
|
||||
.trn-setup{display:flex;flex-direction:column;gap:14px}
|
||||
.trn-players-grid{display:grid;grid-template-columns:1fr 1fr;gap:7px}
|
||||
.trn-player-slot{background:var(--s2);border:1px solid var(--bd);border-radius:11px;padding:9px 11px;display:flex;align-items:center;gap:7px}
|
||||
.trn-p-num{font-family:'Bebas Neue',sans-serif;font-size:17px;color:var(--gold);width:20px;text-align:center}
|
||||
.trn-p-name{font-size:11px;letter-spacing:1px;color:var(--txt);font-weight:600}
|
||||
.trn-size-row{display:flex;gap:7px}
|
||||
.trn-size-btn{flex:1;padding:11px;background:var(--s2);border:1.5px solid var(--bd);border-radius:11px;font-family:'Bebas Neue',sans-serif;font-size:15px;letter-spacing:3px;color:var(--dim);cursor:pointer;text-align:center;transition:all .2s}
|
||||
.trn-size-btn.sel{background:rgba(232,184,75,.08);border-color:var(--gold);color:var(--gold)}
|
||||
.trn-start-btn{padding:13px;border:none;border-radius:11px;font-family:'Bebas Neue',sans-serif;font-size:15px;letter-spacing:5px;color:var(--bg);background:linear-gradient(135deg,var(--g2),var(--gold));cursor:pointer;box-shadow:0 4px 20px rgba(232,184,75,.4)}
|
||||
.trn-bracket{display:flex;flex-direction:column;gap:10px}
|
||||
.trn-round-title{font-family:'Bebas Neue',sans-serif;font-size:13px;letter-spacing:4px;color:var(--gold);text-align:center;margin-bottom:3px}
|
||||
.trn-match{background:var(--s1);border:1px solid var(--bd);border-radius:11px;overflow:hidden}
|
||||
.trn-match.active{border-color:var(--gold);box-shadow:0 0 14px rgba(232,184,75,.2)}
|
||||
.trn-match.done{opacity:.6}
|
||||
.trn-m-row{display:flex;align-items:center;padding:9px 13px;border-bottom:1px solid rgba(255,255,255,.04)}
|
||||
.trn-m-row:last-child{border-bottom:none}
|
||||
.trn-m-row.won{background:rgba(232,184,75,.07)}
|
||||
.trn-m-name{flex:1;font-size:12px;font-weight:600;color:var(--txt);letter-spacing:.5px}
|
||||
.trn-m-row.won .trn-m-name{color:var(--g2)}
|
||||
.trn-m-score{font-family:'Bebas Neue',sans-serif;font-size:15px;color:var(--dim);letter-spacing:2px;width:22px;text-align:center}
|
||||
.trn-m-row.won .trn-m-score{color:var(--gold)}
|
||||
.trn-m-play{padding:7px 0;background:rgba(232,184,75,.06);border-top:1px solid var(--bd);text-align:center;font-family:'Bebas Neue',sans-serif;font-size:11px;letter-spacing:3px;color:var(--gold);cursor:pointer}
|
||||
.trn-m-play:active{background:rgba(232,184,75,.12)}
|
||||
.trn-m-tbd{padding:9px 13px;font-size:10px;letter-spacing:2px;color:var(--dim);text-align:center}
|
||||
.trn-winner-banner{background:linear-gradient(135deg,rgba(232,184,75,.12),rgba(232,184,75,.04));border:1px solid var(--gold);border-radius:13px;padding:18px;text-align:center;margin-bottom:14px}
|
||||
.trn-w-ico{font-size:36px;animation:efloat 2s ease-in-out infinite alternate}
|
||||
@keyframes efloat{from{transform:translateY(0)}to{transform:translateY(-8px)}}
|
||||
.trn-w-title{font-family:'Bebas Neue',sans-serif;font-size:clamp(20px,5.5vw,30px);letter-spacing:5px;background:linear-gradient(135deg,var(--g2),var(--gold));-webkit-background-clip:text;background-clip:text;color:transparent;margin-top:5px}
|
||||
.trn-w-sub{font-size:9px;letter-spacing:3px;color:var(--dim);margin-top:3px}
|
||||
.trn-new-btn{width:100%;padding:12px;border:none;border-radius:11px;font-family:'Bebas Neue',sans-serif;font-size:14px;letter-spacing:4px;color:var(--bg);background:linear-gradient(135deg,var(--g2),var(--gold));cursor:pointer;margin-top:7px}
|
||||
.trn-p-input{flex:1;background:transparent;border:none;outline:none;font-family:'Oxanium',sans-serif;font-size:11px;font-weight:600;color:var(--gold);letter-spacing:.5px;width:0;min-width:0;caret-color:var(--gold)}
|
||||
.trn-p-slot-edit{background:rgba(232,184,75,.07);border-color:var(--g3)}
|
||||
.trn-challenge{position:absolute;inset:0;z-index:55;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;background:rgba(168,85,247,.1);animation:fade-in .2s ease-out both}
|
||||
.trn-ch-ico{font-size:clamp(24px,7vw,38px);animation:fshake .4s ease-in-out}
|
||||
.trn-ch-title{font-family:'Bebas Neue',sans-serif;font-size:clamp(12px,3.5vw,18px);letter-spacing:5px;color:var(--pur);text-align:center}
|
||||
.trn-ch-sub{font-size:8px;letter-spacing:2px;color:var(--dim);text-transform:uppercase;text-align:center;padding:0 14px}
|
||||
.dbl-challenge{font-family:'Bebas Neue',sans-serif;font-size:clamp(9px,2.5vw,12px);letter-spacing:3px;color:var(--pur);position:relative;z-index:1;text-transform:uppercase;text-align:center;padding:2px 10px;background:rgba(168,85,247,.12);border:1px solid rgba(168,85,247,.35);border-radius:8px;animation:wfade 1.2s ease-in-out infinite alternate}
|
||||
|
||||
/* HISTORY */
|
||||
.hist{position:fixed;inset:0;z-index:250;display:flex;flex-direction:column;background:var(--bg);animation:cfg-in .35s cubic-bezier(.16,1,.3,1) both}
|
||||
.hist-hd{display:flex;align-items:center;justify-content:space-between;padding:clamp(14px,3.5vh,24px) 18px 10px;border-bottom:1px solid var(--bd);flex-shrink:0}
|
||||
.hist-title{font-family:'Bebas Neue',sans-serif;font-size:clamp(16px,4.5vw,22px);letter-spacing:5px;color:var(--gold)}
|
||||
.hist-close{width:34px;height:34px;border-radius:50%;background:var(--s2);border:1px solid var(--bd);color:var(--dim);font-size:15px;display:flex;align-items:center;justify-content:center;cursor:pointer}
|
||||
.hist-body{flex:1;overflow-y:auto;padding:12px 14px 36px;-webkit-overflow-scrolling:touch}
|
||||
.hist-body::-webkit-scrollbar{display:none}
|
||||
.hist-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:10px;opacity:.4}
|
||||
.hist-empty-ico{font-size:36px}
|
||||
.hist-empty-lbl{font-family:'Bebas Neue',sans-serif;font-size:15px;letter-spacing:4px;color:var(--dim)}
|
||||
.hist-entry{background:var(--s1);border:1px solid var(--bd);border-radius:13px;padding:11px 13px;margin-bottom:9px;animation:cfg-in .3s cubic-bezier(.16,1,.3,1) both}
|
||||
.hist-entry.w0{border-left:3px solid var(--gold)}
|
||||
.hist-entry.w1{border-left:3px solid var(--blu)}
|
||||
.hist-row1{display:flex;align-items:center;justify-content:space-between;margin-bottom:7px}
|
||||
.hist-date{font-size:9px;letter-spacing:2px;color:var(--dim);text-transform:uppercase}
|
||||
.hist-badge{font-family:'Bebas Neue',sans-serif;font-size:10px;letter-spacing:3px;padding:2px 9px;border-radius:18px}
|
||||
.hist-badge.b0{background:rgba(232,184,75,.15);color:var(--gold);border:1px solid var(--g3)}
|
||||
.hist-badge.b1{background:rgba(75,158,232,.15);color:var(--blu);border:1px solid rgba(75,158,232,.4)}
|
||||
.hist-players{display:flex;gap:7px;margin-bottom:7px}
|
||||
.hist-player{flex:1;background:var(--s2);border-radius:9px;padding:8px 10px;display:flex;flex-direction:column;gap:4px}
|
||||
.hist-p-top{display:flex;align-items:center;gap:5px}
|
||||
.hist-p-name{font-family:'Bebas Neue',sans-serif;font-size:11px;letter-spacing:3px;color:var(--txt);flex:1}
|
||||
.hist-p-time{font-family:'Bebas Neue',sans-serif;font-size:clamp(14px,4vw,20px);color:var(--gold);letter-spacing:2px;line-height:1}
|
||||
.hist-player.winner .hist-p-time{color:var(--g2)}
|
||||
.hist-p-meta{font-size:8px;letter-spacing:1px;color:var(--dim)}
|
||||
.hist-footer{display:flex;align-items:center;justify-content:space-between;margin-top:3px}
|
||||
.hist-meta{font-size:8px;letter-spacing:2px;color:var(--dim)}
|
||||
.hist-clear{font-size:9px;letter-spacing:2px;color:var(--red);opacity:.6;background:none;border:none;cursor:pointer;font-family:'Oxanium',sans-serif;text-transform:uppercase}
|
||||
|
||||
/* END */
|
||||
.end{position:fixed;inset:0;z-index:200;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:clamp(9px,2.2vh,16px);background:rgba(6,10,14,.97);overflow:hidden}
|
||||
.end::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 80% 50% at 50% 50%,rgba(232,184,75,.07) 0%,transparent 70%);animation:eburst 2s ease-in-out infinite alternate}
|
||||
@keyframes eburst{from{transform:scale(1);opacity:.7}to{transform:scale(1.15);opacity:1}}
|
||||
.end > *{animation:estag .55s cubic-bezier(.16,1,.3,1) both;position:relative;z-index:1}
|
||||
.end > *:nth-child(1){animation-delay:.0s}.end > *:nth-child(2){animation-delay:.1s}.end > *:nth-child(3){animation-delay:.18s}.end > *:nth-child(4){animation-delay:.26s}.end > *:nth-child(5){animation-delay:.34s}.end > *:nth-child(6){animation-delay:.4s}
|
||||
@keyframes estag{from{opacity:0;transform:translateY(22px) scale(.92)}to{opacity:1;transform:translateY(0) scale(1)}}
|
||||
.end-ico{font-size:clamp(38px,10vw,54px);filter:drop-shadow(0 0 20px rgba(232,184,75,.5));animation:efloat 2s ease-in-out infinite alternate}
|
||||
.end-title{font-family:'Bebas Neue',sans-serif;font-size:clamp(24px,7vw,40px);letter-spacing:7px;background:linear-gradient(135deg,var(--g2),var(--gold),#b8882b);background-size:200%;-webkit-background-clip:text;background-clip:text;color:transparent;text-align:center;animation:shimmer 3s ease-in-out infinite}
|
||||
.end-reason{font-size:10px;letter-spacing:5px;color:var(--dim);text-transform:uppercase}
|
||||
.end-cubes{display:flex;align-items:center;gap:18px}
|
||||
.end-cube-card{display:flex;flex-direction:column;align-items:center;gap:4px;opacity:.45;transition:opacity .3s}
|
||||
.end-cube-card.winner{opacity:1}
|
||||
.end-cube-lbl{font-family:'Bebas Neue',sans-serif;font-size:9px;letter-spacing:3px;color:var(--dim);text-transform:uppercase}
|
||||
.end-cube-card.winner .end-cube-lbl{color:var(--gold)}
|
||||
.end-cube-sub{font-size:8px;letter-spacing:1px;color:var(--dim)}
|
||||
.end-stats{display:flex;gap:clamp(14px,4.5vw,24px);flex-wrap:wrap;justify-content:center;background:var(--s1);border:1px solid var(--bd);border-radius:14px;padding:clamp(9px,2.2vw,14px) clamp(14px,3.5vw,22px);box-shadow:0 4px 30px rgba(0,0,0,.4)}
|
||||
.stat{display:flex;flex-direction:column;align-items:center;gap:2px}
|
||||
.stat-v{font-family:'Bebas Neue',sans-serif;font-size:clamp(18px,5vw,26px);color:var(--gold);letter-spacing:2px}
|
||||
.stat-l{font-size:7px;letter-spacing:3px;color:var(--dim);text-transform:uppercase}
|
||||
.end-btns{display:flex;gap:9px;flex-wrap:wrap;justify-content:center}
|
||||
.btn1{font-family:'Bebas Neue',sans-serif;font-size:13px;letter-spacing:5px;color:var(--bg);background:linear-gradient(110deg,var(--g3),var(--g2),var(--gold));background-size:200%;border:none;border-radius:11px;padding:13px 26px;cursor:pointer;box-shadow:0 0 26px rgba(232,184,75,.5);animation:shimmer 3s ease-in-out infinite;transition:transform .18s}
|
||||
.btn1:active{transform:scale(.94)}
|
||||
.btn2{font-family:'Bebas Neue',sans-serif;font-size:13px;letter-spacing:5px;color:var(--txt);background:var(--s2);border:1px solid var(--bd);border-radius:11px;padding:13px 26px;cursor:pointer;transition:transform .18s,border-color .2s}
|
||||
.btn2:active{transform:scale(.94);border-color:var(--g3)}
|
||||
|
||||
/* RUBIK'S CUBE */
|
||||
.rcube{display:grid;grid-template-columns:repeat(3,1fr);gap:1.5px;padding:2px;background:var(--s3);border-radius:4px;box-shadow:0 2px 8px rgba(0,0,0,.4)}
|
||||
.rcube.sm{width:22px;height:22px;gap:1px;padding:1.5px}
|
||||
.rcube.md{width:30px;height:30px;gap:1.5px;padding:2px}
|
||||
.rcube.lg{width:42px;height:42px;gap:2px;padding:3px}
|
||||
.rc{border-radius:1.5px}
|
||||
.p1c0{background:#e84b4b}.p1c1{background:#e8b84b}.p1c2{background:#38e878}
|
||||
.p1c3{background:#4b9ee8}.p1c4{background:#e8b84b}.p1c5{background:#e84b4b}
|
||||
.p1c6{background:#38e878}.p1c7{background:#4b9ee8}.p1c8{background:#e8b84b}
|
||||
.p2c0{background:#4b9ee8}.p2c1{background:#38e878}.p2c2{background:#e8b84b}
|
||||
.p2c3{background:#e84b4b}.p2c4{background:#4b9ee8}.p2c5{background:#38e878}
|
||||
.p2c6{background:#e8b84b}.p2c7{background:#e84b4b}.p2c8{background:#4b9ee8}
|
||||
|
||||
/* USER MANAGER */
|
||||
.um{position:fixed;inset:0;z-index:260;display:flex;flex-direction:column;background:var(--bg);animation:cfg-in .35s cubic-bezier(.16,1,.3,1) both}
|
||||
.um-hd{display:flex;align-items:center;gap:8px;padding:clamp(14px,3.5vh,24px) 18px 10px;border-bottom:1px solid var(--bd);flex-shrink:0}
|
||||
.um-title{font-family:'Bebas Neue',sans-serif;font-size:clamp(16px,4.5vw,22px);letter-spacing:5px;color:var(--gold);flex:1}
|
||||
.um-add-btn{font-family:'Bebas Neue',sans-serif;font-size:12px;letter-spacing:3px;color:var(--bg);background:linear-gradient(135deg,var(--g2),var(--gold));border:none;border-radius:9px;padding:7px 13px;cursor:pointer;white-space:nowrap}
|
||||
.um-close{width:34px;height:34px;border-radius:50%;background:var(--s2);border:1px solid var(--bd);color:var(--dim);font-size:15px;display:flex;align-items:center;justify-content:center;cursor:pointer;flex-shrink:0}
|
||||
.um-body{flex:1;overflow-y:auto;padding:12px 14px 36px;-webkit-overflow-scrolling:touch}
|
||||
.um-body::-webkit-scrollbar{display:none}
|
||||
.um-card{background:var(--s1);border:1px solid var(--bd);border-radius:13px;padding:12px 13px;margin-bottom:9px;display:flex;flex-direction:column;gap:9px;transition:border-color .2s}
|
||||
.um-card.editing{border-color:var(--gold);background:rgba(232,184,75,.04)}
|
||||
.um-row{display:flex;align-items:center;gap:10px}
|
||||
.um-av{font-size:24px;line-height:1;width:44px;height:44px;background:var(--s2);border:1.5px solid var(--bd);border-radius:10px;display:flex;align-items:center;justify-content:center;cursor:pointer;flex-shrink:0;transition:border-color .2s}
|
||||
.um-av:active{transform:scale(.9)}
|
||||
.um-av.open{border-color:var(--gold)}
|
||||
.um-info{flex:1;display:flex;flex-direction:column;gap:3px;min-width:0}
|
||||
.um-name{font-weight:700;font-size:clamp(13px,3.8vw,15px);color:var(--txt);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.um-meta{display:flex;gap:8px;align-items:center}
|
||||
.um-elo{font-family:'Bebas Neue',sans-serif;font-size:13px;color:var(--grn);letter-spacing:1px}
|
||||
.um-streak{font-size:10px;color:var(--red)}
|
||||
.um-wins{font-size:10px;color:var(--dim)}
|
||||
.um-actions{display:flex;gap:6px;flex-shrink:0}
|
||||
.um-btn{width:32px;height:32px;border-radius:9px;background:var(--s2);border:1px solid var(--bd);font-size:14px;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:all .15s}
|
||||
.um-btn.edit:active{background:var(--s3);border-color:var(--gold)}
|
||||
.um-btn.del:active{background:rgba(240,64,64,.1);border-color:var(--red)}
|
||||
.um-name-input{background:var(--s2);border:1px solid var(--gold);border-radius:9px;padding:7px 11px;font-family:'Oxanium',sans-serif;font-size:clamp(13px,3.8vw,15px);font-weight:700;color:var(--gold);caret-color:var(--gold);outline:none;width:100%}
|
||||
.um-av-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:5px;background:var(--s2);border-radius:10px;padding:8px}
|
||||
.um-av-item{aspect-ratio:1;display:flex;align-items:center;justify-content:center;font-size:17px;border-radius:7px;cursor:pointer;border:1.5px solid transparent;transition:all .15s}
|
||||
.um-av-item.sel{border-color:var(--gold);background:rgba(232,184,75,.12)}
|
||||
.um-av-item:active{transform:scale(.88)}
|
||||
.um-save-row{display:flex;gap:7px}
|
||||
.um-save-btn{flex:1;padding:8px;border:none;border-radius:9px;font-family:'Bebas Neue',sans-serif;font-size:12px;letter-spacing:3px;color:var(--bg);background:linear-gradient(135deg,var(--g2),var(--gold));cursor:pointer}
|
||||
.um-cancel-btn{padding:8px 14px;border:1px solid var(--bd);background:transparent;border-radius:9px;font-size:10px;letter-spacing:2px;color:var(--dim);cursor:pointer;text-transform:uppercase}
|
||||
|
||||
/* PLAYER SELECT */
|
||||
.ps{position:fixed;inset:0;z-index:170;display:flex;flex-direction:column;background:var(--bg);animation:cfg-in .35s cubic-bezier(.16,1,.3,1) both}
|
||||
.ps-half{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:14px 14px 20px;gap:10px;overflow:hidden}
|
||||
.ps-half.flip{transform:rotate(180deg)}
|
||||
.ps-half.on0{background:radial-gradient(ellipse 80% 70% at center,rgba(232,184,75,.07) 0%,transparent 70%)}
|
||||
.ps-half.on1{background:radial-gradient(ellipse 80% 70% at center,rgba(75,158,232,.07) 0%,transparent 70%)}
|
||||
.ps-label{font-size:9px;letter-spacing:5px;color:var(--dim);text-transform:uppercase;font-weight:700}
|
||||
.ps-sel-display{display:flex;align-items:center;gap:8px;height:32px}
|
||||
.ps-sel-av{font-size:22px;line-height:1}
|
||||
.ps-sel-name{font-family:'Bebas Neue',sans-serif;font-size:clamp(14px,4vw,20px);letter-spacing:3px;color:var(--gold)}
|
||||
.ps-sel-name.blue{color:var(--blu)}
|
||||
.ps-sel-empty{font-size:9px;letter-spacing:3px;color:var(--dim);text-transform:uppercase}
|
||||
.ps-grid{display:flex;flex-wrap:wrap;gap:6px;justify-content:center;max-width:360px}
|
||||
.ps-chip{display:flex;align-items:center;gap:7px;padding:7px 11px;background:var(--s1);border:1.5px solid var(--bd);border-radius:11px;cursor:pointer;transition:all .18s cubic-bezier(.34,1.56,.64,1)}
|
||||
.ps-chip:active{transform:scale(.91)}
|
||||
.ps-chip.sel0{background:rgba(232,184,75,.1);border-color:var(--gold);box-shadow:0 0 12px rgba(232,184,75,.3)}
|
||||
.ps-chip.sel1{background:rgba(75,158,232,.1);border-color:var(--blu);box-shadow:0 0 12px rgba(75,158,232,.3)}
|
||||
.ps-chip.other-taken{opacity:.3;pointer-events:none}
|
||||
.ps-chip-av{font-size:16px;line-height:1}
|
||||
.ps-chip-name{font-size:11px;font-weight:700;color:var(--txt);white-space:nowrap}
|
||||
.ps-chip-elo{font-family:'Bebas Neue',sans-serif;font-size:10px;color:var(--grn);letter-spacing:1px}
|
||||
.ps-div{height:3px;flex-shrink:0;background:linear-gradient(90deg,transparent,var(--gold),var(--g2),var(--gold),transparent);background-size:200%;animation:dflow 3s linear infinite;position:relative}
|
||||
.ps-vs{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);background:var(--bg);border:1.5px solid var(--gold);border-radius:20px;padding:3px 13px;font-family:'Bebas Neue',sans-serif;font-size:13px;letter-spacing:4px;color:var(--gold)}
|
||||
.ps-footer{position:absolute;bottom:0;left:0;right:0;padding:7px 14px clamp(10px,2.5vh,20px);background:linear-gradient(to top,rgba(6,10,14,.98) 55%,transparent);display:flex;gap:8px;z-index:20}
|
||||
.ps-go{flex:1;padding:13px;border:none;border-radius:13px;font-family:'Bebas Neue',sans-serif;font-size:15px;letter-spacing:6px;cursor:pointer;transition:all .2s;text-transform:uppercase}
|
||||
.ps-go.ready{color:var(--bg);background:linear-gradient(110deg,var(--g3),var(--g2),var(--gold));background-size:250%;box-shadow:0 4px 28px rgba(232,184,75,.5);animation:shimmer 3s ease-in-out infinite}
|
||||
.ps-go.wait{color:var(--dim);background:var(--s2)}
|
||||
.ps-back{padding:13px 16px;border:1px solid var(--bd);background:transparent;border-radius:13px;font-size:11px;letter-spacing:2px;color:var(--dim);cursor:pointer;text-transform:uppercase}
|
||||
|
||||
/* STREAK */
|
||||
.streak-badge{display:inline-flex;align-items:center;gap:3px;background:rgba(240,64,64,.1);border:1px solid rgba(240,64,64,.3);border-radius:10px;padding:2px 7px;font-family:'Bebas Neue',sans-serif;font-size:10px;letter-spacing:2px;color:var(--red);animation:str-pulse 1.8s ease-in-out infinite alternate}
|
||||
@keyframes str-pulse{from{box-shadow:0 0 3px rgba(240,64,64,.2)}to{box-shadow:0 0 10px rgba(240,64,64,.5)}}
|
||||
.chaos-lbl{font-size:8px;letter-spacing:3px;color:var(--pur);text-transform:uppercase;animation:cpulse .8s ease-in-out infinite alternate}
|
||||
@keyframes cpulse{from{opacity:.4}to{opacity:1}}
|
||||
.p-av{font-size:clamp(16px,4.5vw,22px);line-height:1}
|
||||
|
||||
/* TOGGLE CHAOS */
|
||||
.toggle.chaos.on{background:var(--pur);border-color:var(--pur)}
|
||||
.toggle.chaos.on .toggle-knob{box-shadow:0 0 8px rgba(168,85,247,.8)}
|
||||
|
||||
/* TIME CHART */
|
||||
.chart-wrap{width:100%;max-width:320px;background:var(--s1);border:1px solid var(--bd);border-radius:12px;padding:9px 12px}
|
||||
.chart-ttl{font-size:8px;letter-spacing:3px;color:var(--dim);text-transform:uppercase;margin-bottom:6px;text-align:center}
|
||||
.chart-leg{display:flex;gap:14px;justify-content:center;margin-top:5px}
|
||||
.chart-li{display:flex;align-items:center;gap:4px;font-size:8px;letter-spacing:2px;color:var(--dim);text-transform:uppercase}
|
||||
.chart-ld{width:8px;height:8px;border-radius:50%}
|
||||
.end-elo{font-size:9px;letter-spacing:2px}
|
||||
.end-elo.pos{color:var(--grn)}
|
||||
.end-elo.neg{color:var(--red)}
|
||||
|
||||
/* PROFILE SCREEN */
|
||||
.prf{position:fixed;inset:0;z-index:260;display:flex;flex-direction:column;background:var(--bg);animation:cfg-in .35s cubic-bezier(.16,1,.3,1) both}
|
||||
.prf-hd{display:flex;align-items:center;justify-content:space-between;padding:clamp(14px,3.5vh,24px) 18px 10px;border-bottom:1px solid var(--bd);flex-shrink:0}
|
||||
.prf-title{font-family:'Bebas Neue',sans-serif;font-size:clamp(16px,4.5vw,22px);letter-spacing:5px;color:var(--gold)}
|
||||
.prf-save-btn{font-family:'Bebas Neue',sans-serif;font-size:12px;letter-spacing:3px;color:var(--bg);background:linear-gradient(135deg,var(--g2),var(--gold));border:none;border-radius:9px;padding:7px 14px;cursor:pointer}
|
||||
.prf-body{flex:1;overflow-y:auto;padding:14px 14px 36px;-webkit-overflow-scrolling:touch}
|
||||
.prf-body::-webkit-scrollbar{display:none}
|
||||
.prf-card{background:var(--s1);border:1px solid var(--bd);border-radius:14px;padding:14px;margin-bottom:12px}
|
||||
.prf-card-title{font-size:9px;letter-spacing:4px;color:var(--dim);text-transform:uppercase;margin-bottom:10px}
|
||||
.prf-top-row{display:flex;align-items:flex-start;gap:10px;margin-bottom:12px}
|
||||
.prf-av-btn{font-size:26px;line-height:1;width:50px;height:50px;border-radius:12px;background:var(--s2);border:1.5px solid var(--bd);display:flex;align-items:center;justify-content:center;cursor:pointer;flex-shrink:0;transition:border-color .2s}
|
||||
.prf-av-btn.open{border-color:var(--gold)}
|
||||
.prf-name-col{flex:1;display:flex;flex-direction:column;gap:5px}
|
||||
.prf-name-input{background:var(--s2);border:1px solid var(--bd);border-radius:9px;padding:8px 11px;font-family:'Oxanium',sans-serif;font-size:clamp(13px,3.8vw,16px);font-weight:700;color:var(--gold);caret-color:var(--gold);outline:none;width:100%;transition:border-color .2s}
|
||||
.prf-name-input:focus{border-color:var(--gold)}
|
||||
.prf-name-hint{font-size:8px;letter-spacing:2px;color:var(--dim);text-transform:uppercase}
|
||||
.prf-stats-row{display:flex;gap:7px;margin-bottom:10px}
|
||||
.prf-stat-box{flex:1;background:var(--s2);border:1px solid var(--bd);border-radius:10px;padding:7px 8px;display:flex;flex-direction:column;align-items:center;gap:2px}
|
||||
.prf-stat-val{font-family:'Bebas Neue',sans-serif;font-size:clamp(16px,4.5vw,22px);color:var(--gold);letter-spacing:2px}
|
||||
.prf-stat-val.green{color:var(--grn)}
|
||||
.prf-stat-val.red{color:var(--red)}
|
||||
.prf-stat-lbl{font-size:7px;letter-spacing:2px;color:var(--dim);text-transform:uppercase}
|
||||
.prf-av-grid{display:grid;grid-template-columns:repeat(8,1fr);gap:5px;background:var(--s2);border-radius:10px;padding:8px;margin-bottom:8px}
|
||||
.prf-av-item{aspect-ratio:1;display:flex;align-items:center;justify-content:center;font-size:17px;border-radius:7px;cursor:pointer;border:1.5px solid transparent;transition:all .15s}
|
||||
.prf-av-item.sel{border-color:var(--gold);background:rgba(232,184,75,.12)}
|
||||
.prf-av-item:active{transform:scale(.88)}
|
||||
.prf-reset-btn{width:100%;padding:8px;border:1px solid rgba(240,64,64,.3);background:transparent;border-radius:9px;font-size:10px;letter-spacing:2px;color:rgba(240,64,64,.6);cursor:pointer;text-transform:uppercase;transition:all .2s}
|
||||
.prf-reset-btn:active{background:rgba(240,64,64,.1);border-color:var(--red)}
|
||||
|
||||
/* CUBE PREP SCREEN */
|
||||
.prep{position:fixed;inset:0;z-index:180;background:var(--bg);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:clamp(12px,3vh,22px);padding:20px;animation:cfg-in .4s cubic-bezier(.16,1,.3,1) both}
|
||||
.prep::before{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 70% 50% at 50% 40%,rgba(75,158,232,.05) 0%,transparent 70%);pointer-events:none}
|
||||
.prep-title{font-family:'Bebas Neue',sans-serif;font-size:clamp(20px,6vw,34px);letter-spacing:7px;background:linear-gradient(135deg,var(--g2),var(--gold));-webkit-background-clip:text;background-clip:text;color:transparent;text-align:center;background-size:200%;animation:shimmer 3s ease-in-out infinite}
|
||||
.prep-sub{font-size:9px;letter-spacing:4px;color:var(--dim);text-transform:uppercase;text-align:center}
|
||||
.prep-players{display:flex;gap:14px;align-items:flex-start;width:100%}
|
||||
.prep-p-card{background:var(--s1);border:1px solid var(--bd);border-radius:14px;padding:10px 8px;display:flex;flex-direction:column;align-items:center;gap:7px;flex:1}
|
||||
.prep-p-name{font-size:9px;letter-spacing:3px;color:var(--dim);text-transform:uppercase;text-align:center}
|
||||
.prep-scramble{display:flex;flex-wrap:wrap;gap:3px;justify-content:center}
|
||||
.prep-move{font-family:'Bebas Neue',sans-serif;font-size:clamp(10px,2.8vw,13px);color:var(--gold);background:var(--s2);border:1px solid var(--bd);border-radius:5px;padding:2px 5px;min-width:22px;text-align:center}
|
||||
.prep-countdown{font-family:'Bebas Neue',sans-serif;font-size:clamp(48px,14vw,86px);color:var(--blu);text-shadow:0 0 40px rgba(75,158,232,.6);letter-spacing:-2px;animation:hbreathe .5s ease-in-out infinite alternate}
|
||||
.prep-ready-btn{padding:13px 34px;border:none;border-radius:13px;font-family:'Bebas Neue',sans-serif;font-size:clamp(14px,4vw,18px);letter-spacing:6px;color:var(--bg);background:linear-gradient(135deg,var(--g2),var(--gold));cursor:pointer;box-shadow:0 4px 28px rgba(232,184,75,.5);transition:transform .18s}
|
||||
.prep-ready-btn:active{transform:scale(.95)}
|
||||
.prep-skip{font-size:9px;letter-spacing:2px;color:var(--dim);text-transform:uppercase;cursor:pointer;text-decoration:underline;text-underline-offset:3px}
|
||||
|
||||
/* SOLO CS TIMER */
|
||||
.scs{position:fixed;inset:0;z-index:200;display:flex;flex-direction:column;background:var(--bg)}
|
||||
.scs-panel{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:clamp(8px,2vh,14px);position:relative;overflow:hidden;cursor:pointer;transition:background .3s}
|
||||
.scs-panel.flip{transform:rotate(180deg)}
|
||||
.scs-panel.idle-bg{background:radial-gradient(ellipse 70% 70% at center,rgba(20,55,130,.7) 0%,rgba(6,10,14,.98) 70%)}
|
||||
.scs-panel.hold-bg{animation:scs-hbg .8s ease-in-out infinite alternate}
|
||||
@keyframes scs-hbg{from{background:radial-gradient(ellipse 65% 65% at center,rgba(10,110,44,.85) 0%,rgba(6,10,14,.98) 68%)}to{background:radial-gradient(ellipse 88% 88% at center,rgba(14,150,58,.9) 0%,rgba(6,10,14,.98) 68%)}}
|
||||
.scs-panel.run-bg{animation:scs-rbg .4s ease-in-out infinite alternate}
|
||||
@keyframes scs-rbg{from{background:radial-gradient(ellipse 70% 70% at center,rgba(140,20,20,.9) 0%,rgba(6,10,14,.98) 68%)}to{background:radial-gradient(ellipse 90% 90% at center,rgba(180,28,28,.95) 0%,rgba(6,10,14,.98) 68%)}}
|
||||
.scs-panel.stop-bg{background:rgba(6,10,14,.98)}
|
||||
.scs-panel.win-bg{animation:scs-wbg .6s ease-in-out infinite alternate}
|
||||
@keyframes scs-wbg{from{background:radial-gradient(ellipse 70% 70% at center,rgba(10,120,48,.9) 0%,rgba(6,10,14,.98) 68%)}to{background:radial-gradient(ellipse 95% 95% at center,rgba(16,160,64,.95) 0%,rgba(6,10,14,.98) 68%)}}
|
||||
.scs-panel.lose-bg{background:rgba(6,10,14,.98)}
|
||||
.scs-state{font-family:'Bebas Neue',sans-serif;font-size:clamp(11px,3vw,14px);letter-spacing:6px;text-transform:uppercase;position:relative;z-index:1}
|
||||
.scs-state.blu{color:#7ec8ff;text-shadow:0 0 20px rgba(100,180,255,1)}
|
||||
.scs-state.grn{color:#7affc0;text-shadow:0 0 20px rgba(80,255,160,1)}
|
||||
.scs-state.red{color:#ff8080;text-shadow:0 0 20px rgba(255,100,100,1)}
|
||||
.scs-state.gold{color:var(--g2);text-shadow:0 0 20px rgba(255,217,122,.9)}
|
||||
.scs-state.dim{color:rgba(200,216,232,.5)}
|
||||
.scs-time{font-family:'Bebas Neue',sans-serif;font-size:clamp(52px,14vw,90px);letter-spacing:-2px;line-height:1;position:relative;z-index:1;transition:color .2s,text-shadow .2s}
|
||||
.scs-time.idle{color:#90d4ff;text-shadow:0 0 40px rgba(100,190,255,.9)}
|
||||
.scs-time.hold{color:#80ffbe;text-shadow:0 0 50px rgba(80,255,160,1);animation:hbreathe .5s ease-in-out infinite alternate}
|
||||
.scs-time.run{color:#ff9090;text-shadow:0 0 40px rgba(255,100,100,1);animation:runpulse .35s ease-in-out infinite alternate}
|
||||
.scs-time.stop{color:rgba(200,216,232,.5)}
|
||||
.scs-time.win{color:#80ffbe;text-shadow:0 0 60px rgba(80,255,160,1);animation:wbreathe .5s ease-in-out infinite alternate}
|
||||
.scs-time.lose{color:rgba(200,216,232,.25)}
|
||||
.scs-hint{font-size:clamp(9px,2.2vw,11px);letter-spacing:3px;color:rgba(200,216,232,.7);text-transform:uppercase;position:relative;z-index:1;font-weight:600}
|
||||
.scs-ring{position:relative;z-index:1;display:flex;align-items:center;justify-content:center}
|
||||
.scs-name{display:flex;align-items:center;gap:7px;position:relative;z-index:1}
|
||||
.scs-av{font-size:clamp(16px,4.5vw,22px);line-height:1}
|
||||
.scs-plbl{font-size:9px;letter-spacing:5px;color:var(--dim);text-transform:uppercase;font-weight:700}
|
||||
.scs-win-lbl{font-family:'Bebas Neue',sans-serif;font-size:clamp(14px,4vw,22px);letter-spacing:5px;color:#80ffbe;text-shadow:0 0 24px rgba(80,255,160,.9);position:relative;z-index:1}
|
||||
.scs-div{height:3px;flex-shrink:0;background:linear-gradient(90deg,transparent,var(--gold),var(--g2),var(--gold),transparent);background-size:200%;animation:dflow 3s linear infinite;position:relative}
|
||||
.scs-vs{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);background:var(--bg);border:1.5px solid var(--gold);border-radius:20px;padding:3px 13px;font-family:'Bebas Neue',sans-serif;font-size:13px;letter-spacing:4px;color:var(--gold)}
|
||||
.scs-footer{position:absolute;bottom:0;left:0;right:0;padding:6px 14px clamp(8px,2vh,18px);background:linear-gradient(to top,rgba(6,10,14,.98) 55%,transparent);display:flex;justify-content:center;gap:10px;z-index:30}
|
||||
.scs-reset{font-family:'Oxanium',sans-serif;font-size:10px;font-weight:700;letter-spacing:2px;color:var(--dim);background:var(--s1);border:1px solid var(--bd);border-radius:10px;padding:8px 13px;cursor:pointer;text-transform:uppercase;transition:all .2s}
|
||||
.scs-reset:active{color:var(--gold);border-color:var(--g3);transform:scale(.92)}
|
||||
.scs-close{font-family:'Oxanium',sans-serif;font-size:10px;font-weight:700;letter-spacing:2px;color:var(--dim);background:var(--s1);border:1px solid var(--bd);border-radius:10px;padding:8px 13px;cursor:pointer;text-transform:uppercase;transition:all .2s}
|
||||
.scs-close:active{color:var(--red);border-color:var(--red);transform:scale(.92)}
|
||||
|
||||
/* SOLO ONE-PLAYER MODE */
|
||||
.scs-solo{position:fixed;inset:0;z-index:200;display:flex;flex-direction:column;background:var(--bg)}
|
||||
.scs-solo-panel{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:clamp(10px,2.5vh,18px);position:relative;overflow:hidden;cursor:pointer;transition:background .3s}
|
||||
.scs-solo-panel.idle-bg{background:radial-gradient(ellipse 70% 70% at center,rgba(20,55,130,.7) 0%,rgba(6,10,14,.98) 70%)}
|
||||
.scs-solo-panel.hold-bg,.scs-solo-panel.ready-bg{animation:scs-hbg .8s ease-in-out infinite alternate}
|
||||
.scs-solo-panel.run-bg{animation:scs-rbg .4s ease-in-out infinite alternate}
|
||||
.scs-solo-panel.stop-bg{background:rgba(6,10,14,.98)}
|
||||
.scs-solo-time{font-family:'Bebas Neue',sans-serif;font-size:clamp(72px,20vw,130px);letter-spacing:-4px;line-height:1;position:relative;z-index:1;transition:color .2s,text-shadow .2s}
|
||||
.scs-solo-time.idle{color:#90d4ff;text-shadow:0 0 40px rgba(100,190,255,.9)}
|
||||
.scs-solo-time.hold{color:#80ffbe;text-shadow:0 0 50px rgba(80,255,160,1);animation:hbreathe .5s ease-in-out infinite alternate}
|
||||
.scs-solo-time.ready{color:#ffffff;text-shadow:0 0 60px rgba(80,255,160,1);animation:wbreathe .4s ease-in-out infinite alternate}
|
||||
.scs-solo-time.run{color:#ff9090;text-shadow:0 0 40px rgba(255,100,100,1)}
|
||||
.scs-solo-time.stop{color:#80ffbe;text-shadow:0 0 50px rgba(80,255,160,.8)}
|
||||
.scs-solo-state{font-family:'Bebas Neue',sans-serif;font-size:clamp(12px,3.5vw,16px);letter-spacing:6px;text-transform:uppercase;position:relative;z-index:1}
|
||||
.scs-solo-state.blu{color:#7ec8ff;text-shadow:0 0 20px rgba(100,180,255,1)}
|
||||
.scs-solo-state.grn{color:#7affc0;text-shadow:0 0 20px rgba(80,255,160,1)}
|
||||
.scs-solo-state.wht{color:#ffffff;text-shadow:0 0 20px rgba(255,255,255,.8)}
|
||||
.scs-solo-state.red{color:#ff8080;text-shadow:0 0 20px rgba(255,100,100,1)}
|
||||
.scs-solo-state.gold{color:var(--g2);text-shadow:0 0 20px rgba(255,217,122,.9)}
|
||||
.scs-solo-hint{font-size:clamp(10px,2.5vw,12px);letter-spacing:3px;color:rgba(200,216,232,.65);text-transform:uppercase;position:relative;z-index:1;font-weight:600}
|
||||
.scs-solo-history{position:relative;z-index:1;display:flex;flex-direction:column;gap:5px;align-items:center;width:100%;max-width:280px}
|
||||
.scs-solo-hist-row{display:flex;align-items:center;justify-content:space-between;width:100%;padding:3px 10px;background:rgba(255,255,255,.05);border-radius:8px;border:1px solid rgba(255,255,255,.07)}
|
||||
.scs-solo-hist-n{font-size:9px;letter-spacing:2px;color:var(--dim);text-transform:uppercase;width:30px}
|
||||
.scs-solo-hist-t{font-family:'Bebas Neue',sans-serif;font-size:15px;letter-spacing:1px;color:var(--gold)}
|
||||
.scs-solo-hist-t.best{color:#80ffbe}
|
||||
.scs-solo-best{display:flex;gap:10px;align-items:center;position:relative;z-index:1}
|
||||
.scs-solo-best-lbl{font-size:9px;letter-spacing:3px;color:var(--dim);text-transform:uppercase}
|
||||
.scs-solo-best-val{font-family:'Bebas Neue',sans-serif;font-size:14px;letter-spacing:2px;color:#80ffbe}
|
||||
.scs-mode-toggle{display:flex;background:var(--s2);border:1px solid var(--bd);border-radius:10px;overflow:hidden}
|
||||
.scs-mode-btn{padding:7px 14px;font-family:'Bebas Neue',sans-serif;font-size:11px;letter-spacing:3px;color:var(--dim);cursor:pointer;transition:all .2s}
|
||||
.scs-mode-btn.active{background:var(--s3);color:var(--gold)}
|
||||
10
ethan/src/main.jsx
Normal file
10
ethan/src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
import App from "./App.jsx";
|
||||
|
||||
createRoot(document.getElementById("root")).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
6
ethan/vite.config.js
Normal file
6
ethan/vite.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
});
|
||||
BIN
favicon.png
Normal file
BIN
favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 652 KiB |
30
index.html
30
index.html
@@ -2,13 +2,22 @@
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#140700" />
|
||||
<meta name="application-name" content="ChessCubing Arena" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="ChessCubing" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Découvrez ChessCubing, l'association et le jeu qui réunit échecs et Rubik's Cube dans un format simple, vivant et spectaculaire."
|
||||
/>
|
||||
<title>ChessCubing Arena | Accueil</title>
|
||||
<link rel="icon" type="image/png" href="logo.png" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="shortcut icon" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" href="logo.png" />
|
||||
<link rel="manifest" href="site.webmanifest" />
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
<body class="home-body">
|
||||
@@ -26,19 +35,14 @@
|
||||
<h1>Les échecs rencontrent le Rubik's Cube</h1>
|
||||
<p class="lead">
|
||||
ChessCubing propose un jeu hybride simple à comprendre, intense à
|
||||
vivre et très plaisant à regarder. On joue un block d'échecs, on
|
||||
vivre et très plaisant à regarder. On joue une partie d'échecs, on
|
||||
passe par une phase cube obligatoire, puis la partie repart avec un
|
||||
nouveau rythme.
|
||||
</p>
|
||||
<div class="hero-pills">
|
||||
<span>Accessible en club</span>
|
||||
<span>Spectaculaire en démonstration</span>
|
||||
<span>Stratégie + vitesse</span>
|
||||
<span>Application officielle</span>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<a class="button primary" href="application.html">Ouvrir l'application</a>
|
||||
<a class="button secondary" href="reglement.html">Lire le règlement</a>
|
||||
<a class="button ghost" href="/ethan/">Ouvrir l'appli d'Ethan</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -46,7 +50,7 @@
|
||||
<div class="preview-card">
|
||||
<p class="micro-label">Le principe</p>
|
||||
<ol class="phase-list">
|
||||
<li>Deux joueurs disputent un block d'échecs court et nerveux.</li>
|
||||
<li>Deux joueurs disputent une partie d'échecs courte et nerveuse.</li>
|
||||
<li>Une phase cube coupe le rythme et relance la tension.</li>
|
||||
<li>Le résultat du cube influence immédiatement la suite du match.</li>
|
||||
</ol>
|
||||
@@ -116,7 +120,7 @@
|
||||
<div class="home-mini-grid">
|
||||
<article class="mini-panel">
|
||||
<span class="micro-label">1</span>
|
||||
<strong>On joue un block d'échecs</strong>
|
||||
<strong>On joue une partie d'échecs</strong>
|
||||
<p>
|
||||
La partie avance par séquences courtes, ce qui garde un très bon
|
||||
rythme et rend l'action facile à suivre.
|
||||
@@ -187,8 +191,8 @@
|
||||
<span class="mini-chip">ChessCubing Twice</span>
|
||||
<h3>Le cube donne l'élan</h3>
|
||||
<p>
|
||||
Le joueur le plus rapide sur la phase cube prend le départ du
|
||||
block suivant et peut même obtenir un double coup dans
|
||||
Le joueur le plus rapide sur la phase cube prend le départ de
|
||||
la partie suivante et peut même obtenir un double coup dans
|
||||
certaines situations.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
202
install-chesscubing-proxmox.sh
Executable file
202
install-chesscubing-proxmox.sh
Executable file
@@ -0,0 +1,202 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_BRANCH="${CHESSCUBING_GIT_BRANCH:-main}"
|
||||
RAW_BASE_URL="https://git.jeannerot.fr/christophe/chesscubing/raw/branch/${REPO_BRANCH}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Bootstrap d'installation ChessCubing pour Proxmox.
|
||||
|
||||
Usage local :
|
||||
./install-chesscubing-proxmox.sh
|
||||
|
||||
Usage en une ligne :
|
||||
bash -c "$(curl -fsSL https://git.jeannerot.fr/christophe/chesscubing/raw/branch/main/install-chesscubing-proxmox.sh)"
|
||||
|
||||
Variables d'environnement reconnues :
|
||||
CHESSCUBING_LOCAL
|
||||
CHESSCUBING_PROXMOX_HOST
|
||||
CHESSCUBING_PROXMOX_USER
|
||||
CHESSCUBING_PROXMOX_PASSWORD
|
||||
CHESSCUBING_SSH_PORT
|
||||
CHESSCUBING_CTID
|
||||
CHESSCUBING_LXC_HOSTNAME
|
||||
CHESSCUBING_LXC_IP
|
||||
CHESSCUBING_LXC_GATEWAY
|
||||
CHESSCUBING_LXC_BRIDGE
|
||||
CHESSCUBING_LXC_CORES
|
||||
CHESSCUBING_LXC_MEMORY
|
||||
CHESSCUBING_LXC_SWAP
|
||||
CHESSCUBING_LXC_DISK_GB
|
||||
CHESSCUBING_TEMPLATE_STORAGE
|
||||
CHESSCUBING_ROOTFS_STORAGE
|
||||
CHESSCUBING_LXC_PASSWORD
|
||||
CHESSCUBING_GIT_BRANCH
|
||||
CHESSCUBING_ETHAN_REPO_URL
|
||||
CHESSCUBING_ETHAN_GIT_BRANCH
|
||||
EOF
|
||||
}
|
||||
|
||||
die() {
|
||||
printf 'Erreur: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
have_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
need_cmd() {
|
||||
have_cmd "$1" || die "La commande '$1' est requise."
|
||||
}
|
||||
|
||||
prompt_default() {
|
||||
local var_name="$1"
|
||||
local prompt_label="$2"
|
||||
local default_value="${3:-}"
|
||||
local current_value="${!var_name:-}"
|
||||
local input=""
|
||||
|
||||
if [[ -n "$current_value" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -n "$default_value" ]]; then
|
||||
read -r -p "$prompt_label [$default_value]: " input
|
||||
printf -v "$var_name" '%s' "${input:-$default_value}"
|
||||
else
|
||||
read -r -p "$prompt_label: " input
|
||||
printf -v "$var_name" '%s' "$input"
|
||||
fi
|
||||
}
|
||||
|
||||
prompt_secret() {
|
||||
local var_name="$1"
|
||||
local prompt_label="$2"
|
||||
local current_value="${!var_name:-}"
|
||||
|
||||
if [[ -n "$current_value" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
read -rsp "$prompt_label: " current_value
|
||||
echo
|
||||
printf -v "$var_name" '%s' "$current_value"
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
need_cmd curl
|
||||
|
||||
LOCAL_MODE="${CHESSCUBING_LOCAL:-}"
|
||||
PROXMOX_HOST="${CHESSCUBING_PROXMOX_HOST:-}"
|
||||
PROXMOX_USER="${CHESSCUBING_PROXMOX_USER:-}"
|
||||
PROXMOX_PASSWORD="${CHESSCUBING_PROXMOX_PASSWORD:-}"
|
||||
SSH_PORT="${CHESSCUBING_SSH_PORT:-22}"
|
||||
CTID="${CHESSCUBING_CTID:-}"
|
||||
LXC_HOSTNAME="${CHESSCUBING_LXC_HOSTNAME:-chesscubing-web}"
|
||||
LXC_IP="${CHESSCUBING_LXC_IP:-dhcp}"
|
||||
LXC_GATEWAY="${CHESSCUBING_LXC_GATEWAY:-}"
|
||||
LXC_BRIDGE="${CHESSCUBING_LXC_BRIDGE:-vmbr0}"
|
||||
LXC_CORES="${CHESSCUBING_LXC_CORES:-2}"
|
||||
LXC_MEMORY="${CHESSCUBING_LXC_MEMORY:-1024}"
|
||||
LXC_SWAP="${CHESSCUBING_LXC_SWAP:-512}"
|
||||
LXC_DISK_GB="${CHESSCUBING_LXC_DISK_GB:-6}"
|
||||
TEMPLATE_STORAGE="${CHESSCUBING_TEMPLATE_STORAGE:-}"
|
||||
ROOTFS_STORAGE="${CHESSCUBING_ROOTFS_STORAGE:-}"
|
||||
LXC_PASSWORD="${CHESSCUBING_LXC_PASSWORD:-}"
|
||||
ETHAN_REPO_URL="${CHESSCUBING_ETHAN_REPO_URL:-https://git.jeannerot.fr/Mineloulou/Chesscubing.git}"
|
||||
ETHAN_REPO_BRANCH="${CHESSCUBING_ETHAN_GIT_BRANCH:-main}"
|
||||
|
||||
if [[ -z "$LOCAL_MODE" && -z "$PROXMOX_HOST" ]]; then
|
||||
if have_cmd pct && have_cmd pveam; then
|
||||
LOCAL_MODE="1"
|
||||
else
|
||||
LOCAL_MODE="0"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$LOCAL_MODE" != "1" ]]; then
|
||||
need_cmd ssh
|
||||
need_cmd sshpass
|
||||
prompt_default PROXMOX_HOST "IP ou nom du serveur Proxmox" ""
|
||||
prompt_default PROXMOX_USER "Utilisateur SSH Proxmox" "root@pam"
|
||||
prompt_secret PROXMOX_PASSWORD "Mot de passe SSH Proxmox"
|
||||
prompt_default SSH_PORT "Port SSH Proxmox" "22"
|
||||
fi
|
||||
|
||||
prompt_default CTID "CTID Proxmox (laisser vide pour auto)" ""
|
||||
prompt_default LXC_HOSTNAME "Nom du LXC" "chesscubing-web"
|
||||
prompt_default LXC_IP "IP du LXC ou dhcp" "dhcp"
|
||||
if [[ "$LXC_IP" != "dhcp" ]]; then
|
||||
prompt_default LXC_GATEWAY "Passerelle du LXC" ""
|
||||
fi
|
||||
prompt_default LXC_BRIDGE "Bridge réseau Proxmox" "vmbr0"
|
||||
|
||||
if [[ "$LOCAL_MODE" != "1" ]]; then
|
||||
[[ -n "$PROXMOX_HOST" ]] || die "Le serveur Proxmox est obligatoire."
|
||||
[[ -n "$PROXMOX_USER" ]] || die "L'utilisateur Proxmox est obligatoire."
|
||||
[[ -n "$PROXMOX_PASSWORD" ]] || die "Le mot de passe Proxmox est obligatoire."
|
||||
fi
|
||||
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
printf 'Téléchargement du script d’installation ChessCubing...\n'
|
||||
curl -fsSL "${RAW_BASE_URL}/scripts/install-proxmox-lxc.sh" -o "$TMP_DIR/install-proxmox-lxc.sh"
|
||||
chmod +x "$TMP_DIR/install-proxmox-lxc.sh"
|
||||
|
||||
cmd=(
|
||||
"$TMP_DIR/install-proxmox-lxc.sh"
|
||||
--hostname "$LXC_HOSTNAME"
|
||||
--lxc-ip "$LXC_IP"
|
||||
--bridge "$LXC_BRIDGE"
|
||||
--cores "$LXC_CORES"
|
||||
--memory "$LXC_MEMORY"
|
||||
--swap "$LXC_SWAP"
|
||||
--disk-gb "$LXC_DISK_GB"
|
||||
--branch "$REPO_BRANCH"
|
||||
--ethan-repo-url "$ETHAN_REPO_URL"
|
||||
--ethan-branch "$ETHAN_REPO_BRANCH"
|
||||
)
|
||||
|
||||
if [[ "$LOCAL_MODE" == "1" ]]; then
|
||||
cmd+=(--local)
|
||||
else
|
||||
cmd+=(
|
||||
--proxmox-host "$PROXMOX_HOST"
|
||||
--proxmox-user "$PROXMOX_USER"
|
||||
--proxmox-password "$PROXMOX_PASSWORD"
|
||||
--ssh-port "$SSH_PORT"
|
||||
)
|
||||
fi
|
||||
|
||||
if [[ -n "$CTID" ]]; then
|
||||
cmd+=(--ctid "$CTID")
|
||||
fi
|
||||
|
||||
if [[ -n "$LXC_GATEWAY" ]]; then
|
||||
cmd+=(--gateway "$LXC_GATEWAY")
|
||||
fi
|
||||
|
||||
if [[ -n "$TEMPLATE_STORAGE" ]]; then
|
||||
cmd+=(--template-storage "$TEMPLATE_STORAGE")
|
||||
fi
|
||||
|
||||
if [[ -n "$ROOTFS_STORAGE" ]]; then
|
||||
cmd+=(--rootfs-storage "$ROOTFS_STORAGE")
|
||||
fi
|
||||
|
||||
if [[ -n "$LXC_PASSWORD" ]]; then
|
||||
cmd+=(--lxc-password "$LXC_PASSWORD")
|
||||
fi
|
||||
|
||||
printf 'Lancement de l’installation LXC ChessCubing...\n'
|
||||
"${cmd[@]}"
|
||||
@@ -5,6 +5,14 @@ server {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location = /ethan {
|
||||
return 301 $scheme://$http_host/ethan/;
|
||||
}
|
||||
|
||||
location /ethan/ {
|
||||
try_files $uri $uri/ /ethan/index.html;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
@@ -2,13 +2,22 @@
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#140700" />
|
||||
<meta name="application-name" content="ChessCubing Arena" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="ChessCubing" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Page règlement officielle du site ChessCubing Arena avec synthèse des formats Twice et Time."
|
||||
/>
|
||||
<title>ChessCubing Arena | Règlement officiel</title>
|
||||
<link rel="icon" type="image/png" href="logo.png" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="shortcut icon" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" href="logo.png" />
|
||||
<link rel="manifest" href="site.webmanifest" />
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
</head>
|
||||
<body class="rules-body">
|
||||
@@ -47,7 +56,7 @@
|
||||
<p class="micro-label">Repères rapides</p>
|
||||
<div class="rule-metrics">
|
||||
<article class="metric-chip">
|
||||
<span>Block</span>
|
||||
<span>Partie</span>
|
||||
<strong>180 s</strong>
|
||||
</article>
|
||||
<article class="metric-chip">
|
||||
@@ -86,7 +95,7 @@
|
||||
</div>
|
||||
<p class="section-copy">
|
||||
Les deux règlements partagent la même colonne vertébrale : un
|
||||
match en blocks successifs, interrompus par une phase cube
|
||||
match en parties successives, interrompues par une phase cube
|
||||
obligatoire.
|
||||
</p>
|
||||
</div>
|
||||
@@ -101,10 +110,10 @@
|
||||
</p>
|
||||
</article>
|
||||
<article class="stage-card">
|
||||
<span class="micro-label">2. Block chess</span>
|
||||
<span class="micro-label">2. Partie d'échecs</span>
|
||||
<strong>180 secondes de jeu</strong>
|
||||
<p>
|
||||
Chaque block comporte une phase d'échecs limitée par une durée
|
||||
Chaque partie comporte une phase d'échecs limitée par une durée
|
||||
fixe et par un quota de coups selon le format FAST, FREEZE ou
|
||||
MASTERS.
|
||||
</p>
|
||||
@@ -123,7 +132,7 @@
|
||||
<strong>Jamais au temps</strong>
|
||||
<p>
|
||||
La partie se termine uniquement par échec et mat ou abandon,
|
||||
jamais par simple chute au temps ou dépassement d'un block.
|
||||
jamais par simple chute au temps ou dépassement d'une partie.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
@@ -159,7 +168,7 @@
|
||||
<li>Vérifier la présence des huit cubes et des caches numérotés.</li>
|
||||
<li>Confirmer des mélanges identiques sous chaque numéro.</li>
|
||||
<li>Préparer l'échiquier et la variante dans l'application.</li>
|
||||
<li>Contrôler le tirage au sort avant le premier block.</li>
|
||||
<li>Contrôler le tirage au sort avant la première partie.</li>
|
||||
<li>Déclencher chaque phase cube au bon moment.</li>
|
||||
<li>Surveiller le respect du plafond de 120 s en mode Time.</li>
|
||||
</ul>
|
||||
@@ -172,7 +181,7 @@
|
||||
<h2>Twice et Time, côte à côte</h2>
|
||||
</div>
|
||||
<p class="section-copy">
|
||||
Les deux formats partagent les blocks et la phase cube, mais leur
|
||||
Les deux formats partagent les parties et la phase cube, mais leur
|
||||
logique d'avantage diffère complètement.
|
||||
</p>
|
||||
</div>
|
||||
@@ -183,24 +192,24 @@
|
||||
<span class="mini-chip">Version V2</span>
|
||||
<h3>ChessCubing Twice</h3>
|
||||
<p>
|
||||
Le gagnant du cube obtient l'initiative sur le block suivant,
|
||||
Le gagnant du cube obtient l'initiative sur la partie suivante,
|
||||
avec une règle de double coup encadrée.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="format-badges">
|
||||
<span>Block : 180 s</span>
|
||||
<span>Partie : 180 s</span>
|
||||
<span>Temps par coup : 20 s max</span>
|
||||
<span>FAST / FREEZE / MASTERS : 6 / 8 / 10</span>
|
||||
</div>
|
||||
|
||||
<div class="format-section">
|
||||
<h4>Début et fin des blocks</h4>
|
||||
<h4>Début et fin des parties</h4>
|
||||
<ul class="rule-list compact">
|
||||
<li>Les Blancs commencent le block 1.</li>
|
||||
<li>Aucun double coup n'est possible au block 1.</li>
|
||||
<li>Un block s'arrête à 180 s ou quand les deux quotas sont atteints.</li>
|
||||
<li>Il est interdit de finir un block avec un roi en échec.</li>
|
||||
<li>Les Blancs commencent la partie 1.</li>
|
||||
<li>Aucun double coup n'est possible à la partie 1.</li>
|
||||
<li>Une partie s'arrête à 180 s ou quand les deux quotas sont atteints.</li>
|
||||
<li>Il est interdit de finir une partie avec un roi en échec.</li>
|
||||
<li>
|
||||
Si le dernier coup donne échec, les coups nécessaires pour
|
||||
parer sont joués hors quota.
|
||||
@@ -215,7 +224,7 @@
|
||||
<li>Les deux joueurs reçoivent un mélange identique.</li>
|
||||
<li>Le joueur le plus rapide gagne la phase cube.</li>
|
||||
<li>En cas d'égalité parfaite, la phase cube est rejouée.</li>
|
||||
<li>Le gagnant du cube commence le block suivant.</li>
|
||||
<li>Le gagnant du cube commence la partie suivante.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -223,10 +232,10 @@
|
||||
<span class="micro-label">Double coup V2</span>
|
||||
<strong>Condition stricte</strong>
|
||||
<p>
|
||||
Le gagnant du cube ne doit pas avoir joué le dernier coup du
|
||||
block précédent. Le premier coup est gratuit, non compté,
|
||||
Le gagnant du cube ne doit pas avoir joué le dernier coup de
|
||||
la partie précédente. Le premier coup est gratuit, non compté,
|
||||
peut capturer mais ne peut pas donner échec. Le second compte
|
||||
comme premier coup du block, peut donner échec, mais ne peut
|
||||
comme premier coup de la partie, peut donner échec, mais ne peut
|
||||
capturer qu'un pion ou une pièce mineure.
|
||||
</p>
|
||||
</div>
|
||||
@@ -262,9 +271,9 @@
|
||||
<div class="format-section">
|
||||
<h4>Structure temporelle</h4>
|
||||
<ul class="rule-list compact">
|
||||
<li>La structure des blocks est identique à celle du Twice.</li>
|
||||
<li>La structure des Blocks est identique à celle du Twice.</li>
|
||||
<li>Les quotas de coups restent les mêmes : 6, 8 ou 10.</li>
|
||||
<li>Chaque block est suivi d'une phase cube obligatoire.</li>
|
||||
<li>Chaque Block est suivi d'une phase cube obligatoire.</li>
|
||||
<li>Le trait est conservé après la phase cube.</li>
|
||||
<li>Aucun système de priorité ou de double coup n'existe.</li>
|
||||
</ul>
|
||||
|
||||
556
scripts/install-proxmox-lxc.sh
Executable file
556
scripts/install-proxmox-lxc.sh
Executable file
@@ -0,0 +1,556 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
trap 'printf "Erreur: echec de la commande [%s] a la ligne %s.\n" "$BASH_COMMAND" "$LINENO" >&2' ERR
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./scripts/install-proxmox-lxc.sh \
|
||||
--proxmox-host 192.168.1.10 \
|
||||
--proxmox-user root@pam \
|
||||
--proxmox-password 'motdepasse'
|
||||
|
||||
./scripts/install-proxmox-lxc.sh --local
|
||||
|
||||
Options principales:
|
||||
--proxmox-host IP ou nom DNS du serveur Proxmox
|
||||
--proxmox-user Utilisateur SSH Proxmox (defaut: root@pam)
|
||||
--proxmox-password Mot de passe SSH Proxmox
|
||||
--ssh-port Port SSH Proxmox (defaut: 22)
|
||||
--local Execute directement sur l'hote Proxmox local
|
||||
--ctid CTID Proxmox. Si vide, le prochain ID libre est utilise
|
||||
--hostname Nom du LXC (defaut: chesscubing-web)
|
||||
--lxc-ip IP du LXC ou 'dhcp' (defaut: dhcp)
|
||||
--gateway Passerelle si IP statique
|
||||
--bridge Bridge reseau Proxmox (defaut: vmbr0)
|
||||
--cores Nombre de vCPU du LXC (defaut: 2)
|
||||
--memory Memoire RAM en Mo (defaut: 1024)
|
||||
--swap Swap en Mo (defaut: 512)
|
||||
--disk-gb Taille disque du LXC en Go (defaut: 6)
|
||||
--template-storage Stockage Proxmox pour les templates
|
||||
--rootfs-storage Stockage Proxmox pour le disque LXC
|
||||
--repo-url Depot Git a deployer
|
||||
--branch Branche Git a deployer (defaut: main)
|
||||
--ethan-repo-url Depot Git de l'application Ethan
|
||||
--ethan-branch Branche Git de l'application Ethan (defaut: main)
|
||||
--lxc-password Mot de passe root du LXC. Genere si absent
|
||||
-h, --help Affiche cette aide
|
||||
|
||||
Exemple:
|
||||
./scripts/install-proxmox-lxc.sh \
|
||||
--proxmox-host 10.0.0.2 \
|
||||
--proxmox-user root@pam \
|
||||
--proxmox-password 'secret'
|
||||
EOF
|
||||
}
|
||||
|
||||
die() {
|
||||
printf 'Erreur: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
need_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || die "La commande '$1' est requise."
|
||||
}
|
||||
|
||||
PROXMOX_HOST=""
|
||||
PROXMOX_USER="root@pam"
|
||||
PROXMOX_PASSWORD="${PROXMOX_PASSWORD:-}"
|
||||
PROXMOX_PORT="22"
|
||||
LOCAL_MODE="0"
|
||||
|
||||
CTID=""
|
||||
LXC_HOSTNAME="chesscubing-web"
|
||||
LXC_IP="dhcp"
|
||||
LXC_GATEWAY=""
|
||||
LXC_BRIDGE="vmbr0"
|
||||
LXC_CORES="2"
|
||||
LXC_MEMORY="1024"
|
||||
LXC_SWAP="512"
|
||||
LXC_DISK_GB="6"
|
||||
TEMPLATE_STORAGE=""
|
||||
ROOTFS_STORAGE=""
|
||||
REPO_URL="https://git.jeannerot.fr/christophe/chesscubing.git"
|
||||
REPO_BRANCH="main"
|
||||
ETHAN_REPO_URL="https://git.jeannerot.fr/Mineloulou/Chesscubing.git"
|
||||
ETHAN_REPO_BRANCH="main"
|
||||
LXC_PASSWORD=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--proxmox-host)
|
||||
PROXMOX_HOST="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--proxmox-user)
|
||||
PROXMOX_USER="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--proxmox-password)
|
||||
PROXMOX_PASSWORD="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--ssh-port)
|
||||
PROXMOX_PORT="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--local)
|
||||
LOCAL_MODE="1"
|
||||
shift
|
||||
;;
|
||||
--ctid)
|
||||
CTID="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--hostname)
|
||||
LXC_HOSTNAME="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--lxc-ip)
|
||||
LXC_IP="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--gateway)
|
||||
LXC_GATEWAY="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--bridge)
|
||||
LXC_BRIDGE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--cores)
|
||||
LXC_CORES="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--memory)
|
||||
LXC_MEMORY="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--swap)
|
||||
LXC_SWAP="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--disk-gb)
|
||||
LXC_DISK_GB="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--template-storage)
|
||||
TEMPLATE_STORAGE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--rootfs-storage)
|
||||
ROOTFS_STORAGE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--repo-url)
|
||||
REPO_URL="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--branch)
|
||||
REPO_BRANCH="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--ethan-repo-url)
|
||||
ETHAN_REPO_URL="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--ethan-branch)
|
||||
ETHAN_REPO_BRANCH="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--lxc-password)
|
||||
LXC_PASSWORD="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "Option inconnue: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$LOCAL_MODE" != "1" && -z "$PROXMOX_HOST" ]]; then
|
||||
if command -v pct >/dev/null 2>&1 && command -v pveam >/dev/null 2>&1; then
|
||||
LOCAL_MODE="1"
|
||||
fi
|
||||
fi
|
||||
|
||||
payload_script="$(mktemp)"
|
||||
cleanup() {
|
||||
rm -f "$payload_script"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
cat >"$payload_script" <<'REMOTE'
|
||||
set -Eeuo pipefail
|
||||
|
||||
trap 'printf "Erreur: echec de la commande [%s] a la ligne %s.\n" "$BASH_COMMAND" "$LINENO" >&2' ERR
|
||||
|
||||
ctid="$1"
|
||||
lxc_hostname="$2"
|
||||
lxc_ip="$3"
|
||||
lxc_gateway="$4"
|
||||
lxc_bridge="$5"
|
||||
lxc_cores="$6"
|
||||
lxc_memory="$7"
|
||||
lxc_swap="$8"
|
||||
lxc_disk_gb="$9"
|
||||
shift 9
|
||||
template_storage="$1"
|
||||
rootfs_storage="$2"
|
||||
repo_url="$3"
|
||||
repo_branch="$4"
|
||||
ethan_repo_url="$5"
|
||||
ethan_repo_branch="$6"
|
||||
lxc_password="$7"
|
||||
|
||||
die() {
|
||||
printf 'Erreur: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
need_remote_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || die "La commande '$1' est absente sur Proxmox."
|
||||
}
|
||||
|
||||
pick_storage() {
|
||||
local candidate=""
|
||||
|
||||
for candidate in "$@"; do
|
||||
if pvesm status 2>/dev/null | awk 'NR > 1 { print $1 }' | grep -Fxq "$candidate"; then
|
||||
printf '%s\n' "$candidate"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
pick_first_dir_storage() {
|
||||
pvesm status 2>/dev/null | awk 'NR > 1 && $2 == "dir" { print $1; exit }'
|
||||
}
|
||||
|
||||
find_ctid_by_hostname() {
|
||||
local wanted="$1"
|
||||
local candidate=""
|
||||
local candidate_hostname=""
|
||||
|
||||
while read -r candidate; do
|
||||
[[ -n "$candidate" ]] || continue
|
||||
candidate_hostname="$(pct config "$candidate" 2>/dev/null | awk -F ': ' '/^hostname:/ { print $2; exit }')"
|
||||
if [[ "$candidate_hostname" == "$wanted" ]]; then
|
||||
printf '%s\n' "$candidate"
|
||||
return 0
|
||||
fi
|
||||
done < <(pct list | awk 'NR > 1 { print $1 }')
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
ct_exec() {
|
||||
pct exec "$ctid" -- bash -lc "$1"
|
||||
}
|
||||
|
||||
need_remote_cmd pct
|
||||
need_remote_cmd pveam
|
||||
need_remote_cmd pvesm
|
||||
|
||||
if [[ -z "$ctid" ]]; then
|
||||
command -v pvesh >/dev/null 2>&1 || die "Impossible de calculer le prochain CTID sans pvesh."
|
||||
ctid="$(pvesh get /cluster/nextid)"
|
||||
fi
|
||||
|
||||
if pct status "$ctid" >/dev/null 2>&1; then
|
||||
die "Le CTID $ctid existe deja."
|
||||
fi
|
||||
|
||||
if existing_ctid="$(find_ctid_by_hostname "$lxc_hostname")"; then
|
||||
die "Un conteneur nomme $lxc_hostname existe deja sous le CTID $existing_ctid."
|
||||
fi
|
||||
|
||||
if [[ -z "$template_storage" ]]; then
|
||||
template_storage="$(pick_storage local)"
|
||||
if [[ -z "$template_storage" ]]; then
|
||||
template_storage="$(pick_first_dir_storage)"
|
||||
fi
|
||||
fi
|
||||
|
||||
[[ -n "$template_storage" ]] || die "Aucun stockage de template detecte. Passe --template-storage."
|
||||
|
||||
if [[ -z "$rootfs_storage" ]]; then
|
||||
rootfs_storage="$(pick_storage local-lvm local-zfs local)"
|
||||
fi
|
||||
|
||||
[[ -n "$rootfs_storage" ]] || die "Aucun stockage rootfs detecte. Passe --rootfs-storage."
|
||||
|
||||
printf 'Mise a jour du catalogue de templates Proxmox...\n'
|
||||
pveam update >/dev/null
|
||||
|
||||
template_name="$(pveam available --section system | awk '/debian-12-standard/ { print $2 }' | sort -V | tail -n 1)"
|
||||
if [[ -z "$template_name" ]]; then
|
||||
template_name="debian-12-standard_12.7-1_amd64.tar.zst"
|
||||
fi
|
||||
|
||||
if ! pveam list "$template_storage" 2>/dev/null | grep -Fq "$template_name"; then
|
||||
printf 'Telechargement du template %s sur %s...\n' "$template_name" "$template_storage"
|
||||
pveam download "$template_storage" "$template_name" >/dev/null
|
||||
fi
|
||||
|
||||
template_ref="${template_storage}:vztmpl/${template_name}"
|
||||
|
||||
if [[ -z "$lxc_password" ]]; then
|
||||
lxc_password="$(od -An -N12 -tx1 /dev/urandom | tr -d ' \n')"
|
||||
fi
|
||||
|
||||
net0="name=eth0,bridge=${lxc_bridge},ip=dhcp"
|
||||
if [[ "$lxc_ip" != "dhcp" ]]; then
|
||||
net0="name=eth0,bridge=${lxc_bridge},ip=${lxc_ip}"
|
||||
if [[ -n "$lxc_gateway" ]]; then
|
||||
net0="${net0},gw=${lxc_gateway}"
|
||||
fi
|
||||
fi
|
||||
|
||||
printf 'Creation du LXC %s (%s)...\n' "$ctid" "$lxc_hostname"
|
||||
pct create "$ctid" "$template_ref" \
|
||||
--hostname "$lxc_hostname" \
|
||||
--cores "$lxc_cores" \
|
||||
--memory "$lxc_memory" \
|
||||
--swap "$lxc_swap" \
|
||||
--rootfs "${rootfs_storage}:${lxc_disk_gb}" \
|
||||
--net0 "$net0" \
|
||||
--onboot 1 \
|
||||
--ostype debian \
|
||||
--password "$lxc_password" \
|
||||
--unprivileged 1
|
||||
|
||||
pct start "$ctid"
|
||||
|
||||
printf 'Attente du demarrage du LXC...\n'
|
||||
for _ in $(seq 1 20); do
|
||||
if pct exec "$ctid" -- true >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
pct exec "$ctid" -- true >/dev/null 2>&1 || die "Le LXC n'est pas joignable apres le demarrage."
|
||||
|
||||
printf 'Installation de nginx, git et rsync dans le conteneur...\n'
|
||||
ct_exec "apt-get update && apt-get install -y ca-certificates git nginx rsync"
|
||||
|
||||
ct_exec "install -d -m 0755 /opt/chesscubing/repo /opt/chesscubing/ethan-repo /var/www/chesscubing/current"
|
||||
|
||||
printf 'Clonage du depot %s...\n' "$repo_url"
|
||||
ct_exec "if [ ! -d /opt/chesscubing/repo/.git ]; then \
|
||||
rm -rf /opt/chesscubing/repo/* /opt/chesscubing/repo/.[!.]* /opt/chesscubing/repo/..?* 2>/dev/null || true; \
|
||||
git clone --branch '$repo_branch' --single-branch '$repo_url' /opt/chesscubing/repo; \
|
||||
else \
|
||||
cd /opt/chesscubing/repo && \
|
||||
git fetch origin '$repo_branch' && \
|
||||
if git show-ref --verify --quiet 'refs/heads/$repo_branch'; then git checkout '$repo_branch'; else git checkout -b '$repo_branch' --track 'origin/$repo_branch'; fi && \
|
||||
git pull --ff-only origin '$repo_branch'; \
|
||||
fi"
|
||||
|
||||
ct_exec "cat > /usr/local/bin/update-chesscubing <<'SCRIPT'
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
trap 'printf \"Erreur: echec de la commande [%s] a la ligne %s.\\n\" \"\$BASH_COMMAND\" \"\$LINENO\" >&2' ERR
|
||||
|
||||
main_repo_dir='/opt/chesscubing/repo'
|
||||
ethan_repo_dir='/opt/chesscubing/ethan-repo'
|
||||
web_root='/var/www/chesscubing/current'
|
||||
main_branch=\"\${1:-${repo_branch}}\"
|
||||
ethan_repo_url='${ethan_repo_url}'
|
||||
ethan_branch='${ethan_repo_branch}'
|
||||
|
||||
sync_git_repo() {
|
||||
local repo_dir=\"\$1\"
|
||||
local repo_url=\"\$2\"
|
||||
local branch=\"\$3\"
|
||||
local label=\"\$4\"
|
||||
|
||||
if [[ -d \"\$repo_dir/.git\" ]]; then
|
||||
cd \"\$repo_dir\"
|
||||
|
||||
if ! git diff --quiet || ! git diff --cached --quiet; then
|
||||
echo \"Le depot \${label} contient des modifications locales. Mise a jour annulee.\" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git fetch origin \"\$branch\"
|
||||
if git show-ref --verify --quiet \"refs/heads/\$branch\"; then
|
||||
git checkout \"\$branch\"
|
||||
else
|
||||
git checkout -b \"\$branch\" --track \"origin/\$branch\"
|
||||
fi
|
||||
git pull --ff-only origin \"\$branch\"
|
||||
return 0
|
||||
fi
|
||||
|
||||
[[ -n \"\$repo_url\" ]] || {
|
||||
echo \"Le depot \${label} est absent et aucune URL n'a ete fournie.\" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
rm -rf \"\$repo_dir\"
|
||||
git clone --branch \"\$branch\" --single-branch \"\$repo_url\" \"\$repo_dir\"
|
||||
}
|
||||
|
||||
publish_static_tree() {
|
||||
local source_dir=\"\$1\"
|
||||
local destination_dir=\"\$2\"
|
||||
|
||||
install -d -m 0755 \"\$destination_dir\"
|
||||
|
||||
rsync -a --delete \
|
||||
--include='*/' \
|
||||
--include='*.html' \
|
||||
--include='*.css' \
|
||||
--include='*.js' \
|
||||
--include='*.mjs' \
|
||||
--include='*.png' \
|
||||
--include='*.jpg' \
|
||||
--include='*.jpeg' \
|
||||
--include='*.svg' \
|
||||
--include='*.webp' \
|
||||
--include='*.ico' \
|
||||
--include='*.pdf' \
|
||||
--include='*.webmanifest' \
|
||||
--exclude='*' \
|
||||
\"\$source_dir/\" \"\$destination_dir/\"
|
||||
}
|
||||
|
||||
sync_git_repo \"\$main_repo_dir\" '${repo_url}' \"\$main_branch\" 'principal'
|
||||
sync_git_repo \"\$ethan_repo_dir\" \"\$ethan_repo_url\" \"\$ethan_branch\" 'Ethan'
|
||||
|
||||
asset_version=\"\$(git -C \"\$main_repo_dir\" rev-parse --short HEAD)-\$(git -C \"\$ethan_repo_dir\" rev-parse --short HEAD)\"
|
||||
|
||||
install -d -m 0755 \"\$web_root\"
|
||||
|
||||
publish_static_tree \"\$main_repo_dir\" \"\$web_root\"
|
||||
publish_static_tree \"\$ethan_repo_dir\" \"\$web_root/ethan\"
|
||||
|
||||
while IFS= read -r -d '' html_file; do
|
||||
LC_ALL=C LANG=C ASSET_VERSION=\"\$asset_version\" perl -0pi -e 's{((?:href|src)=\")(?!https?://|data:|//)([^\"?]+?\.(?:css|js|mjs|png|jpg|jpeg|svg|webp|ico|pdf|webmanifest))(?:\?[^\"]*)?(\")}{\$1 . \$2 . \"?v=\" . \$ENV{ASSET_VERSION} . \$3}ge' \"\$html_file\"
|
||||
done < <(find \"\$web_root\" -type f -name '*.html' -print0)
|
||||
|
||||
chown -R www-data:www-data \"\$web_root\"
|
||||
|
||||
nginx -t
|
||||
systemctl reload nginx
|
||||
SCRIPT
|
||||
chmod +x /usr/local/bin/update-chesscubing"
|
||||
|
||||
ct_exec "cat > /etc/nginx/sites-available/chesscubing.conf <<'NGINX'
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name _;
|
||||
|
||||
root /var/www/chesscubing/current;
|
||||
index index.html;
|
||||
|
||||
location = /ethan {
|
||||
return 301 \$scheme://\$http_host/ethan/;
|
||||
}
|
||||
|
||||
location /ethan/ {
|
||||
try_files \$uri \$uri/ /ethan/index.html;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files \$uri \$uri/ /index.html;
|
||||
}
|
||||
|
||||
location ~* \.(?:css|js|mjs|png|jpg|jpeg|svg|webp|ico|pdf|webmanifest)$ {
|
||||
expires -1;
|
||||
add_header Cache-Control 'no-cache, no-store, must-revalidate';
|
||||
}
|
||||
}
|
||||
NGINX
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
ln -sf /etc/nginx/sites-available/chesscubing.conf /etc/nginx/sites-enabled/chesscubing.conf"
|
||||
|
||||
printf 'Publication du site dans le LXC...\n'
|
||||
ct_exec "/usr/local/bin/update-chesscubing '$repo_branch'"
|
||||
ct_exec "systemctl enable nginx >/dev/null && systemctl restart nginx"
|
||||
|
||||
container_ip="$(pct exec "$ctid" -- bash -lc "hostname -I | awk '{print \$1}'" 2>/dev/null | tr -d '\r' || true)"
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Installation terminee.
|
||||
- CTID: $ctid
|
||||
- Nom du LXC: $lxc_hostname
|
||||
- Mot de passe root du LXC: $lxc_password
|
||||
- URL probable: http://${container_ip:-<ip_du_lxc>}
|
||||
|
||||
Pour mettre a jour l'application plus tard:
|
||||
./scripts/update-proxmox-lxc.sh --proxmox-host <ip-proxmox> --proxmox-user <user> --proxmox-password '<motdepasse>' --ctid $ctid
|
||||
EOF
|
||||
REMOTE
|
||||
|
||||
if [[ "$LOCAL_MODE" == "1" ]]; then
|
||||
printf 'Creation du LXC "%s" en local sur cet hote Proxmox...\n' "$LXC_HOSTNAME"
|
||||
bash "$payload_script" \
|
||||
"$CTID" \
|
||||
"$LXC_HOSTNAME" \
|
||||
"$LXC_IP" \
|
||||
"$LXC_GATEWAY" \
|
||||
"$LXC_BRIDGE" \
|
||||
"$LXC_CORES" \
|
||||
"$LXC_MEMORY" \
|
||||
"$LXC_SWAP" \
|
||||
"$LXC_DISK_GB" \
|
||||
"$TEMPLATE_STORAGE" \
|
||||
"$ROOTFS_STORAGE" \
|
||||
"$REPO_URL" \
|
||||
"$REPO_BRANCH" \
|
||||
"$ETHAN_REPO_URL" \
|
||||
"$ETHAN_REPO_BRANCH" \
|
||||
"$LXC_PASSWORD"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
[[ -n "$PROXMOX_HOST" ]] || die "Merci de fournir --proxmox-host."
|
||||
[[ -n "$PROXMOX_USER" ]] || die "Merci de fournir --proxmox-user."
|
||||
|
||||
if [[ -z "$PROXMOX_PASSWORD" ]]; then
|
||||
read -rsp "Mot de passe SSH pour ${PROXMOX_USER}@${PROXMOX_HOST}: " PROXMOX_PASSWORD
|
||||
echo
|
||||
fi
|
||||
|
||||
need_cmd ssh
|
||||
need_cmd sshpass
|
||||
|
||||
printf 'Creation du LXC "%s" sur %s...\n' "$LXC_HOSTNAME" "$PROXMOX_HOST"
|
||||
|
||||
sshpass -p "$PROXMOX_PASSWORD" \
|
||||
ssh \
|
||||
-p "$PROXMOX_PORT" \
|
||||
-o StrictHostKeyChecking=accept-new \
|
||||
-o PreferredAuthentications=password \
|
||||
-o PubkeyAuthentication=no \
|
||||
"$PROXMOX_USER@$PROXMOX_HOST" \
|
||||
bash -s -- \
|
||||
"$CTID" \
|
||||
"$LXC_HOSTNAME" \
|
||||
"$LXC_IP" \
|
||||
"$LXC_GATEWAY" \
|
||||
"$LXC_BRIDGE" \
|
||||
"$LXC_CORES" \
|
||||
"$LXC_MEMORY" \
|
||||
"$LXC_SWAP" \
|
||||
"$LXC_DISK_GB" \
|
||||
"$TEMPLATE_STORAGE" \
|
||||
"$ROOTFS_STORAGE" \
|
||||
"$REPO_URL" \
|
||||
"$REPO_BRANCH" \
|
||||
"$ETHAN_REPO_URL" \
|
||||
"$ETHAN_REPO_BRANCH" \
|
||||
"$LXC_PASSWORD" < "$payload_script"
|
||||
346
scripts/update-proxmox-lxc.sh
Executable file
346
scripts/update-proxmox-lxc.sh
Executable file
@@ -0,0 +1,346 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
trap 'printf "Erreur: echec de la commande [%s] a la ligne %s.\n" "$BASH_COMMAND" "$LINENO" >&2' ERR
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./scripts/update-proxmox-lxc.sh \
|
||||
--proxmox-host 192.168.1.10 \
|
||||
--proxmox-user root@pam \
|
||||
--proxmox-password 'motdepasse' \
|
||||
--ctid 120
|
||||
|
||||
./scripts/update-proxmox-lxc.sh --local --ctid 120
|
||||
|
||||
Options principales:
|
||||
--proxmox-host IP ou nom DNS du serveur Proxmox
|
||||
--proxmox-user Utilisateur SSH Proxmox (defaut: root@pam)
|
||||
--proxmox-password Mot de passe SSH Proxmox
|
||||
--ssh-port Port SSH Proxmox (defaut: 22)
|
||||
--local Execute directement sur l'hote Proxmox local
|
||||
--ctid CTID du LXC a mettre a jour
|
||||
--hostname Nom du LXC si le CTID n'est pas fourni (defaut: chesscubing-web)
|
||||
--branch Branche Git a deployer (defaut: main)
|
||||
--ethan-repo-url Depot Git de l'application Ethan
|
||||
--ethan-branch Branche Git de l'application Ethan (defaut: main)
|
||||
-h, --help Affiche cette aide
|
||||
EOF
|
||||
}
|
||||
|
||||
die() {
|
||||
printf 'Erreur: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
need_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || die "La commande '$1' est requise."
|
||||
}
|
||||
|
||||
PROXMOX_HOST=""
|
||||
PROXMOX_USER="root@pam"
|
||||
PROXMOX_PASSWORD="${PROXMOX_PASSWORD:-}"
|
||||
PROXMOX_PORT="22"
|
||||
LOCAL_MODE="0"
|
||||
|
||||
CTID=""
|
||||
LXC_HOSTNAME="chesscubing-web"
|
||||
REPO_BRANCH="main"
|
||||
ETHAN_REPO_URL="https://git.jeannerot.fr/Mineloulou/Chesscubing.git"
|
||||
ETHAN_REPO_BRANCH="main"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--proxmox-host)
|
||||
PROXMOX_HOST="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--proxmox-user)
|
||||
PROXMOX_USER="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--proxmox-password)
|
||||
PROXMOX_PASSWORD="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--ssh-port)
|
||||
PROXMOX_PORT="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--local)
|
||||
LOCAL_MODE="1"
|
||||
shift
|
||||
;;
|
||||
--ctid)
|
||||
CTID="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--hostname)
|
||||
LXC_HOSTNAME="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--branch)
|
||||
REPO_BRANCH="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--ethan-repo-url)
|
||||
ETHAN_REPO_URL="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--ethan-branch)
|
||||
ETHAN_REPO_BRANCH="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "Option inconnue: $1"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$LOCAL_MODE" != "1" && -z "$PROXMOX_HOST" ]]; then
|
||||
if command -v pct >/dev/null 2>&1 && command -v pveam >/dev/null 2>&1; then
|
||||
LOCAL_MODE="1"
|
||||
fi
|
||||
fi
|
||||
|
||||
payload_script="$(mktemp)"
|
||||
cleanup() {
|
||||
rm -f "$payload_script"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
cat >"$payload_script" <<'REMOTE'
|
||||
set -Eeuo pipefail
|
||||
|
||||
trap 'printf "Erreur: echec de la commande [%s] a la ligne %s.\n" "$BASH_COMMAND" "$LINENO" >&2' ERR
|
||||
|
||||
ctid="$1"
|
||||
lxc_hostname="$2"
|
||||
repo_branch="$3"
|
||||
ethan_repo_url="$4"
|
||||
ethan_repo_branch="$5"
|
||||
|
||||
die() {
|
||||
printf 'Erreur: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
find_ctid_by_hostname() {
|
||||
local wanted="$1"
|
||||
local candidate=""
|
||||
local candidate_hostname=""
|
||||
|
||||
while read -r candidate; do
|
||||
[[ -n "$candidate" ]] || continue
|
||||
candidate_hostname="$(pct config "$candidate" 2>/dev/null | awk -F ': ' '/^hostname:/ { print $2; exit }')"
|
||||
if [[ "$candidate_hostname" == "$wanted" ]]; then
|
||||
printf '%s\n' "$candidate"
|
||||
return 0
|
||||
fi
|
||||
done < <(pct list | awk 'NR > 1 { print $1 }')
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
command -v pct >/dev/null 2>&1 || die "La commande 'pct' est absente sur Proxmox."
|
||||
|
||||
if [[ -z "$ctid" ]]; then
|
||||
ctid="$(find_ctid_by_hostname "$lxc_hostname" || true)"
|
||||
fi
|
||||
|
||||
[[ -n "$ctid" ]] || die "Impossible de retrouver le LXC. Passe --ctid ou --hostname."
|
||||
|
||||
if ! pct status "$ctid" >/dev/null 2>&1; then
|
||||
die "Le conteneur $ctid est introuvable."
|
||||
fi
|
||||
|
||||
detected_hostname="$(pct config "$ctid" 2>/dev/null | awk -F ': ' '/^hostname:/ { print $2; exit }')"
|
||||
if [[ -n "$detected_hostname" ]]; then
|
||||
lxc_hostname="$detected_hostname"
|
||||
fi
|
||||
|
||||
if ! pct status "$ctid" | grep -q "running"; then
|
||||
pct start "$ctid"
|
||||
sleep 5
|
||||
fi
|
||||
|
||||
ct_exec() {
|
||||
pct exec "$ctid" -- bash -lc "$1"
|
||||
}
|
||||
|
||||
ct_exec "cat > /usr/local/bin/update-chesscubing <<'SCRIPT'
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
trap 'printf \"Erreur: echec de la commande [%s] a la ligne %s.\\n\" \"\$BASH_COMMAND\" \"\$LINENO\" >&2' ERR
|
||||
|
||||
main_repo_dir='/opt/chesscubing/repo'
|
||||
ethan_repo_dir='/opt/chesscubing/ethan-repo'
|
||||
web_root='/var/www/chesscubing/current'
|
||||
main_branch=\"\${1:-${repo_branch}}\"
|
||||
ethan_repo_url='${ethan_repo_url}'
|
||||
ethan_branch='${ethan_repo_branch}'
|
||||
|
||||
sync_git_repo() {
|
||||
local repo_dir=\"\$1\"
|
||||
local repo_url=\"\$2\"
|
||||
local branch=\"\$3\"
|
||||
local label=\"\$4\"
|
||||
|
||||
if [[ -d \"\$repo_dir/.git\" ]]; then
|
||||
cd \"\$repo_dir\"
|
||||
|
||||
if ! git diff --quiet || ! git diff --cached --quiet; then
|
||||
echo \"Le depot \${label} contient des modifications locales. Mise a jour annulee.\" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git fetch origin \"\$branch\"
|
||||
if git show-ref --verify --quiet \"refs/heads/\$branch\"; then
|
||||
git checkout \"\$branch\"
|
||||
else
|
||||
git checkout -b \"\$branch\" --track \"origin/\$branch\"
|
||||
fi
|
||||
git pull --ff-only origin \"\$branch\"
|
||||
return 0
|
||||
fi
|
||||
|
||||
[[ -n \"\$repo_url\" ]] || {
|
||||
echo \"Le depot \${label} est absent et aucune URL n'a ete fournie.\" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
rm -rf \"\$repo_dir\"
|
||||
git clone --branch \"\$branch\" --single-branch \"\$repo_url\" \"\$repo_dir\"
|
||||
}
|
||||
|
||||
publish_static_tree() {
|
||||
local source_dir=\"\$1\"
|
||||
local destination_dir=\"\$2\"
|
||||
|
||||
install -d -m 0755 \"\$destination_dir\"
|
||||
|
||||
rsync -a --delete \
|
||||
--include='*/' \
|
||||
--include='*.html' \
|
||||
--include='*.css' \
|
||||
--include='*.js' \
|
||||
--include='*.mjs' \
|
||||
--include='*.png' \
|
||||
--include='*.jpg' \
|
||||
--include='*.jpeg' \
|
||||
--include='*.svg' \
|
||||
--include='*.webp' \
|
||||
--include='*.ico' \
|
||||
--include='*.pdf' \
|
||||
--include='*.webmanifest' \
|
||||
--exclude='*' \
|
||||
\"\$source_dir/\" \"\$destination_dir/\"
|
||||
}
|
||||
|
||||
sync_git_repo \"\$main_repo_dir\" '' \"\$main_branch\" 'principal'
|
||||
sync_git_repo \"\$ethan_repo_dir\" \"\$ethan_repo_url\" \"\$ethan_branch\" 'Ethan'
|
||||
|
||||
asset_version=\"\$(git -C \"\$main_repo_dir\" rev-parse --short HEAD)-\$(git -C \"\$ethan_repo_dir\" rev-parse --short HEAD)\"
|
||||
|
||||
install -d -m 0755 \"\$web_root\"
|
||||
|
||||
publish_static_tree \"\$main_repo_dir\" \"\$web_root\"
|
||||
publish_static_tree \"\$ethan_repo_dir\" \"\$web_root/ethan\"
|
||||
|
||||
while IFS= read -r -d '' html_file; do
|
||||
LC_ALL=C LANG=C ASSET_VERSION=\"\$asset_version\" perl -0pi -e 's{((?:href|src)=\")(?!https?://|data:|//)([^\"?]+?\.(?:css|js|mjs|png|jpg|jpeg|svg|webp|ico|pdf|webmanifest))(?:\?[^\"]*)?(\")}{\$1 . \$2 . \"?v=\" . \$ENV{ASSET_VERSION} . \$3}ge' \"\$html_file\"
|
||||
done < <(find \"\$web_root\" -type f -name '*.html' -print0)
|
||||
|
||||
chown -R www-data:www-data \"\$web_root\"
|
||||
|
||||
nginx -t
|
||||
systemctl reload nginx
|
||||
SCRIPT
|
||||
chmod +x /usr/local/bin/update-chesscubing"
|
||||
|
||||
ct_exec "cat > /etc/nginx/sites-available/chesscubing.conf <<'NGINX'
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name _;
|
||||
|
||||
root /var/www/chesscubing/current;
|
||||
index index.html;
|
||||
|
||||
location = /ethan {
|
||||
return 301 \$scheme://\$http_host/ethan/;
|
||||
}
|
||||
|
||||
location /ethan/ {
|
||||
try_files \$uri \$uri/ /ethan/index.html;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files \$uri \$uri/ /index.html;
|
||||
}
|
||||
|
||||
location ~* \.(?:css|js|mjs|png|jpg|jpeg|svg|webp|ico|pdf|webmanifest)$ {
|
||||
expires -1;
|
||||
add_header Cache-Control 'no-cache, no-store, must-revalidate';
|
||||
}
|
||||
}
|
||||
NGINX
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
ln -sf /etc/nginx/sites-available/chesscubing.conf /etc/nginx/sites-enabled/chesscubing.conf"
|
||||
|
||||
pct exec "$ctid" -- /usr/local/bin/update-chesscubing "$repo_branch"
|
||||
|
||||
container_ip="$(pct exec "$ctid" -- bash -lc "hostname -I | awk '{print \$1}'" 2>/dev/null | tr -d '\r' || true)"
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Mise a jour terminee.
|
||||
- CTID: $ctid
|
||||
- Nom du LXC: $lxc_hostname
|
||||
- URL probable: http://${container_ip:-<ip_du_lxc>}
|
||||
EOF
|
||||
REMOTE
|
||||
|
||||
if [[ "$LOCAL_MODE" == "1" ]]; then
|
||||
printf 'Mise a jour du LXC ChessCubing en local sur cet hote Proxmox...\n'
|
||||
bash "$payload_script" \
|
||||
"$CTID" \
|
||||
"$LXC_HOSTNAME" \
|
||||
"$REPO_BRANCH" \
|
||||
"$ETHAN_REPO_URL" \
|
||||
"$ETHAN_REPO_BRANCH"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
[[ -n "$PROXMOX_HOST" ]] || die "Merci de fournir --proxmox-host."
|
||||
[[ -n "$PROXMOX_USER" ]] || die "Merci de fournir --proxmox-user."
|
||||
|
||||
if [[ -z "$PROXMOX_PASSWORD" ]]; then
|
||||
read -rsp "Mot de passe SSH pour ${PROXMOX_USER}@${PROXMOX_HOST}: " PROXMOX_PASSWORD
|
||||
echo
|
||||
fi
|
||||
|
||||
need_cmd ssh
|
||||
need_cmd sshpass
|
||||
|
||||
printf 'Mise a jour du LXC ChessCubing sur %s...\n' "$PROXMOX_HOST"
|
||||
|
||||
sshpass -p "$PROXMOX_PASSWORD" \
|
||||
ssh \
|
||||
-p "$PROXMOX_PORT" \
|
||||
-o StrictHostKeyChecking=accept-new \
|
||||
-o PreferredAuthentications=password \
|
||||
-o PubkeyAuthentication=no \
|
||||
"$PROXMOX_USER@$PROXMOX_HOST" \
|
||||
bash -s -- \
|
||||
"$CTID" \
|
||||
"$LXC_HOSTNAME" \
|
||||
"$REPO_BRANCH" \
|
||||
"$ETHAN_REPO_URL" \
|
||||
"$ETHAN_REPO_BRANCH" < "$payload_script"
|
||||
17
site.webmanifest
Normal file
17
site.webmanifest
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "ChessCubing Arena",
|
||||
"short_name": "ChessCubing",
|
||||
"description": "Application officielle de match ChessCubing pour chrono, cube et arbitrage.",
|
||||
"start_url": "/application.html",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#140700",
|
||||
"theme_color": "#140700",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/logo.png",
|
||||
"sizes": "2048x2048",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
1393
styles.css
1393
styles.css
File diff suppressed because it is too large
Load Diff
BIN
transparent.png
BIN
transparent.png
Binary file not shown.
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 73 KiB |
153
update-chesscubing-proxmox.sh
Executable file
153
update-chesscubing-proxmox.sh
Executable file
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_BRANCH="${CHESSCUBING_GIT_BRANCH:-main}"
|
||||
RAW_BASE_URL="https://git.jeannerot.fr/christophe/chesscubing/raw/branch/${REPO_BRANCH}"
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Bootstrap de mise à jour ChessCubing pour Proxmox.
|
||||
|
||||
Usage local :
|
||||
./update-chesscubing-proxmox.sh
|
||||
|
||||
Usage en une ligne :
|
||||
bash -c "$(curl -fsSL https://git.jeannerot.fr/christophe/chesscubing/raw/branch/main/update-chesscubing-proxmox.sh)"
|
||||
|
||||
Variables d'environnement reconnues :
|
||||
CHESSCUBING_LOCAL
|
||||
CHESSCUBING_PROXMOX_HOST
|
||||
CHESSCUBING_PROXMOX_USER
|
||||
CHESSCUBING_PROXMOX_PASSWORD
|
||||
CHESSCUBING_SSH_PORT
|
||||
CHESSCUBING_CTID
|
||||
CHESSCUBING_LXC_HOSTNAME
|
||||
CHESSCUBING_GIT_BRANCH
|
||||
CHESSCUBING_ETHAN_REPO_URL
|
||||
CHESSCUBING_ETHAN_GIT_BRANCH
|
||||
EOF
|
||||
}
|
||||
|
||||
die() {
|
||||
printf 'Erreur: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
have_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
need_cmd() {
|
||||
have_cmd "$1" || die "La commande '$1' est requise."
|
||||
}
|
||||
|
||||
prompt_default() {
|
||||
local var_name="$1"
|
||||
local prompt_label="$2"
|
||||
local default_value="${3:-}"
|
||||
local current_value="${!var_name:-}"
|
||||
local input=""
|
||||
|
||||
if [[ -n "$current_value" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -n "$default_value" ]]; then
|
||||
read -r -p "$prompt_label [$default_value]: " input
|
||||
printf -v "$var_name" '%s' "${input:-$default_value}"
|
||||
else
|
||||
read -r -p "$prompt_label: " input
|
||||
printf -v "$var_name" '%s' "$input"
|
||||
fi
|
||||
}
|
||||
|
||||
prompt_secret() {
|
||||
local var_name="$1"
|
||||
local prompt_label="$2"
|
||||
local current_value="${!var_name:-}"
|
||||
|
||||
if [[ -n "$current_value" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
read -rsp "$prompt_label: " current_value
|
||||
echo
|
||||
printf -v "$var_name" '%s' "$current_value"
|
||||
}
|
||||
|
||||
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
need_cmd curl
|
||||
|
||||
LOCAL_MODE="${CHESSCUBING_LOCAL:-}"
|
||||
PROXMOX_HOST="${CHESSCUBING_PROXMOX_HOST:-}"
|
||||
PROXMOX_USER="${CHESSCUBING_PROXMOX_USER:-}"
|
||||
PROXMOX_PASSWORD="${CHESSCUBING_PROXMOX_PASSWORD:-}"
|
||||
SSH_PORT="${CHESSCUBING_SSH_PORT:-22}"
|
||||
CTID="${CHESSCUBING_CTID:-}"
|
||||
LXC_HOSTNAME="${CHESSCUBING_LXC_HOSTNAME:-chesscubing-web}"
|
||||
ETHAN_REPO_URL="${CHESSCUBING_ETHAN_REPO_URL:-https://git.jeannerot.fr/Mineloulou/Chesscubing.git}"
|
||||
ETHAN_REPO_BRANCH="${CHESSCUBING_ETHAN_GIT_BRANCH:-main}"
|
||||
|
||||
if [[ -z "$LOCAL_MODE" && -z "$PROXMOX_HOST" ]]; then
|
||||
if have_cmd pct && have_cmd pveam; then
|
||||
LOCAL_MODE="1"
|
||||
else
|
||||
LOCAL_MODE="0"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$LOCAL_MODE" != "1" ]]; then
|
||||
need_cmd ssh
|
||||
need_cmd sshpass
|
||||
prompt_default PROXMOX_HOST "IP ou nom du serveur Proxmox" ""
|
||||
prompt_default PROXMOX_USER "Utilisateur SSH Proxmox" "root@pam"
|
||||
prompt_secret PROXMOX_PASSWORD "Mot de passe SSH Proxmox"
|
||||
prompt_default SSH_PORT "Port SSH Proxmox" "22"
|
||||
fi
|
||||
|
||||
if [[ "$LOCAL_MODE" != "1" ]]; then
|
||||
[[ -n "$PROXMOX_HOST" ]] || die "Le serveur Proxmox est obligatoire."
|
||||
[[ -n "$PROXMOX_USER" ]] || die "L'utilisateur Proxmox est obligatoire."
|
||||
[[ -n "$PROXMOX_PASSWORD" ]] || die "Le mot de passe Proxmox est obligatoire."
|
||||
fi
|
||||
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
printf 'Téléchargement du script de mise à jour ChessCubing...\n'
|
||||
curl -fsSL "${RAW_BASE_URL}/scripts/update-proxmox-lxc.sh" -o "$TMP_DIR/update-proxmox-lxc.sh"
|
||||
chmod +x "$TMP_DIR/update-proxmox-lxc.sh"
|
||||
|
||||
cmd=(
|
||||
"$TMP_DIR/update-proxmox-lxc.sh"
|
||||
--branch "$REPO_BRANCH"
|
||||
--ethan-repo-url "$ETHAN_REPO_URL"
|
||||
--ethan-branch "$ETHAN_REPO_BRANCH"
|
||||
)
|
||||
|
||||
if [[ "$LOCAL_MODE" == "1" ]]; then
|
||||
cmd+=(--local)
|
||||
else
|
||||
cmd+=(
|
||||
--proxmox-host "$PROXMOX_HOST"
|
||||
--proxmox-user "$PROXMOX_USER"
|
||||
--proxmox-password "$PROXMOX_PASSWORD"
|
||||
--ssh-port "$SSH_PORT"
|
||||
)
|
||||
fi
|
||||
|
||||
if [[ -n "$CTID" ]]; then
|
||||
cmd+=(--ctid "$CTID")
|
||||
else
|
||||
cmd+=(--hostname "$LXC_HOSTNAME")
|
||||
fi
|
||||
|
||||
printf 'Lancement de la mise à jour ChessCubing...\n'
|
||||
"${cmd[@]}"
|
||||
Reference in New Issue
Block a user