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(),