feat: v2.0.0 — 14 tickets implémentés

#53 résumé req/IPs par statut dans Erreurs 404
#54 titre onglet avec compteur non résolus
#2  Tout/Aucun dans resize (tailles + formats)
#7  backup filename affiché dans résultats resize
#8  flash message résumé après upload
#6  renommage inline sur preview_text + preview_other
#23 filtre + tri par nom/taille/date dans corbeille
#20 sélection multiple + batch restore/delete corbeille
#45 /sitemap.xml (assets publics)
#52 ignoreip fail2ban sync sur Ignorer/Retirer une IP
#1  cairosvg>=2.7 dans requirements.txt
#51 ignored_ips.json exclu du rsync --delete
#48 as_cache/ exclu du rsync --delete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Alpinux 2026-05-06 21:04:03 +02:00
parent ef255d605f
commit a6d7bc2c8a
139 changed files with 1030 additions and 18 deletions

View file

@ -1,5 +1,26 @@
# Changelog — Alpinux Static # Changelog — Alpinux Static
## [2.0.0] — 2026-05-06
### Ajouté
- **Erreurs 404 (#53)** : barre de résumé rouge/verte affichant le total requêtes·IPs ventilé entre erreurs actives et résolues (calculé après vérification batch)
- **Erreurs 404 (#54)** : titre de l'onglet navigateur mis à jour avec le nombre d'erreurs non résolues (`Erreurs 404 (12) — Static Alpinux`)
- **Resize (#2)** : boutons « Tout / Aucun » pour les groupes Tailles et Formats
- **Resize (#7)** : nom du fichier backup affiché dans les résultats de génération
- **Upload (#8)** : message de résumé après upload (X envoyé(s), Y sauvegardé(s), Z ignoré(s)…)
- **Renommage (#6)** : bouton ✏️ disponible sur les pages `preview_text` et `preview_other` (idem `preview_image`)
- **Corbeille (#23)** : filtre texte + tri par nom / taille / date
- **Corbeille (#20)** : sélection multiple — cases à cocher, « Tout sélectionner », restaurer/supprimer en lot
- **Sitemap (#45)** : route `/sitemap.xml` listant les assets publics (logo/, wiki/, error/)
- **Ignorer IP (#52)** : « Ignorer cette IP » ajoute/retire automatiquement l'IP dans `fail2ban ignoreip global-blacklist`
- **SVG (#1)** : `cairosvg>=2.7` ajouté à `requirements.txt` (`libcairo2` disponible sur le serveur)
### Corrigé
- **Déploiement (#51)** : `ignored_ips.json` exclu du rsync `--delete` — les IPs ignorées ne sont plus effacées au déploiement
- **Déploiement (#48)** : `as_cache/` exclu du rsync `--delete` — le cache RIPE local est préservé entre déploiements
---
## [1.10.0] — 2026-05-06 ## [1.10.0] — 2026-05-06
### Modifié ### Modifié

View file

@ -1 +1 @@
1.10.0 2.0.0

View file

@ -24,7 +24,8 @@ except ImportError:
from PIL import Image from PIL import Image
from flask import (Flask, redirect, url_for, session, request, from flask import (Flask, redirect, url_for, session, request,
render_template, abort, send_from_directory, jsonify) render_template, abort, send_from_directory, jsonify, flash,
get_flashed_messages)
from authlib.integrations.flask_client import OAuth from authlib.integrations.flask_client import OAuth
from werkzeug.middleware.proxy_fix import ProxyFix from werkzeug.middleware.proxy_fix import ProxyFix
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
@ -1260,6 +1261,14 @@ def errors_ignore():
ips.add(ip) if action == "add" else ips.discard(ip) ips.add(ip) if action == "add" else ips.discard(ip)
_save_ignored_ips(ips) _save_ignored_ips(ips)
_invalidate_404_cache() _invalidate_404_cache()
try:
fb_action = "addignoreip" if action == "add" else "delignoreip"
subprocess.run(
["/usr/bin/sudo", "/usr/bin/fail2ban-client", "set", "global-blacklist", fb_action, ip],
capture_output=True, timeout=10, check=False
)
except Exception:
pass
return jsonify({"ok": True, "action": action, "ip": ip}) return jsonify({"ok": True, "action": action, "ip": ip})
@ -1506,6 +1515,44 @@ def trash_preview(subpath):
return render_template("preview_other.html", **ctx) return render_template("preview_other.html", **ctx)
@app.route("/trash/restore-batch", methods=["POST"])
def trash_restore_batch():
redir = _require_admin()
if redir:
return jsonify({"error": "not authorized"}), 403
paths = request.form.getlist("paths")
result = {"ok": 0, "error": 0, "conflict": []}
for p in paths:
ok, r = _trash_restore(p, "rename")
if ok:
result["ok"] += 1
elif r == "conflict":
result["conflict"].append(p)
else:
result["error"] += 1
return jsonify(result)
@app.route("/trash/delete-batch", methods=["POST"])
def trash_delete_batch():
redir = _require_admin()
if redir:
return jsonify({"error": "not authorized"}), 403
paths = request.form.getlist("paths")
ok = 0
for trash_rel in paths:
target = (TRASH_ROOT / trash_rel).resolve()
if not str(target).startswith(str(TRASH_ROOT)):
continue
if target.is_file():
target.unlink()
ok += 1
info = Path(str(target) + ".trashinfo")
if info.exists():
info.unlink()
return jsonify({"ok": ok})
@app.route("/trash/empty", methods=["POST"]) @app.route("/trash/empty", methods=["POST"])
def trash_empty(): def trash_empty():
redir = _require_admin() redir = _require_admin()
@ -1613,6 +1660,7 @@ def upload_file():
if conflict not in ("backup", "overwrite", "rename", "skip"): if conflict not in ("backup", "overwrite", "rename", "skip"):
conflict = "overwrite" conflict = "overwrite"
stats = {"ok": 0, "backup": 0, "renamed": 0, "skipped": 0, "error": 0}
for f in files: for f in files:
name = secure_filename(f.filename or "") name = secure_filename(f.filename or "")
if not name: if not name:
@ -1620,15 +1668,32 @@ def upload_file():
out_path = dest / name out_path = dest / name
if out_path.exists(): if out_path.exists():
if conflict == "skip": if conflict == "skip":
stats["skipped"] += 1
continue continue
elif conflict == "backup": elif conflict == "backup":
try: try:
out_path.rename(_backup_path(out_path)) out_path.rename(_backup_path(out_path))
stats["backup"] += 1
except Exception: except Exception:
stats["error"] += 1
continue continue
elif conflict == "rename": elif conflict == "rename":
out_path = _auto_rename(out_path) out_path = _auto_rename(out_path)
f.save(out_path) stats["renamed"] += 1
try:
f.save(out_path)
stats["ok"] += 1
except Exception:
stats["error"] += 1
parts = []
if stats["ok"]: parts.append(f"{stats['ok']} envoyé(s)")
if stats["backup"]: parts.append(f"{stats['backup']} sauvegardé(s)")
if stats["renamed"]: parts.append(f"{stats['renamed']} renommé(s)")
if stats["skipped"]: parts.append(f"{stats['skipped']} ignoré(s)")
if stats["error"]: parts.append(f"{stats['error']} erreur(s)")
if parts:
flash(", ".join(parts), "upload")
return redirect(url_for("browse", subpath=subpath) if subpath else url_for("browse")) return redirect(url_for("browse", subpath=subpath) if subpath else url_for("browse"))
@ -1783,6 +1848,7 @@ def resize_image():
out_path = parent / out_name out_path = parent / out_name
out_rel = str(out_path.relative_to(ASSETS_ROOT)) out_rel = str(out_path.relative_to(ASSETS_ROOT))
bak_name = None
if out_path.exists(): if out_path.exists():
if conflict == "skip": if conflict == "skip":
errors.append({"name": out_name, "reason": "Fichier existant, ignoré"}) errors.append({"name": out_name, "reason": "Fichier existant, ignoré"})
@ -1791,6 +1857,7 @@ def resize_image():
try: try:
bak = _backup_path(out_path) bak = _backup_path(out_path)
out_path.rename(bak) out_path.rename(bak)
bak_name = bak.name
except Exception as exc: except Exception as exc:
errors.append({"name": out_name, "reason": f"Backup impossible : {exc}"}) errors.append({"name": out_name, "reason": f"Backup impossible : {exc}"})
continue continue
@ -1799,10 +1866,15 @@ def resize_image():
out_name = out_path.name out_name = out_path.name
out_rel = str(out_path.relative_to(ASSETS_ROOT)) out_rel = str(out_path.relative_to(ASSETS_ROOT))
def _cr(n, p):
d = {"name": n, "path": p}
if bak_name: d["backup"] = bak_name
return d
try: try:
if fmt == "svg" and is_svg: if fmt == "svg" and is_svg:
shutil.copy2(target, out_path) shutil.copy2(target, out_path)
created.append({"name": out_name, "path": out_rel}) created.append(_cr(out_name, out_rel))
elif fmt == "svg" and not is_svg: elif fmt == "svg" and not is_svg:
errors.append({"name": out_name, "reason": "Conversion raster→SVG non supportée"}) errors.append({"name": out_name, "reason": "Conversion raster→SVG non supportée"})
@ -1828,7 +1900,7 @@ def resize_image():
buf.seek(0) buf.seek(0)
img = Image.open(buf).convert("RGB") img = Image.open(buf).convert("RGB")
img.save(out_path, format="JPEG", quality=90) img.save(out_path, format="JPEG", quality=90)
created.append({"name": out_name, "path": out_rel}) created.append(_cr(out_name, out_rel))
else: else:
img = Image.open(target) img = Image.open(target)
@ -1846,7 +1918,7 @@ def resize_image():
elif fmt == "ico": elif fmt == "ico":
img = img.convert("RGBA") img = img.convert("RGBA")
img.save(out_path, format="ICO", sizes=[(w, h)]) img.save(out_path, format="ICO", sizes=[(w, h)])
created.append({"name": out_name, "path": out_rel}) created.append(_cr(out_name, out_rel))
except Exception as exc: except Exception as exc:
errors.append({"name": out_name, "reason": str(exc)}) errors.append({"name": out_name, "reason": str(exc)})
@ -1869,6 +1941,24 @@ def robots():
return send_from_directory(ASSETS_ROOT, "robots.txt") return send_from_directory(ASSETS_ROOT, "robots.txt")
@app.route("/sitemap.xml")
def sitemap():
base = "https://static.alpinux.org"
urls = []
for top in sorted(_PUBLIC_TOP):
d = ASSETS_ROOT / top
if d.is_dir():
for f in sorted(d.rglob("*")):
if f.is_file():
urls.append(f"{base}/{f.relative_to(ASSETS_ROOT).as_posix()}")
xml = ['<?xml version="1.0" encoding="UTF-8"?>',
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">']
for u in urls:
xml.append(f" <url><loc>{u}</loc></url>")
xml.append("</urlset>")
return app.response_class("\n".join(xml), mimetype="application/xml")
@app.route("/<path:subpath>") @app.route("/<path:subpath>")
def cdn_file(subpath): def cdn_file(subpath):
top = Path(subpath).parts[0] top = Path(subpath).parts[0]

View file

@ -5,3 +5,4 @@ gunicorn>=21.0
python-dotenv>=1.0 python-dotenv>=1.0
Pillow>=10.0 Pillow>=10.0
psycopg2-binary>=2.9 psycopg2-binary>=2.9
cairosvg>=2.7

View file

@ -404,3 +404,31 @@ footer { background: var(--blue-dark); color: rgba(255,255,255,.6); margin-top:
.sec-tab--active { background: var(--blue); color: #fff; border-color: var(--blue); font-weight: 600; } .sec-tab--active { background: var(--blue); color: #fff; border-color: var(--blue); font-weight: 600; }
.tab-badge { display: inline-flex; align-items: center; justify-content: center; background: rgba(255,255,255,.25); border-radius: 9px; font-size: .72rem; font-weight: 700; min-width: 1.2rem; padding: 0 .3rem; margin-left: .3rem; vertical-align: middle; } .tab-badge { display: inline-flex; align-items: center; justify-content: center; background: rgba(255,255,255,.25); border-radius: 9px; font-size: .72rem; font-weight: 700; min-width: 1.2rem; padding: 0 .3rem; margin-left: .3rem; vertical-align: middle; }
.sec-tab:not(.sec-tab--active) .tab-badge { background: #e5e7eb; color: #6b7280; } .sec-tab:not(.sec-tab--active) .tab-badge { background: #e5e7eb; color: #6b7280; }
/* ── Résumé erreurs 404 ────────────────────────────────────────── */
.err-summary { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: .75rem; }
.err-summary-item { display: inline-flex; align-items: center; gap: .35rem; font-size: .82rem; font-weight: 600; padding: .2rem .65rem; border-radius: 20px; }
.err-summary-item--active { background: #fee2e2; color: #991b1b; }
.err-summary-item--ok { background: #dcfce7; color: #166534; }
/* ── Backup hint dans les résultats de resize ──────────────────── */
.resize-backup-hint { font-size: .75rem; color: var(--muted); margin-left: .4rem; }
/* ── Sélecteur Tout/Aucun (resize) ────────────────────────────── */
.resize-sel-btns { font-size: .75rem; margin-left: .5rem; }
.resize-sel-btns a { color: var(--blue); text-decoration: none; cursor: pointer; }
.resize-sel-btns a:hover { text-decoration: underline; }
/* ── Flash messages (upload) ───────────────────────────────────── */
.flash-bar { padding: .55rem 1.25rem; border-radius: 6px; font-size: .88rem; margin: .5rem 0; }
.flash-bar--upload { background: #f0fdf4; color: #166534; border: 1px solid #bbf7d0; }
/* ── Corbeille : filtre + tri ──────────────────────────────────── */
.trash-toolbar { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; margin-bottom: .75rem; }
.trash-filter { padding: .35rem .75rem; border: 1px solid var(--border); border-radius: 6px; font-size: .88rem; width: 220px; }
.trash-sort-btn { background: none; border: 1px solid var(--border); border-radius: 6px; padding: .25rem .6rem; font-size: .8rem; cursor: pointer; color: var(--muted); white-space: nowrap; }
.trash-sort-btn.active { background: var(--blue-light); color: var(--blue); border-color: var(--blue); font-weight: 600; }
.trash-batch-bar { display: none; align-items: center; gap: .5rem; background: var(--blue-light); border-radius: 6px; padding: .4rem .75rem; font-size: .85rem; }
.trash-batch-bar.visible { display: flex; }
.col-check { width: 2rem; text-align: center; }
.trash-select-count { font-weight: 600; color: var(--blue); }

View file

@ -8,9 +8,10 @@
subpath chemin courant (browse uniquement, pour la ligne "..") subpath chemin courant (browse uniquement, pour la ligne "..")
breadcrumb liste de crumbs (browse uniquement) breadcrumb liste de crumbs (browse uniquement)
#} #}
<table class="file-table"> <table class="file-table" id="file-table">
<thead> <thead>
<tr> <tr>
{% if mode == 'trash' %}<th class="col-check"><input type="checkbox" id="select-all-cb" title="Tout sélectionner"></th>{% endif %}
<th class="col-icon"></th> <th class="col-icon"></th>
<th>Nom</th> <th>Nom</th>
{% if mode == 'trash' %} {% if mode == 'trash' %}
@ -44,7 +45,8 @@
{% endif %} {% endif %}
{% for e in entries %} {% for e in entries %}
<tr> <tr data-name="{{ e.name|lower }}" data-size="{{ e.size or 0 }}" data-date="{{ e.deleted_at.isoformat() if mode == 'trash' and e.deleted_at else '' }}">
{% if mode == 'trash' %}<td class="col-check"><input type="checkbox" class="row-cb" data-path="{{ e.path }}" data-name="{{ e.name }}"></td>{% endif %}
<td class="col-icon"> <td class="col-icon">
{%- if e.is_dir -%}📁 {%- if e.is_dir -%}📁
{%- elif e.is_image -%}🖼 {%- elif e.is_image -%}🖼

View file

@ -52,6 +52,11 @@
</header> </header>
<main> <main>
{% with msgs = get_flashed_messages(with_categories=true) %}
{% for cat, msg in msgs %}
<div class="flash-bar flash-bar--{{ cat }}">{{ msg }}</div>
{% endfor %}
{% endwith %}
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>

View file

@ -28,6 +28,7 @@
<input type="search" id="err-search" class="err-search" placeholder="Filtrer par chemin ou adresse IP…" autocomplete="off"> <input type="search" id="err-search" class="err-search" placeholder="Filtrer par chemin ou adresse IP…" autocomplete="off">
<span class="err-search-count" id="err-search-count"></span> <span class="err-search-count" id="err-search-count"></span>
</div> </div>
<div id="err-summary" class="err-summary" style="display:none"></div>
<table class="file-table err-table"> <table class="file-table err-table">
<thead> <thead>
<tr> <tr>
@ -273,19 +274,43 @@
dot.title = still_404 ? 'Toujours actif' : 'Résolu'; dot.title = still_404 ? 'Toujours actif' : 'Résolu';
} }
function updateSummary() {
const el = document.getElementById('err-summary');
if (!el) return;
let aReqs = 0, aIPs = 0, oReqs = 0, oIPs = 0;
errRows.forEach(row => {
if (row.style.display === 'none') return;
const dot = row.querySelector('.err-status-dot');
const reqs = parseInt(row.querySelector('.hits-badge')?.textContent, 10) || 0;
const ips = parseInt(row.querySelector('.col-err-ips')?.textContent, 10) || 0;
if (dot?.classList.contains('err-status-dot--active')) { aReqs += reqs; aIPs += ips; }
else if (dot?.classList.contains('err-status-dot--ok')) { oReqs += reqs; oIPs += ips; }
});
if (!aReqs && !oReqs) { el.style.display = 'none'; return; }
el.style.display = '';
el.innerHTML =
`<span class="err-summary-item err-summary-item--active">● ${aReqs} req. · ${aIPs} IPs actives</span>` +
`<span class="err-summary-item err-summary-item--ok">● ${oReqs} req. · ${oIPs} IPs résolues</span>`;
const stillActive = document.querySelectorAll('.err-status-dot--active').length;
document.title = stillActive > 0
? `Erreurs 404 (${stillActive}) — Static Alpinux`
: `Erreurs 404 — Static Alpinux`;
}
document.querySelectorAll('.err-status-dot').forEach(dot => { document.querySelectorAll('.err-status-dot').forEach(dot => {
dot.style.cursor = 'pointer'; dot.style.cursor = 'pointer';
dot.addEventListener('click', async () => { dot.addEventListener('click', async () => {
dot.textContent = '○'; dot.className = 'err-status-dot'; dot.textContent = '○'; dot.className = 'err-status-dot';
const r = await fetch(DETAIL_URL + '?path=' + encodeURIComponent(dot.dataset.path)).then(r => r.json()); const r = await fetch(DETAIL_URL + '?path=' + encodeURIComponent(dot.dataset.path)).then(r => r.json());
applyDotResult(dot, r.still_404); applyDotResult(dot, r.still_404);
updateSummary();
}); });
}); });
/* Auto-vérification au chargement : tous les dots actifs (rouges) */ /* Auto-vérification au chargement : tous les dots actifs (rouges) */
(async () => { (async () => {
const activeDots = Array.from(document.querySelectorAll('.err-status-dot--active')); const activeDots = Array.from(document.querySelectorAll('.err-status-dot--active'));
if (!activeDots.length) return; if (!activeDots.length) { updateSummary(); return; }
activeDots.forEach(d => { d.textContent = '○'; d.className = 'err-status-dot'; }); activeDots.forEach(d => { d.textContent = '○'; d.className = 'err-status-dot'; });
try { try {
const paths = activeDots.map(d => d.dataset.path); const paths = activeDots.map(d => d.dataset.path);
@ -298,6 +323,7 @@
} catch { } catch {
activeDots.forEach(d => { d.textContent = '●'; d.className = 'err-status-dot err-status-dot--active'; }); activeDots.forEach(d => { d.textContent = '●'; d.className = 'err-status-dot err-status-dot--active'; });
} }
updateSummary();
})(); })();
/* ═══════════════════════════════════════════════ /* ═══════════════════════════════════════════════

View file

@ -110,7 +110,9 @@
<div class="resize-body"> <div class="resize-body">
<div class="resize-group"> <div class="resize-group">
<div class="resize-group-label">Tailles (px)</div> <div class="resize-group-label">Tailles (px)
<span class="resize-sel-btns"><a href="#" class="btn-sel" data-grp="sz" data-val="all">Tout</a> · <a href="#" class="btn-sel" data-grp="sz" data-val="none">Aucun</a></span>
</div>
<div class="resize-chips"> <div class="resize-chips">
{% for size in [32, 64, 100, 128, 200, 300, 500, 600, 1024] %} {% for size in [32, 64, 100, 128, 200, 300, 500, 600, 1024] %}
<label class="chip"> <label class="chip">
@ -122,7 +124,9 @@
</div> </div>
<div class="resize-group"> <div class="resize-group">
<div class="resize-group-label">Formats</div> <div class="resize-group-label">Formats
<span class="resize-sel-btns"><a href="#" class="btn-sel" data-grp="fmt" data-val="all">Tout</a> · <a href="#" class="btn-sel" data-grp="fmt" data-val="none">Aucun</a></span>
</div>
<div class="resize-chips"> <div class="resize-chips">
{% for fmt in ['png', 'jpg', 'ico'] %} {% for fmt in ['png', 'jpg', 'ico'] %}
<label class="chip"> <label class="chip">
@ -300,6 +304,15 @@
[...szCbs, ...fmtCbs].forEach(c => c.addEventListener('change', updateBtn)); [...szCbs, ...fmtCbs].forEach(c => c.addEventListener('change', updateBtn));
updateBtn(); updateBtn();
document.querySelectorAll('.btn-sel').forEach(a => {
a.addEventListener('click', e => {
e.preventDefault();
const cbs = document.querySelectorAll(`.resize-${a.dataset.grp}`);
cbs.forEach(cb => { if (!cb.disabled) cb.checked = a.dataset.val === 'all'; });
updateBtn();
});
});
function appendDims(fd) { function appendDims(fd) {
szCbs.forEach(c => { if (c.checked) fd.append('sizes', c.value); }); szCbs.forEach(c => { if (c.checked) fd.append('sizes', c.value); });
fmtCbs.forEach(c => { if (c.checked) fd.append('formats', c.value); }); fmtCbs.forEach(c => { if (c.checked) fd.append('formats', c.value); });
@ -325,7 +338,9 @@
html += '<p class="resize-ok-title">✓ ' + data.created.length + ' fichier(s) créé(s)</p>'; html += '<p class="resize-ok-title">✓ ' + data.created.length + ' fichier(s) créé(s)</p>';
html += '<ul class="resize-list">'; html += '<ul class="resize-list">';
data.created.forEach(f => { data.created.forEach(f => {
html += '<li><a href="/browse/' + f.path + '">' + f.name + '</a></li>'; let line = '<a href="/browse/' + escHtml(f.path) + '">' + escHtml(f.name) + '</a>';
if (f.backup) line += ' <span class="resize-backup-hint" title="' + escHtml(f.backup) + '">← backup</span>';
html += '<li>' + line + '</li>';
}); });
html += '</ul>'; html += '</ul>';
} }

View file

@ -11,9 +11,53 @@
<a href="{{ url_for('browse', subpath=parent_path) if parent_path else url_for('browse') }}" <a href="{{ url_for('browse', subpath=parent_path) if parent_path else url_for('browse') }}"
class="back-link">← Retour</a> class="back-link">← Retour</a>
{% endif %} {% endif %}
<h1>{{ filename }}</h1> <h1 id="preview-title">{{ filename }}</h1>
{% if not from_trash %}{% include '_preview_nav.html' %}{% endif %} {% if not from_trash %}
<button type="button" id="rename-toggle" class="btn-icon" title="Renommer ce fichier">✏️</button>
{% include '_preview_nav.html' %}
{% endif %}
</div> </div>
{% if not from_trash %}
<div id="rename-inline" class="rename-inline" style="display:none">
<input type="text" id="rename-input" class="rename-input" value="{{ filename }}">
<button type="button" id="rename-save" class="btn-rename-ok" title="Valider"></button>
<button type="button" id="rename-cancel" class="btn-rename-cancel" title="Annuler"></button>
<span id="rename-error" class="rename-error"></span>
</div>
<script>
(function () {
const RENAME_URL = {{ url_for('rename_file') | tojson }};
const FILE_PATH = {{ subpath | tojson }};
const renameToggle = document.getElementById('rename-toggle');
const renameInline = document.getElementById('rename-inline');
const renameInput = document.getElementById('rename-input');
const renameSave = document.getElementById('rename-save');
const renameCancel = document.getElementById('rename-cancel');
const renameError = document.getElementById('rename-error');
renameToggle.addEventListener('click', () => {
const open = renameInline.style.display !== 'none';
renameInline.style.display = open ? 'none' : 'flex';
if (!open) { renameInput.focus(); renameInput.select(); }
renameError.textContent = '';
});
renameCancel.addEventListener('click', () => { renameInline.style.display = 'none'; renameError.textContent = ''; });
async function doRename() {
const newName = renameInput.value.trim();
if (!newName) return;
renameSave.disabled = true;
const fd = new FormData(); fd.append('path', FILE_PATH); fd.append('new_name', newName);
try {
const data = await fetch(RENAME_URL, { method: 'POST', body: fd }).then(r => r.json());
if (data.error) { renameError.textContent = data.error; return; }
window.location.href = data.browse_url;
} catch { renameError.textContent = 'Erreur réseau.'; }
finally { renameSave.disabled = false; }
}
renameSave.addEventListener('click', doRename);
renameInput.addEventListener('keydown', e => { if (e.key === 'Enter') doRename(); if (e.key === 'Escape') renameCancel.click(); });
})();
</script>
{% endif %}
<div class="preview-meta"> <div class="preview-meta">
<span>Taille : <strong>{{ filesize }}</strong></span> <span>Taille : <strong>{{ filesize }}</strong></span>
<span>Modifié : <strong>{{ mtime.strftime('%d/%m/%Y %H:%M') }}</strong></span> <span>Modifié : <strong>{{ mtime.strftime('%d/%m/%Y %H:%M') }}</strong></span>

View file

@ -11,9 +11,20 @@
<a href="{{ url_for('browse', subpath=parent_path) if parent_path else url_for('browse') }}" <a href="{{ url_for('browse', subpath=parent_path) if parent_path else url_for('browse') }}"
class="back-link">← Retour</a> class="back-link">← Retour</a>
{% endif %} {% endif %}
<h1>{{ filename }}</h1> <h1 id="preview-title">{{ filename }}</h1>
{% if not from_trash %}{% include '_preview_nav.html' %}{% endif %} {% if not from_trash %}
<button type="button" id="rename-toggle" class="btn-icon" title="Renommer ce fichier">✏️</button>
{% include '_preview_nav.html' %}
{% endif %}
</div> </div>
{% if not from_trash %}
<div id="rename-inline" class="rename-inline" style="display:none">
<input type="text" id="rename-input" class="rename-input" value="{{ filename }}">
<button type="button" id="rename-save" class="btn-rename-ok" title="Valider"></button>
<button type="button" id="rename-cancel" class="btn-rename-cancel" title="Annuler"></button>
<span id="rename-error" class="rename-error"></span>
</div>
{% endif %}
<div class="preview-meta"> <div class="preview-meta">
<span>Taille : <strong>{{ filesize }}</strong></span> <span>Taille : <strong>{{ filesize }}</strong></span>
<span>Modifié : <strong>{{ mtime.strftime('%d/%m/%Y %H:%M') }}</strong></span> <span>Modifié : <strong>{{ mtime.strftime('%d/%m/%Y %H:%M') }}</strong></span>
@ -23,6 +34,41 @@
</div> </div>
</section> </section>
{% if not from_trash %}
<script>
(function () {
const RENAME_URL = {{ url_for('rename_file') | tojson }};
const FILE_PATH = {{ subpath | tojson }};
const renameToggle = document.getElementById('rename-toggle');
const renameInline = document.getElementById('rename-inline');
const renameInput = document.getElementById('rename-input');
const renameSave = document.getElementById('rename-save');
const renameCancel = document.getElementById('rename-cancel');
const renameError = document.getElementById('rename-error');
renameToggle.addEventListener('click', () => {
const open = renameInline.style.display !== 'none';
renameInline.style.display = open ? 'none' : 'flex';
if (!open) { renameInput.focus(); renameInput.select(); }
renameError.textContent = '';
});
renameCancel.addEventListener('click', () => { renameInline.style.display = 'none'; renameError.textContent = ''; });
async function doRename() {
const newName = renameInput.value.trim();
if (!newName) return;
renameSave.disabled = true;
const fd = new FormData(); fd.append('path', FILE_PATH); fd.append('new_name', newName);
try {
const data = await fetch(RENAME_URL, { method: 'POST', body: fd }).then(r => r.json());
if (data.error) { renameError.textContent = data.error; return; }
window.location.href = data.browse_url;
} catch { renameError.textContent = 'Erreur réseau.'; }
finally { renameSave.disabled = false; }
}
renameSave.addEventListener('click', doRename);
renameInput.addEventListener('keydown', e => { if (e.key === 'Enter') doRename(); if (e.key === 'Escape') renameCancel.click(); });
})();
</script>
{% endif %}
<div class="preview-text-wrap"> <div class="preview-text-wrap">
<div class="text-bar"> <div class="text-bar">
<span>{{ filename }}</span> <span>{{ filename }}</span>

View file

@ -20,6 +20,17 @@
<p class="trash-info">Les fichiers sont supprimés définitivement après 30 jours.</p> <p class="trash-info">Les fichiers sont supprimés définitivement après 30 jours.</p>
{% if entries %} {% if entries %}
<div class="trash-toolbar">
<input type="search" id="trash-filter" class="trash-filter" placeholder="Filtrer par nom…" autocomplete="off">
<button type="button" class="trash-sort-btn active" data-sort="date-desc">Date ▼</button>
<button type="button" class="trash-sort-btn" data-sort="name-asc">Nom ↑</button>
<button type="button" class="trash-sort-btn" data-sort="size-desc">Taille ▼</button>
</div>
<div id="trash-batch-bar" class="trash-batch-bar">
<span class="trash-select-count" id="batch-count">0 sélectionné(s)</span>
<button type="button" id="btn-restore-batch" class="btn btn-sm">♻️ Restaurer</button>
<button type="button" id="btn-delete-batch" class="btn btn-sm btn-danger">🗑 Supprimer</button>
</div>
{% set mode = 'trash' %} {% set mode = 'trash' %}
{% include '_file_table.html' %} {% include '_file_table.html' %}
{% else %} {% else %}
@ -41,7 +52,9 @@
<script> <script>
(function () { (function () {
const RESTORE_URL = {{ url_for('trash_restore') | tojson }}; const RESTORE_URL = {{ url_for('trash_restore') | tojson }};
const RESTORE_BATCH_URL = {{ url_for('trash_restore_batch') | tojson }};
const DELETE_BATCH_URL = {{ url_for('trash_delete_batch') | tojson }};
const conflictPanel = document.getElementById('restore-conflict-panel'); const conflictPanel = document.getElementById('restore-conflict-panel');
const conflictName = document.getElementById('restore-conflict-name'); const conflictName = document.getElementById('restore-conflict-name');
const confirmBtn = document.getElementById('restore-confirm'); const confirmBtn = document.getElementById('restore-confirm');
@ -86,6 +99,99 @@
conflictPanel.style.display = 'none'; conflictPanel.style.display = 'none';
pendingPath = null; pendingPath = null;
}); });
/* ── Filtre ── */
const filterIn = document.getElementById('trash-filter');
const tbody = document.querySelector('#file-table tbody');
filterIn?.addEventListener('input', () => {
const q = filterIn.value.trim().toLowerCase();
tbody?.querySelectorAll('tr').forEach(row => {
row.style.display = (!q || (row.dataset.name || '').includes(q)) ? '' : 'none';
});
updateBatchBar();
});
/* ── Tri ── */
let currentSort = 'date-desc';
document.querySelectorAll('.trash-sort-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.trash-sort-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentSort = btn.dataset.sort;
sortTable(currentSort);
});
});
function sortTable(key) {
if (!tbody) return;
const rows = Array.from(tbody.querySelectorAll('tr'));
rows.sort((a, b) => {
if (key === 'name-asc') return (a.dataset.name || '').localeCompare(b.dataset.name || '');
if (key === 'size-desc') return parseInt(b.dataset.size || 0, 10) - parseInt(a.dataset.size || 0, 10);
/* date-desc */ return (b.dataset.date || '').localeCompare(a.dataset.date || '');
});
rows.forEach(r => tbody.appendChild(r));
}
/* ── Sélection multiple ── */
const selectAll = document.getElementById('select-all-cb');
const batchBar = document.getElementById('trash-batch-bar');
const batchCount = document.getElementById('batch-count');
function selectedCbs() {
return Array.from(document.querySelectorAll('.row-cb:checked'));
}
function updateBatchBar() {
const sel = selectedCbs().length;
if (batchBar) batchBar.classList.toggle('visible', sel > 0);
if (batchCount) batchCount.textContent = `${sel} sélectionné(s)`;
if (selectAll) {
const all = document.querySelectorAll('.row-cb:not(:disabled)').length;
selectAll.indeterminate = sel > 0 && sel < all;
selectAll.checked = sel > 0 && sel === all;
}
}
selectAll?.addEventListener('change', () => {
document.querySelectorAll('.row-cb').forEach(cb => {
const row = cb.closest('tr');
if (!row || row.style.display !== 'none') cb.checked = selectAll.checked;
});
updateBatchBar();
});
document.addEventListener('change', e => {
if (e.target.classList.contains('row-cb')) updateBatchBar();
});
/* ── Restaurer la sélection ── */
document.getElementById('btn-restore-batch')?.addEventListener('click', async () => {
const paths = selectedCbs().map(cb => cb.dataset.path);
if (!paths.length) return;
const fd = new FormData();
paths.forEach(p => fd.append('paths', p));
try {
const j = await fetch(RESTORE_BATCH_URL, { method: 'POST', body: fd }).then(r => r.json());
const msg = `${j.ok || 0} fichier(s) restauré(s)` + (j.conflict?.length ? `, ${j.conflict.length} conflit(s) ignoré(s)` : '') + (j.error ? `, ${j.error} erreur(s)` : '');
alert(msg);
location.reload();
} catch { alert('Erreur réseau.'); }
});
/* ── Supprimer la sélection ── */
document.getElementById('btn-delete-batch')?.addEventListener('click', async () => {
const cbs = selectedCbs();
if (!cbs.length) return;
const names = cbs.map(cb => cb.dataset.name).join(', ');
if (!confirm(`Supprimer définitivement ${cbs.length} fichier(s) ?\n${names}`)) return;
const fd = new FormData();
cbs.forEach(cb => fd.append('paths', cb.dataset.path));
try {
await fetch(DELETE_BATCH_URL, { method: 'POST', body: fd });
location.reload();
} catch { alert('Erreur réseau.'); }
});
})(); })();
</script> </script>

61
error/400.html Executable file
View file

@ -0,0 +1,61 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>ERROR 400 - Bad Request!</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="robots" content="noindex" />
<style type="text/css"><!--
body {
color: #444444;
background-color: #EEEEEE;
font-family: 'Trebuchet MS', sans-serif;
font-size: 80%;
}
h1 {}
h2 { font-size: 1.2em; }
#page{
background-color: #FFFFFF;
width: 60%;
margin: 24px auto;
padding: 12px;
}
#header {
padding: 6px ;
text-align: center;
}
.status3xx { background-color: #475076; color: #FFFFFF; }
.status4xx { background-color: #C55042; color: #FFFFFF; }
.status5xx { background-color: #F2E81A; color: #000000; }
#content {
padding: 4px 0 24px 0;
}
#footer {
color: #666666;
background: #f9f9f9;
padding: 10px 20px;
border-top: 5px #efefef solid;
font-size: 0.8em;
text-align: center;
}
#footer a {
color: #999999;
}
--></style>
</head>
<body>
<div id="page">
<div id="header" class="status4xx">
<h1>ERROR 400 - Bad Request!</h1>
</div>
<div id="content">
<h2>The following error occurred:</h2>
<p>You have used invalid syntax.</p>
<P>Please contact the <!--WEBMASTER//-->webmaster<!--WEBMASTER//--> with any queries.</p>
</div>
<div id="footer">
<p>Powered by <a href="https://www.ispconfig.org">ISPConfig</a></p>
</div>
</div>
</body>
</html>

61
error/401.html Executable file
View file

@ -0,0 +1,61 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>ERROR 401 - Unauthorized!</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="robots" content="noindex" />
<style type="text/css"><!--
body {
color: #444444;
background-color: #EEEEEE;
font-family: 'Trebuchet MS', sans-serif;
font-size: 80%;
}
h1 {}
h2 { font-size: 1.2em; }
#page{
background-color: #FFFFFF;
width: 60%;
margin: 24px auto;
padding: 12px;
}
#header {
padding: 6px ;
text-align: center;
}
.status3xx { background-color: #475076; color: #FFFFFF; }
.status4xx { background-color: #C55042; color: #FFFFFF; }
.status5xx { background-color: #F2E81A; color: #000000; }
#content {
padding: 4px 0 24px 0;
}
#footer {
color: #666666;
background: #f9f9f9;
padding: 10px 20px;
border-top: 5px #efefef solid;
font-size: 0.8em;
text-align: center;
}
#footer a {
color: #999999;
}
--></style>
</head>
<body>
<div id="page">
<div id="header" class="status4xx">
<h1>ERROR 401 - Unauthorized!</h1>
</div>
<div id="content">
<h2>The following error occurred:</h2>
<p>The URL requested requires authorisation.</p>
<P>Please contact the <!--WEBMASTER//-->webmaster<!--WEBMASTER//--> with any queries.</p>
</div>
<div id="footer">
<p>Powered by <a href="https://www.ispconfig.org">ISPConfig</a></p>
</div>
</div>
</body>
</html>

61
error/403.html Executable file
View file

@ -0,0 +1,61 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>ERROR 403 - Forbidden!</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="robots" content="noindex" />
<style type="text/css"><!--
body {
color: #444444;
background-color: #EEEEEE;
font-family: 'Trebuchet MS', sans-serif;
font-size: 80%;
}
h1 {}
h2 { font-size: 1.2em; }
#page{
background-color: #FFFFFF;
width: 60%;
margin: 24px auto;
padding: 12px;
}
#header {
padding: 6px ;
text-align: center;
}
.status3xx { background-color: #475076; color: #FFFFFF; }
.status4xx { background-color: #C55042; color: #FFFFFF; }
.status5xx { background-color: #F2E81A; color: #000000; }
#content {
padding: 4px 0 24px 0;
}
#footer {
color: #666666;
background: #f9f9f9;
padding: 10px 20px;
border-top: 5px #efefef solid;
font-size: 0.8em;
text-align: center;
}
#footer a {
color: #999999;
}
--></style>
</head>
<body>
<div id="page">
<div id="header" class="status4xx">
<h1>ERROR 403 - Forbidden!</h1>
</div>
<div id="content">
<h2>The following error occurred:</h2>
<p>You are not permitted to access the requested URL.</p>
<P>Please contact the <!--WEBMASTER//-->webmaster<!--WEBMASTER//--> with any queries.</p>
</div>
<div id="footer">
<p>Powered by <a href="https://www.ispconfig.org">ISPConfig</a></p>
</div>
</div>
</body>
</html>

61
error/404.html Executable file
View file

@ -0,0 +1,61 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>ERROR 404 - Not Found!</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="robots" content="noindex" />
<style type="text/css"><!--
body {
color: #444444;
background-color: #EEEEEE;
font-family: 'Trebuchet MS', sans-serif;
font-size: 80%;
}
h1 {}
h2 { font-size: 1.2em; }
#page{
background-color: #FFFFFF;
width: 60%;
margin: 24px auto;
padding: 12px;
}
#header {
padding: 6px ;
text-align: center;
}
.status3xx { background-color: #475076; color: #FFFFFF; }
.status4xx { background-color: #C55042; color: #FFFFFF; }
.status5xx { background-color: #F2E81A; color: #000000; }
#content {
padding: 4px 0 24px 0;
}
#footer {
color: #666666;
background: #f9f9f9;
padding: 10px 20px;
border-top: 5px #efefef solid;
font-size: 0.8em;
text-align: center;
}
#footer a {
color: #999999;
}
--></style>
</head>
<body>
<div id="page">
<div id="header" class="status4xx">
<h1>ERROR 404 - Not Found!</h1>
</div>
<div id="content">
<h2>The following error occurred:</h2>
<p>The requested URL was not found on this server.</p>
<P>Please check the URL or contact the <!--WEBMASTER//-->webmaster<!--WEBMASTER//-->.</p>
</div>
<div id="footer">
<p>Powered by <a href="https://www.ispconfig.org">ISPConfig</a></p>
</div>
</div>
</body>
</html>

61
error/405.html Executable file
View file

@ -0,0 +1,61 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>ERROR 405 - Method Not Allowed!</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="robots" content="noindex" />
<style type="text/css"><!--
body {
color: #444444;
background-color: #EEEEEE;
font-family: 'Trebuchet MS', sans-serif;
font-size: 80%;
}
h1 {}
h2 { font-size: 1.2em; }
#page{
background-color: #FFFFFF;
width: 60%;
margin: 24px auto;
padding: 12px;
}
#header {
padding: 6px ;
text-align: center;
}
.status3xx { background-color: #475076; color: #FFFFFF; }
.status4xx { background-color: #C55042; color: #FFFFFF; }
.status5xx { background-color: #F2E81A; color: #000000; }
#content {
padding: 4px 0 24px 0;
}
#footer {
color: #666666;
background: #f9f9f9;
padding: 10px 20px;
border-top: 5px #efefef solid;
font-size: 0.8em;
text-align: center;
}
#footer a {
color: #999999;
}
--></style>
</head>
<body>
<div id="page">
<div id="header" class="status4xx">
<h1>ERROR 405 - Method Not Allowed!</h1>
</div>
<div id="content">
<h2>The following error occurred:</h2>
<p>The method used is not permitted.</p>
<P>Please contact the <!--WEBMASTER//-->webmaster<!--WEBMASTER//--> with any queries.</p>
</div>
<div id="footer">
<p>Powered by <a href="https://www.ispconfig.org">ISPConfig</a></p>
</div>
</div>
</body>
</html>

61
error/500.html Executable file
View file

@ -0,0 +1,61 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>ERROR 500 - Internal Server Error!</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="robots" content="noindex" />
<style type="text/css"><!--
body {
color: #444444;
background-color: #EEEEEE;
font-family: 'Trebuchet MS', sans-serif;
font-size: 80%;
}
h1 {}
h2 { font-size: 1.2em; }
#page{
background-color: #FFFFFF;
width: 60%;
margin: 24px auto;
padding: 12px;
}
#header {
padding: 6px ;
text-align: center;
}
.status3xx { background-color: #475076; color: #FFFFFF; }
.status4xx { background-color: #C55042; color: #FFFFFF; }
.status5xx { background-color: #F2E81A; color: #000000; }
#content {
padding: 4px 0 24px 0;
}
#footer {
color: #666666;
background: #f9f9f9;
padding: 10px 20px;
border-top: 5px #efefef solid;
font-size: 0.8em;
text-align: center;
}
#footer a {
color: #999999;
}
--></style>
</head>
<body>
<div id="page">
<div id="header" class="status5xx">
<h1>ERROR 500 - Internal Server Error!</h1>
</div>
<div id="content">
<h2>The following error occurred:</h2>
<p>The requested URL caused an internal server error.</p>
<P>If you get this message repeatedly please contact the <!--WEBMASTER//-->webmaster<!--WEBMASTER//-->.</p>
</div>
<div id="footer">
<p>Powered by <a href="https://www.ispconfig.org">ISPConfig</a></p>
</div>
</div>
</body>
</html>

