fix(leptos): correct shared-types timestamps to match JSON wire format

This commit is contained in:
asepharyana
2026-07-03 07:24:28 +07:00
parent 7b746350ca
commit f67bc5125b
12 changed files with 431 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
# API/WS endpoints — set these before building
VITE_BE_API_URL=http://localhost:3001
VITE_BE_WS_URL=ws://localhost:3001/ws
+3
View File
@@ -0,0 +1,3 @@
[workspace]
resolver = "2"
members = ["shared-types", "frontend"]
@@ -0,0 +1,4 @@
[toolchain]
channel = "nightly-2026-06-01"
components = ["rust-src", "rustc-dev"]
targets = ["wasm32-unknown-unknown"]
@@ -0,0 +1,7 @@
[package]
name = "shared-types"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1", features = ["derive"] }
@@ -0,0 +1,83 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DashboardStats {
pub total_messages: u64,
pub total_users: u64,
pub total_flagged: u64,
pub total_clean: u64,
pub total_warned: u64,
pub total_error: u64,
pub total_voice_recordings: u64,
pub total_profiles: u64,
pub today_messages: u64,
pub today_flagged: u64,
pub active_users_24h: u64,
pub top_channels: Vec<TopChannel>,
pub moderation_overview: ModerationOverview,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TopChannel {
pub channel_id: String,
pub channel_name: String,
pub message_count: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModerationOverview {
pub pending: u64,
pub processing: u64,
pub error: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DashboardUser {
pub user_id: String,
pub username: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub profile_summary: Option<String>,
pub total_messages: u64,
pub flagged_count: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_message_at: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trust_score: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DashboardUserDetail {
#[serde(flatten)]
pub user: DashboardUser,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_analyzed_at: Option<i64>,
pub clean_message_streak: u64,
pub total_infractions: u64,
pub clean_count: u64,
pub recent_messages: Vec<super::message::MessageRecord>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DashboardChannel {
pub channel_id: String,
pub channel_name: String,
pub guild_id: String,
pub total_messages: u64,
pub flagged_count: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_message_at: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub culture_summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_analyzed_at: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DashboardChannelDetail {
#[serde(flatten)]
pub channel: DashboardChannel,
pub clean_count: u64,
pub recent_messages: Vec<super::message::MessageRecord>,
}
@@ -0,0 +1,35 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Guild {
pub id: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Channel {
pub id: String,
pub name: String,
#[serde(rename = "type")]
#[serde(skip_serializing_if = "Option::is_none")]
pub channel_type: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GuildVoiceEntry {
pub guild_id: String,
pub channel_id: String,
pub channel_name: String,
pub connected_at: i64,
}
// ── Config ────────────────────────────────────────────────
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub monitor_guild_id: Option<String>,
}
@@ -0,0 +1,7 @@
pub mod message;
pub mod guild;
pub mod voice;
pub mod media;
pub mod dashboard;
pub mod recording;
pub mod ui_state;
@@ -0,0 +1,30 @@
use serde::{Deserialize, Serialize};
pub type MediaMode = String; // "music" | "screen"
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MediaItem {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub source: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mode: Option<MediaMode>,
#[serde(rename = "durationMs")]
#[serde(skip_serializing_if = "Option::is_none")]
pub duration_ms: Option<u64>,
#[serde(rename = "thumbnailUrl")]
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MediaState {
pub playing: bool,
#[serde(rename = "musicVolume")]
pub music_volume: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub current: Option<MediaItem>,
pub queue: Vec<MediaItem>,
}
@@ -0,0 +1,170 @@
use serde::{Deserialize, Serialize};
// ── AI Status ─────────────────────────────────────────────
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum AiStatus {
Pending,
Processing,
Clean,
Warn,
Flagged,
Error,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum AiSeverity {
None,
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum AiRecommendedAction {
None,
Monitor,
Warn,
Review,
Delete,
Escalate,
}
// ── Message Metadata ──────────────────────────────────────
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MessageMetadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub stickers: Option<Vec<StickerInfo>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attachments: Option<Vec<AttachmentRef>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub embeds: Option<Vec<EmbedInfo>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel: Option<ChannelRef>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StickerInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AttachmentRef {
pub name: String,
pub url: String,
#[serde(rename = "contentType")]
#[serde(skip_serializing_if = "Option::is_none")]
pub content_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmbedInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image: Option<EmbedMedia>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail: Option<EmbedMedia>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmbedMedia {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub width: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub height: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChannelRef {
pub channel_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thread_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thread_name: Option<String>,
}
// ── Message Record ────────────────────────────────────────
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessageRecord {
pub id: String,
pub guild_id: String,
pub channel_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub thread_id: Option<String>,
pub user_id: String,
pub username: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar_url: Option<String>,
pub content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub edited_content: Option<String>,
#[serde(rename = "type")]
pub msg_type: String, // "text" | "edited" | "deleted"
pub created_at: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub edited_at: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deleted_at: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ai_status: Option<AiStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ai_severity: Option<AiSeverity>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ai_confidence: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ai_moderation_flags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ai_moderation_score: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ai_analysis: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ai_categories: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ai_recommended_action: Option<AiRecommendedAction>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ai_error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ai_analyzed_at: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<MessageMetadata>,
}
// ── Pagination ────────────────────────────────────────────
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageResult<T> {
pub data: Vec<T>,
#[serde(rename = "nextCursor")]
pub next_cursor: Option<String>,
}
// ── Attachment ────────────────────────────────────────────
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AttachmentRecord {
pub id: String,
pub message_id: String,
pub guild_id: String,
pub channel_id: String,
pub filename: String,
pub size: u64,
#[serde(rename = "type")]
pub mime_type: String,
pub discord_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub uploaded_url: Option<String>,
pub upload_status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub upload_error: Option<String>,
pub created_at: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub uploaded_at: Option<i64>,
}
@@ -0,0 +1,36 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VoiceRecording {
pub id: String,
pub user_id: String,
pub username: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub guild_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channel_name: Option<String>,
pub filename: String,
pub size_bytes: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub download_url: Option<String>,
pub upload_status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub upload_error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub transcription: Option<String>,
pub created_at: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub uploaded_at: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VoiceRecordingListResponse {
pub items: Vec<VoiceRecording>,
#[serde(rename = "nextCursor")]
pub next_cursor: Option<String>,
pub has_more: bool,
}
@@ -0,0 +1,29 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum Tab {
Messages,
Live,
Dashboard,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UiState {
#[serde(skip_serializing_if = "Option::is_none")]
pub selected_guild: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub selected_voice_guild: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub selected_voice_channel: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub selected_text_guild: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub selected_text_channel: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub active_tab: Option<Tab>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_listening: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_streaming: Option<bool>,
}
@@ -0,0 +1,24 @@
use serde::{Deserialize, Serialize};
use crate::guild::GuildVoiceEntry;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VoiceStatus {
pub connected: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub active_guild_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub active_channel_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub active_channel_name: Option<String>,
pub connections: Vec<GuildVoiceEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActiveSpeaker {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub user_id: String,
pub username: String,
pub avatar: String,
pub speaking: bool,
}