Web dashboard: dashboard module scaffold for parallel steps
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NHyYupFdBiR4VfoUM7NjSM
This commit is contained in:
@@ -41,3 +41,26 @@ tests, rustfmt defaults, no `unwrap()` outside tests, tracing spans).
|
|||||||
- When you finish, write a short handoff at
|
- When you finish, write a short handoff at
|
||||||
`docs/plans/briefs/web-dashboard/handoff-step<N>.md`: what landed, deviations
|
`docs/plans/briefs/web-dashboard/handoff-step<N>.md`: what landed, deviations
|
||||||
from the plan and why, anything left for the next step, test counts.
|
from the plan and why, anything left for the next step, test counts.
|
||||||
|
|
||||||
|
## Parallel steps (added after step 2)
|
||||||
|
|
||||||
|
From step 3 on, steps may run **in parallel in separate git worktrees**, so
|
||||||
|
file ownership matters:
|
||||||
|
|
||||||
|
- `src/web/dashboard/mod.rs` already declares one submodule per page group
|
||||||
|
(`runs`, `articles`, `ratings`, `profile`, `settings`, `jobs`, `stats`,
|
||||||
|
`users`), each exposing `routes() -> Router<AppState>` that the dashboard
|
||||||
|
router merges under the admin layer. Put your routes in **your** submodule's
|
||||||
|
`routes()`; do not touch `web::router` in `src/web/mod.rs` for dashboard
|
||||||
|
routes. Only step 3 edits the `overview` handler in `dashboard/mod.rs`.
|
||||||
|
- You own: your `src/web/dashboard/<group>.rs` files, your templates under
|
||||||
|
`src/web/templates/dashboard/`, partials you introduce, your tests.
|
||||||
|
- Shared files you may touch **additively only** (no reformatting, reordering
|
||||||
|
or renaming of existing code): `src/web/mod.rs` (helpers), `src/db.rs` (new
|
||||||
|
query helpers appended inside `impl Db`), `src/main.rs`, `src/lib.rs`,
|
||||||
|
`Cargo.toml`, `src/web/static/app.css` and `app.js` (append a block at the
|
||||||
|
end under a `/* step N: … */` comment), `src/web/templates/layout.html`.
|
||||||
|
Keep such edits small so merges stay trivial.
|
||||||
|
- If your worktree is on its own branch, finish by committing **one** commit
|
||||||
|
on that branch (message `Web dashboard step N: …`); the orchestrator merges.
|
||||||
|
If you are on `web-dashboard` itself, do not commit.
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
//! Dashboard: articles pages. Filled in by web dashboard plan step 3.
|
||||||
|
|
||||||
|
use axum::Router;
|
||||||
|
|
||||||
|
use crate::server::AppState;
|
||||||
|
|
||||||
|
/// Routes contributed by this page group (merged by `dashboard::router`).
|
||||||
|
pub fn routes() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
//! Dashboard: jobs pages. Filled in by web dashboard plan step 6.
|
||||||
|
|
||||||
|
use axum::Router;
|
||||||
|
|
||||||
|
use crate::server::AppState;
|
||||||
|
|
||||||
|
/// Routes contributed by this page group (merged by `dashboard::router`).
|
||||||
|
pub fn routes() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
//! The operator dashboard (`/dashboard/*`, dashboard plan §9–§14).
|
||||||
|
//!
|
||||||
|
//! Every route here sits under the admin `permission_required!` layer that
|
||||||
|
//! `web::router` applies to the merged router; handlers can therefore trust
|
||||||
|
//! that `AuthSession::user()` is an admin. One submodule per page group; each
|
||||||
|
//! exposes `routes()` and this module merges them.
|
||||||
|
|
||||||
|
pub mod articles;
|
||||||
|
pub mod jobs;
|
||||||
|
pub mod profile;
|
||||||
|
pub mod ratings;
|
||||||
|
pub mod runs;
|
||||||
|
pub mod settings;
|
||||||
|
pub mod stats;
|
||||||
|
pub mod users;
|
||||||
|
|
||||||
|
use askama::Template;
|
||||||
|
use axum::Router;
|
||||||
|
use axum::response::{IntoResponse, Response};
|
||||||
|
use axum::routing::get;
|
||||||
|
|
||||||
|
use crate::server::AppState;
|
||||||
|
use crate::web::session::{AuthSession, Viewer};
|
||||||
|
use crate::web::{Html, Page, WebError};
|
||||||
|
|
||||||
|
/// Every dashboard route, without the admin layer (applied by the caller).
|
||||||
|
pub fn router() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/dashboard", get(overview))
|
||||||
|
.merge(runs::routes())
|
||||||
|
.merge(articles::routes())
|
||||||
|
.merge(ratings::routes())
|
||||||
|
.merge(profile::routes())
|
||||||
|
.merge(settings::routes())
|
||||||
|
.merge(jobs::routes())
|
||||||
|
.merge(stats::routes())
|
||||||
|
.merge(users::routes())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "dashboard/overview.html")]
|
||||||
|
struct OverviewTemplate {
|
||||||
|
page: Page,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `GET /dashboard` — the overview (§9.1). Step 3 fills this in.
|
||||||
|
async fn overview(auth: AuthSession) -> Result<Response, WebError> {
|
||||||
|
let viewer = auth.user().await.map(Viewer::from);
|
||||||
|
Ok(Html(OverviewTemplate {
|
||||||
|
page: Page::new("Overview", viewer, "dashboard"),
|
||||||
|
})
|
||||||
|
.into_response())
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
//! Dashboard: profile pages. Filled in by web dashboard plan step 4.
|
||||||
|
|
||||||
|
use axum::Router;
|
||||||
|
|
||||||
|
use crate::server::AppState;
|
||||||
|
|
||||||
|
/// Routes contributed by this page group (merged by `dashboard::router`).
|
||||||
|
pub fn routes() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
//! Dashboard: ratings pages. Filled in by web dashboard plan step 4.
|
||||||
|
|
||||||
|
use axum::Router;
|
||||||
|
|
||||||
|
use crate::server::AppState;
|
||||||
|
|
||||||
|
/// Routes contributed by this page group (merged by `dashboard::router`).
|
||||||
|
pub fn routes() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
//! Dashboard: runs pages. Filled in by web dashboard plan step 3.
|
||||||
|
|
||||||
|
use axum::Router;
|
||||||
|
|
||||||
|
use crate::server::AppState;
|
||||||
|
|
||||||
|
/// Routes contributed by this page group (merged by `dashboard::router`).
|
||||||
|
pub fn routes() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
//! Dashboard: settings pages. Filled in by web dashboard plan step 5.
|
||||||
|
|
||||||
|
use axum::Router;
|
||||||
|
|
||||||
|
use crate::server::AppState;
|
||||||
|
|
||||||
|
/// Routes contributed by this page group (merged by `dashboard::router`).
|
||||||
|
pub fn routes() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
//! Dashboard: stats pages. Filled in by web dashboard plan step 6.
|
||||||
|
|
||||||
|
use axum::Router;
|
||||||
|
|
||||||
|
use crate::server::AppState;
|
||||||
|
|
||||||
|
/// Routes contributed by this page group (merged by `dashboard::router`).
|
||||||
|
pub fn routes() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
//! Dashboard: users pages. Filled in by web dashboard plan step 7.
|
||||||
|
|
||||||
|
use axum::Router;
|
||||||
|
|
||||||
|
use crate::server::AppState;
|
||||||
|
|
||||||
|
/// Routes contributed by this page group (merged by `dashboard::router`).
|
||||||
|
pub fn routes() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
}
|
||||||
+2
-15
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod dashboard;
|
||||||
pub mod issue;
|
pub mod issue;
|
||||||
pub mod public;
|
pub mod public;
|
||||||
pub mod rate;
|
pub mod rate;
|
||||||
@@ -367,7 +368,7 @@ pub fn router(config: &crate::config::Config) -> axum::Router<crate::server::App
|
|||||||
))
|
))
|
||||||
.route_layer(from_fn(map_forbidden));
|
.route_layer(from_fn(map_forbidden));
|
||||||
let dashboard = axum::Router::new()
|
let dashboard = axum::Router::new()
|
||||||
.route("/dashboard", get(dashboard_stub))
|
.merge(dashboard::router())
|
||||||
.route("/rate", post(rate::post))
|
.route("/rate", post(rate::post))
|
||||||
.route_layer(permission_required!(
|
.route_layer(permission_required!(
|
||||||
session::Backend,
|
session::Backend,
|
||||||
@@ -390,20 +391,6 @@ pub fn router(config: &crate::config::Config) -> axum::Router<crate::server::App
|
|||||||
.merge(dashboard)
|
.merge(dashboard)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "dashboard/overview.html")]
|
|
||||||
struct OverviewTemplate {
|
|
||||||
page: Page,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn dashboard_stub(auth: session::AuthSession) -> Result<Response, WebError> {
|
|
||||||
let viewer = auth.user().await.map(session::Viewer::from);
|
|
||||||
Ok(Html(OverviewTemplate {
|
|
||||||
page: Page::new("Overview", viewer, "dashboard"),
|
|
||||||
})
|
|
||||||
.into_response())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn map_forbidden(request: Request, next: Next) -> Response {
|
async fn map_forbidden(request: Request, next: Next) -> Response {
|
||||||
let mut response = next.run(request).await;
|
let mut response = next.run(request).await;
|
||||||
if response.status() == StatusCode::FORBIDDEN {
|
if response.status() == StatusCode::FORBIDDEN {
|
||||||
|
|||||||
Reference in New Issue
Block a user