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

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)