Signed-off-by: OpenClaw Bot <ai-agent@topdoglabs.com>
This commit is contained in:
parent
bca83c682d
commit
7caf290332
50
Dockerfile
Normal file
50
Dockerfile
Normal file
@ -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"]
|
||||
193
app/documents/[id]/page.tsx
Normal file
193
app/documents/[id]/page.tsx
Normal file
@ -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<DocumentType, typeof FileText> = {
|
||||
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<Document | null> {
|
||||
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<Metadata> {
|
||||
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 (
|
||||
<DashboardLayout>
|
||||
<div className="space-y-6">
|
||||
{/* Header with back button */}
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/documents">
|
||||
<Button variant="outline" size="sm">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Back to Documents
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Document Header */}
|
||||
<div className="border rounded-lg p-6 bg-card">
|
||||
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={cn('p-3 rounded-lg shrink-0', colorClass)}>
|
||||
<Icon className="w-6 h-6" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-xl sm:text-2xl font-semibold">{document.title}</h1>
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground mt-2">
|
||||
<span className="flex items-center gap-1">
|
||||
<Folder className="w-4 h-4" />
|
||||
{document.folder}
|
||||
</span>
|
||||
<span>•</span>
|
||||
<span>{formatFileSize(document.size)}</span>
|
||||
<span>•</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="w-4 h-4" />
|
||||
Updated {formatDate(document.updatedAt)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<Link href={`/documents?id=${document.id}&edit=true`}>
|
||||
<Button variant="outline" size="sm">
|
||||
<Edit3 className="w-4 h-4 mr-2" />
|
||||
Edit
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
{document.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-4 pt-4 border-t">
|
||||
{document.tags.map((tag) => (
|
||||
<Badge key={tag} variant="secondary" className="text-xs">
|
||||
#{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Document Content */}
|
||||
<div className="border rounded-lg p-6 bg-card">
|
||||
<div className="prose prose-base dark:prose-invert max-w-none prose-headings:mt-6 prose-headings:mb-4 prose-p:my-3 prose-ul:my-3 prose-ol:my-3">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{document.content || '*No content*'}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer with back button */}
|
||||
<div className="flex items-center justify-center pt-4">
|
||||
<Link href="/documents">
|
||||
<Button variant="outline">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Back to Documents
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
67
docker-compose.yml
Normal file
67
docker-compose.yml
Normal file
@ -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:
|
||||
Loading…
Reference in New Issue
Block a user