From 37ec6ffef8ad354b7ee0a83cadcfef2c0a5d81ba Mon Sep 17 00:00:00 2001 From: Artem Zhiganov Date: Tue, 3 Feb 2026 21:41:16 +0100 Subject: [PATCH 1/4] Document observability safety rule and span context in CLAUDE.md Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index df074b9..3afebbf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -150,6 +150,10 @@ Results log to Braintrust under project `harmonica-facilitation` and are viewabl `src/lib/braintrust.ts` provides `getBraintrustLogger()` for production LLM call logging and `traceOperation()` for hierarchical spans. Optional — logs a warning if `BRAINTRUST_API_KEY` is not set. The `/api/admin/evals` route queries Braintrust experiments for the admin dashboard. +**Observability safety rule:** All observability code (Braintrust, PostHog) in `LLM.chat()` is wrapped in its own try/catch. Observability must never crash a participant's response. If logging fails, warn to console and move on. When adding new tracing or analytics to the LLM path, always wrap in a non-fatal try/catch. + +**Span context:** `traceOperation()` passes a `Span` object to its callback via `{ operation, span }`. When calling `LLM.chat()` inside a `traceOperation` callback, always pass `span` through so the SDK uses `span.log()` instead of the top-level `logger.log()` (which throws inside spans). + ## Code Style - Prefer React Server Components; minimize `'use client'` From 03159be30fa397623efd9efa04d04dce13bce8e5 Mon Sep 17 00:00:00 2001 From: Artem Zhiganov Date: Sat, 7 Feb 2026 12:56:40 +0100 Subject: [PATCH 2/4] Add database-driven Templates admin panel Port templates from static JSON to a database-backed system with full CRUD admin UI at /admin/templates. Templates are seeded from the existing 9 templates.json entries. The /create flow now fetches templates from the API instead of importing the static file. Co-Authored-By: Claude Opus 4.6 --- src/app/admin/Sidebar.tsx | 6 + src/app/admin/templates/page.tsx | 418 +++++++++++++++++++ src/app/api/admin/templates/[id]/route.ts | 60 +++ src/app/api/admin/templates/route.ts | 50 +++ src/app/create/choose-template.tsx | 113 +++-- src/db/migrations/032_add_templates_table.ts | 188 +++++++++ src/lib/db.ts | 106 +++++ src/lib/schema.ts | 18 + 8 files changed, 926 insertions(+), 33 deletions(-) create mode 100644 src/app/admin/templates/page.tsx create mode 100644 src/app/api/admin/templates/[id]/route.ts create mode 100644 src/app/api/admin/templates/route.ts create mode 100644 src/db/migrations/032_add_templates_table.ts diff --git a/src/app/admin/Sidebar.tsx b/src/app/admin/Sidebar.tsx index c313910..3e10bb7 100644 --- a/src/app/admin/Sidebar.tsx +++ b/src/app/admin/Sidebar.tsx @@ -6,6 +6,7 @@ import { usePathname } from 'next/navigation'; import { Boxes, // For types/categories FileText, // For instructions/prompts + LayoutTemplate, // For templates FlaskConical, // For evals Settings, } from 'lucide-react'; @@ -22,6 +23,11 @@ const navigation = [ href: '/admin/prompts', icon: FileText, // Changed to FileText to represent instructions/documents }, + { + name: 'Templates', + href: '/admin/templates', + icon: LayoutTemplate, + }, { name: 'Evals', href: '/admin/evals', diff --git a/src/app/admin/templates/page.tsx b/src/app/admin/templates/page.tsx new file mode 100644 index 0000000..0f4bb75 --- /dev/null +++ b/src/app/admin/templates/page.tsx @@ -0,0 +1,418 @@ +'use client'; + +import { useState, useEffect, useCallback } from 'react'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { Label } from '@/components/ui/label'; +import { Badge } from '@/components/ui/badge'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Plus, Edit2, Trash2, Loader2 } from 'lucide-react'; +import { useToast } from 'hooks/use-toast'; +import { format } from 'date-fns'; + +interface Template { + id: string; + title: string; + description: string | null; + icon: string | null; + facilitation_prompt: string | null; + default_session_name: string | null; + is_public: boolean; + created_at: string; + updated_at: string; +} + +const ICON_OPTIONS = [ + { value: 'lightbulb', label: 'Lightbulb' }, + { value: 'history', label: 'History' }, + { value: 'grid-2x2', label: 'Grid' }, + { value: 'target', label: 'Target' }, + { value: 'list-ordered', label: 'List Ordered' }, + { value: 'activity-square', label: 'Activity' }, + { value: 'list-checks', label: 'List Checks' }, + { value: 'shield-alert', label: 'Shield Alert' }, + { value: 'sparkles', label: 'Sparkles' }, + { value: 'leaf', label: 'Leaf' }, + { value: 'map', label: 'Map' }, +]; + +const emptyForm = { + title: '', + description: '', + icon: '', + facilitation_prompt: '', + default_session_name: '', +}; + +export default function TemplatesPage() { + const [templates, setTemplates] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [showCreateDialog, setShowCreateDialog] = useState(false); + const [editingTemplate, setEditingTemplate] = useState