fix: timing flash + couleur idle LED + diagnostic auto-brightness
🚀 Deploy — JH Photomaton / 🔍 Vérification (push) Has been cancelled
🚀 Deploy — JH Photomaton / 🍓 Deploy sur le Pi (push) Has been cancelled

This commit is contained in:
2026-07-20 19:06:16 +02:00
parent f192ffc92f
commit 19557b20fb
10 changed files with 247 additions and 37 deletions
+27 -5
View File
@@ -32,6 +32,8 @@ filename="${1:-}"
media_type="${2:-}" media_type="${2:-}"
action_config_name="${3:-}" action_config_name="${3:-}"
copies="${4:-1}" copies="${4:-1}"
# Liste CSV d'imprimantes à exclure du load-balancing (optionnel, passé par JH-Photomaton)
skip_printers="${5:-}"
# --- Noms des imprimantes CUPS (WiFi) --- # --- Noms des imprimantes CUPS (WiFi) ---
PRINTER_1="Selphy_Blanche_WiFi" PRINTER_1="Selphy_Blanche_WiFi"
@@ -90,24 +92,44 @@ fi
printer_1_status=$(lpstat -p "$PRINTER_1" 2>/dev/null || echo "not found") printer_1_status=$(lpstat -p "$PRINTER_1" 2>/dev/null || echo "not found")
printer_2_status=$(lpstat -p "$PRINTER_2" 2>/dev/null || echo "not found") printer_2_status=$(lpstat -p "$PRINTER_2" 2>/dev/null || echo "not found")
# Verifie si une imprimante est dans la liste skip_printers (CSV)
is_skipped() { [[ ",$skip_printers," == *",$1,"* ]]; }
# Une imprimante est utilisable si CUPS la connait ET qu'elle n'est pas exclue
p1_ok=false
p2_ok=false
[[ "$printer_1_status" != "not found" ]] && ! is_skipped "$PRINTER_1" && p1_ok=true
[[ "$printer_2_status" != "not found" ]] && ! is_skipped "$PRINTER_2" && p2_ok=true
SELECTED_PRINTER="" SELECTED_PRINTER=""
if [[ "$printer_1_status" == *"idle"* ]]; then if $p1_ok && [[ "$printer_1_status" == *"idle"* ]]; then
SELECTED_PRINTER="$PRINTER_1" SELECTED_PRINTER="$PRINTER_1"
log_info "Imprimante selectionnee: $PRINTER_1 (idle)" log_info "Imprimante selectionnee: $PRINTER_1 (idle)"
elif [[ "$printer_2_status" == *"idle"* ]]; then elif $p2_ok && [[ "$printer_2_status" == *"idle"* ]]; then
SELECTED_PRINTER="$PRINTER_2" SELECTED_PRINTER="$PRINTER_2"
log_info "Imprimante selectionnee: $PRINTER_2 (idle)" log_info "Imprimante selectionnee: $PRINTER_2 (idle)"
else elif $p1_ok && $p2_ok; then
# Les deux sont accessibles mais aucune n'est idle — load balance sur les jobs
jobs_1=$(lpstat -o "$PRINTER_1" 2>/dev/null | wc -l) jobs_1=$(lpstat -o "$PRINTER_1" 2>/dev/null | wc -l)
jobs_2=$(lpstat -o "$PRINTER_2" 2>/dev/null | wc -l) jobs_2=$(lpstat -o "$PRINTER_2" 2>/dev/null | wc -l)
if [ "$jobs_1" -le "$jobs_2" ] 2>/dev/null; then if [ "$jobs_1" -le "$jobs_2" ] 2>/dev/null; then
SELECTED_PRINTER="$PRINTER_1" SELECTED_PRINTER="$PRINTER_1"
else else
SELECTED_PRINTER="$PRINTER_2" SELECTED_PRINTER="$PRINTER_2"
fi fi
log_info "Aucune imprimante idle, file sur: $SELECTED_PRINTER (P1=$jobs_1 jobs, P2=$jobs_2 jobs)" log_info "Load balancing: $SELECTED_PRINTER (P1=$jobs_1 jobs, P2=$jobs_2 jobs)"
elif $p1_ok; then
SELECTED_PRINTER="$PRINTER_1"
log_info "Seule imprimante disponible: $PRINTER_1 (P2 inaccessible ou exclue)"
elif $p2_ok; then
SELECTED_PRINTER="$PRINTER_2"
log_info "Seule imprimante disponible: $PRINTER_2 (P1 inaccessible ou exclue)"
else
log_error "Aucune imprimante accessible dans CUPS"
echo "PRINT_ERROR::no_printer_available"
[ -f "$filename_print" ] && rm -f "$filename_print" 2>/dev/null
exit 1
fi fi
# --- Impression --- # --- Impression ---
+55
View File
@@ -1,5 +1,6 @@
"""API de contrôle des LEDs WS2812b.""" """API de contrôle des LEDs WS2812b."""
import asyncio
import logging import logging
from fastapi import APIRouter, Request, Query from fastapi import APIRouter, Request, Query
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
@@ -103,6 +104,60 @@ async def get_effects(request: Request):
} }
@router.get("/leds/ambient-lux")
async def get_ambient_lux(request: Request):
"""Retourne la luminosité ambiante mesurée (dernière valeur connue)."""
led = request.app.state.led_service
lux = led._ambient_lux
return {
"ambient_lux": lux,
"known": lux >= 0,
"auto_brightness": request.app.state.config.leds.auto_brightness,
"description": (
"sombre (flash plein)" if lux < 50 and lux >= 0 else
"luminosité moyenne (flash adapté)" if lux < 180 and lux >= 0 else
"clair (flash réduit)" if lux >= 180 else
"non mesuré"
),
}
@router.post("/leds/measure-ambient")
async def measure_ambient_lux(request: Request):
"""Déclenche une mesure de luminosité ambiante via snapshot liveview.
Utile pour diagnostiquer si le flux liveview est accessible depuis JH-Photomaton.
"""
pb = request.app.state.photobooth_service
led = request.app.state.led_service
snapshot = await pb.get_liveview_snapshot()
if not snapshot:
return JSONResponse({
"ok": False,
"error": "Aucun endpoint liveview accessible sur photobooth-app. "
"Vérifiez que photobooth-app est démarré et que le flux vidéo est actif.",
}, status_code=503)
try:
from PIL import Image
import io
img = Image.open(io.BytesIO(snapshot)).convert("L").resize((80, 60))
pixels = list(img.getdata())
avg = sum(pixels) / len(pixels)
led.set_ambient_lux(avg)
return {
"ok": True,
"ambient_lux": round(avg, 1),
"description": (
"sombre → flash plein (255)" if avg < 50 else
f"moyen → flash {int(255 - (avg - 50) / 130 * 135)}" if avg < 180 else
"clair → flash réduit (80)"
),
}
except Exception as e:
return JSONResponse({"ok": False, "error": str(e)}, status_code=500)
@router.put("/leds/effect/{effect_name}") @router.put("/leds/effect/{effect_name}")
async def update_effect( async def update_effect(
request: Request, request: Request,
+22 -8
View File
@@ -168,8 +168,6 @@ async def gallery_print_request(request: Request, body: dict = Body(...)):
"""Demande d'impression depuis la galerie publique. """Demande d'impression depuis la galerie publique.
Vérifie que l'impression est activée et que le quota session n'est pas dépassé. Vérifie que l'impression est activée et que le quota session n'est pas dépassé.
""" """
from pathlib import Path as _Path
cfg_print = request.app.state.config.print cfg_print = request.app.state.config.print
# ── Activation ──────────────────────────────────────────────────────────── # ── Activation ────────────────────────────────────────────────────────────
@@ -191,15 +189,21 @@ async def gallery_print_request(request: Request, body: dict = Body(...)):
"quota_exceeded": True, "quota_exceeded": True,
}, status_code=429) }, status_code=429)
# ── Résolution du fichier ───────────────────────────────────────────────── # ── Résolution du fichier via l'API photobooth-app ───────────────────────
media_dir = _Path(request.app.state.config.photobooth.media_dir) # On passe par get_media_item() comme le fait admin_print_photo — le champ
candidates = sorted(media_dir.glob(f"{photo_id}*")) # "processed" contient le chemin réel sur le disque (ex: media/processed_full/xxx.jpg).
if not candidates: # Un simple glob sur photo_id ne marche pas car l'id interne ≠ nom de fichier.
pb = request.app.state.photobooth_service
item = await pb.get_media_item(photo_id)
if not item:
return JSONResponse({"error": "Photo introuvable"}, status_code=404) return JSONResponse({"error": "Photo introuvable"}, status_code=404)
filepath = str(candidates[0]) file_path = pb.media_file_path(item)
if not file_path or not file_path.exists():
logger.error("Fichier manquant pour %s: %s", photo_id, file_path)
return JSONResponse({"error": "Fichier photo introuvable sur le disque"}, status_code=404)
filepath = str(file_path)
# ── Enqueue ─────────────────────────────────────────────────────────────── # ── Enqueue ───────────────────────────────────────────────────────────────
pb = request.app.state.photobooth_service
printer_svc = request.app.state.printer_service printer_svc = request.app.state.printer_service
ws = request.app.state.ws_manager ws = request.app.state.ws_manager
led = request.app.state.led_service led = request.app.state.led_service
@@ -234,6 +238,16 @@ async def gallery_print_request(request: Request, body: dict = Body(...)):
# ── Gestion avancée des imprimantes CUPS ────────────────────────────────────── # ── Gestion avancée des imprimantes CUPS ──────────────────────────────────────
@router.post("/print/printers/{printer_name}/set-excluded")
async def set_printer_excluded(request: Request, printer_name: str, excluded: bool = Query(...)):
"""Exclut (ou réintègre) une imprimante du load-balancing JH-Photomaton.
N'agit pas sur CUPS — sert uniquement à ignorer une imprimante temporairement hors ligne."""
config_svc = request.app.state.config_service
config_svc.save_printer_excluded(printer_name, excluded)
logger.info("Imprimante %s : excluded=%s", printer_name, excluded)
return {"ok": True, "printer": printer_name, "excluded": excluded}
@router.post("/print/printers/{printer_name}/enable") @router.post("/print/printers/{printer_name}/enable")
async def printer_enable(request: Request, printer_name: str): async def printer_enable(request: Request, printer_name: str):
"""Active une imprimante CUPS (cupsenable + cupsaccept).""" """Active une imprimante CUPS (cupsenable + cupsaccept)."""
+18
View File
@@ -228,6 +228,24 @@ class ConfigService:
self._config.print.user_print_quota = quota self._config.print.user_print_quota = quota
self._config.print.user_print_max_copies = max_copies self._config.print.user_print_max_copies = max_copies
def save_printer_excluded(self, printer_name: str, excluded: bool):
"""Marque (ou démarque) une imprimante comme exclue du load-balancing."""
with open(self.path, encoding="utf-8") as f:
raw = yaml.safe_load(f) or {}
printers = raw.get("print", {}).get("printers", [])
for p in printers:
if isinstance(p, dict) and p.get("name") == printer_name:
p["excluded"] = excluded
break
raw.setdefault("print", {})["printers"] = printers
with open(self.path, "w", encoding="utf-8") as f:
yaml.dump(raw, f, allow_unicode=True, default_flow_style=False)
if self._config:
for p in self._config.print.printers:
if isinstance(p, dict) and p.get("name") == printer_name:
p["excluded"] = excluded
break
def save_print_mode(self, mode: str): def save_print_mode(self, mode: str):
with open(self.path, encoding="utf-8") as f: with open(self.path, encoding="utf-8") as f:
raw = yaml.safe_load(f) or {} raw = yaml.safe_load(f) or {}
+23 -7
View File
@@ -191,26 +191,40 @@ class PhotoboothService:
"""Tente de recuperer une frame JPEG du liveview de photobooth-app. """Tente de recuperer une frame JPEG du liveview de photobooth-app.
Essaie dans l'ordre : Essaie dans l'ordre :
1. /api/stream/snapshot endpoint snapshot direct (si disponible) 1. Endpoints snapshot direct (retour JPEG immédiat)
2. /stream.mjpg flux MJPEG classique, extrait la premiere frame 2. Flux MJPEG extrait la premiere frame
3. /api/stream flux MJPEG alternatif
Retourne des bytes JPEG ou None si rien n'est disponible. Retourne des bytes JPEG ou None si rien n'est disponible.
""" """
# 1. Snapshot direct # 1. Endpoints snapshot direct (differentes versions de photobooth-app)
for path in ("/api/stream/snapshot",): snapshot_paths = (
"/api/stream/snapshot",
"/api/video/preview/snapshot",
"/api/livestream/snapshot",
"/api/cam/stream/snapshot",
)
for path in snapshot_paths:
try: try:
async with httpx.AsyncClient(timeout=2.0) as client: async with httpx.AsyncClient(timeout=2.0) as client:
r = await client.get(f"{self._base}{path}") r = await client.get(f"{self._base}{path}")
if r.status_code == 200: if r.status_code == 200:
ct = r.headers.get("content-type", "") ct = r.headers.get("content-type", "")
if "jpeg" in ct or "image" in ct: if "jpeg" in ct or "image" in ct:
logger.debug("Snapshot OK via %s", path)
return r.content return r.content
except Exception as e: except Exception as e:
logger.debug("Snapshot %s: %s", path, e) logger.debug("Snapshot %s: %s", path, e)
# 2. Premiere frame d'un flux MJPEG # 2. Premiere frame d'un flux MJPEG (differentes versions)
for path in ("/stream.mjpg", "/api/stream"): mjpeg_paths = (
"/stream.mjpg",
"/api/stream",
"/api/video/stream",
"/api/video/preview",
"/api/livestream",
"/livestream",
)
for path in mjpeg_paths:
try: try:
async with httpx.AsyncClient(timeout=3.0) as client: async with httpx.AsyncClient(timeout=3.0) as client:
async with client.stream("GET", f"{self._base}{path}") as resp: async with client.stream("GET", f"{self._base}{path}") as resp:
@@ -223,12 +237,14 @@ class PhotoboothService:
if start >= 0: if start >= 0:
end = buf.find(b"\xff\xd9", start) end = buf.find(b"\xff\xd9", start)
if end >= 0: if end >= 0:
logger.debug("MJPEG frame OK via %s", path)
return buf[start:end + 2] return buf[start:end + 2]
if len(buf) > 500_000: if len(buf) > 500_000:
break break
except Exception as e: except Exception as e:
logger.debug("MJPEG %s: %s", path, e) logger.debug("MJPEG %s: %s", path, e)
logger.debug("Aucun endpoint liveview disponible sur %s", self._base)
return None return None
async def close(self): async def close(self):
+13 -5
View File
@@ -244,7 +244,13 @@ class PrinterService:
if not Path(filename).exists(): if not Path(filename).exists():
return {"success": False, "error": f"Fichier introuvable: {filename}"} return {"success": False, "error": f"Fichier introuvable: {filename}"}
cmd = ["/bin/bash", script, filename, "image", "default", str(copies)] # Imprimantes exclues du load-balancing (flag excluded dans config)
excluded_list = ",".join(
self._printer_name(p)
for p in self._cfg.printers
if isinstance(p, dict) and p.get("excluded", False)
)
cmd = ["/bin/bash", script, filename, "image", "default", str(copies), excluded_list]
try: try:
proc = subprocess.run( proc = subprocess.run(
cmd, capture_output=True, text=True, timeout=120 cmd, capture_output=True, text=True, timeout=120
@@ -361,11 +367,13 @@ class PrinterService:
statuses = [] statuses = []
for p in self._cfg.printers: for p in self._cfg.printers:
name = self._printer_name(p) name = self._printer_name(p)
excluded = isinstance(p, dict) and bool(p.get("excluded", False))
status = await asyncio.to_thread(self._get_printer_status, name) status = await asyncio.to_thread(self._get_printer_status, name)
stats = await self._get_printer_db_stats(name) stats = await self._get_printer_db_stats(name)
statuses.append({ statuses.append({
"name": name, "name": name,
"label": self._printer_label(p), "label": self._printer_label(p),
"excluded": excluded,
**status, **status,
**stats, **stats,
}) })
@@ -619,8 +627,8 @@ class PrinterService:
async def enable_printer(self, printer_name: str) -> bool: async def enable_printer(self, printer_name: str) -> bool:
"""Active l'imprimante CUPS (cupsenable) et accepte les nouveaux jobs.""" """Active l'imprimante CUPS (cupsenable) et accepte les nouveaux jobs."""
try: try:
r1 = subprocess.run(["cupsenable", printer_name], capture_output=True, timeout=10) r1 = subprocess.run(["sudo", "cupsenable", printer_name], capture_output=True, timeout=10)
r2 = subprocess.run(["cupsaccept", printer_name], capture_output=True, timeout=10) r2 = subprocess.run(["sudo", "cupsaccept", printer_name], capture_output=True, timeout=10)
return r1.returncode == 0 and r2.returncode == 0 return r1.returncode == 0 and r2.returncode == 0
except Exception as e: except Exception as e:
logger.error("Erreur enable printer %s: %s", printer_name, e) logger.error("Erreur enable printer %s: %s", printer_name, e)
@@ -629,7 +637,7 @@ class PrinterService:
async def disable_printer(self, printer_name: str) -> bool: async def disable_printer(self, printer_name: str) -> bool:
"""Désactive l'imprimante CUPS (cupsdisable).""" """Désactive l'imprimante CUPS (cupsdisable)."""
try: try:
result = subprocess.run(["cupsdisable", printer_name], capture_output=True, timeout=10) result = subprocess.run(["sudo", "cupsdisable", printer_name], capture_output=True, timeout=10)
return result.returncode == 0 return result.returncode == 0
except Exception as e: except Exception as e:
logger.error("Erreur disable printer %s: %s", printer_name, e) logger.error("Erreur disable printer %s: %s", printer_name, e)
@@ -638,7 +646,7 @@ class PrinterService:
async def reject_jobs(self, printer_name: str) -> bool: async def reject_jobs(self, printer_name: str) -> bool:
"""Refuse les nouveaux jobs (cupsreject) sans stopper l'impression en cours.""" """Refuse les nouveaux jobs (cupsreject) sans stopper l'impression en cours."""
try: try:
result = subprocess.run(["cupsreject", printer_name], capture_output=True, timeout=10) result = subprocess.run(["sudo", "cupsreject", printer_name], capture_output=True, timeout=10)
return result.returncode == 0 return result.returncode == 0
except Exception as e: except Exception as e:
logger.error("Erreur reject printer %s: %s", printer_name, e) logger.error("Erreur reject printer %s: %s", printer_name, e)
+2 -2
View File
@@ -83,9 +83,9 @@ leds:
mode: solid mode: solid
idle: idle:
color: color:
- 0 - 50
- 30 - 30
- 80 - 10
mode: solid mode: solid
speed: 0.025 speed: 0.025
printing: printing:
+45 -9
View File
@@ -52,10 +52,13 @@
.printer-actions { display: flex; gap: .5rem; flex-wrap: wrap; } .printer-actions { display: flex; gap: .5rem; flex-wrap: wrap; }
.btn-xs { padding: .3rem .65rem; font-size: .8rem; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; transition: opacity .2s; } .btn-xs { padding: .3rem .65rem; font-size: .8rem; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; transition: opacity .2s; }
.btn-xs:hover { opacity: .8; } .btn-xs:hover { opacity: .8; }
.btn-enable { background: #1a7340; color: #fff; } .btn-enable { background: #1a7340; color: #fff; }
.btn-disable { background: #784700; color: #fff; } .btn-disable { background: #784700; color: #fff; }
.btn-clear { background: #7a0000; color: #fff; } .btn-clear { background: #7a0000; color: #fff; }
.btn-reject { background: #555; color: #fff; } .btn-reject { background: #555; color: #fff; }
.btn-exclude { background: #4a2080; color: #fff; }
.btn-include { background: #205080; color: #fff; }
.bs-excluded { background: rgba(100,40,160,.2); color: #b085f0; border: 1px solid rgba(100,40,160,.3); }
/* Jobs CUPS détaillés */ /* Jobs CUPS détaillés */
.cups-jobs { font-size: .82rem; } .cups-jobs { font-size: .82rem; }
@@ -131,10 +134,13 @@
<span class="badge-printer badge-state bs-{{ p.state }}"> <span class="badge-printer badge-state bs-{{ p.state }}">
{% if p.state == 'idle' %}✅ Disponible {% if p.state == 'idle' %}✅ Disponible
{% elif p.state == 'printing' %}🖨 En impression {% elif p.state == 'printing' %}🖨 En impression
{% elif p.state == 'disabled' %}⏸ Désactivée {% elif p.state == 'disabled' %}⏸ Désactivée CUPS
{% elif p.state == 'offline' %}❌ Hors ligne {% elif p.state == 'offline' %}❌ Hors ligne
{% else %}❓ {{ p.state }}{% endif %} {% else %}❓ {{ p.state }}{% endif %}
</span> </span>
{% if p.excluded %}
<span class="badge-printer bs-excluded">🚫 Exclue du load-balancing</span>
{% endif %}
<span class="badge-printer badge-accept {{ 'bs-accept' if p.accepting else 'bs-reject' }}"> <span class="badge-printer badge-accept {{ 'bs-accept' if p.accepting else 'bs-reject' }}">
{{ '✓ Accepte les jobs' if p.accepting else '✗ Refuse les jobs' }} {{ '✓ Accepte les jobs' if p.accepting else '✗ Refuse les jobs' }}
</span> </span>
@@ -193,15 +199,20 @@
<!-- Actions imprimante --> <!-- Actions imprimante -->
<div class="printer-actions"> <div class="printer-actions">
{% if p.state == 'disabled' %} {% if p.state == 'disabled' %}
<button class="btn-xs btn-enable" onclick="printerAction('{{ p.name }}','enable')">▶ Activer</button> <button class="btn-xs btn-enable" onclick="printerAction('{{ p.name }}','enable')">▶ Activer CUPS</button>
{% else %} {% else %}
<button class="btn-xs btn-disable" onclick="printerAction('{{ p.name }}','disable')">⏸ Désactiver</button> <button class="btn-xs btn-disable" onclick="printerAction('{{ p.name }}','disable')">⏸ Désactiver CUPS</button>
{% endif %} {% endif %}
{% if p.accepting %} {% if p.accepting %}
<button class="btn-xs btn-reject" onclick="printerAction('{{ p.name }}','reject')">🚫 Refuser jobs</button> <button class="btn-xs btn-reject" onclick="printerAction('{{ p.name }}','reject')">🚫 Refuser jobs</button>
{% else %} {% else %}
<button class="btn-xs btn-enable" onclick="printerAction('{{ p.name }}','enable')">✓ Accepter jobs</button> <button class="btn-xs btn-enable" onclick="printerAction('{{ p.name }}','enable')">✓ Accepter jobs</button>
{% endif %} {% endif %}
{% if p.excluded %}
<button class="btn-xs btn-include" onclick="setExcluded('{{ p.name }}', false)">✓ Réintégrer</button>
{% else %}
<button class="btn-xs btn-exclude" onclick="setExcluded('{{ p.name }}', true)">🚫 Exclure</button>
{% endif %}
{% if p.jobs_count %} {% if p.jobs_count %}
<button class="btn-xs btn-clear" onclick="clearJobs('{{ p.name }}')">✕ Vider file</button> <button class="btn-xs btn-clear" onclick="clearJobs('{{ p.name }}')">✕ Vider file</button>
{% endif %} {% endif %}
@@ -426,16 +437,33 @@ async function refreshPrinters() {
acceptBadge.textContent = p.accepting ? '✓ Accepte les jobs' : '✗ Refuse les jobs'; acceptBadge.textContent = p.accepting ? '✓ Accepte les jobs' : '✗ Refuse les jobs';
} }
// Badge excluded
const statesDiv = card.querySelector('.printer-states');
if (statesDiv) {
const excBadge = statesDiv.querySelector('.bs-excluded');
if (p.excluded && !excBadge) {
const b = document.createElement('span');
b.className = 'badge-printer bs-excluded';
b.textContent = '🚫 Exclue du load-balancing';
statesDiv.appendChild(b);
} else if (!p.excluded && excBadge) {
excBadge.remove();
}
}
// Boutons actions // Boutons actions
const actionsDiv = card.querySelector('.printer-actions'); const actionsDiv = card.querySelector('.printer-actions');
if (actionsDiv) { if (actionsDiv) {
actionsDiv.innerHTML = ` actionsDiv.innerHTML = `
${p.state === 'disabled' ${p.state === 'disabled'
? `<button class="btn-xs btn-enable" onclick="printerAction('${p.name}','enable')">▶ Activer</button>` ? `<button class="btn-xs btn-enable" onclick="printerAction('${p.name}','enable')">▶ Activer CUPS</button>`
: `<button class="btn-xs btn-disable" onclick="printerAction('${p.name}','disable')">⏸ Désactiver</button>`} : `<button class="btn-xs btn-disable" onclick="printerAction('${p.name}','disable')">⏸ Désactiver CUPS</button>`}
${p.accepting ${p.accepting
? `<button class="btn-xs btn-reject" onclick="printerAction('${p.name}','reject')">🚫 Refuser jobs</button>` ? `<button class="btn-xs btn-reject" onclick="printerAction('${p.name}','reject')">🚫 Refuser jobs</button>`
: `<button class="btn-xs btn-enable" onclick="printerAction('${p.name}','enable')">✓ Accepter jobs</button>`} : `<button class="btn-xs btn-enable" onclick="printerAction('${p.name}','enable')">✓ Accepter jobs</button>`}
${p.excluded
? `<button class="btn-xs btn-include" onclick="setExcluded('${p.name}',false)">✓ Réintégrer</button>`
: `<button class="btn-xs btn-exclude" onclick="setExcluded('${p.name}',true)">🚫 Exclure</button>`}
${p.jobs_count ? `<button class="btn-xs btn-clear" onclick="clearJobs('${p.name}')">✕ Vider file</button>` : ''} ${p.jobs_count ? `<button class="btn-xs btn-clear" onclick="clearJobs('${p.name}')">✕ Vider file</button>` : ''}
`; `;
} }
@@ -472,6 +500,14 @@ async function printerAction(name, action) {
} catch(err) { showToast('Erreur : ' + err.message, 'error'); } } catch(err) { showToast('Erreur : ' + err.message, 'error'); }
} }
async function setExcluded(name, excluded) {
try {
await api('POST', `/api/print/printers/${name}/set-excluded?excluded=${excluded}`);
showToast(excluded ? `🚫 ${name} exclue du load-balancing` : `✓ ${name} réintégrée`, excluded ? 'info' : 'success');
setTimeout(refreshPrinters, 400);
} catch(err) { showToast('Erreur : ' + err.message, 'error'); }
}
async function clearJobs(printer) { async function clearJobs(printer) {
if (!confirm(`Vider toute la file CUPS de ${printer} ?`)) return; if (!confirm(`Vider toute la file CUPS de ${printer} ?`)) return;
try { try {
+41
View File
@@ -394,6 +394,16 @@ select.field {
<p id="auto-brightness-status" style="font-size:.78rem; color:var(--text-muted); margin-top:.5rem;"> <p id="auto-brightness-status" style="font-size:.78rem; color:var(--text-muted); margin-top:.5rem;">
&#x23F3; Chargement… &#x23F3; Chargement…
</p> </p>
<div style="display:flex; gap:.5rem; align-items:center; margin-top:.5rem; flex-wrap:wrap;">
<button class="btn btn-ghost btn-sm" onclick="measureAmbient()" id="btn-measure-lux">
&#x1F4F7; Tester la mesure
</button>
<span id="lux-status" style="font-size:.8rem; color:var(--text-muted);"></span>
</div>
<div id="lux-info" style="font-size:.78rem; margin-top:.4rem; color:var(--text-muted); display:none;">
Luminosité mesurée : <strong id="lux-val"></strong> / 255 &nbsp;&nbsp;
<span id="lux-desc"></span>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -875,6 +885,37 @@ async function loadFlashConfig() {
} catch(e) { console.warn('loadFlashConfig:', e); } } catch(e) { console.warn('loadFlashConfig:', e); }
} }
async function measureAmbient() {
const btn = document.getElementById('btn-measure-lux');
const status = document.getElementById('lux-status');
const info = document.getElementById('lux-info');
btn.disabled = true;
status.textContent = '⏳ Mesure en cours…';
status.style.color = 'var(--text-muted)';
try {
const r = await api('POST', '/api/leds/measure-ambient');
if (r.ok) {
document.getElementById('lux-val').textContent = r.ambient_lux;
document.getElementById('lux-desc').textContent = r.description;
info.style.display = '';
status.textContent = '✅ Mesure réussie';
status.style.color = '#4caf50';
showToast(`✅ Luminosité : ${r.ambient_lux} — ${r.description}`, 'success');
} else {
status.textContent = '❌ ' + (r.error || 'Erreur inconnue');
status.style.color = '#e05050';
showToast('❌ Mesure impossible : ' + r.error, 'error');
}
} catch(e) {
status.textContent = '❌ Liveview inaccessible';
status.style.color = '#e05050';
showToast('❌ Liveview inaccessible (endpoint 404). Vérifiez que photobooth-app est démarré.', 'error');
} finally {
btn.disabled = false;
setTimeout(() => { status.textContent = ''; }, 6000);
}
}
async function setAutoBrightness(enabled) { async function setAutoBrightness(enabled) {
try { try {
await api('PUT', `/api/leds/auto-brightness?enabled=${enabled}`); await api('PUT', `/api/leds/auto-brightness?enabled=${enabled}`);
+1 -1
View File
@@ -503,7 +503,7 @@
"backends": { "backends": {
"enable_livestream": true, "enable_livestream": true,
"retry_capture": 3, "retry_capture": 3,
"countdown_camera_capture_offset": 0.2, "countdown_camera_capture_offset": 0.5,
"index_backend_stills": 0, "index_backend_stills": 0,
"index_backend_video": 0, "index_backend_video": 0,
"index_backend_multicam": 0, "index_backend_multicam": 0,