import { NextRequest, NextResponse } from "next/server"; import { promises as fs } from "fs"; import path from "path"; import { parseManicTimeFile, serializeToManicTime, computeSyncPlan, generateBackupFilename, validateHierarchy, manicTimeTagToCreateData, } from "@/modules/tag-manager/services/manictime-service"; import type { Tag } from "@/core/tagging/types"; import { prisma } from "@/core/storage/prisma"; import type { Prisma } from "@prisma/client"; const NAMESPACE = "tags"; /** * Get the ManicTime Tags.txt file path from environment. * Default: /mnt/manictime/Tags.txt (Docker SMB mount) */ function getTagsFilePath(): string { return process.env.MANICTIME_TAGS_PATH ?? "/mnt/manictime/Tags.txt"; } /** * Load all tags from the database (same namespace as TagService). */ async function loadArchiToolsTags(): Promise { const records = await prisma.keyValueStore.findMany({ where: { namespace: NAMESPACE }, }); return records.map((r) => r.value as unknown as Tag); } /** * GET /api/manictime * Read and parse the ManicTime Tags.txt file. * Returns parsed tags + sync plan against ArchiTools. */ export async function GET() { const filePath = getTagsFilePath(); try { await fs.access(filePath); } catch { return NextResponse.json( { error: "file_not_found", message: `Fișierul Tags.txt nu a fost găsit la: ${filePath}. Verificați montarea SMB și variabila MANICTIME_TAGS_PATH.`, filePath, }, { status: 404 }, ); } try { const content = await fs.readFile(filePath, "utf-8"); const parsed = parseManicTimeFile(content); const archiToolsTags = await loadArchiToolsTags(); const syncPlan = computeSyncPlan(parsed.tags, archiToolsTags); return NextResponse.json({ filePath, lastModified: (await fs.stat(filePath)).mtime.toISOString(), totalLines: parsed.tags.length, groups: parsed.groups.length, syncPlan: { addToArchiTools: syncPlan.addedToArchiTools.length, addToManicTime: syncPlan.addedToManicTime.length, unchanged: syncPlan.unchanged, conflicts: syncPlan.conflicts, }, manicTimeTags: parsed.tags, archiToolsCount: archiToolsTags.length, }); } catch (error) { return NextResponse.json( { error: "read_error", message: `Eroare la citirea fișierului: ${error instanceof Error ? error.message : "necunoscută"}`, }, { status: 500 }, ); } } /** * POST /api/manictime * Perform bidirectional sync. * * Body: { direction: 'pull' | 'push' | 'both' } * - pull: Import new tags from ManicTime → ArchiTools * - push: Export ArchiTools tags → ManicTime Tags.txt (with backup) * - both: Pull then push */ export async function POST(request: NextRequest) { const filePath = getTagsFilePath(); let body: { direction?: string } = {}; try { body = (await request.json()) as { direction?: string }; } catch { return NextResponse.json({ error: "invalid_json" }, { status: 400 }); } const direction = body.direction ?? "both"; if (!["pull", "push", "both"].includes(direction)) { return NextResponse.json( { error: "invalid_direction", message: "Direction must be pull, push, or both", }, { status: 400 }, ); } try { const archiToolsTags = await loadArchiToolsTags(); let pulled = 0; let pushed = false; // ── Pull: ManicTime → ArchiTools ── if (direction === "pull" || direction === "both") { let content: string; try { content = await fs.readFile(filePath, "utf-8"); } catch { return NextResponse.json( { error: "file_not_found", message: `Fișierul Tags.txt nu a fost găsit la: ${filePath}`, }, { status: 404 }, ); } const parsed = parseManicTimeFile(content); const syncPlan = computeSyncPlan(parsed.tags, archiToolsTags); // Import new tags into ArchiTools DB const existingKeys = new Set( archiToolsTags.map((t) => `${t.category}::${t.label.toLowerCase()}`), ); for (const mt of syncPlan.addedToArchiTools) { const data = manicTimeTagToCreateData(mt); if (!data) continue; const key = `${data.category}::${data.label.toLowerCase()}`; if (existingKeys.has(key)) continue; const id = crypto.randomUUID(); const tag: Tag = { ...data, id, createdAt: new Date().toISOString(), }; const tagJson = JSON.parse( JSON.stringify(tag), ) as Prisma.InputJsonValue; await prisma.keyValueStore.upsert({ where: { namespace_key: { namespace: NAMESPACE, key: id } }, update: { value: tagJson }, create: { namespace: NAMESPACE, key: id, value: tagJson, }, }); existingKeys.add(key); pulled++; } } // ── Push: ArchiTools → ManicTime ── if (direction === "push" || direction === "both") { // Re-load to include any newly pulled tags const allTags = await loadArchiToolsTags(); // Validate hierarchy before writing const warnings = validateHierarchy(allTags); // Create backup if file exists try { await fs.access(filePath); const backupPath = generateBackupFilename(filePath); await fs.copyFile(filePath, backupPath); } catch { // File doesn't exist yet, no backup needed } // Ensure directory exists const dir = path.dirname(filePath); await fs.mkdir(dir, { recursive: true }); // Write new content const newContent = serializeToManicTime(allTags); await fs.writeFile(filePath, newContent, "utf-8"); pushed = true; return NextResponse.json({ success: true, direction, pulled, pushed, totalTags: allTags.length, warnings, timestamp: new Date().toISOString(), }); } return NextResponse.json({ success: true, direction, pulled, pushed, totalTags: archiToolsTags.length + pulled, warnings: [], timestamp: new Date().toISOString(), }); } catch (error) { return NextResponse.json( { error: "sync_error", message: `Eroare la sincronizare: ${error instanceof Error ? error.message : "necunoscută"}`, }, { status: 500 }, ); } }