From 7caf290332ff27600ce517c0e0acc00ce6cb1b66 Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Fri, 27 Feb 2026 13:22:00 -0600 Subject: [PATCH] Signed-off-by: OpenClaw Bot --- Dockerfile | 50 ++++++++++ app/documents/[id]/page.tsx | 193 ++++++++++++++++++++++++++++++++++++ docker-compose.yml | 67 +++++++++++++ 3 files changed, 310 insertions(+) create mode 100644 Dockerfile create mode 100644 app/documents/[id]/page.tsx create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..639ae4f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,50 @@ +FROM node:22-bookworm-slim + +WORKDIR /app + +ENV NEXT_TELEMETRY_DISABLED=1 + +# Build-time NEXT_PUBLIC vars for client bundles. +ARG NEXT_PUBLIC_SUPABASE_URL +ARG NEXT_PUBLIC_SUPABASE_ANON_KEY +ARG NEXT_PUBLIC_GOOGLE_CLIENT_ID +ARG NEXT_PUBLIC_MISSION_CONTROL_URL +ARG NEXT_PUBLIC_GANTT_BOARD_URL +ARG NEXT_PUBLIC_BLOG_BACKUP_URL +ARG NEXT_PUBLIC_GITEA_URL +ARG NEXT_PUBLIC_GITHUB_URL +ARG NEXT_PUBLIC_VERCEL_URL +ARG NEXT_PUBLIC_SUPABASE_SITE_URL +ARG NEXT_PUBLIC_GOOGLE_URL +ARG NEXT_PUBLIC_GOOGLE_CALENDAR_URL +ARG NEXT_PUBLIC_GOOGLE_CALENDAR_SETTINGS_URL +ARG NEXT_PUBLIC_GANTT_API_BASE_URL +ARG NEXT_PUBLIC_APP_URL + +ENV NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL \ + NEXT_PUBLIC_SUPABASE_ANON_KEY=$NEXT_PUBLIC_SUPABASE_ANON_KEY \ + NEXT_PUBLIC_GOOGLE_CLIENT_ID=$NEXT_PUBLIC_GOOGLE_CLIENT_ID \ + NEXT_PUBLIC_MISSION_CONTROL_URL=$NEXT_PUBLIC_MISSION_CONTROL_URL \ + NEXT_PUBLIC_GANTT_BOARD_URL=$NEXT_PUBLIC_GANTT_BOARD_URL \ + NEXT_PUBLIC_BLOG_BACKUP_URL=$NEXT_PUBLIC_BLOG_BACKUP_URL \ + NEXT_PUBLIC_GITEA_URL=$NEXT_PUBLIC_GITEA_URL \ + NEXT_PUBLIC_GITHUB_URL=$NEXT_PUBLIC_GITHUB_URL \ + NEXT_PUBLIC_VERCEL_URL=$NEXT_PUBLIC_VERCEL_URL \ + NEXT_PUBLIC_SUPABASE_SITE_URL=$NEXT_PUBLIC_SUPABASE_SITE_URL \ + NEXT_PUBLIC_GOOGLE_URL=$NEXT_PUBLIC_GOOGLE_URL \ + NEXT_PUBLIC_GOOGLE_CALENDAR_URL=$NEXT_PUBLIC_GOOGLE_CALENDAR_URL \ + NEXT_PUBLIC_GOOGLE_CALENDAR_SETTINGS_URL=$NEXT_PUBLIC_GOOGLE_CALENDAR_SETTINGS_URL \ + NEXT_PUBLIC_GANTT_API_BASE_URL=$NEXT_PUBLIC_GANTT_API_BASE_URL \ + NEXT_PUBLIC_APP_URL=$NEXT_PUBLIC_APP_URL + +COPY package*.json ./ +RUN npm ci + +COPY . . +RUN npm run build && npm prune --omit=dev + +RUN mkdir -p /app/data + +EXPOSE 8301 + +CMD ["npm", "run", "start", "--", "-H", "0.0.0.0", "-p", "8301"] diff --git a/app/documents/[id]/page.tsx b/app/documents/[id]/page.tsx new file mode 100644 index 0000000..5ffdc0e --- /dev/null +++ b/app/documents/[id]/page.tsx @@ -0,0 +1,193 @@ +import { Metadata } from 'next'; +import { notFound } from 'next/navigation'; +import Link from 'next/link'; +import { createClient } from '@supabase/supabase-js'; +import { DashboardLayout } from '@/components/layout/sidebar'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { ArrowLeft, FileText, FileType, FileIcon, FileCode, Edit3, Trash2, Clock, Folder } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { Document, DocumentType, DOCUMENT_TYPE_COLORS } from '@/types/documents'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; + +const typeIcons: Record = { + markdown: FileType, + text: FileText, + pdf: FileIcon, + code: FileCode, +}; + +function getSupabaseServerClient() { + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; + const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY; + + if (!supabaseUrl || !supabaseServiceKey) { + throw new Error('Server configuration error: Missing Supabase credentials'); + } + + return createClient(supabaseUrl, supabaseServiceKey, { + auth: { + autoRefreshToken: false, + persistSession: false, + }, + }); +} + +async function getDocument(id: string): Promise { + try { + const supabase = getSupabaseServerClient(); + + const { data, error } = await supabase + .from('mission_control_documents') + .select('*') + .eq('id', id) + .single(); + + if (error || !data) { + console.error('Error fetching document:', error); + return null; + } + + return { + id: data.id, + title: data.title, + content: data.content, + type: data.type as DocumentType, + folder: data.folder || 'General', + tags: data.tags || [], + createdAt: data.created_at, + updatedAt: data.updated_at, + size: data.size || 0, + description: data.description, + }; + } catch (err) { + console.error('Failed to fetch document:', err); + return null; + } +} + +function formatFileSize(bytes: number): string { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; +} + +function formatDate(dateString: string): string { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }); +} + +interface PageProps { + params: Promise<{ id: string }>; +} + +export async function generateMetadata({ params }: PageProps): Promise { + const { id } = await params; + const document = await getDocument(id); + + return { + title: document ? `${document.title} - Mission Control` : 'Document - Mission Control', + }; +} + +export default async function DocumentPage({ params }: PageProps) { + const { id } = await params; + const document = await getDocument(id); + + if (!document) { + notFound(); + } + + const Icon = typeIcons[document.type]; + const colorClass = DOCUMENT_TYPE_COLORS[document.type]; + + return ( + +
+ {/* Header with back button */} +
+ + + +
+ + {/* Document Header */} +
+
+
+
+ +
+
+

