update grabber (sql+req)
This commit is contained in:
parent
532358e2fc
commit
fc3f179250
84
app.py
84
app.py
@ -3,50 +3,76 @@ from fastapi.responses import HTMLResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
import json
|
||||
from grabber import Grabber
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
app = FastAPI()
|
||||
from grabber import Grabber, flotte, create_db_and_tables
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
create_db_and_tables()
|
||||
yield
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
ordi1 = Grabber()
|
||||
|
||||
@app.post("/endpoint")
|
||||
async def receive_info(request: Request):
|
||||
# Lire le body brut
|
||||
body = await request.body()
|
||||
print(body)
|
||||
|
||||
# Parser le JSON
|
||||
try:
|
||||
data = json.loads(body)
|
||||
except json.JSONDecodeError:
|
||||
raise HTTPException(status_code=400, detail="Invalid JSON")
|
||||
|
||||
hw = data["HARDWARE"]
|
||||
sw = data["SOFTWARE"]
|
||||
sw = data.get("SOFTWARE", {})
|
||||
# On récupère l'adresse MAC, c'est notre nouvel identifiant unique
|
||||
mac = sw.get("mac_address")
|
||||
hostname = sw.get("hostname", "Inconnu")
|
||||
|
||||
ordi1.motherboard = hw["motherboard"]
|
||||
ordi1.cpu_model = hw["cpu_model"]
|
||||
ordi1.cpu_id = hw["cpu_id"]
|
||||
ordi1.cpu_cores = hw["cpu_cores"]
|
||||
ordi1.cpu_threads = hw["cpu_threads"]
|
||||
ordi1.cpu_frequency_min = hw["cpu_frequency_min"]
|
||||
ordi1.cpu_frequency_cur = hw["cpu_frequency_cur"]
|
||||
ordi1.cpu_frequency_max = hw["cpu_frequency_max"]
|
||||
if not mac:
|
||||
raise HTTPException(status_code=400, detail="Adresse MAC manquante dans le JSON")
|
||||
|
||||
ordi1.hostname = sw["hostname"]
|
||||
ordi1.os = sw["os"]
|
||||
ordi1.arch = sw["arch"]
|
||||
ordi1.desktop_env = sw["desktop_env"]
|
||||
ordi1.window_manager = sw["window_manager"]
|
||||
ordi1.kernel = sw["kernel"]
|
||||
# Si la machine n'est pas encore connue par son adresse MAC
|
||||
if mac not in flotte:
|
||||
print(f"Nouvelle machine détectée : {hostname} ({mac})")
|
||||
flotte[mac] = Grabber(mac, hostname)
|
||||
|
||||
ordi_actuel = flotte[mac]
|
||||
ordi_actuel.update(data)
|
||||
ordi_actuel.save()
|
||||
|
||||
print(f"Hostname is {ordi1.hostname}")
|
||||
print(f"Motherboard serial is {ordi1.motherboard}")
|
||||
return {"status": "ok", "mac": mac}
|
||||
|
||||
return {"status": "ok"}
|
||||
@app.get("/")
|
||||
async def list_ordis(request: Request):
|
||||
"""Affiche la liste des ordis, identifiés par MAC mais affichés par Hostname."""
|
||||
# On crée des liens qui pointent vers /ordi/ADRESSE_MAC
|
||||
# Mais pour l'humain, on affiche "Hostname (Mac)"
|
||||
list_items = []
|
||||
for mac, grabber_obj in flotte.items():
|
||||
nom_affiche = f"{grabber_obj.hostname} <small>({mac})</small>"
|
||||
list_items.append(f'<li><a href="/ordi/{mac}">{nom_affiche}</a></li>')
|
||||
|
||||
liens_html = "".join(list_items)
|
||||
|
||||
return HTMLResponse(f"""
|
||||
<html>
|
||||
<head>
|
||||
<title>Dashboard Grabber</title>
|
||||
<style>body{{font-family:sans-serif; padding:20px;}} li{{margin:5px 0;}}</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Tableau de bord Grabber</h1>
|
||||
<h2>Machines connectées</h2>
|
||||
<ul>{liens_html or "En attente de données..."}</ul>
|
||||
</body>
|
||||
</html>
|
||||
""")
|
||||
|
||||
@app.get("/ordi1")
|
||||
async def show_info(request: Request):
|
||||
return templates.TemplateResponse("item.html", {"request": request, "ordi": ordi1})
|
||||
# L'URL attend maintenant une adresse MAC (ex: /ordi/00:11:22:33:44:55)
|
||||
@app.get("/ordi/{mac_address}")
|
||||
async def show_info(request: Request, mac_address: str):
|
||||
if mac_address in flotte:
|
||||
return templates.TemplateResponse("item.html", {"request": request, "ordi": flotte[mac_address]})
|
||||
else:
|
||||
return HTMLResponse("<h1>Machine introuvable</h1>", status_code=404)
|
||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
125
grabber.py
125
grabber.py
@ -1,36 +1,95 @@
|
||||
#!/usr/bin/python3
|
||||
import configparser
|
||||
import requests
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from sqlmodel import Field, Session, SQLModel, create_engine
|
||||
|
||||
class Grabber():
|
||||
motherboard = " "
|
||||
cpu_model = " "
|
||||
cpu_id = " "
|
||||
cpu_cores = " "
|
||||
cpu_threads = " "
|
||||
cpu_frequency_min = " "
|
||||
cpu_frequency_cur = " "
|
||||
cpu_frequency_max = " "
|
||||
ram_size = " "
|
||||
ram_slots = " "
|
||||
ram_number = " "
|
||||
# --- CONFIGURATION BASE DE DONNÉES ---
|
||||
DB_FILE = "grabberman.db"
|
||||
sqlite_url = f"sqlite:///{DB_FILE}"
|
||||
engine = create_engine(sqlite_url, echo=False)
|
||||
|
||||
hostname = " "
|
||||
os = " "
|
||||
arch = " "
|
||||
desktop_env = " "
|
||||
window_manager = " "
|
||||
kernel = " "
|
||||
# --- MODÈLE DE DONNÉES (TABLE SQL) ---
|
||||
class SystemLog(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
date_scan: datetime = Field(default_factory=datetime.now)
|
||||
|
||||
# NOUVEAU : mac_address devient un champ indexé important
|
||||
mac_address: str = Field(index=True)
|
||||
hostname: str
|
||||
|
||||
def fetch_summary(self):
|
||||
return
|
||||
def shutdown():
|
||||
return
|
||||
def status(self):
|
||||
return
|
||||
def link_to_user(self,user):
|
||||
return
|
||||
def remove_user_access(self):
|
||||
return
|
||||
def show_users(self):
|
||||
return
|
||||
# Champs Hardware
|
||||
motherboard: Optional[str] = None
|
||||
cpu_model: Optional[str] = None
|
||||
cpu_id: Optional[str] = None
|
||||
cpu_cores: Optional[str] = None
|
||||
cpu_threads: Optional[str] = None
|
||||
cpu_frequency_min: Optional[str] = None
|
||||
cpu_frequency_cur: Optional[str] = None
|
||||
cpu_frequency_max: Optional[str] = None
|
||||
gpu_model: Optional[str] = None
|
||||
ram_slots: Optional[str] = None
|
||||
|
||||
# Champs Software
|
||||
os: Optional[str] = None
|
||||
arch: Optional[str] = None
|
||||
desktop_env: Optional[str] = None
|
||||
window_manager: Optional[str] = None
|
||||
kernel: Optional[str] = None
|
||||
|
||||
def create_db_and_tables():
|
||||
SQLModel.metadata.create_all(engine)
|
||||
|
||||
# --- GESTION DE LA FLOTTE EN MÉMOIRE ---
|
||||
# Le dictionnaire stockera maintenant : {"AA:BB:CC:DD:EE:FF": GrabberObject}
|
||||
flotte = {}
|
||||
|
||||
class Grabber:
|
||||
def __init__(self, mac_address, hostname="Inconnu"):
|
||||
self.mac_address = mac_address
|
||||
self.hostname = hostname
|
||||
self.data_cache = {}
|
||||
|
||||
def update(self, json_data):
|
||||
"""Met à jour les données et prépare l'objet SQLModel."""
|
||||
hw = json_data.get("HARDWARE", {})
|
||||
sw = json_data.get("SOFTWARE", {})
|
||||
|
||||
# Mise à jour du hostname s'il a changé, mais on garde la MAC comme ancre
|
||||
if "hostname" in sw:
|
||||
self.hostname = sw["hostname"]
|
||||
|
||||
# On prépare les données pour la DB
|
||||
self.data_cache = {
|
||||
"mac_address": self.mac_address, # On n'oublie pas la MAC
|
||||
"hostname": self.hostname,
|
||||
"motherboard": hw.get("motherboard", "N/A"),
|
||||
"cpu_model": hw.get("cpu_model", "N/A"),
|
||||
"cpu_id": hw.get("cpu_id", "N/A"),
|
||||
"cpu_cores": hw.get("cpu_cores", "N/A"),
|
||||
"cpu_threads": hw.get("cpu_threads", "N/A"),
|
||||
"cpu_frequency_min": hw.get("cpu_frequency_min", "N/A"),
|
||||
"cpu_frequency_cur": hw.get("cpu_frequency_cur", "N/A"),
|
||||
"cpu_frequency_max": hw.get("cpu_frequency_max", "N/A"),
|
||||
"gpu_model": hw.get("gpu_model", "N/A"),
|
||||
"ram_slots": hw.get("ram_slots", "N/A"),
|
||||
"os": sw.get("os", "N/A"),
|
||||
"arch": sw.get("arch", "N/A"),
|
||||
"desktop_env": sw.get("desktop_env", "N/A"),
|
||||
"window_manager": sw.get("window_manager", "N/A"),
|
||||
"kernel": sw.get("kernel", "N/A")
|
||||
}
|
||||
|
||||
def save(self):
|
||||
"""Enregistre les données via SQLModel."""
|
||||
try:
|
||||
log_entry = SystemLog(**self.data_cache)
|
||||
with Session(engine) as session:
|
||||
session.add(log_entry)
|
||||
session.commit()
|
||||
session.refresh(log_entry)
|
||||
print(f"Sauvegarde réussie pour {self.hostname} ({self.mac_address})")
|
||||
except Exception as e:
|
||||
print(f"Erreur SQLModel : {e}")
|
||||
|
||||
# Permet d'accéder aux propriétés comme ordi.cpu_model dans le template
|
||||
def __getattr__(self, name):
|
||||
return self.data_cache.get(name, "N/A")
|
||||
61
grabber.sh
61
grabber.sh
@ -40,7 +40,7 @@ echo ""
|
||||
|
||||
#----- Verify dependecies available -----
|
||||
REQUIRED_CMDS_SIMPLE=(inxi dmidecode lscpu lsblk nproc numfmt)
|
||||
REQUIRED_CMDS_FULL=(inxi dmidecode lscpu lsblk nproc numfmt python3 jq)
|
||||
REQUIRED_CMDS_FULL=(inxi dmidecode lscpu lsblk nproc numfmt python3 jq sqlite3)
|
||||
|
||||
requirements_simple() {
|
||||
echo -n "Checking dependencies... "
|
||||
@ -184,48 +184,6 @@ done
|
||||
TOTAL_STORAGE=$(numfmt --to iec $TOTAL_STORAGE)
|
||||
#---------------------------------
|
||||
|
||||
# Compile Hardware informations
|
||||
hardware() {
|
||||
echo "[HARDWARE]" >> $SUM_FILE
|
||||
echo "MB_SERIAL = $MB_SERIAL" >> $SUM_FILE
|
||||
echo "" >> $SUM_FILE
|
||||
|
||||
echo "--- CPU DATA ---" >> $SUM_FILE
|
||||
echo "CPU_MODEL = $CPU_MODEL" >> $SUM_FILE
|
||||
echo "CPU_ID = $CPU_ID" >> $SUM_FILE
|
||||
echo "CPU_CORES=$CPU_CORES" >> $SUM_FILE
|
||||
echo "CPU_THREADS=$CPU_THREADS" >> $SUM_FILE
|
||||
echo "CPU_FREQUENCY_MIN=$CPU_FREQUENCY_MIN" >> $SUM_FILE
|
||||
echo "CPU_FREQUENCY_CUR=$CPU_FREQUENCY_CUR" >> $SUM_FILE
|
||||
echo "CPU_FREQUENCY_MAX=$CPU_FREQUENCY_MAX" >> $SUM_FILE
|
||||
echo "" >> $SUM_FILE
|
||||
|
||||
echo "--- GPU DATA ---" >> $SUM_FILE
|
||||
echo "GPU_MODEL=$GPU_MODEL" >> $SUM_FILE
|
||||
echo "" >> $SUM_FILE
|
||||
|
||||
echo "--- RAM DATA ---" >> $SUM_FILE
|
||||
echo "RAM_SIZE = $RAM_SIZE" >> $SUM_FILE
|
||||
echo "RAM_SLOTS=$RAM_SLOTS" >> $SUM_FILE
|
||||
echo "RAM_NUMBER=$RAM_NUMBER" >> $SUM_FILE
|
||||
|
||||
for i in $(seq 1 $RAM_SLOTS_NUMBER); do
|
||||
R_SIZE=$(sudo dmidecode --type=memory | grep "Size:" | grep -v "Volatile" | grep -v "Cache" | grep -v "Logical" | cut -d: -f2 | sed -n "${i}p" | sed 's/\ //')
|
||||
R_SLOT=$i
|
||||
R_FREQ=$(sudo dmidecode --type=memory | grep Speed | grep -v "Memory" | cut -d: -f2 | sed -n "${i}p" | sed 's/\ //')
|
||||
|
||||
echo "RAM_${i}_SIZE=$R_SIZE" >> $SUM_FILE
|
||||
echo "RAM_${i}_SLOT=$R_SLOT" >> $SUM_FILE
|
||||
echo "RAM_${i}_FREQ=$R_FREQ" >> $SUM_FILE
|
||||
done
|
||||
echo "" >> $SUM_FILE
|
||||
|
||||
echo "--- STORAGE DATA ---" >> $SUM_FILE
|
||||
disks_partitions
|
||||
echo "STORAGE = $TOTAL_STORAGE" >> $SUM_FILE
|
||||
echo "" >> $SUM_FILE
|
||||
}
|
||||
|
||||
################################################
|
||||
|
||||
######## SOFTWARE PART #########################
|
||||
@ -233,17 +191,7 @@ OS=$(lsb_release -a | grep Description | cut -f2)
|
||||
ARCH=$(uname -a | cut -d' ' -f10)
|
||||
KERNEL=$(uname -r)
|
||||
HOSTNAME=$(hostname)
|
||||
|
||||
# Compile Software informations
|
||||
software() {
|
||||
echo "[SOFTWARE]"
|
||||
echo "OS = $OS"
|
||||
echo "HOSTNAME = $HOSTNAME"
|
||||
echo "ARCHITECTURE = $ARCH"
|
||||
echo "KERNEL = $KERNEL"
|
||||
echo "DESKTOP_ENV = $XDG_CURRENT_DESKTOP"
|
||||
echo "WINDOW_MANAGER = $XDG_SESSION_TYPE"
|
||||
} >> $SUM_FILE
|
||||
MAC_ADDRESS=$(cat /sys/class/net/$(ls /sys/class/net | grep -vE '^(lo|docker|veth|br)' | head -n 1)/address)
|
||||
|
||||
###############################################
|
||||
|
||||
@ -261,6 +209,7 @@ json_file() {
|
||||
--arg gpu_model "$GPU_MODEL" \
|
||||
--arg ram_slots "$RAM_SLOTS" \
|
||||
--arg hostname "$HOSTNAME" \
|
||||
--arg mac_address "$MAC_ADDRESS" \
|
||||
--arg os "$OS" \
|
||||
--arg arch "$ARCH" \
|
||||
--arg desktop_env "$XDG_CURRENT_DESKTOP" \
|
||||
@ -281,6 +230,7 @@ json_file() {
|
||||
},
|
||||
SOFTWARE: {
|
||||
hostname: $hostname,
|
||||
mac_address:$mac_address,
|
||||
os: $os,
|
||||
arch: $arch,
|
||||
desktop_env: $desktop_env,
|
||||
@ -311,10 +261,7 @@ python_venv() {
|
||||
echo "It's grabbin time!"
|
||||
hello
|
||||
echo "Fetching hardware data..."
|
||||
hardware
|
||||
echo "Fetching software data..."
|
||||
software
|
||||
echo "Writing everything in summary.txt"
|
||||
if [ "$choice" = "1" ]; then
|
||||
echo "Grabber has complete his mission! Find every logs saved in your home repository inside the /grabber folder."
|
||||
echo "See you space cowboy..."
|
||||
|
||||
BIN
grabberman.db
BIN
grabberman.db
Binary file not shown.
@ -1,4 +1,4 @@
|
||||

|
||||

|
||||
|
||||
# Grabber - Fetch all your PC
|
||||
|
||||
|
||||
@ -39,4 +39,5 @@ starlette==0.50.0
|
||||
typing-inspection==0.4.2
|
||||
typing_extensions==4.15.0
|
||||
urllib3==2.6.3
|
||||
uvicorn==0.40.0
|
||||
uvicorn==0.40.0
|
||||
sqlmodel==0.0.32
|
||||
@ -5,45 +5,83 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Informations Système</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
h1 { color: #333; }
|
||||
body { font-family: Arial, sans-serif; margin: 20px; background-color: #f9f9f9; }
|
||||
h1 { color: #333; text-align: center; }
|
||||
|
||||
/* Style pour le conteneur d'un seul PC */
|
||||
.computer-container {
|
||||
background-color: white;
|
||||
border: 2px solid #333;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 40px; /* Espace entre chaque ordi */
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.computer-header {
|
||||
background-color: #333;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.section { margin-bottom: 20px; border: 1px solid #ccc; padding: 10px; }
|
||||
.section h2 { margin-top: 0; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
.section h2 { margin-top: 0; color: #555; border-bottom: 2px solid #ddd; padding-bottom: 5px;}
|
||||
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 10px; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
|
||||
th { background-color: #f2f2f2; }
|
||||
th { background-color: #f2f2f2; width: 30%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Informations de l'Ordinateur</h1>
|
||||
<h1>Parc Informatique ({{ ordinateurs|length }} machines)</h1>
|
||||
|
||||
<div class="section">
|
||||
<h2>[HARDWARE]</h2>
|
||||
<table>
|
||||
<tr><th>Propriété</th><th>Valeur</th></tr>
|
||||
<tr><td>Hostname</td><td>{{ ordi.hostname }}</td></tr>
|
||||
<tr><td>CPU</td><td>{{ ordi.cpu_model }}</td></tr>
|
||||
<tr><td>ID CPU</td><td>{{ ordi.cpu_id }}</td></tr>
|
||||
<tr><td>Nombre de Cœurs CPU</td><td>{{ ordi.cpu_cores }}</td></tr>
|
||||
<tr><td>Nombre de Threads CPU</td><td>{{ ordi.cpu_threads }}</td></tr>
|
||||
<tr><td>Fréquence Min CPU</td><td>{{ ordi.cpu_frequency_min }}</td></tr>
|
||||
<tr><td>Fréquence Courante CPU</td><td>{{ ordi.cpu_frequency_cur }}</td></tr>
|
||||
<tr><td>Fréquence Max CPU</td><td>{{ ordi.cpu_frequency_max }}</td></tr>
|
||||
<tr><td>Nombre de slots de RAM</td><td>{{ ordi.ram_slots }}</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
{% for ordi in ordinateurs %}
|
||||
|
||||
<div class="computer-container">
|
||||
<div class="computer-header">
|
||||
<h2>🖥️ {{ ordi.hostname }} <small>(MAC: {{ ordi.mac_address }})</small></h2>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>[SOFTWARE]</h2>
|
||||
<table>
|
||||
<tr><th>Propriété</th><th>Valeur</th></tr>
|
||||
<tr><td>Hostname</td><td>{{ ordi.hostname }}</td></tr>
|
||||
<tr><td>OS</td><td>{{ ordi.os }}</td></tr>
|
||||
<tr><td>Architecture</td><td>{{ ordi.arch }}</td></tr>
|
||||
<tr><td>Desktop</td><td>{{ ordi.desktop_env }}</td></tr>
|
||||
<tr><td>Window Manager</td><td>{{ ordi.window_manager }}</td></tr>
|
||||
<tr><td>Kernel</td><td>{{ ordi.kernel }}</td></tr>
|
||||
</table>
|
||||
<div class="section">
|
||||
<h2>[HARDWARE]</h2>
|
||||
<table>
|
||||
<tr><th>Propriété</th><th>Valeur</th></tr>
|
||||
<tr><td>Série Carte Mère</td><td>{{ ordi.mb_serial }}</td></tr>
|
||||
<tr><td>Série Châssis</td><td>{{ ordi.chassis_serial }}</td></tr>
|
||||
<tr><td>CPU</td><td>{{ ordi.cpu }}</td></tr>
|
||||
<tr><td>CPU ID</td><td>{{ ordi.cpu_id }}</td></tr>
|
||||
<tr><td>Nombre de Cœurs CPU</td><td>{{ ordi.cpu_cores_number }}</td></tr>
|
||||
<tr><td>Nombre de Threads CPU</td><td>{{ ordi.cpu_threads_number }}</td></tr>
|
||||
<tr><td>Fréquence Min CPU</td><td>{{ ordi.cpu_frequency_min }}</td></tr>
|
||||
<tr><td>Fréquence Courante CPU</td><td>{{ ordi.cpu_frequency_cur }}</td></tr>
|
||||
<tr><td>Fréquence Max CPU</td><td>{{ ordi.cpu_frequency_max }}</td></tr>
|
||||
<tr><td>Modèle GPU</td><td>{{ ordi.gpu_model }}</td></tr>
|
||||
<tr><td>Stockage Total</td><td>{{ ordi.stockage_total }}</td></tr>
|
||||
<tr><td>Nombre de slots de RAM</td><td>{{ ordi.ram_slots_number }}</td></tr>
|
||||
<tr><td>Génération RAM</td><td>{{ ordi.ram_gen }}</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>[SOFTWARE]</h2>
|
||||
<table>
|
||||
<tr><th>Propriété</th><th>Valeur</th></tr>
|
||||
<tr><td>Hostname</td><td>{{ ordi.hostname }}</td></tr>
|
||||
<tr><td>OS</td><td>{{ ordi.os }}</td></tr>
|
||||
<tr><td>Architecture</td><td>{{ ordi.arch }}</td></tr>
|
||||
<tr><td>Desktop</td><td>{{ ordi.desktop }}</td></tr>
|
||||
<tr><td>Window Manager</td><td>{{ ordi.wm }}</td></tr>
|
||||
<tr><td>Kernel</td><td>{{ ordi.kernel }}</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endfor %}
|
||||
{% if not ordinateurs %}
|
||||
<p style="text-align:center; color: red;">Aucun ordinateur enregistré dans la base de données.</p>
|
||||
{% endif %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
Reference in New Issue
Block a user