Compare commits
No commits in common. "11a3ae72eea3fc89a8d00d3bac398e284c64dd5e" and "1ba1b652834cdffa33819b398d19e53cda55caf9" have entirely different histories.
11a3ae72ee
...
1ba1b65283
7 changed files with 12 additions and 235 deletions
|
|
@ -18,11 +18,12 @@ ADMIN_EMAILS=cedric.alpinux@acemail.fr
|
||||||
ASSETS_ROOT=<chemin absolu>
|
ASSETS_ROOT=<chemin absolu>
|
||||||
|
|
||||||
# ── Statistiques GoAccess ─────────────────────────────────────────────
|
# ── Statistiques GoAccess ─────────────────────────────────────────────
|
||||||
# Rapport HTML généré par ISPConfig (mis à jour chaque nuit automatiquement)
|
# Rapport HTML affiché dans l'onglet Statistiques
|
||||||
STATS_FILE=/var/www/clients/client1/web17/web/stats/goaindex.html
|
STATS_FILE=/opt/static-cdn/goaccess.html
|
||||||
# Rapport JSON pour les badges "Vues" dans le navigateur de fichiers
|
# Rapport JSON pour les badges "Vues" dans le navigateur de fichiers
|
||||||
STATS_JSON=
|
STATS_JSON=/opt/static-cdn/goaccess.json
|
||||||
# Fichier de log Apache (utilisé seulement si STATS_GENERATE_CMD est défini)
|
# Fichier de log Apache à analyser (nécessaire pour la génération à la demande)
|
||||||
STATS_LOG_FILE=/var/log/ispconfig/httpd/static.alpinux.org/access.log
|
STATS_LOG_FILE=/var/log/ispconfig/httpd/static.alpinux.org/access.log
|
||||||
# Laisser vide : ISPConfig gère la génération
|
# Commande complète de génération (optionnel — remplace la commande par défaut)
|
||||||
|
# Exemple : goaccess /var/log/... --log-format=COMBINED -o /opt/static-cdn/goaccess.html
|
||||||
STATS_GENERATE_CMD=
|
STATS_GENERATE_CMD=
|
||||||
|
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
# Changelog — Alpinux Static
|
|
||||||
|
|
||||||
## [1.4.2] — 2026-05-06
|
|
||||||
|
|
||||||
### Corrigé
|
|
||||||
- Statistiques : la page affichait une erreur de génération alors qu'ISPConfig génère déjà le rapport GoAccess chaque nuit — `STATS_FILE` pointe maintenant directement sur le fichier ISPConfig
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [1.4.1] — 2026-05-06
|
|
||||||
|
|
||||||
### Corrigé
|
|
||||||
- Changelog : `TypeError` au rendu — clé `items` du dict en conflit avec la méthode Python `dict.items()` en Jinja2 (renommée `entries`)
|
|
||||||
- Sélecteur de largeur : boutons L et ∞ sans effet — `max-width:none` + `margin:auto` sur flex child rétrécissait `<main>` ; remplacé par `data-cw` sur `<html>` + sélecteurs CSS d'attribut
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [1.4.0] — 2026-05-06
|
|
||||||
|
|
||||||
### Ajouté
|
|
||||||
- Sélecteur de largeur du contenu dans le header : Étroit (900 px), Normal (1 200 px), Large (1 600 px), Plein — préférence mémorisée dans le navigateur
|
|
||||||
- Changelog avec numéro de version sémantique, accessible depuis le footer
|
|
||||||
|
|
||||||
### Corrigé
|
|
||||||
- Décalage de largeur entre les pages dû à l'apparition/disparition de la scrollbar (`scrollbar-gutter: stable`)
|
|
||||||
- Labels EXIF non traduits : `ResolutionUnit`, `XResolution`, `YResolution`
|
|
||||||
- Valeur numérique brute pour `ColorSpace` → libellé lisible (sRGB, Adobe RGB, Non calibré)
|
|
||||||
|
|
||||||
### Modifié
|
|
||||||
- Contenu principal en pleine largeur (suppression du `max-width` fixe sur `<main>`)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [1.3.0] — 2026-05-06
|
|
||||||
|
|
||||||
### Ajouté
|
|
||||||
- Prévisualisation des fichiers depuis la corbeille (propriétés, métadonnées, téléchargement)
|
|
||||||
- Footer sur toutes les pages avec liens de navigation et numéro de version
|
|
||||||
- Stats corbeille dans le tableau de bord (nombre de fichiers, taille totale, date du plus ancien)
|
|
||||||
|
|
||||||
### Modifié
|
|
||||||
- Header compact : avatar initiale + prénom + icône déconnexion (suppression du bouton texte)
|
|
||||||
- Suppression du lien « Tableau de bord » dans la navigation (doublon avec le logo)
|
|
||||||
- `user`, `humansize`, `trash_count` centralisés dans le context processor Flask (disponibles sur toutes les pages)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [1.2.0] — 2026-05-06
|
|
||||||
|
|
||||||
### Ajouté
|
|
||||||
- Corbeille : mise à la corbeille depuis `/browse`, restauration avec gestion des conflits (écraser / renommer), suppression définitive
|
|
||||||
- Purge automatique des éléments en corbeille depuis plus de 30 jours
|
|
||||||
- Badge dans la navigation indiquant le nombre d'éléments en corbeille
|
|
||||||
- Page `/trash` avec tableau partagé (mode `browse` / `trash`) et bouton « Vider la corbeille »
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [1.1.0] — 2026-05-06
|
|
||||||
|
|
||||||
### Ajouté
|
|
||||||
- Redimensionnement d'images depuis la prévisualisation : tailles prédéfinies, dimension libre, formats PNG/JPG/ICO
|
|
||||||
- Gestion des conflits lors du redimensionnement (backup, écraser, renommer, ignorer)
|
|
||||||
- Renommage de fichiers inline dans `/browse` et la page de prévisualisation
|
|
||||||
- Affichage des métadonnées image (dimensions, format, mode couleur, DPI, EXIF)
|
|
||||||
- Dimension libre avec contrainte de proportions (mode « carré »)
|
|
||||||
|
|
||||||
### Corrigé
|
|
||||||
- Échec du redimensionnement sur les fichiers ICO en mode palette (conversion RGBA avant LANCZOS)
|
|
||||||
- Aucune sélection de taille ou format → copie à l'identique (comportement par défaut)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## [1.0.0] — 2026-05-03
|
|
||||||
|
|
||||||
### Ajouté
|
|
||||||
- Upload de fichiers par glisser-déposer avec gestion des conflits (écraser, backup, renommer, ignorer)
|
|
||||||
- Parcourir les assets CDN depuis `/browse` avec fil d'Ariane
|
|
||||||
- Statistiques de consultation via GoAccess (`/stats`), générées à la demande
|
|
||||||
- Authentification SSO via AlpID (Keycloak / OpenID Connect)
|
|
||||||
- Recherche dans les fichiers
|
|
||||||
- Scripts rsync `push-assets.sh` et `pull-assets.sh` pour la synchronisation locale ↔ serveur
|
|
||||||
- Script `deploy-app.sh` pour le déploiement de l'application Flask
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
1.4.2
|
|
||||||
52
app/app.py
52
app/app.py
|
|
@ -46,12 +46,12 @@ _gen_lock = threading.Lock()
|
||||||
_gen_state = {"generating": False}
|
_gen_state = {"generating": False}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_APP_VERSION = (Path(__file__).parent / "VERSION").read_text().strip()
|
_APP_VERSION = subprocess.check_output(
|
||||||
|
["git", "rev-parse", "--short", "HEAD"], stderr=subprocess.DEVNULL, text=True
|
||||||
|
).strip()
|
||||||
except Exception:
|
except Exception:
|
||||||
_APP_VERSION = "—"
|
_APP_VERSION = "—"
|
||||||
|
|
||||||
_CHANGELOG_FILE = Path(__file__).parent / "CHANGELOG.md"
|
|
||||||
|
|
||||||
|
|
||||||
_HIDDEN = frozenset({
|
_HIDDEN = frozenset({
|
||||||
".git", "scripts", "app",
|
".git", "scripts", "app",
|
||||||
|
|
@ -434,52 +434,6 @@ def dashboard():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/changelog")
|
|
||||||
def changelog():
|
|
||||||
redir = _require_admin()
|
|
||||||
if redir:
|
|
||||||
return redir
|
|
||||||
sections = _parse_changelog()
|
|
||||||
return render_template("changelog.html", sections=sections)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_changelog():
|
|
||||||
"""Parse CHANGELOG.md into a list of version dicts."""
|
|
||||||
try:
|
|
||||||
text = _CHANGELOG_FILE.read_text()
|
|
||||||
except Exception:
|
|
||||||
return []
|
|
||||||
|
|
||||||
sections = []
|
|
||||||
current = None
|
|
||||||
current_group = None
|
|
||||||
|
|
||||||
for line in text.splitlines():
|
|
||||||
if line.startswith("## "):
|
|
||||||
if current:
|
|
||||||
if current_group:
|
|
||||||
current["groups"].append(current_group)
|
|
||||||
sections.append(current)
|
|
||||||
m = re.match(r"## \[(.+?)\] — (.+)", line)
|
|
||||||
current = {"version": m.group(1) if m else line[3:],
|
|
||||||
"date": m.group(2) if m else "",
|
|
||||||
"groups": []}
|
|
||||||
current_group = None
|
|
||||||
elif line.startswith("### ") and current is not None:
|
|
||||||
if current_group:
|
|
||||||
current["groups"].append(current_group)
|
|
||||||
current_group = {"title": line[4:], "entries": []}
|
|
||||||
elif line.startswith("- ") and current_group is not None:
|
|
||||||
current_group["entries"].append(line[2:])
|
|
||||||
|
|
||||||
if current:
|
|
||||||
if current_group:
|
|
||||||
current["groups"].append(current_group)
|
|
||||||
sections.append(current)
|
|
||||||
|
|
||||||
return sections
|
|
||||||
|
|
||||||
|
|
||||||
# ── Navigateur de fichiers ────────────────────────────────────────────
|
# ── Navigateur de fichiers ────────────────────────────────────────────
|
||||||
|
|
||||||
@app.route("/browse/")
|
@app.route("/browse/")
|
||||||
|
|
|
||||||
|
|
@ -49,18 +49,8 @@ header { background: var(--blue-dark); color: #fff; }
|
||||||
border: 1px solid rgba(255,255,255,.3); transition: background .15s, color .15s; }
|
border: 1px solid rgba(255,255,255,.3); transition: background .15s, color .15s; }
|
||||||
.btn-logout-icon:hover { background: rgba(255,255,255,.15); color: #fff; text-decoration: none; }
|
.btn-logout-icon:hover { background: rgba(255,255,255,.15); color: #fff; text-decoration: none; }
|
||||||
|
|
||||||
.width-switcher { display: flex; gap: 2px; align-items: center; flex-shrink: 0; }
|
|
||||||
.width-switcher button { background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.2);
|
|
||||||
color: rgba(255,255,255,.6); border-radius: 4px; padding: .2rem .45rem; font-size: .72rem;
|
|
||||||
font-weight: 700; cursor: pointer; transition: background .15s, color .15s; line-height: 1.3; }
|
|
||||||
.width-switcher button:hover { background: rgba(255,255,255,.18); color: #fff; }
|
|
||||||
.width-switcher button.ws-active { background: rgba(255,255,255,.25); color: #fff; border-color: rgba(255,255,255,.5); }
|
|
||||||
|
|
||||||
/* ── Mise en page ─────────────────────────────────────────────── */
|
/* ── Mise en page ─────────────────────────────────────────────── */
|
||||||
main { width: 100%; margin: 2rem auto; padding: 0 1.5rem 3rem; display: flex; flex-direction: column; gap: 1.5rem; }
|
main { max-width: 1400px; margin: 2rem auto; padding: 0 1.5rem 3rem; display: flex; flex-direction: column; gap: 1.5rem; }
|
||||||
html[data-cw="900"] main { max-width: 900px; }
|
|
||||||
html[data-cw="1200"] main { max-width: 1200px; }
|
|
||||||
html[data-cw="1600"] main { max-width: 1600px; }
|
|
||||||
|
|
||||||
/* ── Carte générique ──────────────────────────────────────────── */
|
/* ── Carte générique ──────────────────────────────────────────── */
|
||||||
.card { background: #fff; border-radius: var(--radius); box-shadow: var(--shadow); padding: 1.5rem 1.8rem; }
|
.card { background: #fff; border-radius: var(--radius); box-shadow: var(--shadow); padding: 1.5rem 1.8rem; }
|
||||||
|
|
@ -286,27 +276,7 @@ footer { background: var(--blue-dark); color: rgba(255,255,255,.6); margin-top:
|
||||||
.footer-nav a { color: rgba(255,255,255,.55); font-size: .82rem; }
|
.footer-nav a { color: rgba(255,255,255,.55); font-size: .82rem; }
|
||||||
.footer-nav a:hover { color: rgba(255,255,255,.9); text-decoration: none; }
|
.footer-nav a:hover { color: rgba(255,255,255,.9); text-decoration: none; }
|
||||||
.footer-version { margin-left: auto; font-size: .75rem; color: rgba(255,255,255,.35);
|
.footer-version { margin-left: auto; font-size: .75rem; color: rgba(255,255,255,.35);
|
||||||
font-family: monospace; text-decoration: none; }
|
font-family: monospace; }
|
||||||
.footer-version:hover { color: rgba(255,255,255,.7); text-decoration: none; }
|
|
||||||
|
|
||||||
/* ── Changelog ──────────────────────────────────────────────────────── */
|
|
||||||
.cl-section { padding: .25rem 0 1rem; }
|
|
||||||
.cl-section--latest .cl-header { margin-bottom: .75rem; }
|
|
||||||
.cl-header { display: flex; align-items: baseline; gap: .75rem; margin-bottom: .6rem; flex-wrap: wrap; }
|
|
||||||
.cl-version { font-size: 1.05rem; font-weight: 700; color: var(--blue-dark); font-family: monospace; }
|
|
||||||
.cl-date { font-size: .82rem; color: var(--muted); }
|
|
||||||
.cl-badge { background: var(--blue); color: #fff; font-size: .68rem; font-weight: 700;
|
|
||||||
padding: .15rem .5rem; border-radius: 20px; letter-spacing: .03em; }
|
|
||||||
.cl-group { margin: .6rem 0 0 0; }
|
|
||||||
.cl-group-title { display: inline-block; font-size: .72rem; font-weight: 700; text-transform: uppercase;
|
|
||||||
letter-spacing: .06em; padding: .1rem .5rem; border-radius: 3px; margin-bottom: .35rem; }
|
|
||||||
.cl-group-title--ajouté { background: #dcfce7; color: #166534; }
|
|
||||||
.cl-group-title--corrigé { background: #fee2e2; color: #991b1b; }
|
|
||||||
.cl-group-title--modifié { background: #fef3c7; color: #92400e; }
|
|
||||||
.cl-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .3rem; }
|
|
||||||
.cl-list li { font-size: .88rem; color: var(--text); padding-left: 1.1rem; position: relative; }
|
|
||||||
.cl-list li::before { content: "–"; position: absolute; left: 0; color: var(--muted); }
|
|
||||||
.cl-sep { border: none; border-top: 1px solid var(--border); margin: 1rem 0; }
|
|
||||||
|
|
||||||
/* ── Corbeille ──────────────────────────────────────────────────────── */
|
/* ── Corbeille ──────────────────────────────────────────────────────── */
|
||||||
.trash-header { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: .75rem; margin-bottom: .5rem; }
|
.trash-header { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: .75rem; margin-bottom: .5rem; }
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,6 @@
|
||||||
<title>{% block title %}CDN{% endblock %} — Static Alpinux</title>
|
<title>{% block title %}CDN{% endblock %} — Static Alpinux</title>
|
||||||
<link rel="icon" type="image/x-icon" href="https://static.alpinux.org/logo/favicon.ico">
|
<link rel="icon" type="image/x-icon" href="https://static.alpinux.org/logo/favicon.ico">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='app.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='app.css') }}">
|
||||||
<script>
|
|
||||||
(function(){var cw=localStorage.getItem('content-width');if(cw)document.documentElement.dataset.cw=cw;})();
|
|
||||||
</script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
@ -31,12 +28,6 @@
|
||||||
value="{{ request.args.get('q', '') }}" aria-label="Recherche">
|
value="{{ request.args.get('q', '') }}" aria-label="Recherche">
|
||||||
<button type="submit" aria-label="Lancer la recherche">🔍</button>
|
<button type="submit" aria-label="Lancer la recherche">🔍</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="width-switcher" title="Largeur du contenu">
|
|
||||||
<button type="button" data-width="900" title="Étroit (~900 px)">S</button>
|
|
||||||
<button type="button" data-width="1200" title="Normal (~1200 px)">M</button>
|
|
||||||
<button type="button" data-width="1600" title="Large (~1600 px)">L</button>
|
|
||||||
<button type="button" data-width="" title="Plein écran">∞</button>
|
|
||||||
</div>
|
|
||||||
<div class="header-user">
|
<div class="header-user">
|
||||||
{% if user %}
|
{% if user %}
|
||||||
<span class="user-chip" title="{{ user.name }}">
|
<span class="user-chip" title="{{ user.name }}">
|
||||||
|
|
@ -64,34 +55,9 @@
|
||||||
<a href="{{ url_for('stats') }}">Statistiques</a>
|
<a href="{{ url_for('stats') }}">Statistiques</a>
|
||||||
<a href="{{ url_for('trash_list') }}">Corbeille</a>
|
<a href="{{ url_for('trash_list') }}">Corbeille</a>
|
||||||
</nav>
|
</nav>
|
||||||
<a href="{{ url_for('changelog') }}" class="footer-version">v {{ app_version }}</a>
|
<span class="footer-version">v {{ app_version }}</span>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
const KEY = 'content-width';
|
|
||||||
const btns = document.querySelectorAll('.width-switcher button');
|
|
||||||
const html = document.documentElement;
|
|
||||||
|
|
||||||
function applyActive(val) {
|
|
||||||
btns.forEach(b => b.classList.toggle('ws-active', b.dataset.width === (val || '')));
|
|
||||||
}
|
|
||||||
|
|
||||||
applyActive(localStorage.getItem(KEY));
|
|
||||||
|
|
||||||
btns.forEach(btn => btn.addEventListener('click', () => {
|
|
||||||
const val = btn.dataset.width;
|
|
||||||
if (val) {
|
|
||||||
localStorage.setItem(KEY, val);
|
|
||||||
html.dataset.cw = val;
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem(KEY);
|
|
||||||
delete html.dataset.cw;
|
|
||||||
}
|
|
||||||
applyActive(val);
|
|
||||||
}));
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
{% extends "base.html" %}
|
|
||||||
{% block title %}Changelog{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<section class="card">
|
|
||||||
<h1 style="font-size:1.2rem; color:var(--blue-dark); margin-bottom:1.5rem">
|
|
||||||
Historique des versions — Alpinux Static
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
{% for sec in sections %}
|
|
||||||
<div class="cl-section{% if loop.first %} cl-section--latest{% endif %}">
|
|
||||||
<div class="cl-header">
|
|
||||||
<span class="cl-version">v {{ sec.version }}</span>
|
|
||||||
{% if sec.date %}<span class="cl-date">{{ sec.date }}</span>{% endif %}
|
|
||||||
{% if loop.first %}<span class="cl-badge">Actuelle</span>{% endif %}
|
|
||||||
</div>
|
|
||||||
{% for grp in sec.groups %}
|
|
||||||
<div class="cl-group">
|
|
||||||
<span class="cl-group-title cl-group-title--{{ grp.title | lower | replace(' ','_') }}">{{ grp.title }}</span>
|
|
||||||
<ul class="cl-list">
|
|
||||||
{% for item in grp.entries %}
|
|
||||||
<li>{{ item }}</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% if not loop.last %}<hr class="cl-sep">{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</section>
|
|
||||||
{% endblock %}
|
|
||||||
Loading…
Reference in a new issue