fixup 3d on admin dash, add databox

This commit is contained in:
Hanashi
2026-07-30 20:18:28 -04:00
parent 17c12836ad
commit 446ab88abb
13 changed files with 209 additions and 155 deletions
@@ -95,6 +95,7 @@ use artcraft_api_defs::moderation::debug_logs::debug_log_entry::*;
use artcraft_api_defs::moderation::debug_logs::moderation_list_all_debug_logs::*;
use artcraft_api_defs::moderation::debug_logs::moderation_list_debug_logs_for_token::*;
use artcraft_api_defs::moderation::debug_logs::moderation_list_debug_logs_for_user::*;
use crate::http_server::endpoints::moderation::dashboards::moderator_list_databox_dashboards_handler::*;
use crate::http_server::endpoints::moderation::jobs::moderation_get_job_by_token_handler::*;
use crate::http_server::endpoints::moderation::user_feature_flags::moderator_edit_user_feature_flags_handler::*;
use crate::http_server::endpoints::moderation::user_feature_flags::moderator_list_all_available_user_feature_flags_handler::*;
@@ -640,6 +641,7 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
crate::http_server::endpoints::omni_api::job_status::batch_get_job_status_handler::omni_api_batch_get_job_status_handler,
// Moderation
crate::http_server::endpoints::moderation::alerts::moderation_send_alert_handler::moderation_send_alert_handler,
crate::http_server::endpoints::moderation::dashboards::moderator_list_databox_dashboards_handler::moderator_list_databox_dashboards_handler,
crate::http_server::endpoints::moderation::info::moderator_token_info_handler::moderator_get_token_info_handler,
crate::http_server::endpoints::moderation::user::moderator_list_subscribing_users_by_signup_date::moderator_list_subscribing_users_by_signup_date_handler,
crate::http_server::endpoints::moderation::user::moderator_list_users_by_signup_date::moderator_list_users_by_signup_date_handler,
@@ -724,6 +726,8 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
crate::http_server::endpoints::image_studio::update_gpt_image_job_status_handler::update_gpt_image_job_status_handler,
),
components(schemas(
DataboxDashboardEntry,
ListDataboxDashboardsResponse,
ModeratorGetUserSpendSummaryResponse,
UserSpendSummaryView,
ModeratorReengagementListResponse,
@@ -0,0 +1 @@
pub mod moderator_list_databox_dashboards_handler;
@@ -0,0 +1,67 @@
use std::sync::Arc;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use serde::Serialize;
use utoipa::ToSchema;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::user_lookup::user_session::require_moderator::require_moderator;
use crate::state::server_state::ServerState;
#[derive(Serialize, ToSchema)]
pub struct ListDataboxDashboardsResponse {
pub success: bool,
/// Configured Databox datawalls, in display order.
pub databoards: Vec<DataboxDashboardEntry>,
}
#[derive(Serialize, ToSchema)]
pub struct DataboxDashboardEntry {
/// Human-readable dashboard name.
pub name: String,
/// Databox datawall id.
pub id: String,
}
/// List the Databox metrics dashboards configured for the admin dashboard.
/// Dashboards whose env vars aren't set are omitted.
#[utoipa::path(
get,
tag = "Moderation",
path = "/v1/moderation/dashboards/databox",
responses(
(status = 200, description = "Success", body = ListDataboxDashboardsResponse),
(status = 401, description = "Not authorized", body = CommonWebError),
(status = 500, description = "Server error", body = CommonWebError),
),
)]
pub async fn moderator_list_databox_dashboards_handler(
http_request: HttpRequest,
server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<ListDataboxDashboardsResponse>, CommonWebError> {
require_moderator(&http_request, &server_state.session_checker, &server_state.mysql_pool).await?;
let databox = &server_state.dashboards.databox;
let databoards = [
("DAUs", databox.daus_id.as_ref()),
("Daily Generations", databox.daily_generations_id.as_ref()),
]
.into_iter()
.filter_map(|(name, maybe_id)| {
maybe_id.map(|id| DataboxDashboardEntry {
name: name.to_string(),
id: id.clone(),
})
})
.collect();
Ok(Json(ListDataboxDashboardsResponse {
success: true,
databoards,
}))
}
@@ -1,4 +1,5 @@
pub mod alerts;
pub mod dashboards;
pub mod debug_logs;
pub mod info;
pub mod ip_bans;
@@ -35,6 +35,7 @@ use crate::http_server::endpoints::moderation::user::moderator_list_users_by_sig
use crate::http_server::endpoints::moderation::user::moderator_user_lookup_by_stripe_customer_id_handler::moderator_user_lookup_by_stripe_customer_id_handler;
use crate::http_server::endpoints::moderation::user::moderator_user_lookup_handler::moderator_user_lookup_handler;
use crate::http_server::endpoints::moderation::alerts::moderation_send_alert_handler::moderation_send_alert_handler;
use crate::http_server::endpoints::moderation::dashboards::moderator_list_databox_dashboards_handler::moderator_list_databox_dashboards_handler;
use crate::http_server::endpoints::moderation::staff_audit_logs::moderator_list_staff_audit_logs_handler::moderator_list_staff_audit_logs_handler;
use crate::http_server::endpoints::moderation::debug_logs::moderation_list_all_debug_logs_handler::moderation_list_all_debug_logs_handler;
use crate::http_server::endpoints::moderation::debug_logs::moderation_list_debug_logs_for_token_handler::moderation_list_debug_logs_for_token_handler;
@@ -106,6 +107,12 @@ pub fn add_moderator_routes<T, B> (app: App<T>) -> App<T>
.route(web::head().to(|| HttpResponse::Ok()))
)
)
.service(web::scope("/dashboards")
.service(web::resource("/databox")
.route(web::get().to(moderator_list_databox_dashboards_handler))
.route(web::head().to(|| HttpResponse::Ok()))
)
)
.service(web::scope("/debug_logs")
.service(web::resource("/list/{token}")
.route(web::get().to(moderation_list_debug_logs_for_token_handler))
@@ -34,6 +34,7 @@ use crate::startup::setup_bans::{
load_static_container_ip_bans, load_troll_user_token_bans,
};
use crate::startup::setup_inference_providers::setup_inference_providers;
use crate::startup::setup_dashboards::setup_dashboards;
use crate::startup::setup_seedance_video_bucket::setup_seedance_video_bucket;
use crate::startup::setup_static_feature_flags::setup_static_feature_flags;
use crate::startup::setup_stripe_artcraft::setup_stripe_artcraft;
@@ -262,6 +263,7 @@ pub async fn setup_dependencies(server_hostname: &str) -> AnyhowResult<SetupResu
},
temp_dir_creator: ScopedTempDirCreator::auto_setup(),
google_sign_in_cert: GoogleSignInCert::new(),
dashboards: setup_dashboards(),
};
Ok(SetupResult {
@@ -1,5 +1,6 @@
pub mod build_dependencies;
pub mod setup_bans;
pub mod setup_dashboards;
pub mod setup_metrics;
pub mod setup_pager;
pub mod setup_seedance_video_bucket;
@@ -0,0 +1,38 @@
//! Optional setup for the admin dashboard's embedded metrics dashboards.
//!
//! Reads the `DATABOX_DASHBOARD_*` env vars. Each is independent and
//! optional — a missing var means that dashboard is simply not offered to
//! the admin dashboard. Never blocks startup.
use std::env;
use log::info;
use crate::state::server_state::{Dashboards, DataboxDashboards};
/// Build the dashboard configuration from the environment.
pub fn setup_dashboards() -> Dashboards {
let daus_id = read_non_empty_env("DATABOX_DASHBOARD_DAUS");
let daily_generations_id = read_non_empty_env("DATABOX_DASHBOARD_DAILY_GENERATIONS");
info!(
"Databox dashboards configured: daus={}, daily_generations={}",
daus_id.is_some(),
daily_generations_id.is_some(),
);
Dashboards {
databox: DataboxDashboards {
daus_id,
daily_generations_id,
},
}
}
/// Read an env var, treating unset or whitespace-only as absent.
fn read_non_empty_env(name: &str) -> Option<String> {
match env::var(name) {
Ok(value) if !value.trim().is_empty() => Some(value.trim().to_string()),
_ => None,
}
}
@@ -114,6 +114,10 @@ pub struct ServerState {
pub google_sign_in_cert: GoogleSignInCert,
pub temp_dir_creator: ScopedTempDirCreator,
/// Embedded metrics dashboards for the admin dashboard, configured from
/// optional env vars at startup.
pub dashboards: Dashboards,
}
#[derive(Clone)]
@@ -276,3 +280,17 @@ pub struct TrollBans {
pub struct ResendData {
pub api_key: String,
}
/// Embedded metrics dashboards surfaced to admins/mods.
#[derive(Clone)]
pub struct Dashboards {
pub databox: DataboxDashboards,
}
/// Databox datawall IDs. Each is `None` when its env var isn't configured,
/// in which case the dashboard is simply omitted from the moderation API.
#[derive(Clone)]
pub struct DataboxDashboards {
pub daus_id: Option<String>,
pub daily_generations_id: Option<String>,
}
@@ -57,6 +57,12 @@ export interface UserReferralListItem {
maybe_referral_code_token: string | null;
}
/** A Databox datawall configured server-side for the admin home page. */
export interface DataboxDashboard {
name: string;
id: string;
}
/** Which table the customer id was found in. */
export type ModeratorStripeCustomerIdSource = "customer_link" | "subscription";
@@ -475,6 +481,25 @@ export class ModerationApi extends ApiManager {
}));
}
// Databox metrics dashboards configured server-side, in display order.
// Dashboards without a configured id are omitted by the server.
public async ListDataboxDashboards(): Promise<
ApiResponse<DataboxDashboard[]>
> {
const endpoint = `${this.getApiSchemeAndHost()}/v1/moderation/dashboards/databox`;
return await this.get<{
success: boolean;
databoards: DataboxDashboard[];
error_message?: string;
}>({ endpoint })
.then((response) => ({
success: response.success,
data: response.databoards || [],
errorMessage: response.error_message,
}))
.catch((err) => ({ success: false, errorMessage: err.message }));
}
public async SendAlert(params: {
title?: string | null;
description?: string | null;
@@ -120,9 +120,7 @@ export function MediaCard({
{/* Top-left: media class + upload indicator */}
<div className="absolute top-1.5 left-1.5 z-10 flex items-center gap-1 flex-wrap">
<span className="px-2 py-1 rounded-md bg-black/40 text-[10px] font-medium text-white backdrop-blur-sm uppercase">
{mediaClass === "dimensional" || mediaClass === "mesh" || mediaClass === "splat"
? "3D"
: mediaClass || "Unknown"}
{mediaClass === "dimensional" ? "3D" : mediaClass || "Unknown"}
</span>
{isUserUpload && (
<span
@@ -1,78 +1,31 @@
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts";
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { IconChartBar, IconDashboard, IconWorld } from "@tabler/icons-react";
import { useEffect, useState } from "react";
import { IconDashboard } from "@tabler/icons-react";
import { ModerationApi, type DataboxDashboard } from "@/api/ModerationApi";
import { Skeleton } from "@/components/ui/skeleton";
import { usePageTitle } from "@/hooks/usePageTitle";
const activeUsersData = [
{ date: "02/05", activeUsers: 812 },
{ date: "02/06", activeUsers: 945 },
{ date: "02/07", activeUsers: 1023 },
{ date: "02/08", activeUsers: 876 },
{ date: "02/09", activeUsers: 654 },
{ date: "02/10", activeUsers: 598 },
{ date: "02/11", activeUsers: 901 },
{ date: "02/12", activeUsers: 1102 },
{ date: "02/13", activeUsers: 1045 },
{ date: "02/14", activeUsers: 978 },
{ date: "02/15", activeUsers: 1134 },
{ date: "02/16", activeUsers: 756 },
{ date: "02/17", activeUsers: 689 },
{ date: "02/18", activeUsers: 1023 },
{ date: "02/19", activeUsers: 1187 },
{ date: "02/20", activeUsers: 1056 },
{ date: "02/21", activeUsers: 998 },
{ date: "02/22", activeUsers: 1210 },
{ date: "02/23", activeUsers: 834 },
{ date: "02/24", activeUsers: 712 },
{ date: "02/25", activeUsers: 967 },
{ date: "02/26", activeUsers: 1089 },
{ date: "02/27", activeUsers: 1145 },
{ date: "02/28", activeUsers: 1034 },
{ date: "03/01", activeUsers: 1267 },
{ date: "03/02", activeUsers: 890 },
{ date: "03/03", activeUsers: 756 },
{ date: "03/04", activeUsers: 1123 },
{ date: "03/05", activeUsers: 1198 },
{ date: "03/06", activeUsers: 704 },
];
const countryData = [
{ country: "United States", activeUsers: 704 },
{ country: "India", activeUsers: 105 },
{ country: "Canada", activeUsers: 100 },
{ country: "China", activeUsers: 74 },
{ country: "Brazil", activeUsers: 59 },
{ country: "Argentina", activeUsers: 57 },
{ country: "Australia", activeUsers: 51 },
];
const activeUsersConfig = {
activeUsers: {
label: "Active Users",
color: "hsl(221, 83%, 53%)",
},
} satisfies ChartConfig;
const countryConfig = {
activeUsers: {
label: "Active Users",
color: "hsl(142, 71%, 45%)",
},
} satisfies ChartConfig;
const totalActiveUsers = activeUsersData.reduce(
(sum, d) => sum + d.activeUsers,
0,
);
function databoxEmbedUrl(id: string): string {
return `https://app.databox.com/datawall/${encodeURIComponent(id)}?i`;
}
export function DashboardHome() {
usePageTitle("Dashboard");
const [databoards, setDataboards] = useState<DataboxDashboard[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
let cancelled = false;
const api = new ModerationApi();
api.ListDataboxDashboards().then((resp) => {
if (cancelled) return;
setDataboards(resp.success && resp.data ? resp.data : []);
setIsLoading(false);
});
return () => {
cancelled = true;
};
}, []);
return (
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-2">
@@ -83,87 +36,27 @@ export function DashboardHome() {
<p className="text-muted-foreground">Welcome to the admin dashboard.</p>
</div>
<div className="grid gap-4 lg:grid-cols-5">
{/* Active Users Over Time */}
<Card className="lg:col-span-3">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<div className="flex items-center gap-2">
<IconChartBar className="size-4 text-muted-foreground" />
<CardTitle className="text-sm font-medium">
Active Users (DUMMY CONTENT RIGHT NOW)
</CardTitle>
</div>
<span className="text-2xl font-bold tabular-nums">
{totalActiveUsers.toLocaleString()}
</span>
</CardHeader>
<CardContent>
<ChartContainer
config={activeUsersConfig}
className="aspect-auto h-[300px] w-full"
{isLoading ? (
<Skeleton className="w-full rounded-xl" style={{ paddingTop: "63%" }} />
) : (
databoards.map((databoard) => (
<div key={databoard.id} className="flex flex-col gap-2">
<h2 className="text-lg font-semibold">{databoard.name}</h2>
<div
className="relative w-full overflow-hidden rounded-xl border"
style={{ paddingTop: "63%" }}
>
<BarChart data={activeUsersData}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="date"
tickLine={false}
axisLine={false}
tickMargin={8}
interval="preserveStartEnd"
/>
<YAxis tickLine={false} axisLine={false} tickMargin={8} />
<ChartTooltip
content={<ChartTooltipContent hideLabel={false} />}
/>
<Bar
dataKey="activeUsers"
fill="var(--color-activeUsers)"
radius={[4, 4, 0, 0]}
/>
</BarChart>
</ChartContainer>
</CardContent>
</Card>
{/* Users by Country */}
<Card className="lg:col-span-2">
<CardHeader className="pb-2">
<div className="flex items-center gap-2">
<IconWorld className="size-4 text-muted-foreground" />
<CardTitle className="text-sm font-medium">
Users by Country (DUMMY CONTENT RIGHT NOW)
</CardTitle>
<iframe
src={databoxEmbedUrl(databoard.id)}
title={databoard.name}
className="absolute inset-0 h-full w-full"
frameBorder="0"
allowFullScreen
/>
</div>
</CardHeader>
<CardContent>
<ChartContainer
config={countryConfig}
className="aspect-auto h-[300px] w-full"
>
<BarChart data={countryData} layout="vertical">
<CartesianGrid horizontal={false} />
<XAxis type="number" tickLine={false} axisLine={false} />
<YAxis
type="category"
dataKey="country"
tickLine={false}
axisLine={false}
tickMargin={8}
width={100}
/>
<ChartTooltip
content={<ChartTooltipContent hideLabel={false} />}
/>
<Bar
dataKey="activeUsers"
fill="var(--color-activeUsers)"
radius={[0, 4, 4, 0]}
/>
</BarChart>
</ChartContainer>
</CardContent>
</Card>
</div>
</div>
))
)}
</div>
);
}
@@ -26,9 +26,8 @@ const MEDIA_CLASS_OPTIONS = [
{ value: "", label: "All" },
{ value: "image", label: "Image" },
{ value: "video", label: "Video" },
// "dimensional" is the deprecated pre-split class; rows persist until the
// backfill lands, so the 3D filter matches all three classes.
{ value: "mesh,splat,dimensional", label: "3D" },
{ value: "mesh", label: "Meshes" },
{ value: "splat", label: "Splats" },
] as const;
const PAGE_SIZE = 24;