{_user_table(users)}
""",
)
@app.post("/admin/users")
def create_user_from_admin(
full_name: str = Form(...),
username: str = Form(...),
password: str = Form(...),
):
full_name = full_name.strip()
username = username.strip()
password = password.strip()
if not full_name or not username or not password:
raise HTTPException(status_code=400, detail="Nama, username, dan password wajib diisi.")
try:
create_user(full_name, username, password)
except Exception as error:
raise HTTPException(
status_code=400,
detail="Username sudah dipakai atau data user tidak valid.",
) from error
return RedirectResponse(url="/admin/users", status_code=303)
@app.get("/admin/users/{user_id}/edit", response_class=HTMLResponse)
def read_edit_user_page(user_id: int):
user = get_user_by_id(user_id)
if user is None:
raise HTTPException(status_code=404, detail="User tidak ditemukan.")
return _html_page(
title=f"Edit User #{user_id}",
active="users",
content=f"""
Edit User #{user_id}
Kosongkan password jika tidak ingin mengganti password.
{_user_form(row=user, action=f"/admin/users/{user_id}/edit", submit_label="Simpan User")}
""",
)
@app.post("/admin/users/{user_id}/edit")
def update_user_from_admin(
user_id: int,
full_name: str = Form(...),
username: str = Form(...),
password: str = Form(""),
):
full_name = full_name.strip()
username = username.strip()
if not full_name or not username:
raise HTTPException(status_code=400, detail="Nama dan username wajib diisi.")
try:
updated = update_user(user_id, full_name, username, password)
except Exception as error:
raise HTTPException(
status_code=400,
detail="Username sudah dipakai atau data user tidak valid.",
) from error
if not updated:
raise HTTPException(status_code=404, detail="User tidak ditemukan.")
return RedirectResponse(url="/admin/users", status_code=303)
@app.post("/admin/users/{user_id}/delete")
def delete_user_from_admin(user_id: int):
delete_user(user_id)
return RedirectResponse(url="/admin/users", status_code=303)
@app.get("/admin/logout", response_class=HTMLResponse)
def read_logout_page():
return _html_page(
title="Logout",
active="logout",
content="""
Logout
Sesi admin ditutup. Saat ini web admin belum memakai session login khusus, jadi halaman ini menjadi pintu keluar visual.
Kembali ke Dashboard
""",
)
@app.get("/admin/students/{student_name}", response_class=HTMLResponse)
def read_student_predictions_page(student_name: str):
decoded_name = unquote(student_name)
rows = [
row
for row in list_prediction_results(limit=500)
if row["student_name"] == decoded_name
]
total = len(rows)
pd_count = sum(1 for row in rows if row["predicted_label"] == "PD")
tpd_count = sum(1 for row in rows if row["predicted_label"] == "TPD")
avg_pd = _average_pd_ratio(rows)
return _html_page(
title=f"Analisis {decoded_name}",
active="dashboard",
content=f"""
{_prediction_table(rows)}
""",
)
@app.get("/admin", response_class=HTMLResponse)
def read_admin_page(
q: str = Query("", alias="q"),
label: str = Query("", alias="label"),
):
rows = list_prediction_results(limit=500)
clean_query = q.strip()
clean_label = label.strip().upper()
if clean_label not in {"PD", "TPD"}:
clean_label = ""
filtered_rows = _filter_prediction_rows(rows, clean_query, clean_label)
total = len(filtered_rows)
pd_count = sum(1 for row in filtered_rows if row["predicted_label"] == "PD")
tpd_count = sum(1 for row in filtered_rows if row["predicted_label"] == "TPD")
avg_pd = _average_pd_ratio(filtered_rows)
grouped_rows = _group_predictions_by_student(filtered_rows)
return _html_page(
title="Admin ConfiVoice",
active="dashboard",
content=f"""
Admin ConfiVoice
Dashboard hasil analisis percaya diri siswa/i dari aplikasi Flutter.
Total Data Tampil{total}
Percaya Diri{pd_count}
Tidak Percaya Diri{tpd_count}
Rata-rata Percaya Diri{avg_pd * 100:.1f}%
Folder Siswa/i
Pilih nama siswa/i untuk melihat semua hasil analisisnya.
{_dashboard_filter_form(clean_query, clean_label)}
{_student_folder_grid(grouped_rows)}
""",
)
def _redirect_admin():
return RedirectResponse(url="/admin", status_code=303)
def _result_payload(
*,
predicted_label,
student_gender,
description,
confidence_percent,
probability_pd_percent,
probability_tpd_percent,
is_valid_audio,
error_message,
audio_duration,
):
return {
"predicted_label": predicted_label,
"student_gender": _clean_student_gender(student_gender),
"description": description,
"confidence": _percent_to_ratio(confidence_percent),
"probability_pd": _percent_to_ratio(probability_pd_percent),
"probability_tpd": _percent_to_ratio(probability_tpd_percent),
"is_valid_audio": is_valid_audio == "1",
"error_message": error_message.strip() or None,
"audio_quality": {"duration": audio_duration},
"voice_indicators": {},
}
def _db_values(
*,
student_name,
student_gender,
predicted_label,
description,
confidence_percent,
probability_pd_percent,
probability_tpd_percent,
is_valid_audio,
error_message,
audio_duration,
):
return (
student_name,
_clean_student_gender(student_gender),
predicted_label.strip() or None,
description.strip() or None,
_percent_to_ratio(confidence_percent),
_percent_to_ratio(probability_pd_percent),
_percent_to_ratio(probability_tpd_percent),
1 if is_valid_audio == "1" else 0,
error_message.strip() or None,
float(audio_duration or 0),
0,
0,
0,
0,
0,
)
def _percent_to_ratio(value):
return max(0, min(float(value or 0), 100)) / 100
def _ratio_to_percent(value):
return f"{float(value or 0) * 100:.2f}"
def _clean_student_gender(value):
gender = (value or "").strip()
return gender if gender in {"Laki-laki", "Perempuan"} else None
def _student_gender_label(value):
return value or "Belum diisi"
def _pd_probability(row):
return float(row.get("probability_pd") or 0)
def _average_pd_ratio(rows):
return sum(_pd_probability(row) for row in rows) / len(rows) if rows else 0
def _filter_prediction_rows(rows, query, label):
query = query.strip().lower()
label = label.strip().upper()
filtered_rows = rows
if query:
filtered_rows = [
row
for row in filtered_rows
if query in (row["student_name"] or "").lower()
]
if label in {"PD", "TPD"}:
filtered_rows = [
row
for row in filtered_rows
if (row["predicted_label"] or "").upper() == label
]
return filtered_rows
def _dashboard_filter_form(query, label):
all_selected = "selected" if not label else ""
pd_selected = "selected" if label == "PD" else ""
tpd_selected = "selected" if label == "TPD" else ""
return f"""
"""
def _group_predictions_by_student(rows):
groups = {}
for row in rows:
student_name = row["student_name"] or "Tanpa Nama"
groups.setdefault(student_name, []).append(row)
return sorted(groups.items(), key=lambda item: item[0].lower())
def _student_folder_grid(grouped_rows):
if not grouped_rows:
return '
Belum ada folder siswa/i.
'
cards = []
for student_name, rows in grouped_rows:
total = len(rows)
latest = rows[0]
gender = _student_gender_label(latest.get("student_gender"))
label = latest.get("predicted_label") or "-"
pd_count = sum(1 for row in rows if (row.get("predicted_label") or "").upper() == "PD")
tpd_count = sum(1 for row in rows if (row.get("predicted_label") or "").upper() == "TPD")
pd_sum = sum(_pd_probability(row) for row in rows)
labels = " ".join(
sorted({(row.get("predicted_label") or "").upper() for row in rows})
)
latest_pd = _pd_probability(latest) * 100
pd_average = _average_pd_ratio(rows) * 100
folder_url = f"/admin/students/{quote(student_name, safe='')}"
cards.append(
f"""
"""
def _user_form(row=None, action="/admin/users", submit_label="Simpan", password_required=False):
row = row or {}
full_name = escape(str(row.get("full_name") or ""))
username = escape(str(row.get("username") or ""))
required = "required" if password_required else ""
password_hint = "Wajib diisi untuk user baru." if password_required else "Kosongkan jika tidak diganti."
return f"""
"""
def _user_table(users):
table_rows = []
for user in users:
table_rows.append(
"