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