61
error/502.html Executable file
View file

@ -0,0 +1,61 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>ERROR 502 - Bad Gateway!</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="robots" content="noindex" />
<style type="text/css"><!--
body {
color: #444444;
background-color: #EEEEEE;
font-family: 'Trebuchet MS', sans-serif;
font-size: 80%;
}
h1 {}
h2 { font-size: 1.2em; }
#page{
background-color: #FFFFFF;
width: 60%;
margin: 24px auto;
padding: 12px;
}
#header {
padding: 6px ;
text-align: center;
}
.status3xx { background-color: #475076; color: #FFFFFF; }
.status4xx { background-color: #C55042; color: #FFFFFF; }
.status5xx { background-color: #F2E81A; color: #000000; }
#content {
padding: 4px 0 24px 0;
}
#footer {
color: #666666;
background: #f9f9f9;
padding: 10px 20px;
border-top: 5px #efefef solid;
font-size: 0.8em;
text-align: center;
}
#footer a {
color: #999999;
}
--></style>
</head>
<body>
<div id="page">
<div id="header" class="status5xx">
<h1>ERROR 502 - Bad Gateway!</h1>
</div>
<div id="content">
<h2>The following error occurred:</h2>
<p>This server received an invalid response from an upstream server it accessed to fulfill the request.</p>
<P>If you get this message repeatedly please contact the <!--WEBMASTER//-->webmaster<!--WEBMASTER//-->.</p>
</div>
<div id="footer">
<p>Powered by <a href="https://www.ispconfig.org">ISPConfig</a></p>
</div>
</div>
</body>
</html>

