{_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.
{_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 _create_admin_session_token(user):
payload = {
"id": user["id"],
"username": user["username"],
"exp": int(time.time()) + ADMIN_SESSION_MAX_AGE,
}
payload_bytes = json.dumps(payload, separators=(",", ":")).encode("utf-8")
encoded_payload = base64.urlsafe_b64encode(payload_bytes).decode("ascii")
signature = hmac.new(
ADMIN_SESSION_SECRET.encode("utf-8"),
encoded_payload.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return f"{encoded_payload}.{signature}"
def _verify_admin_session_token(token):
try:
encoded_payload, signature = token.split(".", 1)
expected_signature = hmac.new(
ADMIN_SESSION_SECRET.encode("utf-8"),
encoded_payload.encode("utf-8"),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(signature, expected_signature):
return None
payload = json.loads(base64.urlsafe_b64decode(encoded_payload.encode("ascii")))
if int(payload.get("exp") or 0) < int(time.time()):
return None
return payload
except Exception:
return None
def _get_admin_user_from_request(request):
token = request.cookies.get(ADMIN_SESSION_COOKIE)
if not token:
return None
payload = _verify_admin_session_token(token)
if not payload:
return None
return get_user_by_id(payload.get("id"))
def _password_eye_icon():
return (
'"
)
def _password_eye_off_icon():
return (
'"
)
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(
"