commit 03f4f11373778a582be0e042ba9ccbed58e66daa Author: kawa Date: Sun Apr 12 20:12:46 2026 +0200 Initial commit - Phone Refurb system diff --git a/README.md b/README.md new file mode 100644 index 0000000..f6b0b0d --- /dev/null +++ b/README.md @@ -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 [imei] [serial] [status] [notes] + +# Détails d'un téléphone +./phone-cli info + +# Ajouter une action +./phone-cli action [status] [notes] + +# Changer le statut +./phone-cli status + +# Lister les ROMs +./phone-cli roms [device_code] + +# Rechercher +./phone-cli search +``` + +### 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* \ No newline at end of file diff --git a/phone-cli b/phone-cli new file mode 100755 index 0000000..f82cc6c --- /dev/null +++ b/phone-cli @@ -0,0 +1,241 @@ +#!/usr/bin/env python3 +""" +Phone Refurb CLI - Gestion du stock de téléphones +Usage: ./phone-cli [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 [args] + +Commands: + add [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 + Détails complets d'un téléphone + + action [status] [notes] + Ajouter une action (status: success, partial, failed) + + status + Changer le statut d'un téléphone + + roms [device_code] + Lister les ROMs disponibles + + search + 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 "), 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() \ No newline at end of file diff --git a/phone-refurb.service b/phone-refurb.service new file mode 100644 index 0000000..0f11aaf --- /dev/null +++ b/phone-refurb.service @@ -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 \ No newline at end of file diff --git a/phones.db b/phones.db new file mode 100644 index 0000000..ac99bbb Binary files /dev/null and b/phones.db differ diff --git a/web-app.log b/web-app.log new file mode 100644 index 0000000..f09523a --- /dev/null +++ b/web-app.log @@ -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 - diff --git a/web-app.py b/web-app.py new file mode 100755 index 0000000..4c150b1 --- /dev/null +++ b/web-app.py @@ -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 = """ + +""" + +NAV = """ + +""" + +def page(content, query=""): + return f""" +Phone Refurb - KAWA + +{CSS} + +{NAV.format(query=query)} +
+{content} +
""" + +@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""" + {p['id']} + {p['brand']} + {p['model']} + {p['model_code'] or '-'} + {p['status']} + {p['os_version'] or '-'} + {'✓' if p['root'] else '✗'} + + Voir + Action + + """ + + content = f""" +
+

📱 Stock

{stats['stock']}
+

🔧 En cours

{stats['wip']}
+

✅ Prêts

{stats['ready']}
+

💰 Vendus

{stats['sold']}
+
+

Téléphones

+ + + {rows} +
IDMarqueModèleCodeStatutOSRootActions
+ """ + return page(content) + +@app.route('/phone/') +def phone(phone_id): + db = get_db() + phone = db.execute('SELECT * FROM phones WHERE id = ?', (phone_id,)).fetchone() + if not phone: + return page("

❌ Téléphone non trouvé

") + + 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"""
+

🔧 Caractéristiques

+
+
CPU: {specs['cpu'] or 'N/A'}
+
GPU: {specs['gpu'] or 'N/A'}
+
RAM: {specs['ram_mb']} Mo
+
Stockage: {specs['storage_gb']} Go
+
Écran: {specs['screen_size'] or 'N/A'}
+
Batterie: {specs['battery_mah']} mAh
+
+
""" + + sw_html = "" + if sw: + bootloader = '🔒 Verrouillé' if sw['bootloader_locked'] else '🔓 Déverrouillé' + root = '✓ Oui' if sw['root'] else '✗ Non' + sw_html = f"""
+

💾 Logiciel

+
+
OS: {sw['os_type']} {sw['os_version'] or ''}
+
SDK: {sw['sdk_version'] or 'N/A'}
+
Kernel: {sw['kernel_version'] or 'N/A'}
+
Bootloader: {bootloader}
+
Root: {root}
+
Recovery: {sw['recovery_type'] or 'Stock'}
+
+
""" + + actions_rows = "" + for a in actions: + actions_rows += f""" + {a['action_date'][:16] if a['action_date'] else '-'} + {a['action_type']} + {a['status']} + {a['notes'] or '-'} + """ + + actions_html = f"""
+

📋 Historique

+ {'' + actions_rows + '
DateActionStatutNotes
' if actions else '

Aucune action

'} + ➕ Ajouter une action +
""" + + content = f""" + ← Retour +
+

📱 {phone['brand']} {phone['model']}

+

Code: {phone['model_code'] or 'N/A'}

+

IMEI: {phone['imei'] or 'N/A'}

+

Serial: {phone['serial'] or 'N/A'}

+

Statut: {phone['status']}

+

Notes: {phone['notes'] or 'Aucune'}

+
+ {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 = """ + ← Retour +
+

➕ Ajouter un téléphone

+
+ + + + + + + + + + + + + + + +
+
+ """ + return page(content) + +@app.route('/action/', 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""" + ← Retour +
+

📝 Action sur {phone['brand']} {phone['model']}

+
+ + + + + + + +
+
+ """ + 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""" + {r['id']} + {r['name']} + {r['version'] or '-'} + {r['android_version'] or '-'} + {r['device_code']} + {r['file_size_mb']}M + {r['rom_type']} + """ + + content = f""" +

💾 ROMs disponibles

+ + + {rows} +
IDNomVersionAndroidDeviceTailleType
+
+

➕ Ajouter une ROM

+
+ + + + + + + + + + + + + + + + + + + +
+
+ """ + 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""" + {s['status']} + {s['cnt']} + """ + + type_rows = "" + for t in type_stats: + type_rows += f""" + {t['action_type']} + {t['cnt']} + """ + + content = f""" +

📊 Statistiques

+
+

📱 Téléphones

{total_phones}
+

🔧 Actions

{total_actions}
+

💾 ROMs

{total_roms}
+
+
+

Actions par statut

+ {action_rows}
StatutNombre
+
+
+

Actions par type

+ {type_rows}
TypeNombre
+
+ """ + 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""" + {p['id']} + {p['brand']} + {p['model']} + {p['model_code'] or '-'} + {p['status']} + """ + + content = f""" +

🔍 Résultats pour "{query}"

+ {'' + rows + '
IDMarqueModèleCodeStatut
' if phones else '

Aucun résultat

'} + """ + return page(content, query=query) + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5050, debug=False) \ No newline at end of file