61
error/503.html Executable file
View file

@ -0,0 +1,61 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>ERROR 503 - Service Unavailable!</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="robots" content="noindex" />
<style type="text/css"><!--
body {
color: #444444;
background-color: #EEEEEE;
font-family: 'Trebuchet MS', sans-serif;
font-size: 80%;
}
h1 {}
h2 { font-size: 1.2em; }
#page{
background-color: #FFFFFF;
width: 60%;
margin: 24px auto;
padding: 12px;
}
#header {
padding: 6px ;
text-align: center;
}
.status3xx { background-color: #475076; color: #FFFFFF; }
.status4xx { background-color: #C55042; color: #FFFFFF; }
.status5xx { background-color: #F2E81A; color: #000000; }
#content {
padding: 4px 0 24px 0;
}
#footer {
color: #666666;
background: #f9f9f9;
padding: 10px 20px;
border-top: 5px #efefef solid;
font-size: 0.8em;
text-align: center;
}
#footer a {
color: #999999;
}
--></style>
</head>
<body>
<div id="page">
<div id="header" class="status5xx">
<h1>ERROR 503 - Service Unavailable!</h1>
</div>
<div id="content">
<h2>The following error occurred:</h2>
<p>The Service is not available at the moment due to a temporary overloading or maintenance of the server. Please try again later.</p>
<P>Please contact the <!--WEBMASTER//-->webmaster<!--WEBMASTER//--> with any queries.</p>
</div>
<div id="footer">
<p>Powered by <a href="https://www.ispconfig.org">ISPConfig</a></p>
</div>
</div>
</body>
</html>

