Initial SFERA platform baseline

This commit is contained in:
2026-05-16 19:03:49 +03:00
commit 3b845c8fce
282 changed files with 55045 additions and 0 deletions
@@ -0,0 +1,79 @@
import { resolveApiUrl } from "@/lib/api";
type RouteContext = {
params: Promise<{ path: string[] }>;
};
export async function GET(request: Request, context: RouteContext) {
return proxy(request, context);
}
export async function POST(request: Request, context: RouteContext) {
return proxy(request, context);
}
export async function PUT(request: Request, context: RouteContext) {
return proxy(request, context);
}
export async function PATCH(request: Request, context: RouteContext) {
return proxy(request, context);
}
export async function DELETE(request: Request, context: RouteContext) {
return proxy(request, context);
}
export async function OPTIONS(request: Request, context: RouteContext) {
return proxy(request, context);
}
export async function HEAD(request: Request, context: RouteContext) {
return proxy(request, context);
}
async function proxy(request: Request, context: RouteContext) {
const { path } = await context.params;
const apiUrl = resolveApiUrl(request.headers.get("host"));
const sourceUrl = new URL(request.url);
const target = `${apiUrl}/${path.join("/")}${sourceUrl.search}`;
const method = request.method;
const hasBody = method !== "GET" && method !== "HEAD" && request.body !== null;
const body = hasBody ? await request.arrayBuffer() : undefined;
const contentType = request.headers.get("Content-Type");
const accept = request.headers.get("Accept") ?? "application/json";
const publicHost = request.headers.get("x-forwarded-host") ?? request.headers.get("host");
const publicProto = request.headers.get("x-forwarded-proto") ?? "http";
const publicOrigin = publicHost ? `${publicProto}://${publicHost}` : sourceUrl.origin;
const headers: Record<string, string> = {
Accept: accept,
"X-Sfera-Public-Origin": publicOrigin
};
if (hasBody) {
headers["Content-Type"] = contentType ?? "application/json";
}
const response = await fetch(target, {
method,
cache: "no-store",
headers,
body
});
const responseHeaders: Record<string, string> = {
"Content-Type": response.headers.get("Content-Type") ?? "application/json"
};
const contentDisposition = response.headers.get("Content-Disposition");
if (contentDisposition) {
responseHeaders["Content-Disposition"] = contentDisposition;
}
const cacheControl = response.headers.get("Cache-Control");
if (cacheControl) {
responseHeaders["Cache-Control"] = cacheControl;
}
return new Response(response.body, {
status: response.status,
headers: responseHeaders
});
}
+113
View File
@@ -0,0 +1,113 @@
import { AlertTriangle } from "lucide-react";
import { headers } from "next/headers";
import { IdeWorkspace } from "@/components/editor/ide-workspace";
import { AppShell } from "@/components/layout/app-shell";
import { Card } from "@/components/ui/card";
import {
getApiHealth,
getProjects,
getProjectWorkspaceData,
resolveApiUrl,
type ProjectSummary
} from "@/lib/api";
import { messages, normalizeLanguage, type UiLanguage } from "@/lib/i18n";
export default async function EditorPage({
searchParams
}: Readonly<{
searchParams?: Promise<{ lang?: string; mode?: string; project?: string; routine?: string }>;
}>) {
const params = await searchParams;
const language = normalizeLanguage(params?.lang);
const requestHeaders = await headers();
const apiUrl = resolveApiUrl(requestHeaders.get("host"));
const bootstrap = await loadEditorBootstrap(apiUrl, language, params?.project);
const projectData =
bootstrap.status === "ok" && bootstrap.projectId ? await getProjectWorkspaceData(bootstrap.projectId, apiUrl, params?.routine, params?.mode) : null;
return (
<AppShell
apiStatus={bootstrap.status}
language={language}
projectId={bootstrap.status === "ok" ? bootstrap.projectId : undefined}
projectOptions={bootstrap.status === "ok" ? bootstrap.projects : []}
>
<main className="pb-8">
{bootstrap.status === "error" ? (
<ErrorState language={language} message={bootstrap.error} />
) : projectData ? (
<IdeWorkspace
key={projectData.projectId}
data={projectData}
initialMode={params?.mode}
language={language}
routineName={params?.routine}
/>
) : (
<ErrorState language={language} message={messages[language].noProjectDataDescription} />
)}
</main>
</AppShell>
);
}
async function loadEditorBootstrap(apiUrl: string, language: UiLanguage, projectId?: string): Promise<
| { status: "ok"; projectId: string | undefined; projects: ProjectSummary[] }
| { status: "error"; error: string }
> {
try {
const [projectsResponse] = await Promise.all([getProjects(apiUrl), getApiHealth(apiUrl)]);
const projects = uniqueProjects(projectsResponse);
return { status: "ok", projectId: projectId || pickDefaultProject(projectsResponse), projects };
} catch (error) {
return {
status: "error",
error: error instanceof Error ? error.message : messages[language].unknownApiError
};
}
}
function ErrorState({
message,
language
}: Readonly<{
message: string;
language: UiLanguage;
}>) {
const t = messages[language];
return (
<div className="mx-auto flex min-h-[70vh] max-w-xl items-center justify-center">
<Card className="w-full">
<div className="flex items-start gap-3">
<AlertTriangle className="mt-1 h-5 w-5 text-destructive" aria-hidden="true" />
<div>
<h1 className="text-base font-semibold">{t.apiUnavailable}</h1>
<p className="mt-2 text-sm leading-6 text-muted-foreground">{message}</p>
</div>
</div>
</Card>
</div>
);
}
function pickDefaultProject(projects: ProjectSummary[]) {
return (
projects.find((project) => project.project_id === "ifcm-upo")?.project_id ??
projects.find((project) => project.project_id !== "demo")?.project_id ??
projects.at(-1)?.project_id ??
projects.find((project) => project.project_id === "demo")?.project_id
);
}
function uniqueProjects(projects: ProjectSummary[]) {
const byId = new Map<string, ProjectSummary>();
for (const project of projects) {
const projectId = project.project_id.trim();
if (projectId) {
byId.set(projectId, { ...project, project_id: projectId, name: project.name || projectId });
}
}
return Array.from(byId.values()).sort((left, right) => (left.name || left.project_id).localeCompare(right.name || right.project_id, "ru"));
}
+70
View File
@@ -0,0 +1,70 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: 210 20% 98%;
--foreground: 222 47% 11%;
--card: 0 0% 100%;
--card-foreground: 222 47% 11%;
--muted: 210 17% 94%;
--muted-foreground: 215 16% 47%;
--border: 214 20% 88%;
--input: 214 20% 88%;
--primary: 198 93% 32%;
--primary-foreground: 0 0% 100%;
--secondary: 168 42% 39%;
--secondary-foreground: 0 0% 100%;
--destructive: 0 72% 45%;
--warning: 38 92% 48%;
--success: 142 54% 36%;
--info: 214 84% 56%;
}
.dark {
--background: 220 24% 10%;
--foreground: 210 20% 96%;
--card: 220 20% 14%;
--card-foreground: 210 20% 96%;
--muted: 220 16% 20%;
--muted-foreground: 215 15% 66%;
--border: 220 16% 24%;
--input: 220 16% 24%;
--primary: 194 80% 46%;
--primary-foreground: 0 0% 100%;
--secondary: 164 52% 43%;
--secondary-foreground: 0 0% 100%;
--destructive: 0 72% 51%;
--warning: 38 92% 52%;
--success: 142 54% 42%;
--info: 214 84% 62%;
}
* {
box-sizing: border-box;
}
html {
background: hsl(var(--background));
}
body {
min-height: 100vh;
margin: 0;
background: hsl(var(--background));
color: hsl(var(--foreground));
font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
letter-spacing: 0;
}
button,
input,
select,
textarea {
font: inherit;
}
::selection {
background: hsl(var(--primary) / 0.18);
}
@@ -0,0 +1,3 @@
export async function GET() {
return Response.json({ status: "ok", service: "sfera-web" });
}
+16
View File
@@ -0,0 +1,16 @@
import type { Metadata } from "next";
import type React from "react";
import "./globals.css";
export const metadata: Metadata = {
title: "SFERA",
description: "Semantic 1C operating workspace"
};
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="ru">
<body>{children}</body>
</html>
);
}
+22
View File
@@ -0,0 +1,22 @@
import { redirect } from "next/navigation";
export default async function HomePage({
searchParams
}: Readonly<{
searchParams?: Promise<{ lang?: string; mode?: string; project?: string; routine?: string }>;
}>) {
const params = await searchParams;
const nextParams = new URLSearchParams();
if (params?.lang === "en") {
nextParams.set("lang", "en");
}
for (const key of ["project", "mode", "routine"] as const) {
const value = params?.[key];
if (value) {
nextParams.set(key, value);
}
}
const suffix = nextParams.size > 0 ? `?${nextParams.toString()}` : "";
redirect(`/editor${suffix}`);
}
@@ -0,0 +1,80 @@
import { headers } from "next/headers";
import { ProjectSetupClient, type ProjectSetup } from "@/components/project-setup/project-setup-client";
import { getProjects, resolveApiUrl } from "@/lib/api";
export default async function ProjectSettingsPage({
searchParams
}: Readonly<{ searchParams?: Promise<{ project?: string; new?: string }> }>) {
const requestHeaders = await headers();
const params = searchParams ? await searchParams : {};
if (params.new === "1") {
return <ProjectSetupClient initialSetup={newProjectSetup()} />;
}
const apiUrl = resolveApiUrl(requestHeaders.get("host"));
const projectId = normalizeProjectId(params.project);
const setup = await loadSetup(apiUrl, projectId);
return <ProjectSetupClient initialSetup={setup} />;
}
function newProjectSetup(): ProjectSetup {
return {
project_id: "__new__",
status: "NOT_CONFIGURED",
message: "Новый проект. Заполните основные параметры и сохраните.",
settings: defaultProjectSettings(""),
current_source: null,
last_import: null,
import_history: [],
import_sources: []
};
}
async function loadSetup(apiUrl: string, projectId: string): Promise<ProjectSetup> {
try {
const response = await fetch(`${apiUrl}/projects/${encodeURIComponent(projectId)}/setup`, { cache: "no-store" });
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
} catch {
const projects = await getProjects(apiUrl).catch(() => []);
const fallbackProjectId = projects.at(0)?.project_id ?? "default";
if (fallbackProjectId !== projectId) {
return loadSetup(apiUrl, fallbackProjectId);
}
return {
project_id: fallbackProjectId,
status: "NOT_CONFIGURED",
message: "Проект не проиндексирован. Выберите способ получения структуры конфигурации.",
settings: defaultProjectSettings("SFERA Project"),
current_source: null,
last_import: null,
import_history: [],
import_sources: []
};
}
}
function defaultProjectSettings(name = "SFERA Project") {
return {
name,
configuration_source: null,
structure_source: null,
platform_version: null,
compatibility_mode: null,
extensions: [],
environments: {},
agent: {},
server_import: {},
privacy_mode: "METADATA_ONLY",
knowledge_sources: [],
task_session_policy: {}
};
}
function normalizeProjectId(projectId?: string) {
const trimmed = projectId?.trim();
return trimmed || "default";
}