refactor(fe): align dashboard with backend & gateway data flow

- API/WS clients: same-origin by default, drop dead imphnen hardcode
- chatbot history: map BE rows {user_message,bot_response,created_at}
- message_deleted WS payload: object {id,deleted_at}, not bare string
- dashboard: wire top-channels chart + live mod queue from /api/review,
  add Users & Channels tabs consuming /api/dashboard/users|channels
- recordings: live WS sync via voice_recording_uploaded; duration_bytes optional
- remove dead widgets with no BE data source (trend chart, heatmap)
This commit is contained in:
Developer
2026-07-31 16:54:41 +07:00
parent 2addfb6492
commit 69213ebd75
17 changed files with 611 additions and 224 deletions
+5 -2
View File
@@ -1,11 +1,14 @@
import type { ChatbotResponse, ChatHistoryMessage } from "@/lib/types";
import type { ChatbotHistoryRow, ChatbotResponse } from "@/lib/types";
import { api } from "./client";
export const chatbotApi = {
send: (message: string) =>
api.post<ChatbotResponse>("/api/chat", { message }),
getHistory: () => api.get<ChatHistoryMessage[]>("/api/chat/history"),
getHistory: () =>
api.get<{ history: ChatbotHistoryRow[]; total: number }>(
"/api/chat/history",
),
clearHistory: () => api.delete<{ ok: boolean }>("/api/chat/history"),
};
+12 -10
View File
@@ -8,21 +8,23 @@ export class ApiError extends Error {
}
}
const REMOTE_API = "https://imphnen.asepharyana.my.id";
/**
* API base URL resolution.
*
* Default: same-origin — the production nginx (gmw-proxy) proxies /api/* to
* the backend, so no cross-origin config is needed. For local dev against a
* remote deployment, set NEXT_PUBLIC_API_URL (e.g. https://imphnen.asepharyana.my.id).
*/
function getBaseUrl(): string {
if (typeof window === "undefined") return REMOTE_API;
const hostname = window.location.hostname;
const override =
typeof process !== "undefined" ? process.env.NEXT_PUBLIC_API_URL : "";
if (override) return override.replace(/\/+$/, "");
// In local dev, route API calls to the remote server
if (hostname === "localhost" || hostname === "127.0.0.1") {
return REMOTE_API;
}
if (typeof window === "undefined") return "";
// Production: nginx proxies /api/* to backend on the same host
const protocol = window.location.protocol.replace(":", "");
const port = window.location.port;
return `${protocol}://${hostname}${port ? `:${port}` : ""}`;
return `${protocol}://${window.location.hostname}${port ? `:${port}` : ""}`;
}
export async function apiRequest<T>(
+2 -1
View File
@@ -8,7 +8,8 @@ export interface VoiceRecording {
channel_name?: string | null;
filename: string;
size_bytes: number;
duration_bytes: number;
/** Present on REST rows; absent on WS voice_recording_uploaded events */
duration_bytes?: number | null;
download_url?: string | null;
upload_status: string;
upload_error?: string | null;
+11 -4
View File
@@ -16,8 +16,15 @@ export interface ChatbotResponse {
timestamp: string;
}
export interface ChatHistoryMessage {
role: string;
content: string;
timestamp: string;
/**
* Chat history row as returned by the backend (GET /api/chat/history →
* { history: ChatbotHistoryRow[], total }).
*/
export interface ChatbotHistoryRow {
id: string;
user_id: string;
user_message: string;
bot_response: string;
context: Record<string, unknown> | null;
created_at: string;
}
+9 -10
View File
@@ -2,21 +2,20 @@ import type { WsEvent, WsStatus } from "./types";
type WsEventCallback = (event: WsEvent) => void;
const REMOTE_WS = "wss://imphnen.asepharyana.my.id/ws";
/**
* WebSocket URL resolution — same-origin by default (gmw-proxy nginx
* proxies /ws to the backend). Override for local dev with NEXT_PUBLIC_WS_URL.
*/
function getWsUrl(): string {
if (typeof window === "undefined") return REMOTE_WS;
const hostname = window.location.hostname;
const override =
typeof process !== "undefined" ? process.env.NEXT_PUBLIC_WS_URL : "";
if (override) return override.replace(/\/+$/, "");
// Always route WS through the remote server (even from local dev)
if (hostname === "localhost" || hostname === "127.0.0.1") {
return REMOTE_WS;
}
if (typeof window === "undefined") return "wss://localhost/ws";
// Production: nginx proxies /ws/* to backend on the same host
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
const port = window.location.port;
return `${protocol}://${hostname}${port ? `:${port}` : ""}/ws`;
return `${protocol}://${window.location.hostname}${port ? `:${port}` : ""}/ws`;
}
export class WsConnection {
+2 -1
View File
@@ -28,7 +28,8 @@ export interface WsBinaryEvent {
export interface WsEventMap {
message_created: MessageRecord;
message_updated: MessageRecord;
message_deleted: string; // message ID
/** Gateway emits { id, deleted_at } — NOT a bare string */
message_deleted: { id: string; deleted_at?: number };
message_analyzed: MessageRecord;
attachment_created: unknown;
attachment_uploaded: unknown;