{document.title}

+
+ + + {document.folder} + + + {formatFileSize(document.size)} + + + + Updated {formatDate(document.updatedAt)} + +
+
+
+
+ + + +
+
+ + {/* Tags */} + {document.tags.length > 0 && ( +
+ {document.tags.map((tag) => ( + + #{tag} + + ))} +
+ )} +
+ + {/* Document Content */} +
+
+ + {document.content || '*No content*'} + +
+
+ + {/* Footer with back button */} +
+ + + +
+
+
+ ); +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7b4fab2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,67 @@ +services: + mission-control: + image: mission-control:latest + pull_policy: build + build: + context: . + dockerfile: Dockerfile + args: + NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL:-} + NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY:-} + NEXT_PUBLIC_GOOGLE_CLIENT_ID: ${NEXT_PUBLIC_GOOGLE_CLIENT_ID:-} + NEXT_PUBLIC_MISSION_CONTROL_URL: ${NEXT_PUBLIC_MISSION_CONTROL_URL:-} + NEXT_PUBLIC_GANTT_BOARD_URL: ${NEXT_PUBLIC_GANTT_BOARD_URL:-} + NEXT_PUBLIC_BLOG_BACKUP_URL: ${NEXT_PUBLIC_BLOG_BACKUP_URL:-} + NEXT_PUBLIC_GITEA_URL: ${NEXT_PUBLIC_GITEA_URL:-} + NEXT_PUBLIC_GITHUB_URL: ${NEXT_PUBLIC_GITHUB_URL:-} + NEXT_PUBLIC_VERCEL_URL: ${NEXT_PUBLIC_VERCEL_URL:-} + NEXT_PUBLIC_SUPABASE_SITE_URL: ${NEXT_PUBLIC_SUPABASE_SITE_URL:-} + NEXT_PUBLIC_GOOGLE_URL: ${NEXT_PUBLIC_GOOGLE_URL:-} + NEXT_PUBLIC_GOOGLE_CALENDAR_URL: ${NEXT_PUBLIC_GOOGLE_CALENDAR_URL:-} + NEXT_PUBLIC_GOOGLE_CALENDAR_SETTINGS_URL: ${NEXT_PUBLIC_GOOGLE_CALENDAR_SETTINGS_URL:-} + NEXT_PUBLIC_GANTT_API_BASE_URL: ${NEXT_PUBLIC_GANTT_API_BASE_URL:-} + NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-} + restart: unless-stopped + ports: + - "${APP_PORT:-8301}:8301" + environment: + NODE_ENV: production + NEXT_TELEMETRY_DISABLED: "1" + NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL:-} + NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY:-} + NEXT_PUBLIC_GOOGLE_CLIENT_ID: ${NEXT_PUBLIC_GOOGLE_CLIENT_ID:-} + NEXT_PUBLIC_MISSION_CONTROL_URL: ${NEXT_PUBLIC_MISSION_CONTROL_URL:-} + NEXT_PUBLIC_GANTT_BOARD_URL: ${NEXT_PUBLIC_GANTT_BOARD_URL:-} + NEXT_PUBLIC_BLOG_BACKUP_URL: ${NEXT_PUBLIC_BLOG_BACKUP_URL:-} + NEXT_PUBLIC_GITEA_URL: ${NEXT_PUBLIC_GITEA_URL:-} + NEXT_PUBLIC_GITHUB_URL: ${NEXT_PUBLIC_GITHUB_URL:-} + NEXT_PUBLIC_VERCEL_URL: ${NEXT_PUBLIC_VERCEL_URL:-} + NEXT_PUBLIC_SUPABASE_SITE_URL: ${NEXT_PUBLIC_SUPABASE_SITE_URL:-} + NEXT_PUBLIC_GOOGLE_URL: ${NEXT_PUBLIC_GOOGLE_URL:-} + NEXT_PUBLIC_GOOGLE_CALENDAR_URL: ${NEXT_PUBLIC_GOOGLE_CALENDAR_URL:-} + NEXT_PUBLIC_GOOGLE_CALENDAR_SETTINGS_URL: ${NEXT_PUBLIC_GOOGLE_CALENDAR_SETTINGS_URL:-} + NEXT_PUBLIC_GANTT_API_BASE_URL: ${NEXT_PUBLIC_GANTT_API_BASE_URL:-} + NEXT_PUBLIC_APP_URL: ${NEXT_PUBLIC_APP_URL:-} + SUPABASE_SERVICE_ROLE_KEY: ${SUPABASE_SERVICE_ROLE_KEY:-} + SUPABASE_URL: ${SUPABASE_URL:-} + SUPABASE_ANON_KEY: ${SUPABASE_ANON_KEY:-} + SUPABASE_SECRET_KEY: ${SUPABASE_SECRET_KEY:-} + GANTT_API_BASE_URL: ${GANTT_API_BASE_URL:-} + GANTT_API_REVALIDATE_SECONDS: ${GANTT_API_REVALIDATE_SECONDS:-} + GANTT_API_BEARER_TOKEN: ${GANTT_API_BEARER_TOKEN:-} + GANTT_API_COOKIE: ${GANTT_API_COOKIE:-} + RESEND_API_KEY: ${RESEND_API_KEY:-} + RESEND_FROM_EMAIL: ${RESEND_FROM_EMAIL:-} + EMAIL_FROM: ${EMAIL_FROM:-} + VERCEL_URL: ${VERCEL_URL:-} + volumes: + - mission_control_data:/app/data + healthcheck: + test: ["CMD-SHELL", "node -e \"fetch('http://127.0.0.1:8301/login').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""] + interval: 30s + timeout: 5s + retries: 5 + start_period: 45s + +volumes: + mission_control_data: