1 Commits

Author SHA1 Message Date
c861a3ba7a Merge pull request 'develop-updateui' (#1) from develop-updateui into master
Reviewed-on: #1
2026-04-02 05:59:21 +00:00
22 changed files with 141 additions and 142 deletions

View File

@@ -2,28 +2,24 @@
apiVersion: v1 apiVersion: v1
kind: ConfigMap kind: ConfigMap
metadata: metadata:
name: stream-ui-config name: stream.ui-config
namespace: stream-production namespace: stream-production
labels: labels:
app: stream-ui app: stream.ui
data: data:
STREAM_API_GRPC_ADDR: "stream.api-svc:9000" STREAM_API_GRPC_ADDR: "stream.api-svc:9000"
GOOGLE_AUTH_FINALIZE_PATH: "/auth/google/finalize" GOOGLE_AUTH_FINALIZE_PATH: "/auth/google/finalize"
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 kind: Service
apiVersion: v1 apiVersion: v1
metadata: metadata:
name: stream-ui-svc name: stream.ui-svc
namespace: stream-production namespace: stream-production
labels: labels:
app: stream-ui app: stream.ui
spec: spec:
selector: selector:
app: stream-ui app: stream.ui
ports: ports:
- protocol: TCP - protocol: TCP
port: 80 port: 80
@@ -33,55 +29,51 @@ spec:
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: stream-ui-dep name: stream.ui-dep
namespace: stream-production namespace: stream-production
labels: labels:
app: stream-ui app: stream.ui
spec: spec:
replicas: 1 replicas: 1
selector: selector:
matchLabels: matchLabels:
app: stream-ui app: stream.ui
template: template:
metadata: metadata:
labels: labels:
app: stream-ui app: stream.ui
spec: spec:
# imagePullSecrets: imagePullSecrets:
# - name: registry-production-secret - name: registry-production-secret
containers: containers:
- name: stream-ui - name: stream.ui
image: registry.awing.vn/stream-production/stream-ui:$BUILD_NUMBER image: registry.awing.vn/stream-production/stream.ui:$BUILD_NUMBER
ports: ports:
- containerPort: 3000 - containerPort: 3000
env: env:
- name: STREAM_API_GRPC_ADDR - name: STREAM_API_GRPC_ADDR
valueFrom: valueFrom:
configMapKeyRef: configMapKeyRef:
name: stream-ui-config name: stream.ui-config
key: STREAM_API_GRPC_ADDR key: STREAM_API_GRPC_ADDR
- name: GOOGLE_AUTH_FINALIZE_PATH - name: GOOGLE_AUTH_FINALIZE_PATH
valueFrom: valueFrom:
configMapKeyRef: configMapKeyRef:
name: stream-ui-config name: stream.ui-config
key: GOOGLE_AUTH_FINALIZE_PATH key: GOOGLE_AUTH_FINALIZE_PATH
- name: STREAM_INTERNAL_AUTH_MARKER - name: STREAM_INTERNAL_AUTH_MARKER
valueFrom: valueFrom:
configMapKeyRef: secretKeyRef:
name: stream-ui-config name: stream.ui-secret
key: STREAM_INTERNAL_AUTH_MARKER key: STREAM_INTERNAL_AUTH_MARKER
- name: STREAM_UI_JWT_SECRET - name: STREAM_UI_JWT_SECRET
valueFrom: valueFrom:
configMapKeyRef: secretKeyRef:
name: stream-ui-config name: stream.ui-secret
key: STREAM_UI_JWT_SECRET key: STREAM_UI_JWT_SECRET
- name: STREAM_UI_REDIS_URL - name: STREAM_UI_REDIS_URL
valueFrom: valueFrom:
configMapKeyRef: secretKeyRef:
name: stream-ui-config name: stream.ui-secret
key: STREAM_UI_REDIS_URL key: STREAM_UI_REDIS_URL
- name: FRONTEND_BASE_URL
valueFrom:
configMapKeyRef:
name: stream-ui-config
key: FRONTEND_BASE_URL

2
components.d.ts vendored
View File

@@ -38,6 +38,7 @@ declare module 'vue' {
CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.vue')['default'] CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.vue')['default']
ClientOnly: typeof import('./src/components/ClientOnly.tsx')['default'] ClientOnly: typeof import('./src/components/ClientOnly.tsx')['default']
CoinsIcon: typeof import('./src/components/icons/CoinsIcon.vue')['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'] Credit: typeof import('./src/components/icons/Credit.vue')['default']
CreditCardIcon: typeof import('./src/components/icons/CreditCardIcon.vue')['default'] CreditCardIcon: typeof import('./src/components/icons/CreditCardIcon.vue')['default']
DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default'] DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default']
@@ -131,6 +132,7 @@ declare global {
const CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.vue')['default'] const CheckMarkIcon: typeof import('./src/components/icons/CheckMarkIcon.vue')['default']
const ClientOnly: typeof import('./src/components/ClientOnly.tsx')['default'] const ClientOnly: typeof import('./src/components/ClientOnly.tsx')['default']
const CoinsIcon: typeof import('./src/components/icons/CoinsIcon.vue')['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 Credit: typeof import('./src/components/icons/Credit.vue')['default']
const CreditCardIcon: typeof import('./src/components/icons/CreditCardIcon.vue')['default'] const CreditCardIcon: typeof import('./src/components/icons/CreditCardIcon.vue')['default']
const DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default'] const DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default']

View File

@@ -1,21 +0,0 @@
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.ui.git",
"branch": "develop-updateui",
"imageName": "stream123/stream.ui",
"dockerfilePath": "Dockerfile",
"kubeConfigYamlPath": ".deploy/stream.ui-production.yaml",
"kubeConfig": "YXBpVmVyc2lvbjogdjEKY2x1c3RlcnM6Ci0gY2x1c3RlcjoKICAgIGNlcnRpZmljYXRlLWF1dGhvcml0eS1kYXRhOiBMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VKa2VrTkRRVkl5WjBGM1NVSkJaMGxDUVVSQlMwSm5aM0ZvYTJwUFVGRlJSRUZxUVdwTlUwVjNTSGRaUkZaUlVVUkVRbWh5VFROTmRHTXlWbmtLWkcxV2VVeFhUbWhSUkVVelRucFJOVTVFU1RCT2VrMTNTR2hqVGsxcVdYZE5lazE0VFVSWmVrNUVUWHBYYUdOT1RYcFpkMDE2U1RSTlJGbDZUa1JOZWdwWGFrRnFUVk5GZDBoM1dVUldVVkZFUkVKb2NrMHpUWFJqTWxaNVpHMVdlVXhYVG1oUlJFVXpUbnBSTlU1RVNUQk9lazEzVjFSQlZFSm5ZM0ZvYTJwUENsQlJTVUpDWjJkeGFHdHFUMUJSVFVKQ2QwNURRVUZUWm5sUVRHaE5kVEJvZFVNelpFb3JlbFZHV0ZVNVYyMHdLM1YxVUhSUFVVTjRSMFZSYkV4ak9Wa0tZV3RsYm1kc1JFZDRTRGs1UjBKcFRFOHlka1pZTm5oalZYcFdka040T0U0NFpqWm9NREpFZVZJNFJGTnZNRWwzVVVSQlQwSm5UbFpJVVRoQ1FXWTRSUXBDUVUxRFFYRlJkMFIzV1VSV1VqQlVRVkZJTDBKQlZYZEJkMFZDTDNwQlpFSm5UbFpJVVRSRlJtZFJWVWhuUTFGWFVVNHlLM1p4TlZKWmRFcFdVVEJNQ2toa00xVlhkMGwzUTJkWlNVdHZXa2w2YWpCRlFYZEpSRk5CUVhkU1VVbG5SbXBDU1hoMFFUVXdRMmwyZFdoVVUzbFZRalpqYjBSU2FWWjBWVVYzUVZrS2VYWjZXRGxHUm5CcVl6aERTVkZFUzBGVFNrZFBaRUZXUW01TmJsRTNWa3BpVVVkWldFRlJSMjlwTmpCRlpuZzVZMUprWTA5UVJWQTFkejA5Q2kwdExTMHRSVTVFSUVORlVsUkpSa2xEUVZSRkxTMHRMUzBLCiAgICBzZXJ2ZXI6IGh0dHBzOi8vNDIuOTYuMTUuMTA5OjY0NDMKICBuYW1lOiBkZWZhdWx0CmNvbnRleHRzOgotIGNvbnRleHQ6CiAgICBjbHVzdGVyOiBkZWZhdWx0CiAgICB1c2VyOiBkZWZhdWx0CiAgbmFtZTogZGVmYXVsdApjdXJyZW50LWNvbnRleHQ6IGRlZmF1bHQKa2luZDogQ29uZmlnCnVzZXJzOgotIG5hbWU6IGRlZmF1bHQKICB1c2VyOgogICAgY2xpZW50LWNlcnRpZmljYXRlLWRhdGE6IExTMHRMUzFDUlVkSlRpQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENrMUpTVUpyUkVORFFWUmxaMEYzU1VKQlowbEpabG95WW10NGJVRTFTRFIzUTJkWlNVdHZXa2w2YWpCRlFYZEpkMGw2UldoTlFqaEhRVEZWUlVGM2Qxa0tZWHBPZWt4WFRuTmhWMVoxWkVNeGFsbFZRWGhPZW1Nd1QxUlJlVTVFWTNwTlFqUllSRlJKTWsxRVRYcE5WRUV5VFhwUmVrMHhiMWhFVkVrelRVUk5lZ3BOVkVFeVRYcFJlazB4YjNkTlJFVllUVUpWUjBFeFZVVkRhRTFQWXpOc2VtUkhWblJQYlRGb1l6TlNiR051VFhoR1ZFRlVRbWRPVmtKQlRWUkVTRTQxQ21NelVteGlWSEJvV2tjeGNHSnFRbHBOUWsxSFFubHhSMU5OTkRsQlowVkhRME54UjFOTk5EbEJkMFZJUVRCSlFVSkZjVE0wUWl0U05tdEVXVzlQY213S1dTdDFiMFpTTjBOdFJUTTVVRk54U1hNeGFYWllWak5aVjBoUmF6bHdSVlpZUm5GWVpYQnZXVmg0TW5KM1pVbFlTVEZTY1dGSU9TdHJPVGw0WkM5c1FRb3ZZamxRWm14UGFsTkVRa2ROUVRSSFFURlZaRVIzUlVJdmQxRkZRWGRKUm05RVFWUkNaMDVXU0ZOVlJVUkVRVXRDWjJkeVFtZEZSa0pSWTBSQmFrRm1Da0puVGxaSVUwMUZSMFJCVjJkQ1V6azFRa0pRWWxWT2MwaFljeXR6WXpoTmNWaHJZWEF3VVhscFJFRkxRbWRuY1docmFrOVFVVkZFUVdkT1NFRkVRa1VLUVdsQ1RXZFJVVGRaY0c5WlMwcDNiMFIyVTBNMlMwVnhaM0VyTkZWTkt6Vkxja2hVV0d0UVFuRTBVazFrUVVsblF6SmhPV0owZDNwdGMwUkZZVFpKVWdwNmRucFpOUzlLUjBKRVZrOUNkM28wV0ZNNU0xaFVkR2h0UW5jOUNpMHRMUzB0UlU1RUlFTkZVbFJKUmtsRFFWUkZMUzB0TFMwS0xTMHRMUzFDUlVkSlRpQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENrMUpTVUpsUkVORFFWSXlaMEYzU1VKQlowbENRVVJCUzBKblozRm9hMnBQVUZGUlJFRnFRV3BOVTBWM1NIZFpSRlpSVVVSRVFtaHlUVE5OZEZreWVIQUtXbGMxTUV4WFRtaFJSRVV6VG5wUk5VNUVTVEJPZWsxM1NHaGpUazFxV1hkTmVrMTRUVVJaZWs1RVRYcFhhR05PVFhwWmQwMTZTVFJOUkZsNlRrUk5lZ3BYYWtGcVRWTkZkMGgzV1VSV1VWRkVSRUpvY2swelRYUlpNbmh3V2xjMU1FeFhUbWhSUkVVelRucFJOVTVFU1RCT2VrMTNWMVJCVkVKblkzRm9hMnBQQ2xCUlNVSkNaMmR4YUd0cVQxQlJUVUpDZDA1RFFVRlNabTFRYTBaT1pYTnNhV05aZFhSUGJrVmtVbmN2S3pCVE0yVkxkSGNyU0ZwbmJIcFVRazF3WVdrS2RYQjFXWFJuVmpad2IwdG9kSGhUYVhFdk5rWktRa0owZWtoSlNsSjRUMlp0V1RnemVtaENVbE5oUlZOdk1FbDNVVVJCVDBKblRsWklVVGhDUVdZNFJRcENRVTFEUVhGUmQwUjNXVVJXVWpCVVFWRklMMEpCVlhkQmQwVkNMM3BCWkVKblRsWklVVFJGUm1kUlZYWmxVVkZVTWpGRVlrSXhOMUJ5U0ZCRVMydzFDa2R4WkVWTmIyZDNRMmRaU1V0dldrbDZhakJGUVhkSlJGTlJRWGRTWjBsb1FVb3pNVVJWTUhSaFRHVnNWVFJpUVcxUlRYSnJNMEpvT0doSWNuUTNhamtLYkRka1p6YzFhelJ5Vlc1MVFXbEZRWGhsVDFCaFVVUTBTWHBNYzBwVmRITkpOWGRWUzBoUFZWTnFWblE1U20xVWMwSTRTVnB0TTBOM1lXODlDaTB0TFMwdFJVNUVJRU5GVWxSSlJrbERRVlJGTFMwdExTMEsKICAgIGNsaWVudC1rZXktZGF0YTogTFMwdExTMUNSVWRKVGlCRlF5QlFVa2xXUVZSRklFdEZXUzB0TFMwdENrMUlZME5CVVVWRlNVTk9hVlp2VG1KVGRHZEJWSEJzT1ZSTlpWbHlOMHBUWkVoRk5qZElWMWxrWkZOc05UTmFSbFpUZEhodlFXOUhRME54UjFOTk5Ea0tRWGRGU0c5VlVVUlJaMEZGVTNKbVowZzFTSEZSVG1sbk5uVldhalkyWjFaSWMwdFpWR1l3T1V0dmFYcFhTemxrV0dSb1dXUkRWREpyVWxaalYzQmtOZ3B0YUdobVNHRjJRalJvWTJwV1IzQnZaak0yVkRNelJqTXJWVVE1ZGpBNUsxVjNQVDBLTFMwdExTMUZUa1FnUlVNZ1VGSkpWa0ZVUlNCTFJWa3RMUzB0TFFvPQo=",
"quay_username": "lethdat",
"quay_token": "htK3xi1/mQdOSQyBxbGVr9Hhpm/ywzNGawjk29lNHZcRXRdec7kc1v9LRE6X1ATE",
"telegram_chat_id": "-4891576755",
"tele_token": "8230541188:AAGNu6-2iBaFu2JkvORtXM9c6dUZQdQdqYU"
}
}'

