Initial commit - Phone Refurb system
This commit is contained in:
170
README.md
Normal file
170
README.md
Normal 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
241
phone-cli
Executable 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
14
phone-refurb.service
Normal 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
|
||||||
10
web-app.log
Normal file
10
web-app.log
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
* Serving Flask app 'web-app'
|
||||||
|
* Debug mode: off
|
||||||
|
[31m[1mWARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.[0m
|
||||||
|
* Running on all addresses (0.0.0.0)
|
||||||
|
* Running on http://127.0.0.1:5050
|
||||||
|
* Running on http://192.168.1.149:5050
|
||||||
|
[33mPress CTRL+C to quit[0m
|
||||||
|
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] "[33mGET /favicon.ico HTTP/1.1[0m" 404 -
|
||||||
457
web-app.py
Executable file
457
web-app.py
Executable 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)
|
||||||
Reference in New Issue
Block a user