From 1b1dc712253bbaf8057a839712f27859440ea4b4 Mon Sep 17 00:00:00 2001 From: nico Date: Mon, 30 Mar 2026 16:58:30 +0800 Subject: [PATCH] tasks/noc-integration/setup-skills-and-notifications/20260330-1610 --- .gemini/hooks/telegram-notify.ts | 74 +++++++++++-------- .../migration.sql | 15 ++++ prisma/schema.prisma | 7 +- scripts/check-sync-data.ts | 22 ++++++ scripts/sync-noc.ts | 2 + src/api/division.ts | 7 +- .../dashboard/division-progress.tsx | 3 +- 7 files changed, 93 insertions(+), 37 deletions(-) create mode 100644 prisma/migrations/20260330074700_add_external_activity_count_to_division/migration.sql create mode 100644 scripts/check-sync-data.ts diff --git a/.gemini/hooks/telegram-notify.ts b/.gemini/hooks/telegram-notify.ts index c958201..2f66a10 100755 --- a/.gemini/hooks/telegram-notify.ts +++ b/.gemini/hooks/telegram-notify.ts @@ -1,49 +1,52 @@ #!/usr/bin/env bun -import { readFileSync } from "node:fs"; +import { readFileSync, existsSync } from "node:fs"; +import { join } from "node:path"; -// Fungsi untuk mencari string terpanjang dalam objek (biasanya balasan AI) -function findLongestString(obj: unknown): string { - let longest = ""; - const search = (item: unknown) => { - if (typeof item === "string") { - if (item.length > longest.length) { - longest = item; - } - } else if (Array.isArray(item)) { - for (const child of item) { - search(child); - } - } else if (item !== null && typeof item === "object") { - for (const value of Object.values(item)) { - search(value); +// Function to manually load .env from project root if process.env is missing keys +function loadEnv() { + const envPath = join(process.cwd(), ".env"); + if (existsSync(envPath)) { + const envContent = readFileSync(envPath, "utf-8"); + const lines = envContent.split("\n"); + for (const line of lines) { + if (line && !line.startsWith("#")) { + const [key, ...valueParts] = line.split("="); + if (key && valueParts.length > 0) { + const value = valueParts.join("=").trim().replace(/^["']|["']$/g, ""); + process.env[key.trim()] = value; + } } } - }; - search(obj); - return longest; + } } async function run() { try { + // Ensure environment variables are loaded + loadEnv(); + const inputRaw = readFileSync(0, "utf-8"); if (!inputRaw) return; - const input = JSON.parse(inputRaw); - // DEBUG: Lihat struktur asli di console terminal (stderr) - console.error("DEBUG KEYS:", Object.keys(input)); + let finalText = ""; + let sessionId = "dashboard-desa-plus"; + + try { + // Try parsing as JSON first + const input = JSON.parse(inputRaw); + sessionId = input.session_id || "dashboard-desa-plus"; + finalText = typeof input === "string" ? input : (input.response || input.text || JSON.stringify(input)); + } catch { + // If not JSON, use raw text + finalText = inputRaw; + } const BOT_TOKEN = process.env.BOT_TOKEN; const CHAT_ID = process.env.CHAT_ID; - const sessionId = input.session_id || "unknown"; - - // Cari teks secara otomatis di seluruh objek JSON - let finalText = findLongestString(input.response || input); - - if (!finalText || finalText.length < 5) { - finalText = - "Teks masih gagal diekstraksi. Struktur: " + - Object.keys(input).join(", "); + if (!BOT_TOKEN || !CHAT_ID) { + console.error("Missing BOT_TOKEN or CHAT_ID in environment variables"); + return; } const message = @@ -51,7 +54,7 @@ async function run() { `🆔 Session: \`${sessionId}\` \n\n` + `🧠 Output:\n${finalText.substring(0, 3500)}`; - await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, { + const res = await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ @@ -61,6 +64,13 @@ async function run() { }), }); + if (!res.ok) { + const errorData = await res.json(); + console.error("Telegram API Error:", errorData); + } else { + console.log("Notification sent successfully!"); + } + process.stdout.write(JSON.stringify({ status: "continue" })); } catch (err) { console.error("Hook Error:", err); diff --git a/prisma/migrations/20260330074700_add_external_activity_count_to_division/migration.sql b/prisma/migrations/20260330074700_add_external_activity_count_to_division/migration.sql new file mode 100644 index 0000000..a71dfbf --- /dev/null +++ b/prisma/migrations/20260330074700_add_external_activity_count_to_division/migration.sql @@ -0,0 +1,15 @@ +-- AlterTable +ALTER TABLE "activity" ALTER COLUMN "villageId" SET DEFAULT 'desa1'; + +-- AlterTable +ALTER TABLE "discussion" ALTER COLUMN "villageId" SET DEFAULT 'desa1'; + +-- AlterTable +ALTER TABLE "division" ADD COLUMN "externalActivityCount" INTEGER NOT NULL DEFAULT 0, +ALTER COLUMN "villageId" SET DEFAULT 'desa1'; + +-- AlterTable +ALTER TABLE "document" ALTER COLUMN "villageId" SET DEFAULT 'desa1'; + +-- AlterTable +ALTER TABLE "event" ALTER COLUMN "villageId" SET DEFAULT 'desa1'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e47612e..fda4c1a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -46,9 +46,10 @@ model Division { villageId String? @default("desa1") // ID Desa dari sistem NOC name String @unique description String? - color String @default("#1E3A5F") - isActive Boolean @default(true) - lastSyncedAt DateTime? // Terakhir kali sinkronisasi dilakukan + color String @default("#1E3A5F") + isActive Boolean @default(true) + externalActivityCount Int @default(0) // Total kegiatan dari sistem NOC (misal: 47) + lastSyncedAt DateTime? // Terakhir kali sinkronisasi dilakukan createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/scripts/check-sync-data.ts b/scripts/check-sync-data.ts new file mode 100644 index 0000000..3cda164 --- /dev/null +++ b/scripts/check-sync-data.ts @@ -0,0 +1,22 @@ +import { prisma } from "../src/utils/db"; + +async function check() { + console.log("--- Checking Division Data in DB ---"); + const divisions = await prisma.division.findMany({ + select: { + name: true, + externalActivityCount: true, + } + }); + console.table(divisions); + + console.log("\n--- Checking API Response for /api/division/ ---"); + // Mocking the mapping logic from src/api/division.ts + const formatted = divisions.map(d => ({ + name: d.name, + activityCount: d.externalActivityCount + })); + console.table(formatted); +} + +check().catch(console.error).finally(() => prisma.$disconnect()); diff --git a/scripts/sync-noc.ts b/scripts/sync-noc.ts index 8653b74..1d440f2 100644 --- a/scripts/sync-noc.ts +++ b/scripts/sync-noc.ts @@ -58,12 +58,14 @@ async function syncActiveDivisions() { externalId: extId, color: div.color || "#1E3A5F", villageId: ID_DESA, + externalActivityCount: div.totalKegiatan || 0, }, create: { externalId: extId, name: name, color: div.color || "#1E3A5F", villageId: ID_DESA, + externalActivityCount: div.totalKegiatan || 0, }, }); } diff --git a/src/api/division.ts b/src/api/division.ts index dee655e..3eed256 100644 --- a/src/api/division.ts +++ b/src/api/division.ts @@ -16,7 +16,12 @@ export const division = new Elysia({ }, }, }); - return { data: divisions }; + return { + data: divisions.map(d => ({ + ...d, + activityCount: d.externalActivityCount || d._count.activities + })) + }; } catch (error) { logger.error({ error }, "Failed to fetch divisions"); set.status = 500; diff --git a/src/components/dashboard/division-progress.tsx b/src/components/dashboard/division-progress.tsx index 750819b..6560cde 100644 --- a/src/components/dashboard/division-progress.tsx +++ b/src/components/dashboard/division-progress.tsx @@ -20,6 +20,7 @@ interface DivisionData { interface DivisionApiResponse { id: string; name: string; + activityCount: number; _count?: { activities: number; }; @@ -40,7 +41,7 @@ export function DivisionProgress() { setData( (res.data.data as DivisionApiResponse[]).map((d) => ({ name: d.name, - value: d._count?.activities || 0, + value: d.activityCount || 0, })), ); }