Initial commit - Phone Refurb system
This commit is contained in:
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