From d473b0f59e71bc33924599af7af7325af4b45b83 Mon Sep 17 00:00:00 2001 From: lethdat Date: Thu, 2 Apr 2026 15:01:05 +0000 Subject: [PATCH] update ssr --- .deploy/stream.ui-production.yaml | 7 +- components.d.ts | 2 - curl.text | 21 ++++++ package.json | 2 +- src/components/DashboardLayout.vue | 2 - src/components/NotificationDrawer.vue | 1 - src/components/dashboard/EmptyState.vue | 6 -- src/index.tsx | 4 ++ src/lib/translation/index.ts | 69 ++++++++++--------- src/lib/utils.ts | 28 ++++---- src/main.ts | 3 +- src/routes/settings/admin/AdTemplates.vue | 7 +- src/routes/settings/admin/Agents.vue | 7 +- src/routes/settings/admin/Jobs.vue | 27 +++++--- src/routes/settings/admin/Payments.vue | 7 +- src/routes/settings/admin/PlayerConfigs.vue | 7 +- src/routes/settings/admin/Users.vue | 7 +- src/routes/settings/admin/Videos.vue | 7 +- .../admin/components/AdminSectionCard.vue | 7 -- .../settings/admin/components/AdminTable.vue | 13 ---- src/server/middlewares/setup.ts | 1 + vite.config.ts | 3 +- 22 files changed, 118 insertions(+), 120 deletions(-) create mode 100644 curl.text diff --git a/.deploy/stream.ui-production.yaml b/.deploy/stream.ui-production.yaml index ac4384a..becff6c 100644 --- a/.deploy/stream.ui-production.yaml +++ b/.deploy/stream.ui-production.yaml @@ -12,6 +12,7 @@ data: STREAM_INTERNAL_AUTH_MARKER: "stream_maker_123xxx" STREAM_UI_JWT_SECRET: "xxx_stream_maker_123_xxx" STREAM_UI_REDIS_URL: "redis://:pass123@47.84.62.226:6379/3" + FRONTEND_BASE_URL: "https://hlstiktok.com" --- kind: Service apiVersion: v1 @@ -79,4 +80,8 @@ spec: configMapKeyRef: name: stream-ui-config key: STREAM_UI_REDIS_URL - + - name: FRONTEND_BASE_URL + valueFrom: + configMapKeyRef: + name: stream-ui-config + key: FRONTEND_BASE_URL diff --git a/components.d.ts b/components.d.ts index 6f7f8d8..51db3fc 100644 --- a/components.d.ts +++ b/components.d.ts @@ -38,7 +38,6 @@ declare module 'vue' { CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.vue')['default'] ClientOnly: typeof import('./src/components/ClientOnly.tsx')['default'] CoinsIcon: typeof import('./src/components/icons/CoinsIcon.vue')['default'] - copy: typeof import('./src/components/icons/UserIcon copy.vue')['default'] Credit: typeof import('./src/components/icons/Credit.vue')['default'] CreditCardIcon: typeof import('./src/components/icons/CreditCardIcon.vue')['default'] DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default'] @@ -132,7 +131,6 @@ declare global { const CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.vue')['default'] const ClientOnly: typeof import('./src/components/ClientOnly.tsx')['default'] const CoinsIcon: typeof import('./src/components/icons/CoinsIcon.vue')['default'] - const copy: typeof import('./src/components/icons/UserIcon copy.vue')['default'] const Credit: typeof import('./src/components/icons/Credit.vue')['default'] const CreditCardIcon: typeof import('./src/components/icons/CreditCardIcon.vue')['default'] const DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default'] diff --git a/curl.text b/curl.text new file mode 100644 index 0000000..a566c48 --- /dev/null +++ b/curl.text @@ -0,0 +1,21 @@ +curl --request POST \ + --url https://api.github.com/repos/lethdat09/builder/dispatches \ + --header 'accept: */*' \ + --header 'authorization: Bearer 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' \ + --header 'content-type: application/json' \ + --header 'user-agent: Thunder Client (https://www.thunderclient.io)' \ + --data '{ + "event_type": "trigger_build", + "client_payload": { + "gitUrl": "https://git.inet.io.vn/stream/stream.api.git", + "branch": "develop-refactor", + "imageName": "stream123/stream.api", + "dockerfilePath": "Dockerfile", + "kubeConfigYamlPath": ".deploy/stream.api-production.yaml", + "kubeConfig": "YXBpVmVyc2lvbjogdjEKY2x1c3RlcnM6Ci0gY2x1c3RlcjoKICAgIGNlcnRpZmljYXRlLWF1dGhvcml0eS1kYXRhOiBMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VKa2VrTkRRVkl5WjBGM1NVSkJaMGxDUVVSQlMwSm5aM0ZvYTJwUFVGRlJSRUZxUVdwTlUwVjNTSGRaUkZaUlVVUkVRbWh5VFROTmRHTXlWbmtLWkcxV2VVeFhUbWhSUkVVelRucFJOVTVFU1RCT2VrMTNTR2hqVGsxcVdYZE5lazE0VFVSWmVrNUVUWHBYYUdOT1RYcFpkMDE2U1RSTlJGbDZUa1JOZWdwWGFrRnFUVk5GZDBoM1dVUldVVkZFUkVKb2NrMHpUWFJqTWxaNVpHMVdlVXhYVG1oUlJFVXpUbnBSTlU1RVNUQk9lazEzVjFSQlZFSm5ZM0ZvYTJwUENsQlJTVUpDWjJkeGFHdHFUMUJSVFVKQ2QwNURRVUZUWm5sUVRHaE5kVEJvZFVNelpFb3JlbFZHV0ZVNVYyMHdLM1YxVUhSUFVVTjRSMFZSYkV4ak9Wa0tZV3RsYm1kc1JFZDRTRGs1UjBKcFRFOHlka1pZTm5oalZYcFdka040T0U0NFpqWm9NREpFZVZJNFJGTnZNRWwzVVVSQlQwSm5UbFpJVVRoQ1FXWTRSUXBDUVUxRFFYRlJkMFIzV1VSV1VqQlVRVkZJTDBKQlZYZEJkMFZDTDNwQlpFSm5UbFpJVVRSRlJtZFJWVWhuUTFGWFVVNHlLM1p4TlZKWmRFcFdVVEJNQ2toa00xVlhkMGwzUTJkWlNVdHZXa2w2YWpCRlFYZEpSRk5CUVhkU1VVbG5SbXBDU1hoMFFUVXdRMmwyZFdoVVUzbFZRalpqYjBSU2FWWjBWVVYzUVZrS2VYWjZXRGxHUm5CcVl6aERTVkZFUzBGVFNrZFBaRUZXUW01TmJsRTNWa3BpVVVkWldFRlJSMjlwTmpCRlpuZzVZMUprWTA5UVJWQTFkejA5Q2kwdExTMHRSVTVFSUVORlVsUkpSa2xEUVZSRkxTMHRMUzBLCiAgICBzZXJ2ZXI6IGh0dHBzOi8vNDIuOTYuMTUuMTA5OjY0NDMKICBuYW1lOiBkZWZhdWx0CmNvbnRleHRzOgotIGNvbnRleHQ6CiAgICBjbHVzdGVyOiBkZWZhdWx0CiAgICB1c2VyOiBkZWZhdWx0CiAgbmFtZTogZGVmYXVsdApjdXJyZW50LWNvbnRleHQ6IGRlZmF1bHQKa2luZDogQ29uZmlnCnVzZXJzOgotIG5hbWU6IGRlZmF1bHQKICB1c2VyOgogICAgY2xpZW50LWNlcnRpZmljYXRlLWRhdGE6IExTMHRMUzFDUlVkSlRpQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENrMUpTVUpyUkVORFFWUmxaMEYzU1VKQlowbEpabG95WW10NGJVRTFTRFIzUTJkWlNVdHZXa2w2YWpCRlFYZEpkMGw2UldoTlFqaEhRVEZWUlVGM2Qxa0tZWHBPZWt4WFRuTmhWMVoxWkVNeGFsbFZRWGhPZW1Nd1QxUlJlVTVFWTNwTlFqUllSRlJKTWsxRVRYcE5WRUV5VFhwUmVrMHhiMWhFVkVrelRVUk5lZ3BOVkVFeVRYcFJlazB4YjNkTlJFVllUVUpWUjBFeFZVVkRhRTFQWXpOc2VtUkhWblJQYlRGb1l6TlNiR051VFhoR1ZFRlVRbWRPVmtKQlRWUkVTRTQxQ21NelVteGlWSEJvV2tjeGNHSnFRbHBOUWsxSFFubHhSMU5OTkRsQlowVkhRME54UjFOTk5EbEJkMFZJUVRCSlFVSkZjVE0wUWl0U05tdEVXVzlQY213S1dTdDFiMFpTTjBOdFJUTTVVRk54U1hNeGFYWllWak5aVjBoUmF6bHdSVlpZUm5GWVpYQnZXVmg0TW5KM1pVbFlTVEZTY1dGSU9TdHJPVGw0WkM5c1FRb3ZZamxRWm14UGFsTkVRa2ROUVRSSFFURlZaRVIzUlVJdmQxRkZRWGRKUm05RVFWUkNaMDVXU0ZOVlJVUkVRVXRDWjJkeVFtZEZSa0pSWTBSQmFrRm1Da0puVGxaSVUwMUZSMFJCVjJkQ1V6azFRa0pRWWxWT2MwaFljeXR6WXpoTmNWaHJZWEF3VVhscFJFRkxRbWRuY1docmFrOVFVVkZFUVdkT1NFRkVRa1VLUVdsQ1RXZFJVVGRaY0c5WlMwcDNiMFIyVTBNMlMwVnhaM0VyTkZWTkt6Vkxja2hVV0d0UVFuRTBVazFrUVVsblF6SmhPV0owZDNwdGMwUkZZVFpKVWdwNmRucFpOUzlLUjBKRVZrOUNkM28wV0ZNNU0xaFVkR2h0UW5jOUNpMHRMUzB0UlU1RUlFTkZVbFJKUmtsRFFWUkZMUzB0TFMwS0xTMHRMUzFDUlVkSlRpQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENrMUpTVUpsUkVORFFWSXlaMEYzU1VKQlowbENRVVJCUzBKblozRm9hMnBQVUZGUlJFRnFRV3BOVTBWM1NIZFpSRlpSVVVSRVFtaHlUVE5OZEZreWVIQUtXbGMxTUV4WFRtaFJSRVV6VG5wUk5VNUVTVEJPZWsxM1NHaGpUazFxV1hkTmVrMTRUVVJaZWs1RVRYcFhhR05PVFhwWmQwMTZTVFJOUkZsNlRrUk5lZ3BYYWtGcVRWTkZkMGgzV1VSV1VWRkVSRUpvY2swelRYUlpNbmh3V2xjMU1FeFhUbWhSUkVVelRucFJOVTVFU1RCT2VrMTNWMVJCVkVKblkzRm9hMnBQQ2xCUlNVSkNaMmR4YUd0cVQxQlJUVUpDZDA1RFFVRlNabTFRYTBaT1pYTnNhV05aZFhSUGJrVmtVbmN2S3pCVE0yVkxkSGNyU0ZwbmJIcFVRazF3WVdrS2RYQjFXWFJuVmpad2IwdG9kSGhUYVhFdk5rWktRa0owZWtoSlNsSjRUMlp0V1RnemVtaENVbE5oUlZOdk1FbDNVVVJCVDBKblRsWklVVGhDUVdZNFJRcENRVTFEUVhGUmQwUjNXVVJXVWpCVVFWRklMMEpCVlhkQmQwVkNMM3BCWkVKblRsWklVVFJGUm1kUlZYWmxVVkZVTWpGRVlrSXhOMUJ5U0ZCRVMydzFDa2R4WkVWTmIyZDNRMmRaU1V0dldrbDZhakJGUVhkSlJGTlJRWGRTWjBsb1FVb3pNVVJWTUhSaFRHVnNWVFJpUVcxUlRYSnJNMEpvT0doSWNuUTNhamtLYkRka1p6YzFhelJ5Vlc1MVFXbEZRWGhsVDFCaFVVUTBTWHBNYzBwVmRITkpOWGRWUzBoUFZWTnFWblE1U20xVWMwSTRTVnB0TTBOM1lXODlDaTB0TFMwdFJVNUVJRU5GVWxSSlJrbERRVlJGTFMwdExTMEsKICAgIGNsaWVudC1rZXktZGF0YTogTFMwdExTMUNSVWRKVGlCRlF5QlFVa2xXUVZSRklFdEZXUzB0TFMwdENrMUlZME5CVVVWRlNVTk9hVlp2VG1KVGRHZEJWSEJzT1ZSTlpWbHlOMHBUWkVoRk5qZElWMWxrWkZOc05UTmFSbFpUZEhodlFXOUhRME54UjFOTk5Ea0tRWGRGU0c5VlVVUlJaMEZGVTNKbVowZzFTSEZSVG1sbk5uVldhalkyWjFaSWMwdFpWR1l3T1V0dmFYcFhTemxrV0dSb1dXUkRWREpyVWxaalYzQmtOZ3B0YUdobVNHRjJRalJvWTJwV1IzQnZaak0yVkRNelJqTXJWVVE1ZGpBNUsxVjNQVDBLTFMwdExTMUZUa1FnUlVNZ1VGSkpWa0ZVUlNCTFJWa3RMUzB0TFFvPQo=", + "quay_username": "lethdat", + "quay_token": "htK3xi1/mQdOSQyBxbGVr9Hhpm/ywzNGawjk29lNHZcRXRdec7kc1v9LRE6X1ATE", + "telegram_chat_id": "-4891576755", + "tele_token": "8230541188:AAGNu6-2iBaFu2JkvORtXM9c6dUZQdQdqYU" + } +}' \ No newline at end of file diff --git a/package.json b/package.json index 9406e59..a690d9c 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "type": "module", "scripts": { "dev": "bun x --bun vite", - "build": "bun x --bun vite build", + "build": "bun x --bun vite build && bun build dist/server/index.js --target=bun --outfile dist/index.js", "preview": "bun x --bun vite preview" }, "dependencies": { diff --git a/src/components/DashboardLayout.vue b/src/components/DashboardLayout.vue index 5ce8c9d..3da885f 100644 --- a/src/components/DashboardLayout.vue +++ b/src/components/DashboardLayout.vue @@ -2,7 +2,6 @@ import Upload from "@/routes/upload/Upload.vue"; import DashboardNav from "./DashboardNav.vue"; import GlobalUploadIndicator from "./GlobalUploadIndicator.vue"; -import PopupAdsRuntime from "./PopupAdsRuntime.vue"; @@ -23,6 +22,5 @@ import PopupAdsRuntime from "./PopupAdsRuntime.vue"; - diff --git a/src/components/NotificationDrawer.vue b/src/components/NotificationDrawer.vue index 3d438b6..3d36046 100644 --- a/src/components/NotificationDrawer.vue +++ b/src/components/NotificationDrawer.vue @@ -22,7 +22,6 @@ const unreadCount = computed(() => notificationStore.unreadCount.value); const mutableNotifications = computed(() => notificationStore.notifications.value.slice(0, 8)); const toggle = (event?: Event) => { - console.log(event); visible.value = !visible.value; if (visible.value && !notificationStore.loaded.value) { void notificationStore.fetchNotifications(); diff --git a/src/components/dashboard/EmptyState.vue b/src/components/dashboard/EmptyState.vue index 201141b..597651d 100644 --- a/src/components/dashboard/EmptyState.vue +++ b/src/components/dashboard/EmptyState.vue @@ -47,9 +47,3 @@ const props = defineProps(); - - diff --git a/src/index.tsx b/src/index.tsx index d1c4fd8..abe3271 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -6,6 +6,7 @@ import { registerRpcRoutes } from './server/routes/rpc'; import { registerSSRRoutes } from './server/routes/ssr'; import { registerWellKnownRoutes } from './server/routes/wellKnown'; import { setupServices } from './server/services/grpcClient'; +import { serveStatic } from 'hono/bun'; const app = new Hono(); // Global middlewares setupMiddlewares(app); @@ -14,6 +15,9 @@ setupServices(app); registerWellKnownRoutes(app); registerAuthRoutes(app); registerRpcRoutes(app); + if (!import.meta.env.DEV) { + app.use(serveStatic({ root: './dist/client' })) + } registerSSRRoutes(app); export default app; diff --git a/src/lib/translation/index.ts b/src/lib/translation/index.ts index 6c4bbe0..b2de798 100644 --- a/src/lib/translation/index.ts +++ b/src/lib/translation/index.ts @@ -1,42 +1,43 @@ import i18next from "i18next"; import I18NextHttpBackend, { HttpBackendOptions } from "i18next-http-backend"; + const backendOptions: HttpBackendOptions = { - loadPath: 'http://localhost:5173/locales/{{lng}}/{{ns}}.json', - request: (_options, url, _payload, callback) => { - fetch(url) - .then((res) => - res.json().then((r) => { - callback(null, { - data: JSON.stringify(r), - status: 200, - }) - }) - ) - .catch(() => { - callback(null, { - status: 500, - data: '', - }) - }) + loadPath: `${process.env.FRONTEND_BASE_URL || 'http://localhost:3000'}/locales/{{lng}}/{{ns}}.json`, + + request: async (_options, url, _payload, callback) => { + try { + const res = await fetch(url); + + if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`); + + const data = await res.json(); + + callback(null, { data, status: 200 }); + } catch (error) { + console.error("Lỗi fetch file ngôn ngữ i18n:", error); + callback(error as any, { status: 500, data: '' }); + } }, } -export const createI18nInstance = (lng: string) => { - console.log('Initializing i18n with language:', lng); -const i18n = i18next.createInstance(); -i18n - .use(I18NextHttpBackend) - .init({ - lng, - supportedLngs: ["en", "vi"], - fallbackLng: "en", - defaultNS: "translation", - ns: ['translation'], - interpolation: { - escapeValue: false, - }, - backend: backendOptions, - }); +export const createI18nInstance = async (lng: string) => { + const i18n = i18next.createInstance(); + + await i18n + .use(I18NextHttpBackend) + .init({ + lng, + supportedLngs: ["en", "vi"], + fallbackLng: "en", + defaultNS: "translation", + ns: ['translation'], + interpolation: { + escapeValue: false, + }, + backend: backendOptions, + }); + return i18n; }; -export default createI18nInstance; + +export default createI18nInstance; \ No newline at end of file diff --git a/src/lib/utils.ts b/src/lib/utils.ts index cbd249f..fa224c1 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -81,18 +81,22 @@ export const formatDate = ( dateOnly: boolean = false ) => { if (!dateString) return ""; - const locale = - typeof document !== "undefined" - ? document.documentElement.lang === "vi" - ? "vi-VN" - : "en-US" - : "en-US"; - return new Date(dateString).toLocaleDateString(locale, { - month: "short", - day: "numeric", - year: "numeric", - ...(dateOnly ? {} : { hour: "2-digit", minute: "2-digit" }), - }); + + const date = new Date(dateString); + if (Number.isNaN(date.getTime())) return dateString; + + const year = date.getUTCFullYear(); + const month = `${date.getUTCMonth() + 1}`.padStart(2, "0"); + const day = `${date.getUTCDate()}`.padStart(2, "0"); + + if (dateOnly) { + return `${year}-${month}-${day}`; + } + + const hours = `${date.getUTCHours()}`.padStart(2, "0"); + const minutes = `${date.getUTCMinutes()}`.padStart(2, "0"); + + return `${year}-${month}-${day} ${hours}:${minutes} UTC`; }; type Status = "success" | "failed" | "pending" | string; export const getStatusSeverity = (status: Status = "") => { diff --git a/src/main.ts b/src/main.ts index c0e879e..7371d96 100644 --- a/src/main.ts +++ b/src/main.ts @@ -32,7 +32,8 @@ export async function createApp(lng: string = 'en') { } }); app.use(pinia); - app.use(I18NextVue, { i18next: createI18nInstance(lng) }); + const i18next = await createI18nInstance(lng); + app.use(I18NextVue, { i18next }); app.use(PiniaColada, { pinia, plugins: [ diff --git a/src/routes/settings/admin/AdTemplates.vue b/src/routes/settings/admin/AdTemplates.vue index 33bb06d..c5d9b2e 100644 --- a/src/routes/settings/admin/AdTemplates.vue +++ b/src/routes/settings/admin/AdTemplates.vue @@ -13,6 +13,7 @@ import AdminMetricCard from "./components/AdminMetricCard.vue"; import AdminPlaceholderTable from "./components/AdminPlaceholderTable.vue"; import AdminSectionShell from "./components/AdminSectionShell.vue"; import { useAdminPageHeader } from "./components/useAdminPageHeader"; +import { formatDate } from "@/lib/utils"; type ListTemplatesResponse = Awaited>; type AdminAdTemplateRow = NonNullable[number]; @@ -238,11 +239,7 @@ const nextPage = async () => { await loadTemplates(); }; -const formatDate = (value?: string) => { - if (!value) return "—"; - const date = new Date(value); - return Number.isNaN(date.getTime()) ? value : date.toLocaleString(); -}; +const formatAdminDate = (value?: string) => formatDate(value || "") || "—"; const columns = computed[]>(() => [ { diff --git a/src/routes/settings/admin/Agents.vue b/src/routes/settings/admin/Agents.vue index 85d67bb..0d1c41c 100644 --- a/src/routes/settings/admin/Agents.vue +++ b/src/routes/settings/admin/Agents.vue @@ -11,6 +11,7 @@ import AdminMetricCard from "./components/AdminMetricCard.vue"; import AdminPlaceholderTable from "./components/AdminPlaceholderTable.vue"; import AdminSectionShell from "./components/AdminSectionShell.vue"; import { useAdminPageHeader } from "./components/useAdminPageHeader"; +import { formatDate } from "@/lib/utils"; type ListAgentsResponse = Awaited>; type AdminAgentRow = NonNullable[number]; @@ -128,11 +129,7 @@ const submitUpdate = async () => { } }; -const formatDate = (value?: string) => { - if (!value) return "—"; - const date = new Date(value); - return Number.isNaN(date.getTime()) ? value : date.toLocaleString(); -}; +const formatAdminDate = (value?: string) => formatDate(value || "") || "—"; const formatCpu = (value?: number) => `${Number(value ?? 0).toFixed(1)}%`; const formatRam = (value?: number) => `${Number(value ?? 0).toFixed(1)} MB`; diff --git a/src/routes/settings/admin/Jobs.vue b/src/routes/settings/admin/Jobs.vue index cdc9559..a5a6efb 100644 --- a/src/routes/settings/admin/Jobs.vue +++ b/src/routes/settings/admin/Jobs.vue @@ -13,6 +13,7 @@ import AdminMetricCard from "./components/AdminMetricCard.vue"; import AdminPlaceholderTable from "./components/AdminPlaceholderTable.vue"; import AdminSectionShell from "./components/AdminSectionShell.vue"; import { useAdminPageHeader } from "./components/useAdminPageHeader"; +import { formatDate } from "@/lib/utils"; type ListJobsResponse = Awaited>; type AdminJobRow = NonNullable[number]; @@ -85,7 +86,7 @@ const selectedMeta = computed(() => { { label: "Priority", value: String(selectedRow.value.priority ?? 0) }, { label: "Progress", value: formatProgress(selectedRow.value.progress) }, { label: "Owner", value: selectedRow.value.userId || "—" }, - { label: "Updated", value: formatDate(selectedRow.value.updatedAt) }, + { label: "Updated", value: formatAdminDate(selectedRow.value.updatedAt) }, ]; }); @@ -180,6 +181,10 @@ const openDetailDialog = async (row: AdminJobRow) => { actionError.value = null; selectedLogs.value = "Loading logs..."; detailOpen.value = true; + if (!row.id) { + selectedLogs.value = "No logs available."; + return; + } try { await loadSelectedLogs(row.id); } catch { @@ -192,6 +197,11 @@ const openLogsDialog = async (row: AdminJobRow) => { actionError.value = null; selectedLogs.value = "Loading logs..."; logsOpen.value = true; + if (!row.id) { + selectedLogs.value = ""; + actionError.value = "Failed to load job logs"; + return; + } try { await loadSelectedLogs(row.id); } catch (err: any) { @@ -266,11 +276,7 @@ const submitRetry = async () => { } }; -const formatDate = (value?: string) => { - if (!value) return "—"; - const date = new Date(value); - return Number.isNaN(date.getTime()) ? value : date.toLocaleString(); -}; +const formatAdminDate = (value?: string): string => formatDate(value || "") || "—"; const formatProgress = (value?: number) => `${Number(value ?? 0).toFixed(2)}%`; @@ -343,7 +349,7 @@ const columns = computed[]>(() => [ id: "updated", header: "Updated", accessorFn: row => row.updatedAt || "", - cell: ({ row }) => h("span", { class: "text-foreground/60" }, formatDate(row.original.updatedAt)), + cell: ({ row }) => h("span", { class: "text-foreground/60" }, formatAdminDate(row.original.updatedAt)), meta: { headerClass: "px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50", cellClass: "px-4 py-3", @@ -393,8 +399,11 @@ useAdminRuntimeMqtt(({ topic, payload }) => { if (selectedRow.value?.id === payload.job_id && typeof payload.line === "string") { const nextLine = payload.line.endsWith("\n") ? payload.line : `${payload.line}\n`; selectedLogs.value = `${selectedLogs.value === "Loading logs..." || selectedLogs.value === "No logs available." ? "" : selectedLogs.value}${nextLine}`; - selectedRow.value.progress = payload.progress ?? selectedRow.value.progress; - selectedRow.value.updatedAt = new Date().toISOString(); + const selected = selectedRow.value; + if (selected) { + selected.progress = payload.progress ?? selected.progress; + selected.updatedAt = new Date().toISOString(); + } } } diff --git a/src/routes/settings/admin/Payments.vue b/src/routes/settings/admin/Payments.vue index b852b2b..2263c7a 100644 --- a/src/routes/settings/admin/Payments.vue +++ b/src/routes/settings/admin/Payments.vue @@ -14,6 +14,7 @@ import AdminMetricCard from "./components/AdminMetricCard.vue"; import AdminPlaceholderTable from "./components/AdminPlaceholderTable.vue"; import AdminSectionShell from "./components/AdminSectionShell.vue"; import { useAdminPageHeader } from "./components/useAdminPageHeader"; +import { formatDate } from "@/lib/utils"; type ListPaymentsResponse = Awaited>; type AdminPaymentRow = NonNullable[number]; @@ -201,11 +202,7 @@ const nextPage = async () => { await loadPayments(); }; -const formatDate = (value?: string) => { - if (!value) return "—"; - const date = new Date(value); - return Number.isNaN(date.getTime()) ? value : date.toLocaleString(); -}; +const formatAdminDate = (value?: string) => formatDate(value || "") || "—"; const formatMoney = (amount?: number, currency?: string) => `${amount ?? 0} ${currency || "USD"}`; diff --git a/src/routes/settings/admin/PlayerConfigs.vue b/src/routes/settings/admin/PlayerConfigs.vue index 01755ff..20349b8 100644 --- a/src/routes/settings/admin/PlayerConfigs.vue +++ b/src/routes/settings/admin/PlayerConfigs.vue @@ -12,6 +12,7 @@ import AdminMetricCard from "./components/AdminMetricCard.vue"; import AdminPlaceholderTable from "./components/AdminPlaceholderTable.vue"; import AdminSectionShell from "./components/AdminSectionShell.vue"; import { useAdminPageHeader } from "./components/useAdminPageHeader"; +import { formatDate } from "@/lib/utils"; type ListConfigsResponse = Awaited>; type AdminPlayerConfigRow = NonNullable[number]; @@ -287,11 +288,7 @@ const nextPage = async () => { await loadConfigs(); }; -const formatDate = (value?: string) => { - if (!value) return "—"; - const date = new Date(value); - return Number.isNaN(date.getTime()) ? value : date.toLocaleString(); -}; +const formatAdminDate = (value?: string) => formatDate(value || "") || "—"; useAdminPageHeader(() => ({ eyebrow: 'Playback', diff --git a/src/routes/settings/admin/Users.vue b/src/routes/settings/admin/Users.vue index bb6c8fe..17299a2 100644 --- a/src/routes/settings/admin/Users.vue +++ b/src/routes/settings/admin/Users.vue @@ -13,6 +13,7 @@ import AdminPlaceholderTable from "./components/AdminPlaceholderTable.vue"; import AdminSectionShell from "./components/AdminSectionShell.vue"; import AdminUserFormFields from "./components/AdminUserFormFields.vue"; import { useAdminPageHeader } from "./components/useAdminPageHeader"; +import { formatDate } from "@/lib/utils"; type ListUsersResponse = Awaited>; type AdminUserRow = NonNullable[number]; @@ -343,11 +344,7 @@ const nextPage = async () => { await loadUsers(); }; -const formatDate = (value?: string) => { - if (!value) return "—"; - const date = new Date(value); - return Number.isNaN(date.getTime()) ? value : date.toLocaleString(); -}; +const formatAdminDate = (value?: string) => formatDate(value || "") || "—"; const roleBadgeClass = (role?: string) => { const normalized = String(role || "USER").toUpperCase(); diff --git a/src/routes/settings/admin/Videos.vue b/src/routes/settings/admin/Videos.vue index 3e2ec36..a4a4dfd 100644 --- a/src/routes/settings/admin/Videos.vue +++ b/src/routes/settings/admin/Videos.vue @@ -13,6 +13,7 @@ import AdminMetricCard from "./components/AdminMetricCard.vue"; import AdminPlaceholderTable from "./components/AdminPlaceholderTable.vue"; import AdminSectionShell from "./components/AdminSectionShell.vue"; import { useAdminPageHeader } from "./components/useAdminPageHeader"; +import { formatDate } from "@/lib/utils"; type ListVideosResponse = Awaited>; type AdminVideoRow = NonNullable[number]; @@ -257,11 +258,7 @@ const nextPage = async () => { await loadVideos(); }; -const formatDate = (value?: string) => { - if (!value) return "—"; - const date = new Date(value); - return Number.isNaN(date.getTime()) ? value : date.toLocaleString(); -}; +const formatAdminDate = (value?: string) => formatDate(value || "") || "—"; const formatBytes = (value?: number) => { const bytes = Number(value || 0); diff --git a/src/routes/settings/admin/components/AdminSectionCard.vue b/src/routes/settings/admin/components/AdminSectionCard.vue index 028a059..cbe79af 100644 --- a/src/routes/settings/admin/components/AdminSectionCard.vue +++ b/src/routes/settings/admin/components/AdminSectionCard.vue @@ -36,10 +36,3 @@ const props = withDefaults(defineProps<{ - - diff --git a/src/routes/settings/admin/components/AdminTable.vue b/src/routes/settings/admin/components/AdminTable.vue index 031c9b9..d585756 100644 --- a/src/routes/settings/admin/components/AdminTable.vue +++ b/src/routes/settings/admin/components/AdminTable.vue @@ -58,16 +58,3 @@ function resolveBodyRowClass(row: Row) { - - diff --git a/src/server/middlewares/setup.ts b/src/server/middlewares/setup.ts index 8175131..431a83a 100644 --- a/src/server/middlewares/setup.ts +++ b/src/server/middlewares/setup.ts @@ -1,5 +1,6 @@ import { RedisClient } from "bun"; import type { Hono } from "hono"; +import { serveStatic } from 'hono/bun' import { contextStorage } from "hono/context-storage"; import { cors } from "hono/cors"; import { languageDetector } from "hono/language"; diff --git a/vite.config.ts b/vite.config.ts index 9a3599a..fe1dbbf 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -11,7 +11,8 @@ export default defineConfig((env) => { // console.log("env:", env, import.meta.env); return { server: { - host: '0.0.0.0' + host: '0.0.0.0', + port: 3000 }, plugins: [ unocss(),