Initial commit - Phone Refurb system

This commit is contained in:
kawa
2026-04-12 20:12:46 +02:00
commit 03f4f11373
6 changed files with 892 additions and 0 deletions

170
README.md Normal file
View File

@@ -0,0 +1,170 @@
# Phone Refurb - Gestion de stock de téléphones
Outil de gestion pour le reconditionnement de téléphones contre l'obsolescence programmée.
## Installation
```bash
cd ~/phone-refurb
python3 -m venv venv
./venv/bin/pip install flask
```
## Lancer la Web App
```bash
# Manuel
./venv/bin/python web-app.py
# Via systemd (recommandé)
sudo cp phone-refurb.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable phone-refurb
sudo systemctl start phone-refurb
```
## Accès
- **LAN:** http://192.168.1.149:5050
- **Mesh KAWA:** http://100.64.0.4:5050
## CLI - phone-cli
### Commandes principales
```bash
# Lister les téléphones
./phone-cli list [status]
# Ajouter un téléphone
./phone-cli add <brand> <model> <model_code> [imei] [serial] [status] [notes]
# Détails d'un téléphone
./phone-cli info <phone_id>
# Ajouter une action
./phone-cli action <phone_id> <action_type> [status] [notes]
# Changer le statut
./phone-cli status <phone_id> <status>
# Lister les ROMs
./phone-cli roms [device_code]
# Rechercher
./phone-cli search <query>
```
### Exemples
```bash
# Ajouter un Galaxy S7
./phone-cli add Samsung "Galaxy S7" SM-G930F
# Ajouter avec IMEI et notes
./phone-cli add Samsung "Galaxy J5" SM-J510FN 352154091234567 51c1e4fc stock "Écran fêlé"
# Lister les téléphones en stock
./phone-cli list stock
# Voir les détails du téléphone #1
./phone-cli info 1
# Enregistrer une action
./phone-cli action 1 flash_rom success "LineageOS 18.1 installée"
# Enregistrer un échec
./phone-cli action 1 unlock_bootloader failed "Bootloader bloqué par opérateur"
# Marquer comme prêt
./phone-cli status 1 ready
# Marquer comme vendu
./phone-cli status 1 sold
# Voir les ROMs pour j5xnlte
./phone-cli roms j5xnlte
# Rechercher
./phone-cli search samsung
./phone-cli search j5
```
## Statuts
| Statut | Description |
|--------|-------------|
| stock | En stock, pas encore traité |
| wip | En cours de traitement |
| paused | Traitement suspendu |
| ready | Prêt à être utilisé/vendu |
| sold | Vendu |
## Types d'actions
| Action | Description |
|--------|-------------|
| backup | Sauvegarde des données |
| unlock_bootloader | Déverrouillage du bootloader |
| flash_recovery | Flash du recovery (TWRP, etc.) |
| flash_rom | Installation d'une ROM |
| root | Root du téléphone |
| test | Tests fonctionnels |
| repair | Réparation matérielle |
| sell | Vente |
| other | Autre action |
## Base de données
SQLite: `phones.db`
### Tables
- **phones** — Téléphones (marque, modèle, IMEI, statut)
- **phone_specs** — Caractéristiques (CPU, RAM, stockage)
- **software_state** — État logiciel (OS, kernel, root)
- **roms** — ROMs disponibles
- **actions** — Historique des actions
### Requêtes utiles
```sql
-- Voir tous les téléphones avec leur OS
SELECT * FROM phone_summary;
-- Téléphones rootés
SELECT * FROM phone_summary WHERE root = 1;
-- Téléphones par marque
SELECT brand, COUNT(*) FROM phones GROUP BY brand;
-- Actions récentes
SELECT p.brand, p.model, a.action_type, a.action_date, a.status
FROM actions a JOIN phones p ON a.phone_id = p.id
ORDER BY a.action_date DESC LIMIT 20;
-- Taux de succès
SELECT status, COUNT(*) as count FROM actions GROUP BY status;
```
## Structure des fichiers
```
~/phone-refurb/
├── phones.db # Base SQLite
├── web-app.py # Web app Flask
├── phone-cli # CLI
├── venv/ # Environnement Python
├── web-app.log # Logs
└── README.md # Cette doc
```
## Intégration KAWA
La web app est accessible depuis n'importe quel nœud du mesh via:
- `http://kawaAgent1:5050` (si DNS mesh configuré)
- `http://100.64.0.4:5050` (IP mesh)
---
*Dernière mise à jour: 2026-04-12*

