Skip to content

Commit 95ad450

Browse files
authored
Merge pull request #49 from RustyRory/staging
chore(release): 0.4.2
2 parents 964fd9c + 8ffbc8e commit 95ad450

14 files changed

Lines changed: 523 additions & 20 deletions

File tree

.github/workflows/staging.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ jobs:
1818
username: ${{ secrets.VPS_USER }}
1919
key: ${{ secrets.VPS_SSH_KEY }}
2020
script: |
21-
cd /var/www/VPS-monitor
21+
cd /var/www/vps-monitor
2222
git pull origin staging
2323
cd /var/www
24-
docker-compose build vps-monitor
25-
docker-compose up -d vps-monitor
24+
docker compose build vps-monitor
25+
docker compose up -d vps-monitor

CHANGELOG.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,42 @@
11
# Changelog
22

3+
## [0.4.2] — 2026-05-19
4+
5+
### Phase 4 — Logs en temps réel via WebSocket
6+
7+
#### Added
8+
9+
- Dépendance `ws` (v8) : serveur WebSocket monté sur le serveur HTTP Express via `noServer: true`
10+
- `GET /api/ws-token` (authentifié) : génère un token à usage unique (TTL 30 s) pour l'upgrade WebSocket — évite d'exposer la session cookie sur la connexion WS
11+
- `streamContainerLogs(name, tail, onData, onEnd)` dans `docker.js` : stream Docker `follow: true` avec démultiplexage stdout/stderr via `modem.demuxStream` + `PassThrough`
12+
- Gestionnaire `server.on('upgrade', ...)` : valide et consomme le token one-shot avant d'accepter la connexion WebSocket (401 + `socket.destroy()` si invalide ou expiré)
13+
- Modal "Logs" dans le dashboard : bouton "Logs" sur chaque card container (running et exited), affichage en temps réel dans un `<pre>` avec auto-scroll, fermeture via bouton ✕ ou clic sur l'overlay
14+
- `showLogs(name)` / `closeLogs()` dans `app.js` : gestion du cycle de vie WebSocket (ouverture, réception, erreur, fermeture propre du stream Docker)
15+
16+
#### Changed
17+
18+
- `server.js` : migration de `app.listen()` vers `http.createServer(app)` + `server.listen()` pour supporter l'upgrade WebSocket
19+
- `sessionMiddleware` extrait en variable nommée (préparation au partage avec le handler d'upgrade)
20+
- Section "Applications web" : ajout du bouton "Ouvrir →" sur les cards dans `renderWebsites` (frontend manquant en 0.4.1)
21+
22+
---
23+
24+
## [0.4.1] — 2026-04-28
25+
26+
### Phase 4 — Navigation vers les applications depuis le dashboard
27+
28+
#### Added
29+
30+
- Bouton "Ouvrir →" sur chaque card application web du dashboard (connecté) — ouvre l'application dans un nouvel onglet via `target="_blank"`
31+
- Nouveau style `.card-link` : bouton discret monospace cohérent avec le design du dashboard, avec transition hover
32+
33+
#### Changed
34+
35+
- `home.html` : remplacement de la card CineMap — TP Laravel en première position par QW APP (`/qw-app/`) — CineMap déplacée après Lucky7
36+
- `home.html` : footer mis à jour — ajout d'un lien Portfolio (`damien-paszkiewicz.vercel.app`) aux côtés du lien GitHub, suppression du label "B3 Dev"
37+
38+
---
39+
340
## [0.4.0] — 2026-04-27
441

542
### Phase 4 — Intégration Nginx + Tests de non-régression

README.md

Lines changed: 168 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,168 @@
1-
# VPSControlPanel
1+
# VPS Monitor
2+
3+
Application web de monitoring pour VPS multi-applications. Supervise les containers Docker et la disponibilité des applications web via une interface légère sans framework.
4+
5+
## Fonctionnalités
6+
7+
- Dashboard temps réel des containers Docker (statut, image, ports, uptime)
8+
- Monitoring HTTP des applications web exposées via Nginx
9+
- Actions Docker : restart, stop, start d'un container
10+
- Authentification par session (login/password, durée 8h)
11+
- Page publique d'accueil avec liens vers les applications
12+
- Rafraîchissement automatique toutes les 5 secondes
13+
- Indicateurs visuels : vert (running/OK), rouge (exited/DOWN), orange (restarting)
14+
- Statut global OK/KO affiché dans le header
15+
16+
## Stack technique
17+
18+
| Couche | Technologie |
19+
|--------|-------------|
20+
| Backend | Node.js 20 + Express 5 |
21+
| Docker | Dockerode (via socket) |
22+
| Frontend | HTML + CSS + JS vanilla (sans build) |
23+
| Auth | express-session |
24+
| Tests | Jest + Supertest |
25+
| Lint | ESLint |
26+
27+
## Structure du projet
28+
29+
```
30+
vps-monitor-app/
31+
├── api/
32+
│ ├── server.js # Serveur Express + routes
33+
│ ├── server.test.js # Tests Jest
34+
│ └── services/
35+
│ ├── docker.js # Intégration Docker socket
36+
│ └── http.js # Checks HTTP des applications
37+
├── public/
38+
│ ├── home.html # Page publique (non connecté)
39+
│ ├── index.html # Dashboard (connecté)
40+
│ ├── login.html # Formulaire de login
41+
│ ├── app.js # Logique frontend
42+
│ └── style.css
43+
├── Dockerfile
44+
└── package.json
45+
```
46+
47+
## API
48+
49+
### Routes publiques
50+
51+
| Méthode | Route | Description |
52+
|---------|-------|-------------|
53+
| GET | `/` | Home publique ou dashboard (selon session) |
54+
| POST | `/auth/login` | Connexion |
55+
| POST | `/auth/logout` | Déconnexion |
56+
57+
### Routes protégées (session requise)
58+
59+
| Méthode | Route | Description |
60+
|---------|-------|-------------|
61+
| GET | `/api/status` | Statut containers + sites web |
62+
| POST | `/api/container/restart` | Redémarre un container |
63+
| POST | `/api/container/stop` | Arrête un container |
64+
| POST | `/api/container/start` | Démarre un container arrêté |
65+
66+
#### Exemple de réponse `GET /api/status`
67+
68+
```json
69+
{
70+
"containers": [
71+
{ "name": "sbv-api", "status": "running", "ports": ["3006:5000"], "image": "sbv-api:latest", "uptime": "2h" }
72+
],
73+
"websites": [
74+
{ "name": "SaintBarth Volley", "url": "/saintbarthvolley/", "status": "OK" }
75+
],
76+
"globalStatus": "OK"
77+
}
78+
```
79+
80+
## Variables d'environnement
81+
82+
Créer un fichier `.env` à la racine de `vps-monitor-app/` :
83+
84+
```env
85+
PORT=3000
86+
BASE_URL=http://localhost:3000
87+
AUTH_USER=admin
88+
AUTH_PASS=your-password
89+
SESSION_SECRET=your-secret
90+
```
91+
92+
## Lancement
93+
94+
### Développement
95+
96+
```bash
97+
cd vps-monitor-app
98+
npm install
99+
npm run dev
100+
```
101+
102+
### Production (Docker)
103+
104+
```yaml
105+
# docker-compose.yml
106+
vps-monitor:
107+
build: ./vps-monitor-app
108+
container_name: vps-monitor
109+
volumes:
110+
- /var/run/docker.sock:/var/run/docker.sock
111+
ports:
112+
- "3020:3000"
113+
env_file: ./vps-monitor-app/.env
114+
restart: unless-stopped
115+
```
116+
117+
```bash
118+
docker compose up -d vps-monitor
119+
```
120+
121+
## Intégration Nginx
122+
123+
Remplace la homepage statique en proxifiant `/` vers vps-monitor :
124+
125+
```nginx
126+
location / {
127+
proxy_pass http://127.0.0.1:3020;
128+
proxy_http_version 1.1;
129+
proxy_set_header Host $host;
130+
proxy_set_header X-Real-IP $remote_addr;
131+
}
132+
```
133+
134+
## Tests et lint
135+
136+
```bash
137+
npm run lint # ESLint
138+
npm test # Jest + Supertest
139+
```
140+
141+
## CI/CD
142+
143+
- **CI** : lint + tests sur toutes les branches et PRs (GitHub Actions)
144+
- **CD** : déploiement automatique sur le VPS via SSH au push sur `staging`
145+
146+
## Applications monitorées
147+
148+
| Application | Route Nginx |
149+
|-------------|-------------|
150+
| TP Vue | `/B3dev-TP_VUE/` |
151+
| SaintBarth Volley | `/saintbarthvolley/` |
152+
| Lucky7 | `/lucky7/` |
153+
| College La Boussole | `/collegelaboussole/` |
154+
| Cinemap | `/cinemap/` |
155+
156+
## Sécurité
157+
158+
- Le socket Docker (`/var/run/docker.sock`) donne un accès total au serveur — déployer uniquement en environnement de confiance
159+
- Toutes les routes `/api/*` et les actions containers sont protégées par la session
160+
- Cookies `httpOnly`, session de 8h
161+
162+
## Évolutions prévues
163+
164+
- Alertes Discord / email
165+
- Historique uptime
166+
- Graphiques CPU / RAM
167+
- Logs en temps réel
168+
- Support multi-serveurs

docs/workflow.md

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,13 +299,59 @@ Recharger Nginx : `nginx -t && nginx -s reload`
299299
- Simuler un container arrêté et vérifier l'affichage (statut rouge)
300300
- Simuler un service HTTP down et vérifier l'affichage
301301

302-
### Étape 15 — Évolutions futures (optionnel)
302+
### Étape 15 — Logs en temps réel via WebSocket
303+
304+
#### Objectif
305+
306+
Afficher les logs d'un container en temps réel dans une modale, avec streaming continu (équivalent `docker logs --follow`).
307+
308+
#### 15.1 — Installer le package WebSocket
309+
310+
```bash
311+
npm install ws
312+
```
313+
314+
#### 15.2 — Mettre à jour `api/services/docker.js`
315+
316+
Ajouter la fonction `streamContainerLogs(name, tail, onData, onEnd)` :
317+
318+
- Appelle `container.logs({ follow: true, tail, stdout: true, stderr: true })`
319+
- Utilise `docker.modem.demuxStream` + deux `PassThrough` pour séparer stdout/stderr
320+
- Retourne le stream brut afin que l'appelant puisse le `destroy()` à la déconnexion
321+
322+
#### 15.3 — Mettre à jour `api/server.js`
323+
324+
- Extraire le middleware `session(...)` dans une variable `sessionMiddleware` pour le réutiliser
325+
- Remplacer `app.listen()` par `http.createServer(app)` + `server.listen()`
326+
- Créer un `WebSocketServer({ noServer: true })`
327+
- Écouter l'événement `upgrade` sur le serveur HTTP : parser la session via `sessionMiddleware`, rejeter avec 401 si non authentifié, sinon déléguer à `wss.handleUpgrade`
328+
- Sur `wss.on('connection')` : lire `name` et `tail` dans l'URL, démarrer `streamContainerLogs`, envoyer chaque chunk via `ws.send()`, détruire le stream Docker à la fermeture du WebSocket
329+
330+
Le chemin d'écoute WebSocket est `/ws/logs?name=<container>&tail=200`.
331+
332+
#### 15.4 — Mettre à jour `public/app.js`
333+
334+
- Remplacer le `fetch` de `showLogs` par une connexion `WebSocket`
335+
- Construire l'URL : `ws://` ou `wss://` selon le protocole courant
336+
- Ajouter les messages au `<pre>` au fil des events `onmessage`, avec auto-scroll
337+
- Stocker le socket dans `activeLogSocket` pour le fermer proprement dans `closeLogs()`
338+
339+
#### 15.5 — Tester
340+
341+
```bash
342+
# Depuis le VPS
343+
docker logs <container> --follow # comportement attendu
344+
# Dans l'app : cliquer "Logs" → la modale doit streamer en direct
345+
```
346+
347+
---
348+
349+
### Étape 16 — Évolutions futures
303350

304351
À planifier selon les besoins :
305352

306353
- Alertes email quand un service tombe
307354
- Historique d'uptime (stockage en fichier ou SQLite)
308355
- Graphiques CPU / RAM via `dockerode.stats`
309-
- Logs en temps réel via WebSocket
310356
- Support multi-serveurs
311357

vps-monitor-app/api/server.js

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import 'dotenv/config';
2+
import { createServer } from 'http';
23
import express from 'express';
34
import session from 'express-session';
5+
import { WebSocketServer } from 'ws';
46
import { fileURLToPath } from 'url';
57
import { dirname, join } from 'path';
6-
import { getContainers, restartContainer, stopContainer, startContainer } from './services/docker.js';
8+
import { getContainers, restartContainer, stopContainer, startContainer, streamContainerLogs } from './services/docker.js';
79
import { checkWebsites } from './services/http.js';
810

911
const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -16,13 +18,15 @@ const SESSION_SECRET = process.env.SESSION_SECRET || 'change-me';
1618

1719
const app = express();
1820

19-
app.use(express.json());
20-
app.use(session({
21+
const sessionMiddleware = session({
2122
secret: SESSION_SECRET,
2223
resave: false,
2324
saveUninitialized: false,
2425
cookie: { httpOnly: true, maxAge: 8 * 60 * 60 * 1000 },
25-
}));
26+
});
27+
28+
app.use(express.json());
29+
app.use(sessionMiddleware);
2630

2731
// --- Routes publiques ---
2832

@@ -113,10 +117,67 @@ app.post('/api/container/start', requireAuth, async (req, res) => {
113117
}
114118
});
115119

