feat(admin): music library admin + fix CRM analytics UTC
Build backend images / build content-svc (push) Failing after 1m7s
Build backend images / build file-svc (push) Failing after 50s
Build backend images / build gateway (push) Failing after 59s
Build backend images / build identity-svc (push) Failing after 56s
Build backend images / build notification-svc (push) Failing after 1m0s
Build backend images / build render-svc (push) Failing after 1m0s
Build backend images / build studio-svc (push) Failing after 56s

- /admin/music: list / upload / delete studio audio tracks (content-svc
  GET/POST/DELETE /v1/music) — fills the legacy music-library gap
- fix: CRM analytics coerced query-bound dates to UTC (Npgsql timestamptz
  rejects Kind=Unspecified) — endpoint was returning 400

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-02 22:17:14 +03:30
parent 2c961b123b
commit 3acd366fda
6 changed files with 44 additions and 4 deletions
+2 -1
View File
@@ -328,7 +328,8 @@
"marketing": "Marketing",
"crm": "CRM",
"ranking": "Ranking",
"stats": "Dashboard"
"stats": "Dashboard",
"music": "Music"
},
"appAdminNodesPage": {
"title": "Render Nodes",
+2 -1
View File
@@ -328,7 +328,8 @@
"marketing": "بازاریابی",
"crm": "مدیریت مشتریان",
"ranking": "رتبه‌بندی",
"stats": "داشبورد"
"stats": "داشبورد",
"music": "موسیقی"
},
"appAdminNodesPage": {
"title": "نودهای رندر",
@@ -13,8 +13,9 @@ public class AdminService(IdentityDbContext db)
public async Task<CrmAnalyticsResponse> GetCrmAnalyticsAsync(Guid tenantId, DateTime start, DateTime end)
{
start = start.Date;
end = end.Date.AddDays(1); // inclusive of the end day
// Query-bound dates arrive Kind=Unspecified; Npgsql requires UTC for timestamptz.
start = DateTime.SpecifyKind(start.Date, DateTimeKind.Utc);
end = DateTime.SpecifyKind(end.Date.AddDays(1), DateTimeKind.Utc); // inclusive of the end day
if ((end - start).TotalDays > 366) end = start.AddDays(366);
var signups = await db.Users
+1
View File
@@ -22,6 +22,7 @@ export default async function AdminLayout({
{ href: "/admin/ranking", label: t("ranking") },
{ href: "/admin/tags", label: t("tags") },
{ href: "/admin/fonts", label: t("fonts") },
{ href: "/admin/music", label: t("music") },
{ href: "/admin/blogs", label: t("blogs") },
{ href: "/admin/slides", label: t("slides") },
{ href: "/admin/files", label: t("media") },
+8
View File
@@ -0,0 +1,8 @@
"use client";
import { AdminResource } from "@/components/admin/AdminResource";
import { musicConfig } from "@/components/admin/admin-resources";
export default function Page() {
return <AdminResource config={musicConfig} />;
}
+28
View File
@@ -109,6 +109,34 @@ export const fontsConfig: ResourceConfig = {
],
};
export const musicConfig: ResourceConfig = {
title: "Music",
description: "Audio tracks available in the studio music library.",
basePath: "music",
listKey: "items",
canCreate: true,
canEdit: false,
canDelete: true,
columns: [
{ key: "name", label: "Name" },
{ key: "genre", label: "Genre" },
{ key: "mood", label: "Mood" },
{ key: "duration_sec", label: "Duration (s)" },
{ key: "is_premium", label: "Premium", render: (r) => badge(!!r.is_premium, "premium", "free") },
],
fields: [
{ key: "name", label: "Name", required: true },
{ key: "caption", label: "Caption" },
{ key: "keywords", label: "Keywords" },
{ key: "url", label: "Audio file", type: "file", required: true },
{ key: "duration_sec", label: "Duration (seconds)", type: "number", required: true },
{ key: "bpm", label: "BPM", type: "number" },
{ key: "genre", label: "Genre" },
{ key: "mood", label: "Mood" },
{ key: "is_premium", label: "Premium", type: "checkbox" },
],
};
export const blogsConfig: ResourceConfig = {
title: "Blog Posts",
description: "CMS articles (also created by the AI SEO generator).",