BIN
logo/alpinux-logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

BIN
logo/alpinux-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
logo/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

1
robots.txt Executable file
View file

@ -0,0 +1 @@
User-agent: *

View file

@ -40,7 +40,9 @@ echo ""
echo -e "${CYAN}[1/4] Synchronisation des fichiers…${RESET}" echo -e "${CYAN}[1/4] Synchronisation des fichiers…${RESET}"
RSYNC_OPTS=(-rlcz --delete --human-readable RSYNC_OPTS=(-rlcz --delete --human-readable
--exclude='.env' --exclude='__pycache__/' --exclude='*.pyc' --exclude='.env' --exclude='__pycache__/' --exclude='*.pyc'
--exclude='venv/' --exclude='.env.example') --exclude='venv/' --exclude='.env.example'
--exclude='ignored_ips.json'
--exclude='as_cache/')
if $DRY_RUN; then if $DRY_RUN; then
rsync --dry-run --itemize-changes "${RSYNC_OPTS[@]}" "$APP_DIR/" "$REMOTE_HOST:$REMOTE_DEST/" rsync --dry-run --itemize-changes "${RSYNC_OPTS[@]}" "$APP_DIR/" "$REMOTE_HOST:$REMOTE_DEST/"

60
standard_index.html Executable file
View file

