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>
|
|
@ -1,5 +1,26 @@
|
|||
# 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
|
||||
|
||||
### Modifié
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
1.10.0
|
||||
2.0.0
|
||||
|
|
|
|||
98
app/app.py
|
|
@ -24,7 +24,8 @@ except ImportError:
|
|||
from PIL import Image
|
||||
|
||||
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 werkzeug.middleware.proxy_fix import ProxyFix
|
||||
from werkzeug.utils import secure_filename
|
||||
|
|
@ -1260,6 +1261,14 @@ def errors_ignore():
|
|||
ips.add(ip) if action == "add" else ips.discard(ip)
|
||||
_save_ignored_ips(ips)
|
||||
_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})
|
||||
|
||||
|
||||
|
|
@ -1506,6 +1515,44 @@ def trash_preview(subpath):
|
|||
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"])
|
||||
def trash_empty():
|
||||
redir = _require_admin()
|
||||
|
|
@ -1613,6 +1660,7 @@ def upload_file():
|
|||
if conflict not in ("backup", "overwrite", "rename", "skip"):
|
||||
conflict = "overwrite"
|
||||
|
||||
stats = {"ok": 0, "backup": 0, "renamed": 0, "skipped": 0, "error": 0}
|
||||
for f in files:
|
||||
name = secure_filename(f.filename or "")
|
||||
if not name:
|
||||
|
|
@ -1620,15 +1668,32 @@ def upload_file():
|
|||
out_path = dest / name
|
||||
if out_path.exists():
|
||||
if conflict == "skip":
|
||||
stats["skipped"] += 1
|
||||
continue
|
||||
elif conflict == "backup":
|
||||
try:
|
||||
out_path.rename(_backup_path(out_path))
|
||||
stats["backup"] += 1
|
||||
except Exception:
|
||||
stats["error"] += 1
|
||||
continue
|
||||
elif conflict == "rename":
|
||||
out_path = _auto_rename(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"))
|
||||
|
||||
|
|
@ -1783,6 +1848,7 @@ def resize_image():
|
|||
out_path = parent / out_name
|
||||
out_rel = str(out_path.relative_to(ASSETS_ROOT))
|
||||
|
||||
bak_name = None
|
||||
if out_path.exists():
|
||||
if conflict == "skip":
|
||||
errors.append({"name": out_name, "reason": "Fichier existant, ignoré"})
|
||||
|
|
@ -1791,6 +1857,7 @@ def resize_image():
|
|||
try:
|
||||
bak = _backup_path(out_path)
|
||||
out_path.rename(bak)
|
||||
bak_name = bak.name
|
||||
except Exception as exc:
|
||||
errors.append({"name": out_name, "reason": f"Backup impossible : {exc}"})
|
||||
continue
|
||||
|
|
@ -1799,10 +1866,15 @@ def resize_image():
|
|||
out_name = out_path.name
|
||||
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:
|
||||
if fmt == "svg" and is_svg:
|
||||
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:
|
||||
errors.append({"name": out_name, "reason": "Conversion raster→SVG non supportée"})
|
||||
|
|
@ -1828,7 +1900,7 @@ def resize_image():
|
|||
buf.seek(0)
|
||||
img = Image.open(buf).convert("RGB")
|
||||
img.save(out_path, format="JPEG", quality=90)
|
||||
created.append({"name": out_name, "path": out_rel})
|
||||
created.append(_cr(out_name, out_rel))
|
||||
|
||||
else:
|
||||
img = Image.open(target)
|
||||
|
|
@ -1846,7 +1918,7 @@ def resize_image():
|
|||
elif fmt == "ico":
|
||||
img = img.convert("RGBA")
|
||||
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:
|
||||
errors.append({"name": out_name, "reason": str(exc)})
|
||||
|
|
@ -1869,6 +1941,24 @@ def robots():
|
|||
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>")
|
||||
def cdn_file(subpath):
|
||||
top = Path(subpath).parts[0]
|
||||
|
|
|
|||
|
|
@ -5,3 +5,4 @@ gunicorn>=21.0
|
|||
python-dotenv>=1.0
|
||||
Pillow>=10.0
|
||||
psycopg2-binary>=2.9
|
||||
cairosvg>=2.7
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
.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; }
|
||||
|
||||
/* ── 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); }
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@
|
|||
subpath – chemin courant (browse uniquement, pour la ligne "..")
|
||||
breadcrumb – liste de crumbs (browse uniquement)
|
||||
#}
|
||||
<table class="file-table">
|
||||
<table class="file-table" id="file-table">
|
||||
<thead>
|
||||
<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>Nom</th>
|
||||
{% if mode == 'trash' %}
|
||||
|
|
@ -44,7 +45,8 @@
|
|||
{% endif %}
|
||||
|
||||
{% 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">
|
||||
{%- if e.is_dir -%}📁
|
||||
{%- elif e.is_image -%}🖼
|
||||
|
|
|
|||
|
|
@ -52,6 +52,11 @@
|
|||
</header>
|
||||
|
||||
<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 %}
|
||||
</main>
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
<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>
|
||||
</div>
|
||||
<div id="err-summary" class="err-summary" style="display:none"></div>
|
||||
<table class="file-table err-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -273,19 +274,43 @@
|
|||
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 => {
|
||||
dot.style.cursor = 'pointer';
|
||||
dot.addEventListener('click', async () => {
|
||||
dot.textContent = '○'; dot.className = 'err-status-dot';
|
||||
const r = await fetch(DETAIL_URL + '?path=' + encodeURIComponent(dot.dataset.path)).then(r => r.json());
|
||||
applyDotResult(dot, r.still_404);
|
||||
updateSummary();
|
||||
});
|
||||
});
|
||||
|
||||
/* Auto-vérification au chargement : tous les dots actifs (rouges) */
|
||||
(async () => {
|
||||
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'; });
|
||||
try {
|
||||
const paths = activeDots.map(d => d.dataset.path);
|
||||
|
|
@ -298,6 +323,7 @@
|
|||
} catch {
|
||||
activeDots.forEach(d => { d.textContent = '●'; d.className = 'err-status-dot err-status-dot--active'; });
|
||||
}
|
||||
updateSummary();
|
||||
})();
|
||||
|
||||
/* ═══════════════════════════════════════════════
|
||||
|
|
|
|||
|
|
@ -110,7 +110,9 @@
|
|||
<div class="resize-body">
|
||||
|
||||
<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">
|
||||
{% for size in [32, 64, 100, 128, 200, 300, 500, 600, 1024] %}
|
||||
<label class="chip">
|
||||
|
|
@ -122,7 +124,9 @@
|
|||
</div>
|
||||
|
||||
<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">
|
||||
{% for fmt in ['png', 'jpg', 'ico'] %}
|
||||
<label class="chip">
|
||||
|
|
@ -300,6 +304,15 @@
|
|||
[...szCbs, ...fmtCbs].forEach(c => c.addEventListener('change', 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) {
|
||||
szCbs.forEach(c => { if (c.checked) fd.append('sizes', 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 += '<ul class="resize-list">';
|
||||
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>';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,53 @@
|
|||
<a href="{{ url_for('browse', subpath=parent_path) if parent_path else url_for('browse') }}"
|
||||
class="back-link">← Retour</a>
|
||||
{% endif %}
|
||||
<h1>{{ filename }}</h1>
|
||||
{% if not from_trash %}{% include '_preview_nav.html' %}{% endif %}
|
||||
<h1 id="preview-title">{{ filename }}</h1>
|
||||
{% if not from_trash %}
|
||||
<button type="button" id="rename-toggle" class="btn-icon" title="Renommer ce fichier">✏️</button>
|
||||
{% include '_preview_nav.html' %}
|
||||
{% endif %}
|
||||
</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">
|
||||
<span>Taille : <strong>{{ filesize }}</strong></span>
|
||||
<span>Modifié : <strong>{{ mtime.strftime('%d/%m/%Y %H:%M') }}</strong></span>
|
||||
|
|
|
|||
|
|
@ -11,9 +11,20 @@
|
|||
<a href="{{ url_for('browse', subpath=parent_path) if parent_path else url_for('browse') }}"
|
||||
class="back-link">← Retour</a>
|
||||
{% endif %}
|
||||
<h1>{{ filename }}</h1>
|
||||
{% if not from_trash %}{% include '_preview_nav.html' %}{% endif %}
|
||||
<h1 id="preview-title">{{ filename }}</h1>
|
||||
{% if not from_trash %}
|
||||
<button type="button" id="rename-toggle" class="btn-icon" title="Renommer ce fichier">✏️</button>
|
||||
{% include '_preview_nav.html' %}
|
||||
{% endif %}
|
||||
</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">
|
||||
<span>Taille : <strong>{{ filesize }}</strong></span>
|
||||
<span>Modifié : <strong>{{ mtime.strftime('%d/%m/%Y %H:%M') }}</strong></span>
|
||||
|
|
@ -23,6 +34,41 @@
|
|||
</div>
|
||||
</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="text-bar">
|
||||
<span>{{ filename }}</span>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,17 @@
|
|||
<p class="trash-info">Les fichiers sont supprimés définitivement après 30 jours.</p>
|
||||
|
||||
{% 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' %}
|
||||
{% include '_file_table.html' %}
|
||||
{% else %}
|
||||
|
|
@ -42,6 +53,8 @@
|
|||
<script>
|
||||
(function () {
|
||||
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 conflictName = document.getElementById('restore-conflict-name');
|
||||
const confirmBtn = document.getElementById('restore-confirm');
|
||||
|
|
@ -86,6 +99,99 @@
|
|||
conflictPanel.style.display = 'none';
|
||||
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>
|
||||
|
||||
|
|
|
|||
61
error/400.html
Executable 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
After Width: | Height: | Size: 114 KiB |
BIN
logo/alpinux-logo.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
logo/favicon.ico
Normal file
|
After Width: | Height: | Size: 22 KiB |
1
robots.txt
Executable file
|
|
@ -0,0 +1 @@
|
|||
User-agent: *
|
||||
|
|
@ -40,7 +40,9 @@ echo ""
|
|||
echo -e "${CYAN}[1/4] Synchronisation des fichiers…${RESET}"
|
||||
RSYNC_OPTS=(-rlcz --delete --human-readable
|
||||
--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
|
||||
rsync --dry-run --itemize-changes "${RSYNC_OPTS[@]}" "$APP_DIR/" "$REMOTE_HOST:$REMOTE_DEST/"
|
||||
|
|
|
|||
60
standard_index.html
Executable 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
|
|
@ -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
|
|
@ -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;
|
||||
?>
|
||||
BIN
wiki/pasted/20230505-195953.png
Normal file
|
After Width: | Height: | Size: 445 B |
BIN
wiki/pasted/20230506-113625.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
wiki/pasted/20230506-113828.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
wiki/technique/2020-12-10_18_24_12.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
wiki/technique/ateliers-dynalab/pasted/20220128-092031.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
wiki/technique/ateliers-dynalab/pasted/20220204-113653.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
wiki/technique/ateliers-dynalab/pasted/20220204-114029.png
Normal file
|
After Width: | Height: | Size: 233 KiB |
BIN
wiki/technique/ateliers-dynalab/pasted/20220204-114228.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
wiki/technique/ateliers-dynalab/pasted/20220204-114346.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
wiki/technique/ateliers-dynalab/pasted/20220204-115237.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
wiki/technique/ateliers-dynalab/pasted/20220204-123551.png
Normal file
|
After Width: | Height: | Size: 538 KiB |
BIN
wiki/technique/ateliers-dynalab/pasted/20220204-123823.png
Normal file
|
After Width: | Height: | Size: 526 KiB |
BIN
wiki/technique/ateliers-dynalab/sbc-nodemcu-esp32-01.png
Normal file
|
After Width: | Height: | Size: 483 KiB |
BIN
wiki/technique/capture_d-ecran_du_2022-12-08_18-36-49.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
wiki/technique/esp/arduino_software_1.8.7.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
wiki/technique/esp/pasted/20220123-224734.png
Normal file
|
After Width: | Height: | Size: 273 KiB |
BIN
wiki/technique/esp/pasted/20220123-224819.png
Normal file
|
After Width: | Height: | Size: 8 KiB |
BIN
wiki/technique/esp/pasted/20220123-224907.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
wiki/technique/esp/pasted/20220123-224934.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
wiki/technique/esp/terminal_alternatives_config_java.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
wiki/technique/esp/terminal_install_arduino.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
wiki/technique/matrix/pasted/20231006-005234.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
wiki/technique/matrix/pasted/20231006-005313.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
wiki/technique/matrix/pasted/20231006-005403.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
wiki/technique/matrix/pasted/20231006-005404.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
wiki/technique/matrix/pasted/20231006-005439.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
wiki/technique/matrix/pasted/20231006-005458.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
wiki/technique/pasted/20201219-092558.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
wiki/technique/pasted/20201219-092818.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
wiki/technique/pasted/20230504-211805.png
Normal file
|
After Width: | Height: | Size: 7 KiB |
BIN
wiki/technique/pratique/10_-_partition_du_disque.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 9 KiB |
BIN
wiki/technique/pratique/2_-_herite_egale_bios.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
wiki/technique/pratique/2_-_herite_egale_bios_2.png
Normal file
|
After Width: | Height: | Size: 494 KiB |
BIN
wiki/technique/pratique/3_-_desactiver_le_demarrage_rapide.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
BIN
wiki/technique/pratique/6_-_welcome_sous_linux_mint.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
wiki/technique/pratique/7_-_installation_de_linux_mint.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
wiki/technique/pratique/8_-_choisir_clavier_francais.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
wiki/technique/pratique/9_-_choix_des_logiciels_non_libres.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
wiki/technique/pratique/boot_unlock.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
wiki/technique/pratique/img_20230511_163727.jpg
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
BIN
wiki/technique/pratique/pasted/20200930-133822.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
wiki/technique/pratique/pasted/20200930-204053.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
wiki/technique/pratique/pasted/20201001-205950.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
wiki/technique/pratique/pasted/20201110-193625.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
wiki/technique/pratique/pasted/20211008-004231.png
Normal file
|
After Width: | Height: | Size: 262 KiB |
BIN
wiki/technique/pratique/pasted/20211008-004245.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
wiki/technique/pratique/pasted/20220408-224502.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
wiki/technique/pratique/pasted/20221112-070510.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
wiki/technique/pratique/pasted/20221112-070544.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
wiki/technique/pratique/pasted/20221112-070632.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
wiki/technique/pratique/pasted/20221112-071102.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
wiki/technique/pratique/pasted/20221112-071144.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
wiki/technique/pratique/pasted/20221112-071231.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
wiki/technique/pratique/pasted/20221112-071250.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
wiki/technique/pratique/pasted/20221112-071411.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
wiki/technique/pratique/pasted/20221112-071503.png
Normal file
|
After Width: | Height: | Size: 232 KiB |
BIN
wiki/technique/pratique/pasted/20221112-071548.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
wiki/technique/pratique/pasted/20221112-071623.png
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
wiki/technique/pratique/pasted/20221112-071657.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
wiki/technique/pratique/pasted/20221201-193135.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
wiki/technique/pratique/pasted/20221201-193257.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
wiki/technique/pratique/pasted/20221201-200839.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
wiki/technique/pratique/pasted/20221213-230255.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
wiki/technique/pratique/pasted/20221213-230333.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
wiki/technique/pratique/pasted/20221213-230452.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
wiki/technique/pratique/pasted/20221213-230516.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
wiki/technique/pratique/pasted/20221213-232715.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
wiki/technique/pratique/pasted/20230922-173451.png
Normal file
|
After Width: | Height: | Size: 36 KiB |