120+
// --- WebSocket : logs en temps réel ---
121+
122+
const wsTokens = new Map();
123+
124+
app.get('/api/ws-token', requireAuth, (_req, res) => {
125+
const token = Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
126+
wsTokens.set(token, Date.now() + 30_000);
127+
res.json({ token });
128+
});
129+
130+
const server = createServer(app);
131+
const wss = new WebSocketServer({ noServer: true });
132+
133+
server.on('upgrade', (req, socket, head) => {
134+
const url = new URL(req.url, 'http://localhost');
135+
const token = url.searchParams.get('token');
136+
const expiry = token && wsTokens.get(token);
137+
138+
if (!expiry || Date.now() > expiry) {
139+
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
140+
socket.destroy();
141+
return;
142+
}
143+
wsTokens.delete(token);
144+
145+
wss.handleUpgrade(req, socket, head, (ws) => {
146+
wss.emit('connection', ws, req);
147+
});
148+
});
149+
150+
wss.on('connection', (ws, req) => {
151+
const url = new URL(req.url, 'http://localhost');
152+
const name = url.searchParams.get('name');
153+
const tail = parseInt(url.searchParams.get('tail') || '200', 10);
154+
155+
if (!name) {
156+
ws.close(1008, 'name requis');
157+
return;
158+
}
159+
160+
let logStream = null;
161+
162+
streamContainerLogs(name, tail,
163+
(data) => { if (ws.readyState === ws.OPEN) ws.send(data); },
164+
() => { if (ws.readyState === ws.OPEN) ws.close(); },
165+
).then((stream) => {
166+
logStream = stream;
167+
}).catch((err) => {
168+
if (ws.readyState === ws.OPEN) ws.send(`Erreur: ${err.message}`);
169+
ws.close();
170+
});
171+
172+
ws.on('close', () => {
173+
if (logStream) logStream.destroy();
174+
});
175+
});
176+
116177
export default app;
117178

118179
if (process.argv[1] === fileURLToPath(import.meta.url)) {
119-
app.listen(PORT, () => {
180+
server.listen(PORT, () => {
120181
console.log(`vps-monitor listening on ${BASE_URL}`);
121182
});
122183
}

vps-monitor-app/api/server.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ jest.unstable_mockModule('./services/docker.js', () => ({
1212
restartContainer: jest.fn().mockResolvedValue(),
1313
stopContainer: jest.fn().mockResolvedValue(),
1414
startContainer: jest.fn().mockResolvedValue(),
15+
streamContainerLogs: jest.fn().mockResolvedValue({ destroy: jest.fn() }),
1516
}));
1617

1718
jest.unstable_mockModule('./services/http.js', () => ({

0 commit comments

Comments
 (0)