@ -0,0 +1,60 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Welcome!</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link rel="shortcut icon" href="/favicon.ico" />
<meta name="robots" content="noindex" />
<style type="text/css"><!--
body {
color: #444444;
background-color: #EEEEEE;
font-family: 'Trebuchet MS', sans-serif;
font-size: 80%;
}
h1 {}
h2 { font-size: 1.2em; }
#page{
background-color: #FFFFFF;
width: 60%;
margin: 24px auto;
padding: 12px;
}
#header{
padding: 6px ;
text-align: center;
}
.header{ background-color: #83A342; color: #FFFFFF; }
#content {
padding: 4px 0 24px 0;
}
#footer {
color: #666666;
background: #f9f9f9;
padding: 10px 20px;
border-top: 5px #efefef solid;
font-size: 0.8em;
text-align: center;
}
#footer a {
color: #999999;
}
--></style>
</head>
<body>
<div id="page">
<div id="header" class="header">
<h1>Welcome to <!--ADRESSE//-->your website!<!--ADRESSE//--></h1>
</div>
<div id="content">
<h2>This is the default index page of your website.</h2>
<p>This file may be deleted or overwritten without any difficulty. This is produced by the file <b>index.html</b> in the <b>web</b> directory.</p>
<p>For questions or problems please contact <!--SUPPORT//-->support<!--SUPPORT//-->.</p>
</div>
<div id="footer">
<p>Powered by <a href="https://www.ispconfig.org">ISPConfig</a></p>
</div>
</div>
</body>
</html>

