Files
phone-refurb/web-app.py
2026-04-12 20:12:46 +02:00

457 lines
18 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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)