241
phone-cli Executable file
View File

@@ -0,0 +1,241 @@
#!/usr/bin/env python3
"""
Phone Refurb CLI - Gestion du stock de téléphones
Usage: ./phone-cli <command> [args]
"""
import sqlite3
import sys
import os
from datetime import datetime
from pathlib import Path
DB_PATH = Path(__file__).parent / "phones.db"
def get_conn():
return sqlite3.connect(DB_PATH)
def cmd_add(brand, model, model_code, imei=None, serial=None, status="stock", notes=None):
"""Ajouter un téléphone"""
conn = get_conn()
c = conn.cursor()
c.execute("""
INSERT INTO phones (brand, model, model_code, imei, serial, status, notes)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (brand, model, model_code, imei, serial, status, notes))
conn.commit()
print(f"✅ Téléphone ajouté (ID: {c.lastrowid})")
conn.close()
def cmd_list(status=None):
"""Lister les téléphones"""
conn = get_conn()
c = conn.cursor()
if status:
c.execute("SELECT * FROM phone_summary WHERE status = ?", (status,))
else:
c.execute("SELECT * FROM phone_summary")
rows = c.fetchall()
conn.close()
if not rows:
print("Aucun téléphone trouvé")
return
print(f"{'ID':<4} {'Marque':<10} {'Modèle':<20} {'Code':<15} {'Statut':<10} {'OS':<10} {'Root':<5}")
print("-" * 80)
for r in rows:
print(f"{r[0]:<4} {r[1]:<10} {r[2]:<20} {r[3] or '':<15} {r[4]:<10} {r[5] or '':<10} {'✓' if r[7] else '✗':<5}")
def cmd_info(phone_id):
"""Détails d'un téléphone"""
conn = get_conn()
c = conn.cursor()
# Infos de base
c.execute("SELECT * FROM phones WHERE id = ?", (phone_id,))
phone = c.fetchone()
if not phone:
print(f"❌ Téléphone {phone_id} non trouvé")
conn.close()
return
print(f"\n📱 {phone[1]} {phone[2]} ({phone[3]})")
print(f" IMEI: {phone[4] or 'N/A'}")
print(f" Serial: {phone[5] or 'N/A'}")
print(f" Statut: {phone[6]}")
print(f" Notes: {phone[7] or 'Aucune'}")
# Specs
c.execute("SELECT * FROM phone_specs WHERE phone_id = ?", (phone_id,))
specs = c.fetchone()
if specs:
print(f"\n🔧 Specs:")
print(f" CPU: {specs[2] or 'N/A'}")
print(f" RAM: {specs[4]} Mo" if specs[4] else "")
print(f" Stockage: {specs[5]} Go" if specs[5] else "")
print(f" Écran: {specs[6] or 'N/A'}")
print(f" Batterie: {specs[8]} mAh" if specs[8] else "")
# Software
c.execute("SELECT * FROM software_state WHERE phone_id = ? ORDER BY date_recorded DESC LIMIT 1", (phone_id,))
sw = c.fetchone()
if sw:
print(f"\n💾 Logiciel:")
print(f" OS: {sw[2]} {sw[3] or ''}")
print(f" SDK: {sw[4] or 'N/A'}")
print(f" Kernel: {sw[5] or 'N/A'}")
print(f" Bootloader: {'🔒 Verrouillé' if sw[6] else '🔓 Déverrouillé'}")
print(f" Root: {'✓' if sw[7] else '✗'}")
print(f" Recovery: {sw[8] or 'Stock'}")
# Actions
c.execute("SELECT action_type, action_date, status, notes FROM actions WHERE phone_id = ? ORDER BY action_date DESC LIMIT 10", (phone_id,))
actions = c.fetchall()
if actions:
print(f"\n📋 Actions récentes:")
for a in actions:
status_icon = "✅" if a[2] == "success" else "⚠️" if a[2] == "partial" else "❌"
print(f" {status_icon} {a[0]} ({a[1][:10]})")
if a[3]:
print(f" {a[3][:50]}")
conn.close()
def cmd_action(phone_id, action_type, status="success", notes=None, rom_id=None):
"""Ajouter une action"""
conn = get_conn()
c = conn.cursor()
c.execute("""
INSERT INTO actions (phone_id, action_type, status, notes, rom_id)
VALUES (?, ?, ?, ?, ?)
""", (phone_id, action_type, status, notes, rom_id))
conn.commit()
# Update phone timestamp
c.execute("UPDATE phones SET updated_at = CURRENT_TIMESTAMP WHERE id = ?", (phone_id,))
conn.commit()
print(f"✅ Action '{action_type}' ajoutée")
conn.close()
def cmd_update_status(phone_id, status):
"""Changer le statut d'un téléphone"""
conn = get_conn()
c = conn.cursor()
c.execute("UPDATE phones SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", (status, phone_id))
conn.commit()
print(f"✅ Statut mis à jour: {status}")
conn.close()
def cmd_roms(device_code=None):
"""Lister les ROMs disponibles"""
conn = get_conn()
c = conn.cursor()
if device_code:
c.execute("SELECT id, name, version, android_version, file_size_mb FROM roms WHERE device_code = ?", (device_code,))
else:
c.execute("SELECT id, name, version, android_version, device_code, file_size_mb FROM roms")
rows = c.fetchall()
conn.close()
print(f"{'ID':<4} {'ROM':<20} {'Version':<25} {'Android':<10} {'Device':<15} {'Size':<10}")
print("-" * 90)
for r in rows:
if device_code:
print(f"{r[0]:<4} {r[1]:<20} {r[2] or '':<25} {r[3] or '':<10} {r[4]}M")
else:
print(f"{r[0]:<4} {r[1]:<20} {r[2] or '':<25} {r[3] or '':<10} {r[4]:<15} {r[5]}M")
def cmd_search(query):
"""Rechercher un téléphone"""
conn = get_conn()
c = conn.cursor()
c.execute("""
SELECT * FROM phone_summary
WHERE brand LIKE ? OR model LIKE ? OR model_code LIKE ? OR status LIKE ?
""", (f"%{query}%", f"%{query}%", f"%{query}%", f"%{query}%"))
rows = c.fetchall()
conn.close()
if not rows:
print("Aucun résultat")
return
print(f"{'ID':<4} {'Marque':<10} {'Modèle':<20} {'Code':<15} {'Statut':<10}")
print("-" * 60)
for r in rows:
print(f"{r[0]:<4} {r[1]:<10} {r[2]:<20} {r[3] or '':<15} {r[4]:<10}")
def print_help():
print("""
Phone Refurb CLI - Gestion du stock de téléphones
Usage: ./phone-cli <command> [args]
Commands:
add <brand> <model> <model_code> [imei] [serial] [status] [notes]
Ajouter un nouveau téléphone
list [status]
Lister les téléphones (filtrer par statut: stock, paused, sold, etc.)
info <phone_id>
Détails complets d'un téléphone
action <phone_id> <action_type> [status] [notes]
Ajouter une action (status: success, partial, failed)
status <phone_id> <status>
Changer le statut d'un téléphone
roms [device_code]
Lister les ROMs disponibles
search <query>
Rechercher un téléphone
Examples:
./phone-cli add Samsung "Galaxy S7" SM-G930F
./phone-cli list stock
./phone-cli info 1
./phone-cli action 1 flash_rom success "LineageOS 18.1 installée"
./phone-cli status 1 ready
./phone-cli roms j5xnlte
./phone-cli search samsung
""")
def main():
if len(sys.argv) < 2:
print_help()
return
cmd = sys.argv[1]
try:
if cmd == "add":
args = sys.argv[2:]
cmd_add(*args[:7] if len(args) >= 3 else (print("Usage: add <brand> <model> <model_code>"), sys.exit(1)))
elif cmd == "list":
cmd_list(sys.argv[2] if len(sys.argv) > 2 else None)
elif cmd == "info":
cmd_info(int(sys.argv[2]))
elif cmd == "action":
args = sys.argv[2:]
cmd_action(int(args[0]), args[1], args[2] if len(args) > 2 else "success", args[3] if len(args) > 3 else None)
elif cmd == "status":
cmd_update_status(int(sys.argv[2]), sys.argv[3])
elif cmd == "roms":
cmd_roms(sys.argv[2] if len(sys.argv) > 2 else None)
elif cmd == "search":
cmd_search(sys.argv[2])
elif cmd in ["help", "-h", "--help"]:
print_help()
else:
print(f"Commande inconnue: {cmd}")
print_help()
except Exception as e:
print(f"❌ Erreur: {e}")
if __name__ == "__main__":
main()

14
phone-refurb.service Normal file
View File

@@ -0,0 +1,14 @@
[Unit]
Description=Phone Refurb Web App
After=network.target
[Service]
Type=simple
User=kawa
WorkingDirectory=/home/kawa/phone-refurb
ExecStart=/home/kawa/phone-refurb/venv/bin/python web-app.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target

BIN
phones.db Normal file

Binary file not shown.

10
web-app.log Normal file
View File

@@ -0,0 +1,10 @@
* Serving Flask app 'web-app'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5050
* Running on http://192.168.1.149:5050
Press CTRL+C to quit
127.0.0.1 - - [12/Apr/2026 20:04:01] "GET / HTTP/1.1" 200 -
192.168.1.123 - - [12/Apr/2026 20:07:13] "GET / HTTP/1.1" 200 -
192.168.1.123 - - [12/Apr/2026 20:07:13] "GET /favicon.ico HTTP/1.1" 404 -

457
web-app.py Executable file
View File

@@ -0,0 +1,457 @@
#!/usr/bin/env python3
"""
Phone Refurb Web App
Accessible depuis le mesh KAWA
"""
from flask import Flask, request, redirect, url_for, g
import sqlite3
from pathlib import Path
app = Flask(__name__)
DB_PATH = Path(__file__).parent / "phones.db"
def get_db():
if 'db' not in g:
g.db = sqlite3.connect(DB_PATH)
g.db.row_factory = sqlite3.Row
return g.db
@app.teardown_appcontext
def close_db(exception):
db = g.pop('db', None)
if db is not None:
db.close()
CSS = """
<style>
* { box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 0; background: #1a1a2e; color: #eee; }
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
h1, h2, h3 { color: #00d4aa; margin-top: 0; }
a { color: #00d4aa; text-decoration: none; }
a:hover { text-decoration: underline; }
table { width: 100%; border-collapse: collapse; background: #16213e; border-radius: 8px; overflow: hidden; }
th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #0f3460; }
th { background: #0f3460; color: #00d4aa; }
tr:hover { background: #1a1a4e; }
.badge { display: inline-block; padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: bold; }
.badge-stock { background: #3498db; }
.badge-paused { background: #f39c12; color: #000; }
.badge-wip { background: #e67e22; }
.badge-ready { background: #27ae60; }
.badge-sold { background: #9b59b6; }
.badge-success { background: #27ae60; }
.badge-partial { background: #f39c12; color: #000; }
.badge-failed { background: #e74c3c; }
.badge-custom { background: #9b59b6; }
.badge-recovery { background: #e67e22; }
.badge-kernel { background: #1abc9c; }
.btn { display: inline-block; padding: 8px 16px; background: #00d4aa; color: #000; border: none; border-radius: 4px; cursor: pointer; text-decoration: none; font-weight: bold; margin: 2px; }
.btn:hover { background: #00b894; }
.btn-sm { padding: 4px 10px; font-size: 12px; }
.card { background: #16213e; border-radius: 8px; padding: 20px; margin-bottom: 20px; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; }
.stat { font-size: 2em; font-weight: bold; color: #00d4aa; margin: 10px 0; }
form { margin: 20px 0; }
input, select, textarea { padding: 10px; border: 1px solid #0f3460; border-radius: 4px; background: #1a1a2e; color: #eee; width: 100%; max-width: 400px; }
textarea { min-height: 80px; }
label { display: block; margin: 10px 0 5px; color: #aaa; }
nav { background: #0f3460; padding: 15px 20px; display: flex; gap: 20px; align-items: center; flex-wrap: wrap; }
nav a { color: #fff; font-weight: bold; }
nav a:hover { color: #00d4aa; }
.search-form input { width: 180px; padding: 8px; }
.mono { font-family: monospace; }
.back-link { margin-bottom: 20px; display: inline-block; }
</style>
"""
NAV = """
<nav>
<a href="/">📱 Phone Refurb</a>
<a href="/roms">💾 ROMs</a>
<a href="/stats">📊 Stats</a>
<a href="/add"> Ajouter</a>
<form class="search-form" action="/search" method="get" style="margin:0;">
<input type="text" name="q" placeholder="Rechercher..." value="{query}">
</form>
</nav>
"""
def page(content, query=""):
return f"""<!DOCTYPE html>
<html><head><title>Phone Refurb - KAWA</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
{CSS}
</head><body>
{NAV.format(query=query)}
<div class="container">
{content}
</div></body></html>"""
@app.route('/')
def index():
db = get_db()
phones = db.execute("""
SELECT p.id, p.brand, p.model, p.model_code, p.status, p.updated_at,
s.os_version, s.root
FROM phones p
LEFT JOIN software_state s ON p.id = s.phone_id
ORDER BY p.updated_at DESC
""").fetchall()
stats = {
'stock': len([p for p in phones if p['status'] == 'stock']),
'wip': len([p for p in phones if p['status'] in ('wip', 'paused')]),
'ready': len([p for p in phones if p['status'] == 'ready']),
'sold': len([p for p in phones if p['status'] == 'sold']),
}
rows = ""
for p in phones:
rows += f"""<tr>
<td><a href="/phone/{p['id']}">{p['id']}</a></td>
<td>{p['brand']}</td>
<td>{p['model']}</td>
<td class="mono">{p['model_code'] or '-'}</td>
<td><span class="badge badge-{p['status']}">{p['status']}</span></td>
<td>{p['os_version'] or '-'}</td>
<td>{'' if p['root'] else ''}</td>
<td>
<a href="/phone/{p['id']}" class="btn btn-sm">Voir</a>
<a href="/action/{p['id']}" class="btn btn-sm">Action</a>
</td>
</tr>"""
content = f"""
<div class="grid">
<div class="card"><h2>📱 Stock</h2><div class="stat">{stats['stock']}</div></div>
<div class="card"><h2>🔧 En cours</h2><div class="stat">{stats['wip']}</div></div>
<div class="card"><h2>✅ Prêts</h2><div class="stat">{stats['ready']}</div></div>
<div class="card"><h2>💰 Vendus</h2><div class="stat">{stats['sold']}</div></div>
</div>
<h2>Téléphones</h2>
<table>
<tr><th>ID</th><th>Marque</th><th>Modèle</th><th>Code</th><th>Statut</th><th>OS</th><th>Root</th><th>Actions</th></tr>
{rows}
</table>
"""
return page(content)
@app.route('/phone/<int:phone_id>')
def phone(phone_id):
db = get_db()
phone = db.execute('SELECT * FROM phones WHERE id = ?', (phone_id,)).fetchone()
if not phone:
return page("<h2>❌ Téléphone non trouvé</h2>")
specs = db.execute('SELECT * FROM phone_specs WHERE phone_id = ?', (phone_id,)).fetchone()
sw = db.execute('SELECT * FROM software_state WHERE phone_id = ? ORDER BY date_recorded DESC LIMIT 1', (phone_id,)).fetchone()
actions = db.execute('SELECT * FROM actions WHERE phone_id = ? ORDER BY action_date DESC LIMIT 20', (phone_id,)).fetchall()
specs_html = ""
if specs:
specs_html = f"""<div class="card">
<h2>🔧 Caractéristiques</h2>
<div class="grid">
<div><strong>CPU:</strong> {specs['cpu'] or 'N/A'}</div>
<div><strong>GPU:</strong> {specs['gpu'] or 'N/A'}</div>
<div><strong>RAM:</strong> {specs['ram_mb']} Mo</div>
<div><strong>Stockage:</strong> {specs['storage_gb']} Go</div>
<div><strong>Écran:</strong> {specs['screen_size'] or 'N/A'}</div>
<div><strong>Batterie:</strong> {specs['battery_mah']} mAh</div>
</div>
</div>"""
sw_html = ""
if sw:
bootloader = '🔒 Verrouillé' if sw['bootloader_locked'] else '🔓 Déverrouillé'
root = '✓ Oui' if sw['root'] else '✗ Non'
sw_html = f"""<div class="card">
<h2>💾 Logiciel</h2>
<div class="grid">
<div><strong>OS:</strong> {sw['os_type']} {sw['os_version'] or ''}</div>
<div><strong>SDK:</strong> {sw['sdk_version'] or 'N/A'}</div>
<div><strong>Kernel:</strong> {sw['kernel_version'] or 'N/A'}</div>
<div><strong>Bootloader:</strong> {bootloader}</div>
<div><strong>Root:</strong> {root}</div>
<div><strong>Recovery:</strong> {sw['recovery_type'] or 'Stock'}</div>
</div>
</div>"""
actions_rows = ""
for a in actions:
actions_rows += f"""<tr>
<td>{a['action_date'][:16] if a['action_date'] else '-'}</td>
<td>{a['action_type']}</td>
<td><span class="badge badge-{a['status']}">{a['status']}</span></td>
<td>{a['notes'] or '-'}</td>
</tr>"""
actions_html = f"""<div class="card">
<h2>📋 Historique</h2>
{'<table><tr><th>Date</th><th>Action</th><th>Statut</th><th>Notes</th></tr>' + actions_rows + '</table>' if actions else '<p>Aucune action</p>'}
<a href="/action/{phone_id}" class="btn"> Ajouter une action</a>
</div>"""
content = f"""
<a href="/" class="back-link">← Retour</a>
<div class="card">
<h2>📱 {phone['brand']} {phone['model']}</h2>
<p><strong>Code:</strong> <span class="mono">{phone['model_code'] or 'N/A'}</span></p>
<p><strong>IMEI:</strong> <span class="mono">{phone['imei'] or 'N/A'}</span></p>
<p><strong>Serial:</strong> <span class="mono">{phone['serial'] or 'N/A'}</span></p>
<p><strong>Statut:</strong> <span class="badge badge-{phone['status']}">{phone['status']}</span></p>
<p><strong>Notes:</strong> {phone['notes'] or 'Aucune'}</p>
</div>
{specs_html}
{sw_html}
{actions_html}
"""
return page(content)
@app.route('/add', methods=['GET', 'POST'])
def add():
if request.method == 'POST':
db = get_db()
db.execute("""
INSERT INTO phones (brand, model, model_code, imei, serial, status, notes)
VALUES (?, ?, ?, ?, ?, ?, ?)
""", (
request.form['brand'],
request.form['model'],
request.form['model_code'],
request.form.get('imei') or None,
request.form.get('serial') or None,
request.form.get('status', 'stock'),
request.form.get('notes') or None
))
db.commit()
return redirect('/')
content = """
<a href="/" class="back-link">← Retour</a>
<div class="card">
<h2> Ajouter un téléphone</h2>
<form method="post">
<label>Marque *</label>
<input type="text" name="brand" required placeholder="Samsung">
<label>Modèle *</label>
<input type="text" name="model" required placeholder="Galaxy J5">
<label>Code modèle *</label>
<input type="text" name="model_code" required placeholder="SM-J510FN">
<label>IMEI</label>
<input type="text" name="imei" placeholder="Optionnel">
<label>Serial</label>
<input type="text" name="serial" placeholder="Optionnel">
<label>Statut</label>
<select name="status">
<option value="stock">Stock</option>
<option value="wip">En cours</option>
<option value="paused">En pause</option>
<option value="ready">Prêt</option>
<option value="sold">Vendu</option>
</select>
<label>Notes</label>
<textarea name="notes"></textarea>
<button type="submit" class="btn">Ajouter</button>
</form>
</div>
"""
return page(content)
@app.route('/action/<int:phone_id>', methods=['GET', 'POST'])
def add_action(phone_id):
db = get_db()
phone = db.execute('SELECT brand, model FROM phones WHERE id = ?', (phone_id,)).fetchone()
if request.method == 'POST':
db.execute("""
INSERT INTO actions (phone_id, action_type, status, notes)
VALUES (?, ?, ?, ?)
""", (
phone_id,
request.form['action_type'],
request.form['status'],
request.form.get('notes') or None
))
db.execute('UPDATE phones SET updated_at = CURRENT_TIMESTAMP WHERE id = ?', (phone_id,))
db.commit()
return redirect(f'/phone/{phone_id}')
content = f"""
<a href="/phone/{phone_id}" class="back-link">← Retour</a>
<div class="card">
<h2>📝 Action sur {phone['brand']} {phone['model']}</h2>
<form method="post">
<label>Type d'action</label>
<select name="action_type">
<option value="backup">Backup</option>
<option value="unlock_bootloader">Unlock bootloader</option>
<option value="flash_recovery">Flash recovery</option>
<option value="flash_rom">Flash ROM</option>
<option value="root">Root</option>
<option value="test">Test</option>
<option value="repair">Réparation</option>
<option value="sell">Vente</option>
<option value="other">Autre</option>
</select>
<label>Statut</label>
<select name="status">
<option value="success">✅ Succès</option>
<option value="partial">⚠️ Partiel</option>
<option value="failed">❌ Échec</option>
</select>
<label>Notes</label>
<textarea name="notes" placeholder="Détails..."></textarea>
<button type="submit" class="btn">Enregistrer</button>
</form>
</div>
"""
return page(content)
@app.route('/roms', methods=['GET', 'POST'])
def roms():
db = get_db()
if request.method == 'POST':
db.execute("""
INSERT INTO roms (name, version, android_version, device_code, source_url, file_path, file_size_mb, rom_type, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (
request.form['name'],
request.form.get('version') or None,
request.form.get('android_version') or None,
request.form['device_code'],
request.form.get('source_url') or None,
request.form.get('file_path') or None,
request.form.get('file_size_mb') or None,
request.form.get('rom_type', 'custom'),
request.form.get('notes') or None
))
db.commit()
return redirect('/roms')
roms = db.execute('SELECT * FROM roms ORDER BY device_code, name').fetchall()
rows = ""
for r in roms:
rows += f"""<tr>
<td>{r['id']}</td>
<td>{r['name']}</td>
<td class="mono">{r['version'] or '-'}</td>
<td>{r['android_version'] or '-'}</td>
<td class="mono">{r['device_code']}</td>
<td>{r['file_size_mb']}M</td>
<td><span class="badge badge-{r['rom_type']}">{r['rom_type']}</span></td>
</tr>"""
content = f"""
<h2>💾 ROMs disponibles</h2>
<table>
<tr><th>ID</th><th>Nom</th><th>Version</th><th>Android</th><th>Device</th><th>Taille</th><th>Type</th></tr>
{rows}
</table>
<div class="card">
<h3> Ajouter une ROM</h3>
<form method="post">
<label>Nom</label>
<input type="text" name="name" required placeholder="LineageOS">
<label>Version</label>
<input type="text" name="version" placeholder="18.1-20230513">
<label>Android version</label>
<input type="text" name="android_version" placeholder="11">
<label>Code device</label>
<input type="text" name="device_code" required placeholder="j5xnlte">
<label>URL source</label>
<input type="text" name="source_url" placeholder="https://...">
<label>Chemin fichier</label>
<input type="text" name="file_path" placeholder="/home/kawa/roms/...">
<label>Taille (Mo)</label>
<input type="number" name="file_size_mb" placeholder="500">
<label>Type</label>
<select name="rom_type">
<option value="custom">Custom ROM</option>
<option value="stock">Stock</option>
<option value="recovery">Recovery</option>
<option value="kernel">Kernel</option>
<option value="gapps">GApps</option>
</select>
<label>Notes</label>
<textarea name="notes"></textarea>
<button type="submit" class="btn">Ajouter</button>
</form>
</div>
"""
return page(content)
@app.route('/stats')
def stats():
db = get_db()
total_phones = db.execute('SELECT COUNT(*) FROM phones').fetchone()[0]
total_actions = db.execute('SELECT COUNT(*) FROM actions').fetchone()[0]
total_roms = db.execute('SELECT COUNT(*) FROM roms').fetchone()[0]
action_stats = db.execute('SELECT status, COUNT(*) as cnt FROM actions GROUP BY status').fetchall()
type_stats = db.execute('SELECT action_type, COUNT(*) as cnt FROM actions GROUP BY action_type ORDER BY cnt DESC').fetchall()
action_rows = ""
for s in action_stats:
action_rows += f"""<tr>
<td><span class="badge badge-{s['status']}">{s['status']}</span></td>
<td>{s['cnt']}</td>
</tr>"""
type_rows = ""
for t in type_stats:
type_rows += f"""<tr>
<td>{t['action_type']}</td>
<td>{t['cnt']}</td>
</tr>"""
content = f"""
<h2>📊 Statistiques</h2>
<div class="grid">
<div class="card"><h3>📱 Téléphones</h3><div class="stat">{total_phones}</div></div>
<div class="card"><h3>🔧 Actions</h3><div class="stat">{total_actions}</div></div>
<div class="card"><h3>💾 ROMs</h3><div class="stat">{total_roms}</div></div>
</div>
<div class="card">
<h3>Actions par statut</h3>
<table><tr><th>Statut</th><th>Nombre</th></tr>{action_rows}</table>
</div>
<div class="card">
<h3>Actions par type</h3>
<table><tr><th>Type</th><th>Nombre</th></tr>{type_rows}</table>
</div>
"""
return page(content)
@app.route('/search')
def search():
query = request.args.get('q', '')
db = get_db()
phones = db.execute("""
SELECT * FROM phones
WHERE brand LIKE ? OR model LIKE ? OR model_code LIKE ? OR notes LIKE ?
ORDER BY updated_at DESC
""", (f'%{query}%', f'%{query}%', f'%{query}%', f'%{query}%')).fetchall()
rows = ""
for p in phones:
rows += f"""<tr>
<td><a href="/phone/{p['id']}">{p['id']}</a></td>
<td>{p['brand']}</td>
<td>{p['model']}</td>
<td class="mono">{p['model_code'] or '-'}</td>
<td><span class="badge badge-{p['status']}">{p['status']}</span></td>
</tr>"""
content = f"""
<h2>🔍 Résultats pour "{query}"</h2>
{'<table><tr><th>ID</th><th>Marque</th><th>Modèle</th><th>Code</th><th>Statut</th></tr>' + rows + '</table>' if phones else '<p>Aucun résultat</p>'}
"""
return page(content, query=query)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5050, debug=False)