use actix_web::{HttpRequest, HttpResponse, Responder, get, web};
use maud::{Markup, html};
use crate::db::{Database, Notification};
use crate::session::SessionStore;
use crate::view::guard::guard_view;
use crate::view::{is_htmx, main_with_content};
fn debug_content(notifications: &[Notification]) -> Markup {
html! {
div class="flex min-h-screen w-full flex-col" {
nav class="navbar bg-base-100 shadow-sm border-b border-base-200 px-4" {
div class="navbar-start" {
a
href="/"
class="btn btn-ghost text-xl"
hx-get="/notify"
hx-target="#main"
hx-swap="innerHTML"
hx-push-url="/" {
"Notify"
}
}
div class="navbar-end gap-2" {
a
href="/install"
class="btn btn-ghost btn-sm"
hx-get="/install"
hx-target="#main"
hx-swap="innerHTML"
hx-push-url="true" {
"Install"
}
a
href="/"
class="btn btn-ghost btn-sm"
hx-get="/notify"
hx-target="#main"
hx-swap="innerHTML"
hx-push-url="/" {
"Back to notifications"
}
}
}
div class="w-full flex-1 p-4 sm:p-8" {
div class="mx-auto w-full max-w-6xl" {
div class="mb-6 flex flex-wrap items-center justify-between gap-3" {
div {
p class="text-sm font-semibold uppercase tracking-widest text-base-content/60" {
"Developer view"
}
h1 class="text-3xl font-bold" { "Notification database" }
p class="mt-1 text-base-content/70" {
(notifications.len()) " total entries"
}
}
}
@if notifications.is_empty() {
div class="card card-border bg-base-200" {
div class="card-body items-center text-center" {
h2 class="card-title" { "No notifications yet" }
p class="text-base-content/70" {
"New notification entries will appear here."
}
}
}
} @else {
div class="overflow-x-auto rounded-box border border-base-300 bg-base-100" {
table class="table table-zebra table-sm" {
thead {
tr {
th { "ID" }
th { "Kind" }
th { "Message" }
th { "Status" }
th { "Created" }
}
}
tbody {
@for notification in notifications {
tr {
th class="font-mono" { (notification.id) }
td class="font-medium" { (¬ification.kind) }
td class="min-w-64 whitespace-normal" { (¬ification.message) }
td {
@if notification.sent {
span class="font-medium text-success" { "Sent" }
} @else {
span class="font-medium text-warning" { "Pending" }
}
}
td class="whitespace-nowrap text-base-content/70" {
(¬ification.created_at)
}
}
}
}
}
}
}
}
}
}
}
}
#[get("/debug")]
pub async fn debug(
req: HttpRequest,
session_store: web::Data<SessionStore>,
db: web::Data<Database>,
) -> impl Responder {
if !session_store.is_authenticated(&req) {
let guard = guard_view(None);
return if is_htmx(&req) {
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(guard.into_string())
} else {
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(main_with_content(guard).into_string())
};
}
match db.list_notifications().await {
Ok(notifications) => {
let content = debug_content(¬ifications);
if is_htmx(&req) {
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(content.into_string())
} else {
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(main_with_content(content).into_string())
}
}
Err(error) => {
log::error!("Failed to load debug notifications: {error}");
HttpResponse::InternalServerError().finish()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use actix_web::{App, cookie::Cookie, test as aw_test};
#[actix_web::test]
async fn test_debug_unauthenticated_returns_guard() {
let session_store = web::Data::new(SessionStore::in_memory());
let db = web::Data::new(Database::new(":memory:"));
let app = aw_test::init_service(
App::new()
.app_data(session_store)
.app_data(db)
.service(debug),
)
.await;
// Direct request wraps guard in full html
let req = aw_test::TestRequest::get().uri("/debug").to_request();
let resp = aw_test::call_service(&app, req).await;
let body = aw_test::read_body(resp).await;
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(body_str.contains("<!DOCTYPE html>"));
assert!(body_str.contains("/assets/daisyui.css"));
assert!(body_str.contains("Unlock"));
// HTMX request returns partial guard
let req_htmx = aw_test::TestRequest::get()
.uri("/debug")
.insert_header(("HX-Request", "true"))
.to_request();
let resp_htmx = aw_test::call_service(&app, req_htmx).await;
let body_htmx = aw_test::read_body(resp_htmx).await;
let body_htmx_str = String::from_utf8(body_htmx.to_vec()).unwrap();
assert!(!body_htmx_str.contains("<!DOCTYPE html>"));
assert!(body_htmx_str.contains("Unlock"));
}
#[actix_web::test]
async fn test_debug_authenticated_lists_notifications() {
let session_store = web::Data::new(SessionStore::in_memory());
let token = session_store.create_session().await;
let cookie = Cookie::build(crate::session::SESSION_COOKIE_NAME, token).finish();
let db = Database::new(&format!(
"/tmp/test_debug_view_{}.db",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
db.init().await.unwrap();
db.save_notification("heart", "Look at your phone", false)
.await
.unwrap();
let db = web::Data::new(db);
let app = aw_test::init_service(
App::new()
.app_data(session_store)
.app_data(db)
.service(debug),
)
.await;
// Direct browser visit gets full layout with stylesheets
let req = aw_test::TestRequest::get()
.uri("/debug")
.cookie(cookie.clone())
.to_request();
let resp = aw_test::call_service(&app, req).await;
let body = aw_test::read_body(resp).await;
let body = String::from_utf8(body.to_vec()).unwrap();
assert!(body.contains("<!DOCTYPE html>"));
assert!(body.contains("/assets/daisyui.css"));
assert!(body.contains("/assets/app.css"));
assert!(body.contains("Notification database"));
assert!(body.contains("Look at your phone"));
assert!(body.contains("Pending"));
assert!(body.contains("class=\"navbar"));
assert!(body.contains("href=\"/install\""));
assert!(body.contains("Install"));
assert!(body.contains("Back to notifications"));
assert!(!body.contains("download=\"notify.json\""));
assert!(!body.contains("Export config"));
// HTMX request gets partial content for innerHTML swap
let req_htmx = aw_test::TestRequest::get()
.uri("/debug")
.cookie(cookie)
.insert_header(("HX-Request", "true"))
.to_request();
let resp_htmx = aw_test::call_service(&app, req_htmx).await;
let body_htmx = aw_test::read_body(resp_htmx).await;
let body_htmx = String::from_utf8(body_htmx.to_vec()).unwrap();
assert!(!body_htmx.contains("<!DOCTYPE html>"));
assert!(body_htmx.contains("Notification database"));
}
}