View File

@@ -3,7 +3,7 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "bun x --bun vite", "dev": "bun x --bun vite",
"build": "bun x --bun vite build && bun build dist/server/index.js --target=bun --outfile dist/index.js", "build": "bun x --bun vite build",
"preview": "bun x --bun vite preview" "preview": "bun x --bun vite preview"
}, },
"dependencies": { "dependencies": {

View File

@@ -2,6 +2,7 @@
import Upload from "@/routes/upload/Upload.vue"; import Upload from "@/routes/upload/Upload.vue";
import DashboardNav from "./DashboardNav.vue"; import DashboardNav from "./DashboardNav.vue";
import GlobalUploadIndicator from "./GlobalUploadIndicator.vue"; import GlobalUploadIndicator from "./GlobalUploadIndicator.vue";
import PopupAdsRuntime from "./PopupAdsRuntime.vue";
</script> </script>
@@ -22,5 +23,6 @@ import GlobalUploadIndicator from "./GlobalUploadIndicator.vue";
</div> </div>
<GlobalUploadIndicator /> <GlobalUploadIndicator />
<Upload /> <Upload />
<PopupAdsRuntime />
</main> </main>
</template> </template>

View File

@@ -22,6 +22,7 @@ const unreadCount = computed(() => notificationStore.unreadCount.value);
const mutableNotifications = computed(() => notificationStore.notifications.value.slice(0, 8)); const mutableNotifications = computed(() => notificationStore.notifications.value.slice(0, 8));
const toggle = (event?: Event) => { const toggle = (event?: Event) => {
console.log(event);
visible.value = !visible.value; visible.value = !visible.value;
if (visible.value && !notificationStore.loaded.value) { if (visible.value && !notificationStore.loaded.value) {
void notificationStore.fetchNotifications(); void notificationStore.fetchNotifications();

View File

@@ -47,3 +47,9 @@ const props = defineProps<Props>();
<slot name="actions" /> <slot name="actions" />
</div> </div>
</template> </template>
<style scoped>
.empty-state {
min-height: 400px;
}
</style>

View File

@@ -6,7 +6,6 @@ import { registerRpcRoutes } from './server/routes/rpc';
import { registerSSRRoutes } from './server/routes/ssr'; import { registerSSRRoutes } from './server/routes/ssr';
import { registerWellKnownRoutes } from './server/routes/wellKnown'; import { registerWellKnownRoutes } from './server/routes/wellKnown';
import { setupServices } from './server/services/grpcClient'; import { setupServices } from './server/services/grpcClient';
import { serveStatic } from 'hono/bun';
const app = new Hono(); const app = new Hono();
// Global middlewares // Global middlewares
setupMiddlewares(app); setupMiddlewares(app);
@@ -15,9 +14,6 @@ setupServices(app);
registerWellKnownRoutes(app); registerWellKnownRoutes(app);
registerAuthRoutes(app); registerAuthRoutes(app);
registerRpcRoutes(app); registerRpcRoutes(app);
if (!import.meta.env.DEV) {
app.use(serveStatic({ root: './dist/client' }))
}
registerSSRRoutes(app); registerSSRRoutes(app);
export default app; export default app;

View File

@@ -1,29 +1,30 @@
import i18next from "i18next"; import i18next from "i18next";
import I18NextHttpBackend, { HttpBackendOptions } from "i18next-http-backend"; import I18NextHttpBackend, { HttpBackendOptions } from "i18next-http-backend";
const backendOptions: HttpBackendOptions = { const backendOptions: HttpBackendOptions = {
loadPath: `${process.env.FRONTEND_BASE_URL || ''}/locales/{{lng}}/{{ns}}.json`, loadPath: 'http://localhost:5173/locales/{{lng}}/{{ns}}.json',
request: (_options, url, _payload, callback) => {
request: async (_options, url, _payload, callback) => { fetch(url)
try { .then((res) =>
const res = await fetch(url); res.json().then((r) => {
callback(null, {
if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`); data: JSON.stringify(r),
status: 200,
const data = await res.json(); })
})
callback(null, { data, status: 200 }); )
} catch (error) { .catch(() => {
console.error("Lỗi fetch file ngôn ngữ i18n:", error); callback(null, {
callback(error as any, { status: 500, data: '' }); status: 500,
} data: '',
})
})
}, },
} }
export const createI18nInstance = (lng: string) => {
console.log('Initializing i18n with language:', lng);
const i18n = i18next.createInstance();
export const createI18nInstance = async (lng: string) => { i18n
const i18n = i18next.createInstance();
await i18n
.use(I18NextHttpBackend) .use(I18NextHttpBackend)
.init({ .init({
lng, lng,
@@ -36,8 +37,6 @@ export const createI18nInstance = async (lng: string) => {
}, },
backend: backendOptions, backend: backendOptions,
}); });
return i18n; return i18n;
}; };
export default createI18nInstance; export default createI18nInstance;

View File

@@ -81,22 +81,18 @@ export const formatDate = (
dateOnly: boolean = false dateOnly: boolean = false
) => { ) => {
if (!dateString) return ""; if (!dateString) return "";
const locale =
const date = new Date(dateString); typeof document !== "undefined"
if (Number.isNaN(date.getTime())) return dateString; ? document.documentElement.lang === "vi"
? "vi-VN"
const year = date.getUTCFullYear(); : "en-US"
const month = `${date.getUTCMonth() + 1}`.padStart(2, "0"); : "en-US";
const day = `${date.getUTCDate()}`.padStart(2, "0"); return new Date(dateString).toLocaleDateString(locale, {
month: "short",
if (dateOnly) { day: "numeric",
return `${year}-${month}-${day}`; year: "numeric",
} ...(dateOnly ? {} : { hour: "2-digit", minute: "2-digit" }),
});
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; type Status = "success" | "failed" | "pending" | string;
export const getStatusSeverity = (status: Status = "") => { export const getStatusSeverity = (status: Status = "") => {

View File

@@ -32,8 +32,7 @@ export async function createApp(lng: string = 'en') {
} }
}); });
app.use(pinia); app.use(pinia);
const i18next = await createI18nInstance(lng); app.use(I18NextVue, { i18next: createI18nInstance(lng) });
app.use(I18NextVue, { i18next });
app.use(PiniaColada, { app.use(PiniaColada, {
pinia, pinia,
plugins: [ plugins: [

View File

@@ -13,7 +13,6 @@ import AdminMetricCard from "./components/AdminMetricCard.vue";
import AdminPlaceholderTable from "./components/AdminPlaceholderTable.vue"; import AdminPlaceholderTable from "./components/AdminPlaceholderTable.vue";
import AdminSectionShell from "./components/AdminSectionShell.vue"; import AdminSectionShell from "./components/AdminSectionShell.vue";
import { useAdminPageHeader } from "./components/useAdminPageHeader"; import { useAdminPageHeader } from "./components/useAdminPageHeader";
import { formatDate } from "@/lib/utils";
type ListTemplatesResponse = Awaited<ReturnType<typeof rpcClient.listAdminAdTemplates>>; type ListTemplatesResponse = Awaited<ReturnType<typeof rpcClient.listAdminAdTemplates>>;
type AdminAdTemplateRow = NonNullable<ListTemplatesResponse["templates"]>[number]; type AdminAdTemplateRow = NonNullable<ListTemplatesResponse["templates"]>[number];
@@ -239,7 +238,11 @@ const nextPage = async () => {
await loadTemplates(); await loadTemplates();
}; };
const formatAdminDate = (value?: string) => formatDate(value || "") || "—"; const formatDate = (value?: string) => {
if (!value) return "—";
const date = new Date(value);
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
};
const columns = computed<ColumnDef<AdminAdTemplateRow>[]>(() => [ const columns = computed<ColumnDef<AdminAdTemplateRow>[]>(() => [
{ {

View File

@@ -11,7 +11,6 @@ import AdminMetricCard from "./components/AdminMetricCard.vue";
import AdminPlaceholderTable from "./components/AdminPlaceholderTable.vue"; import AdminPlaceholderTable from "./components/AdminPlaceholderTable.vue";
import AdminSectionShell from "./components/AdminSectionShell.vue"; import AdminSectionShell from "./components/AdminSectionShell.vue";
import { useAdminPageHeader } from "./components/useAdminPageHeader"; import { useAdminPageHeader } from "./components/useAdminPageHeader";
import { formatDate } from "@/lib/utils";
type ListAgentsResponse = Awaited<ReturnType<typeof rpcClient.listAdminAgents>>; type ListAgentsResponse = Awaited<ReturnType<typeof rpcClient.listAdminAgents>>;
type AdminAgentRow = NonNullable<ListAgentsResponse["agents"]>[number]; type AdminAgentRow = NonNullable<ListAgentsResponse["agents"]>[number];
@@ -129,7 +128,11 @@ const submitUpdate = async () => {
} }
}; };
const formatAdminDate = (value?: string) => formatDate(value || "") || "—"; const formatDate = (value?: string) => {
if (!value) return "—";
const date = new Date(value);
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
};
const formatCpu = (value?: number) => `${Number(value ?? 0).toFixed(1)}%`; const formatCpu = (value?: number) => `${Number(value ?? 0).toFixed(1)}%`;
const formatRam = (value?: number) => `${Number(value ?? 0).toFixed(1)} MB`; const formatRam = (value?: number) => `${Number(value ?? 0).toFixed(1)} MB`;

View File

@@ -13,7 +13,6 @@ import AdminMetricCard from "./components/AdminMetricCard.vue";
import AdminPlaceholderTable from "./components/AdminPlaceholderTable.vue"; import AdminPlaceholderTable from "./components/AdminPlaceholderTable.vue";
import AdminSectionShell from "./components/AdminSectionShell.vue"; import AdminSectionShell from "./components/AdminSectionShell.vue";
import { useAdminPageHeader } from "./components/useAdminPageHeader"; import { useAdminPageHeader } from "./components/useAdminPageHeader";
import { formatDate } from "@/lib/utils";
type ListJobsResponse = Awaited<ReturnType<typeof rpcClient.listAdminJobs>>; type ListJobsResponse = Awaited<ReturnType<typeof rpcClient.listAdminJobs>>;
type AdminJobRow = NonNullable<ListJobsResponse["jobs"]>[number]; type AdminJobRow = NonNullable<ListJobsResponse["jobs"]>[number];
@@ -86,7 +85,7 @@ const selectedMeta = computed(() => {
{ label: "Priority", value: String(selectedRow.value.priority ?? 0) }, { label: "Priority", value: String(selectedRow.value.priority ?? 0) },
{ label: "Progress", value: formatProgress(selectedRow.value.progress) }, { label: "Progress", value: formatProgress(selectedRow.value.progress) },
{ label: "Owner", value: selectedRow.value.userId || "—" }, { label: "Owner", value: selectedRow.value.userId || "—" },
{ label: "Updated", value: formatAdminDate(selectedRow.value.updatedAt) }, { label: "Updated", value: formatDate(selectedRow.value.updatedAt) },
]; ];
}); });
@@ -181,10 +180,6 @@ const openDetailDialog = async (row: AdminJobRow) => {
actionError.value = null; actionError.value = null;
selectedLogs.value = "Loading logs..."; selectedLogs.value = "Loading logs...";
detailOpen.value = true; detailOpen.value = true;
if (!row.id) {
selectedLogs.value = "No logs available.";
return;
}
try { try {
await loadSelectedLogs(row.id); await loadSelectedLogs(row.id);
} catch { } catch {
@@ -197,11 +192,6 @@ const openLogsDialog = async (row: AdminJobRow) => {
actionError.value = null; actionError.value = null;
selectedLogs.value = "Loading logs..."; selectedLogs.value = "Loading logs...";
logsOpen.value = true; logsOpen.value = true;
if (!row.id) {
selectedLogs.value = "";
actionError.value = "Failed to load job logs";
return;
}
try { try {
await loadSelectedLogs(row.id); await loadSelectedLogs(row.id);
} catch (err: any) { } catch (err: any) {
@@ -276,7 +266,11 @@ const submitRetry = async () => {
} }
}; };
const formatAdminDate = (value?: string): string => formatDate(value || "") || "—"; const formatDate = (value?: string) => {
if (!value) return "—";
const date = new Date(value);
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
};
const formatProgress = (value?: number) => `${Number(value ?? 0).toFixed(2)}%`; const formatProgress = (value?: number) => `${Number(value ?? 0).toFixed(2)}%`;
@@ -349,7 +343,7 @@ const columns = computed<ColumnDef<AdminJobRow>[]>(() => [
id: "updated", id: "updated",
header: "Updated", header: "Updated",
accessorFn: row => row.updatedAt || "", accessorFn: row => row.updatedAt || "",
cell: ({ row }) => h("span", { class: "text-foreground/60" }, formatAdminDate(row.original.updatedAt)), cell: ({ row }) => h("span", { class: "text-foreground/60" }, formatDate(row.original.updatedAt)),
meta: { meta: {
headerClass: "px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50", headerClass: "px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-foreground/50",
cellClass: "px-4 py-3", cellClass: "px-4 py-3",
@@ -399,11 +393,8 @@ useAdminRuntimeMqtt(({ topic, payload }) => {
if (selectedRow.value?.id === payload.job_id && typeof payload.line === "string") { if (selectedRow.value?.id === payload.job_id && typeof payload.line === "string") {
const nextLine = payload.line.endsWith("\n") ? payload.line : `${payload.line}\n`; 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}`; selectedLogs.value = `${selectedLogs.value === "Loading logs..." || selectedLogs.value === "No logs available." ? "" : selectedLogs.value}${nextLine}`;
const selected = selectedRow.value; selectedRow.value.progress = payload.progress ?? selectedRow.value.progress;
if (selected) { selectedRow.value.updatedAt = new Date().toISOString();
selected.progress = payload.progress ?? selected.progress;
selected.updatedAt = new Date().toISOString();
}
} }
} }

View File

@@ -14,7 +14,6 @@ import AdminMetricCard from "./components/AdminMetricCard.vue";
import AdminPlaceholderTable from "./components/AdminPlaceholderTable.vue"; import AdminPlaceholderTable from "./components/AdminPlaceholderTable.vue";
import AdminSectionShell from "./components/AdminSectionShell.vue"; import AdminSectionShell from "./components/AdminSectionShell.vue";
import { useAdminPageHeader } from "./components/useAdminPageHeader"; import { useAdminPageHeader } from "./components/useAdminPageHeader";
import { formatDate } from "@/lib/utils";
type ListPaymentsResponse = Awaited<ReturnType<typeof rpcClient.listAdminPayments>>; type ListPaymentsResponse = Awaited<ReturnType<typeof rpcClient.listAdminPayments>>;
type AdminPaymentRow = NonNullable<ListPaymentsResponse["payments"]>[number]; type AdminPaymentRow = NonNullable<ListPaymentsResponse["payments"]>[number];
@@ -202,7 +201,11 @@ const nextPage = async () => {
await loadPayments(); await loadPayments();
}; };
const formatAdminDate = (value?: string) => formatDate(value || "") || "—"; const formatDate = (value?: string) => {
if (!value) return "—";
const date = new Date(value);
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
};
const formatMoney = (amount?: number, currency?: string) => `${amount ?? 0} ${currency || "USD"}`; const formatMoney = (amount?: number, currency?: string) => `${amount ?? 0} ${currency || "USD"}`;

View File

@@ -12,7 +12,6 @@ import AdminMetricCard from "./components/AdminMetricCard.vue";
import AdminPlaceholderTable from "./components/AdminPlaceholderTable.vue"; import AdminPlaceholderTable from "./components/AdminPlaceholderTable.vue";
import AdminSectionShell from "./components/AdminSectionShell.vue"; import AdminSectionShell from "./components/AdminSectionShell.vue";
import { useAdminPageHeader } from "./components/useAdminPageHeader"; import { useAdminPageHeader } from "./components/useAdminPageHeader";
import { formatDate } from "@/lib/utils";
type ListConfigsResponse = Awaited<ReturnType<typeof rpcClient.listAdminPlayerConfigs>>; type ListConfigsResponse = Awaited<ReturnType<typeof rpcClient.listAdminPlayerConfigs>>;
type AdminPlayerConfigRow = NonNullable<ListConfigsResponse["configs"]>[number]; type AdminPlayerConfigRow = NonNullable<ListConfigsResponse["configs"]>[number];
@@ -288,7 +287,11 @@ const nextPage = async () => {
await loadConfigs(); await loadConfigs();
}; };
const formatAdminDate = (value?: string) => formatDate(value || "") || "—"; const formatDate = (value?: string) => {
if (!value) return "—";
const date = new Date(value);
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
};
useAdminPageHeader(() => ({ useAdminPageHeader(() => ({
eyebrow: 'Playback', eyebrow: 'Playback',

View File

@@ -13,7 +13,6 @@ import AdminPlaceholderTable from "./components/AdminPlaceholderTable.vue";
import AdminSectionShell from "./components/AdminSectionShell.vue"; import AdminSectionShell from "./components/AdminSectionShell.vue";
import AdminUserFormFields from "./components/AdminUserFormFields.vue"; import AdminUserFormFields from "./components/AdminUserFormFields.vue";
import { useAdminPageHeader } from "./components/useAdminPageHeader"; import { useAdminPageHeader } from "./components/useAdminPageHeader";
import { formatDate } from "@/lib/utils";
type ListUsersResponse = Awaited<ReturnType<typeof rpcClient.listAdminUsers>>; type ListUsersResponse = Awaited<ReturnType<typeof rpcClient.listAdminUsers>>;
type AdminUserRow = NonNullable<ListUsersResponse["users"]>[number]; type AdminUserRow = NonNullable<ListUsersResponse["users"]>[number];
@@ -344,7 +343,11 @@ const nextPage = async () => {
await loadUsers(); await loadUsers();
}; };
const formatAdminDate = (value?: string) => formatDate(value || "") || "—"; const formatDate = (value?: string) => {
if (!value) return "—";
const date = new Date(value);
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
};
const roleBadgeClass = (role?: string) => { const roleBadgeClass = (role?: string) => {
const normalized = String(role || "USER").toUpperCase(); const normalized = String(role || "USER").toUpperCase();

View File

@@ -13,7 +13,6 @@ import AdminMetricCard from "./components/AdminMetricCard.vue";
import AdminPlaceholderTable from "./components/AdminPlaceholderTable.vue"; import AdminPlaceholderTable from "./components/AdminPlaceholderTable.vue";
import AdminSectionShell from "./components/AdminSectionShell.vue"; import AdminSectionShell from "./components/AdminSectionShell.vue";
import { useAdminPageHeader } from "./components/useAdminPageHeader"; import { useAdminPageHeader } from "./components/useAdminPageHeader";
import { formatDate } from "@/lib/utils";
type ListVideosResponse = Awaited<ReturnType<typeof rpcClient.listAdminVideos>>; type ListVideosResponse = Awaited<ReturnType<typeof rpcClient.listAdminVideos>>;
type AdminVideoRow = NonNullable<ListVideosResponse["videos"]>[number]; type AdminVideoRow = NonNullable<ListVideosResponse["videos"]>[number];
@@ -258,7 +257,11 @@ const nextPage = async () => {
await loadVideos(); await loadVideos();
}; };
const formatAdminDate = (value?: string) => formatDate(value || "") || "—"; const formatDate = (value?: string) => {
if (!value) return "—";
const date = new Date(value);
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
};
const formatBytes = (value?: number) => { const formatBytes = (value?: number) => {
const bytes = Number(value || 0); const bytes = Number(value || 0);

View File

@@ -36,3 +36,10 @@ const props = withDefaults(defineProps<{
<slot /> <slot />
</SettingsSectionCard> </SettingsSectionCard>
</template> </template>
<style scoped>
.admin-section-card {
border-radius: 0.5rem;
overflow: hidden;
}
</style>

View File

@@ -58,3 +58,16 @@ function resolveBodyRowClass(row: Row<TData>) {
</BaseTable> </BaseTable>
</div> </div>
</template> </template>
<style scoped>
.admin-primer-table :deep(th) {
padding: 0.5rem 0.75rem !important;
font-size: 0.75rem !important;
line-height: 1rem !important;
font-weight: 600 !important;
}
.admin-primer-table :deep(td) {
padding: 0.625rem 0.75rem !important;
}
</style>

View File

@@ -1,6 +1,5 @@
import { RedisClient } from "bun"; import { RedisClient } from "bun";
import type { Hono } from "hono"; import type { Hono } from "hono";
import { serveStatic } from 'hono/bun'
import { contextStorage } from "hono/context-storage"; import { contextStorage } from "hono/context-storage";
import { cors } from "hono/cors"; import { cors } from "hono/cors";
import { languageDetector } from "hono/language"; import { languageDetector } from "hono/language";

View File

@@ -11,8 +11,7 @@ export default defineConfig((env) => {
// console.log("env:", env, import.meta.env); // console.log("env:", env, import.meta.env);
return { return {
server: { server: {
host: '0.0.0.0', host: '0.0.0.0'
port: 3000
}, },
plugins: [ plugins: [
unocss(), unocss(),