9
stats/.htaccess Normal file
View file

@ -0,0 +1,9 @@
AuthType Basic
AuthName "Members Only"
AuthUserFile /var/www/clients/client1/web17/web/stats/.htpasswd_stats
require valid-user
DirectoryIndex index.html index.php
Header set Content-Security-Policy "default-src * 'self' 'unsafe-inline' 'unsafe-eval' data:;"
<Files "goaindex.html">
AddDefaultCharset UTF-8
</Files>

68
stats/index.php Normal file
View file

@ -0,0 +1,68 @@
<?php
$yearmonth_text = "Jump to previous stats: ";
$awstatsindex = 'awsindex.html';
$script = "<script>function load_content(url){var iframe = document.getElementById(\"content\");iframe.src = url;}</script>\n";
if ($handle = opendir('.'))
{
while(false !== ($file = readdir($handle)))
{
if (substr($file,0,1) != "." && is_dir($file))
{
$orderkey = substr($file,0,4).substr($file,5,2);
if (substr($file,5,2) < 10 )
{
$orderkey = substr($file,0,4)."0".substr($file,5,2);
}
$awprev[$orderkey] = $file;
}
}
$month = date("n");
$year = date("Y");
if (date("d") == 1)
{
$month = date("m")-1;
if (date("m") == 1)
{
$year = date("Y")-1;
$month = "12";
}
}
$current = $year.$month;
if ( $month < 10 ) {
$current = $year."0".$month;
}
$awprev[$current] = $year."-".$month;
closedir($handle);
}
arsort($awprev);
$options = "";
foreach ($awprev as $key => $value) {
// Define name for the index file
$awstatsindex = 'awsindex.html';
if(!file_exists($value.'/awsindex.html') && file_exists($value.'/goaindex.html')) {
$awstatsindex = 'goaindex.html';
}
// Set name for first entry in month list
if($key == $current) $options .= "<option selected=\"selected\" value=\"{$awstatsindex}\">{$value}</option>\n";
else $options .= "<option value=\"{$value}/{$awstatsindex}\">{$value}</option>\n";
}
$html = "<!DOCTYPE html>\n<html>\n<head>\n<title>Stats</title>\n";
$html .= "<style>\nhtml,body {margin:0px;padding:0px;width:100%;height:100%;background-color: #ccc;}\n";
$html .= "#header\n{\nwidth:100%;margin:0px auto;\nheight:20px;\nposition:fixed;\npadding:4px;\ntext-align:center;\n}\n";
$html .= "iframe {width:100%;height:90%;margin:0px;margin-top:40px;border:0px;padding:0px;}\n</style>\n</head>\n<body>\n";
$html .= $script;
$html .= "<div id=\"header\">{$yearmonth_text}\n";
$html .= "<select name=\"awdate\" onchange=\"load_content(this.value)\">\n";
$html .= $options;
$html .= "</select>\n</div>\n<iframe src=\"awsindex.html\" id=\"content\"></iframe>\n";
$html .= "</body></html>";
echo $html;
?>

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Some files were not shown because too many files have changed in this diff Show more