diff --git a/web/app/equipment/list/page.tsx b/web/app/equipment/list/page.tsx
index 704773e..54c4f2c 100644
--- a/web/app/equipment/list/page.tsx
+++ b/web/app/equipment/list/page.tsx
@@ -92,7 +92,7 @@ export default function EquipmentListPage() {
(INITIAL_DATA);
- const [submitting, setSubmitting] = useState(false);
- const [submitResult, setSubmitResult] = useState<{ status: string; message: string } | null>(null);
-
- const updateField = (field: keyof FormData, value: unknown) => {
- setFormData((prev) => ({ ...prev, [field]: value }));
- };
-
- const addColumn = () => {
- setFormData((prev) => ({
- ...prev,
- columns: [...prev.columns, { name: "", type: "float", unit: "" }],
- }));
- };
-
- const updateColumn = (index: number, field: string, value: string) => {
- setFormData((prev) => {
- const cols = [...prev.columns];
- cols[index] = { ...cols[index], [field]: value };
- return { ...prev, columns: cols };
- });
- };
-
- const removeColumn = (index: number) => {
- setFormData((prev) => ({
- ...prev,
- columns: prev.columns.filter((_, i) => i !== index),
- }));
- };
-
- const handleSubmit = async () => {
- setSubmitting(true);
- setSubmitResult(null);
-
- // Auto-populate ownership from the authenticated user
- const identity: UserIdentity = { id: user.id, organization: user.organization, role: user.role };
- const payload = { ...formData, owner_id: user.id, owner_org: user.organization };
- const result = await registerEquipment(payload, identity);
-
- if (result) {
- setSubmitResult({ status: "success", message: result.message });
- } else {
- setSubmitResult({
- status: "error",
- message: "Registration failed. Is the FastAPI server running?",
- });
- }
- setSubmitting(false);
- };
-
- const renderInputField = (
- label: string,
- field: keyof FormData,
- placeholder: string,
- type: string = "text"
- ) => (
-
-
- updateField(field, e.target.value)}
- placeholder={placeholder}
- className="w-full px-3 py-2 rounded-lg border border-[hsl(var(--border))] bg-[hsl(var(--background))] text-sm focus:outline-none focus:ring-2 focus:ring-[hsl(var(--ring))]"
- />
-
- );
-
- const renderStep = () => {
- switch (currentStep) {
- case 1:
- return (
-
-
Equipment Metadata
-
- Provide basic information about the equipment being registered.
-
-
- {renderInputField("Equipment Name", "name", "e.g., ICP Etcher")}
- {renderInputField("Domain ID", "domain_id", "e.g., etcher (lowercase, no spaces)")}
- {renderInputField("Equipment Model", "equipment_model", "e.g., PlasmaTherm Apex SLR")}
- {renderInputField("Location", "location", "e.g., Birck Cleanroom Room 1234")}
-
-
-
-
-
- );
-
- case 2:
- return (
-
-
Data Source Configuration
-
- Configure how the platform connects to your equipment's data source.
-
-
-
-
-
- {(formData.source_type === "postgresql" ||
- formData.source_type === "mysql") && (
-
- {renderInputField("Host", "connection_host", "db.example.com")}
- {renderInputField("Port", "connection_port", "5432")}
- {renderInputField("Database Name", "connection_database", "etcher_db")}
-
- )}
-
- );
-
- case 3:
- return (
-
-
Schema Definition
-
- Define the columns in your dataset. These will become available as features or targets.
-
-
-
-
- );
-
- case 4:
- return (
-
-
Features & Targets
-
- Designate which columns are ML features (inputs) and which are optimization targets (outputs).
-
-
-
-
Primary Target
- {renderInputField("Column Name", "primary_target", "e.g., AvgEtchRate")}
-
-
-
-
-
-
-
Secondary Target
- {renderInputField("Column Name", "secondary_target", "e.g., RangeEtchRate")}
-
-
-
-
-
-
-
- );
-
- case 5:
- return (
-
-
Outlier Rules & DT Mode
-
- Configure data quality rules and the digital twin operating mode.
-
-
-
-
-
-
- {renderInputField("Threshold", "outlier_threshold", "e.g., 3.0 for Z-score")}
-
-
-
-
- {[
- {
- value: "full",
- label: "Full Optimization",
- desc: "ML training + Pareto optimization + recipe proposals",
- icon: Gauge,
- },
- {
- value: "dashboard",
- label: "Dashboard Only",
- desc: "Data visualization and monitoring only",
- icon: Columns3,
- },
- ].map((mode) => (
-
- ))}
-
-
-
- );
-
- case 6:
- return (
-
-
Review & Submit
-
- Review your configuration before submitting for admin approval.
-
-
-
- {JSON.stringify(
- {
- domain: {
- id: formData.domain_id || "your_domain_id",
- display_name: formData.name || "Equipment Name",
- description: formData.description || "",
- version: "1.0.0",
- equipment: formData.equipment_model || "",
- },
- database: {
- cache_db: `pareto_cache_${formData.domain_id || "domain"}.db`,
- production_db: `pareto_production_${formData.domain_id || "domain"}.db`,
- },
- azure: {
- container: `${formData.domain_id || "domain"}-data`,
- dataset_blob: "full_dataset.csv",
- state_blob: "last_processed_time.txt",
- processed_runs_blob: "processed_runs.json",
- },
- targets: {
- primary: {
- id: formData.primary_target || "primary_metric",
- objective: formData.primary_objective,
- },
- secondary: {
- id: formData.secondary_target || "secondary_metric",
- objective: formData.secondary_objective,
- },
- },
- dt_mode: formData.dt_mode,
- },
- null,
- 2
- )}
-
-
-
- {/* Submission result feedback */}
- {submitResult && (
-
- {submitResult.status === "success" ? (
-
- ) : (
-
- )}
-
{submitResult.message}
-
- )}
-
- {!submitResult && (
-
-
-
- On submit, this configuration will be saved as a domain config JSON and sent to a platform admin for review.
-
-
- )}
-
- );
- }
- };
-
- return (
-
-
-
- Register New Equipment
-
-
- Multi-step wizard to register equipment and configure its digital twin.
-
-
-
- {/* Step Indicator */}
-
- {STEPS.map((step, i) => (
-
-
- {i < STEPS.length - 1 && (
-
- )}
-
- ))}
-
-
- {/* Step Content */}
-
- {renderStep()}
-
-
- {/* Navigation Buttons */}
-
-
- {currentStep < 6 ? (
-
- ) : (
-
- )}
-
-
- );
+import { redirect } from "next/navigation";
+
+/**
+ * /equipment now redirects to the equipment list.
+ * Registration is at /equipment/register (separate wizard).
+ */
+export default function EquipmentRedirect() {
+ redirect("/equipment/list");
}
diff --git a/web/app/equipment/register/page.tsx b/web/app/equipment/register/page.tsx
new file mode 100644
index 0000000..ab7d1f2
--- /dev/null
+++ b/web/app/equipment/register/page.tsx
@@ -0,0 +1,469 @@
+"use client";
+
+import { useState } from "react";
+import { cn } from "@/lib/utils";
+import { registerEquipment, type UserIdentity } from "@/lib/api-client";
+import { useAuth } from "@/lib/auth-context";
+import {
+ ChevronRight,
+ ChevronLeft,
+ Check,
+ Cpu,
+ Database,
+ SlidersHorizontal,
+ AlertTriangle,
+ Loader2,
+ Plus,
+ Trash2,
+} from "lucide-react";
+
+const STEPS = [
+ { id: 1, label: "Equipment Identity", icon: Cpu },
+ { id: 2, label: "Equipment Parameters", icon: SlidersHorizontal },
+ { id: 3, label: "Data Source & Review", icon: Database },
+];
+
+interface EquipmentParam {
+ name: string;
+ unit: string;
+ min_value: string;
+ max_value: string;
+ description: string;
+}
+
+interface FormData {
+ // Step 1 — Equipment Identity
+ name: string;
+ equipment_type: string;
+ manufacturer: string;
+ model: string;
+ location: string;
+ serial_number: string;
+ description: string;
+ // Step 2 — Equipment Parameters (hardware limits)
+ parameters: EquipmentParam[];
+ // Step 3 — Data Source
+ source_type: string;
+ connection_host: string;
+ connection_port: string;
+ connection_database: string;
+}
+
+const INITIAL_DATA: FormData = {
+ name: "",
+ equipment_type: "",
+ manufacturer: "",
+ model: "",
+ location: "",
+ serial_number: "",
+ description: "",
+ parameters: [
+ { name: "RF Power", unit: "W", min_value: "0", max_value: "2000", description: "RF generator power" },
+ { name: "Pressure", unit: "mTorr", min_value: "0", max_value: "100", description: "Chamber pressure" },
+ ],
+ source_type: "postgresql",
+ connection_host: "",
+ connection_port: "5432",
+ connection_database: "",
+};
+
+export default function EquipmentRegisterPage() {
+ const { user } = useAuth();
+ const [currentStep, setCurrentStep] = useState(1);
+ const [formData, setFormData] = useState(INITIAL_DATA);
+ const [submitting, setSubmitting] = useState(false);
+ const [submitResult, setSubmitResult] = useState<{ status: string; message: string } | null>(null);
+
+ const updateField = (field: keyof FormData, value: unknown) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
+ const addParameter = () => {
+ setFormData((prev) => ({
+ ...prev,
+ parameters: [...prev.parameters, { name: "", unit: "", min_value: "", max_value: "", description: "" }],
+ }));
+ };
+
+ const updateParameter = (index: number, field: keyof EquipmentParam, value: string) => {
+ setFormData((prev) => {
+ const params = [...prev.parameters];
+ params[index] = { ...params[index], [field]: value };
+ return { ...prev, parameters: params };
+ });
+ };
+
+ const removeParameter = (index: number) => {
+ setFormData((prev) => ({
+ ...prev,
+ parameters: prev.parameters.filter((_, i) => i !== index),
+ }));
+ };
+
+ const handleSubmit = async () => {
+ setSubmitting(true);
+ setSubmitResult(null);
+
+ const identity: UserIdentity = { id: user.id, organization: user.organization, role: user.role };
+ const payload = {
+ ...formData,
+ domain_id: formData.name.toLowerCase().replace(/\s+/g, "_"),
+ owner_id: user.id,
+ owner_org: user.organization,
+ };
+ const result = await registerEquipment(payload, identity);
+
+ if (result) {
+ setSubmitResult({ status: "success", message: result.message });
+ } else {
+ setSubmitResult({
+ status: "error",
+ message: "Registration failed. Is the FastAPI server running?",
+ });
+ }
+ setSubmitting(false);
+ };
+
+ const inputClass =
+ "w-full px-3 py-2 rounded-lg border border-[hsl(var(--border))] bg-[hsl(var(--background))] text-sm focus:outline-none focus:ring-2 focus:ring-[hsl(var(--ring))]";
+
+ const renderInputField = (
+ label: string,
+ field: keyof FormData,
+ placeholder: string,
+ type: string = "text",
+ ) => (
+
+
+ updateField(field, e.target.value)}
+ placeholder={placeholder}
+ className={inputClass}
+ />
+
+ );
+
+ const renderStep = () => {
+ switch (currentStep) {
+ /* ─── Step 1: Equipment Identity ─── */
+ case 1:
+ return (
+
+
Equipment Identity
+
+ Register the physical machine. This is a one-time setup done
+ by the tool owner or system admin.
+
+
+ {renderInputField("Equipment Name", "name", "e.g., Etcher_01")}
+
+
+
+
+ {renderInputField("Manufacturer", "manufacturer", "e.g., Lam Research")}
+ {renderInputField("Model", "model", "e.g., PlasmaTherm Apex SLR")}
+ {renderInputField("Location", "location", "e.g., Fab 2 – Bay A")}
+ {renderInputField("Serial Number", "serial_number", "e.g., PT-2024-0891")}
+
+
+
+
+
+ );
+
+ /* ─── Step 2: Equipment Parameters (Hardware Limits) ─── */
+ case 2:
+ return (
+
+
Equipment Parameters
+
+ Define the hardware limits for this equipment — the physical min/max values
+ from the manufacturer spec sheet. These are not experiment settings.
+
+
+ {/* Header row */}
+
+ Parameter Name
+ Unit
+ Min Value
+ Max Value
+ Description
+
+
+
+ {/* Parameter rows */}
+
+ {formData.parameters.map((param, i) => (
+
+ updateParameter(i, "name", e.target.value)}
+ placeholder="e.g., RF Power"
+ className={inputClass}
+ />
+ updateParameter(i, "unit", e.target.value)}
+ placeholder="W"
+ className={inputClass}
+ />
+ updateParameter(i, "min_value", e.target.value)}
+ placeholder="0"
+ className={inputClass}
+ />
+ updateParameter(i, "max_value", e.target.value)}
+ placeholder="2000"
+ className={inputClass}
+ />
+ updateParameter(i, "description", e.target.value)}
+ placeholder="Description"
+ className={inputClass}
+ />
+
+
+ ))}
+
+
+
+
+ );
+
+ /* ─── Step 3: Data Source & Review ─── */
+ case 3:
+ return (
+
+
+
Data Source Configuration
+
+ How the platform connects to this equipment's data.
+
+
+
+
+
+ {(formData.source_type === "postgresql" ||
+ formData.source_type === "mysql") && (
+
+ {renderInputField("Host", "connection_host", "db.example.com")}
+ {renderInputField("Port", "connection_port", "5432")}
+ {renderInputField("Database Name", "connection_database", "etcher_db")}
+
+ )}
+
+
+
+
+ {/* Review Summary */}
+
+
Review
+
+
+
+
+ | Equipment |
+ {formData.name || "—"} |
+
+
+ | Type |
+ {formData.equipment_type || "—"} |
+
+
+ | Manufacturer |
+ {formData.manufacturer || "—"} |
+
+
+ | Location |
+ {formData.location || "—"} |
+
+
+ | Parameters |
+ {formData.parameters.filter(p => p.name).length} hardware parameters defined |
+
+
+ | Data Source |
+ {formData.source_type}{formData.connection_host ? ` — ${formData.connection_host}` : ""} |
+
+
+
+
+
+
+ {/* Submission feedback */}
+ {submitResult && (
+
+ {submitResult.status === "success" ? (
+
+ ) : (
+
+ )}
+
{submitResult.message}
+
+ )}
+
+ {!submitResult && (
+
+
+
+ On submit, this equipment will be registered and sent to a platform admin for review.
+
+
+ )}
+
+ );
+ }
+ };
+
+ const totalSteps = STEPS.length;
+
+ return (
+
+
+
+ Register New Equipment
+
+
+ Stage 0 — One-time setup for a physical machine and its hardware parameters.
+
+
+
+ {/* Step Indicator */}
+
+ {STEPS.map((step, i) => (
+
+
+ {i < STEPS.length - 1 && (
+
+ )}
+
+ ))}
+
+
+ {/* Step Content */}
+
+ {renderStep()}
+
+
+ {/* Navigation Buttons */}
+
+
+ {currentStep < totalSteps ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
diff --git a/web/app/experiment/new/page.tsx b/web/app/experiment/new/page.tsx
new file mode 100644
index 0000000..29db648
--- /dev/null
+++ b/web/app/experiment/new/page.tsx
@@ -0,0 +1,639 @@
+"use client";
+
+import { useState } from "react";
+import { cn } from "@/lib/utils";
+import { useAuth } from "@/lib/auth-context";
+import {
+ ChevronRight,
+ ChevronLeft,
+ Check,
+ FlaskConical,
+ ClipboardList,
+ FileCheck,
+ AlertTriangle,
+ Loader2,
+ Plus,
+ Trash2,
+} from "lucide-react";
+
+const STEPS = [
+ { id: 1, label: "Experiment Type", icon: FlaskConical },
+ { id: 2, label: "Experiment Definition", icon: ClipboardList },
+ { id: 3, label: "Review & Submit", icon: FileCheck },
+];
+
+/* ─── Mock registered equipment (would come from API) ─── */
+const REGISTERED_EQUIPMENT = [
+ { id: "ETCHER_01", name: "Etcher_01", type: "ICP Etcher", location: "Fab 2 – Bay A" },
+ { id: "CYTOMETER_01", name: "Cytometer_01", type: "Flow Cytometer", location: "Birck Lab 202" },
+];
+
+interface TypeParam {
+ name: string;
+ unit: string;
+ is_variable: boolean;
+ default_value: string;
+ min_value: string;
+ max_value: string;
+}
+
+interface PlannedParam {
+ name: string;
+ planned_value: string;
+ unit: string;
+}
+
+interface FormData {
+ // Step 1 — Experiment Type
+ type_name: string;
+ type_description: string;
+ scientific_objective: string;
+ type_parameters: TypeParam[];
+ // Step 2 — Experiment Definition
+ selected_equipment: string;
+ experiment_name: string;
+ planned_date: string;
+ planned_parameters: PlannedParam[];
+}
+
+const INITIAL_DATA: FormData = {
+ type_name: "",
+ type_description: "",
+ scientific_objective: "",
+ type_parameters: [
+ { name: "Gas Flow", unit: "sccm", is_variable: true, default_value: "50", min_value: "10", max_value: "200" },
+ { name: "RF Power", unit: "W", is_variable: true, default_value: "800", min_value: "500", max_value: "1200" },
+ { name: "Temperature", unit: "°C", is_variable: false, default_value: "700", min_value: "700", max_value: "700" },
+ ],
+ selected_equipment: "",
+ experiment_name: "",
+ planned_date: "",
+ planned_parameters: [],
+};
+
+export default function NewExperimentPage() {
+ const { user } = useAuth();
+ const [currentStep, setCurrentStep] = useState(1);
+ const [formData, setFormData] = useState(INITIAL_DATA);
+ const [submitting, setSubmitting] = useState(false);
+ const [submitResult, setSubmitResult] = useState<{ status: string; message: string } | null>(null);
+
+ const updateField = (field: keyof FormData, value: unknown) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ };
+
+ /* ─── Type Parameter management ─── */
+ const addTypeParam = () => {
+ setFormData((prev) => ({
+ ...prev,
+ type_parameters: [...prev.type_parameters, { name: "", unit: "", is_variable: true, default_value: "", min_value: "", max_value: "" }],
+ }));
+ };
+
+ const updateTypeParam = (index: number, field: keyof TypeParam, value: string | boolean) => {
+ setFormData((prev) => {
+ const params = [...prev.type_parameters];
+ params[index] = { ...params[index], [field]: value };
+ return { ...prev, type_parameters: params };
+ });
+ };
+
+ const removeTypeParam = (index: number) => {
+ setFormData((prev) => ({
+ ...prev,
+ type_parameters: prev.type_parameters.filter((_, i) => i !== index),
+ }));
+ };
+
+ /* ─── Planned Parameter management ─── */
+ const addPlannedParam = () => {
+ setFormData((prev) => ({
+ ...prev,
+ planned_parameters: [...prev.planned_parameters, { name: "", planned_value: "", unit: "" }],
+ }));
+ };
+
+ const updatePlannedParam = (index: number, field: keyof PlannedParam, value: string) => {
+ setFormData((prev) => {
+ const params = [...prev.planned_parameters];
+ params[index] = { ...params[index], [field]: value };
+ return { ...prev, planned_parameters: params };
+ });
+ };
+
+ const removePlannedParam = (index: number) => {
+ setFormData((prev) => ({
+ ...prev,
+ planned_parameters: prev.planned_parameters.filter((_, i) => i !== index),
+ }));
+ };
+
+ /* ─── Auto-populate planned params from type params ─── */
+ const populatePlannedFromType = () => {
+ const planned = formData.type_parameters
+ .filter((p) => p.is_variable && p.name)
+ .map((p) => ({
+ name: p.name,
+ planned_value: p.default_value,
+ unit: p.unit,
+ }));
+ updateField("planned_parameters", planned);
+ };
+
+ const handleSubmit = async () => {
+ setSubmitting(true);
+ setSubmitResult(null);
+
+ // Simulate submission (replace with real API call)
+ await new Promise((r) => setTimeout(r, 1200));
+
+ setSubmitResult({
+ status: "success",
+ message: `Experiment "${formData.experiment_name}" submitted successfully. Run it from the Experiment Runs page.`,
+ });
+ setSubmitting(false);
+ };
+
+ const inputClass =
+ "w-full px-3 py-2 rounded-lg border border-[hsl(var(--border))] bg-[hsl(var(--background))] text-sm focus:outline-none focus:ring-2 focus:ring-[hsl(var(--ring))]";
+
+ const renderStep = () => {
+ switch (currentStep) {
+ /* ─── Step 1: Experiment Type (Recipe Template) ─── */
+ case 1:
+ return (
+
+
+
Experiment Type Definition
+
+ Define a reusable recipe template. This captures the scientific intent —
+ which parameters are variable vs. fixed, and their allowed ranges.
+
+
+
+
+
+
+
+
+
+
+
Parameter Template
+
+ Define which parameters this experiment type uses. Mark parameters as "Variable"
+ if the optimizer should explore them, or "Fixed" if locked.
+
+
+ {/* Header row */}
+
+ Name
+ Unit
+ Variable?
+ Default
+ Min
+ Max
+
+
+
+ {formData.type_parameters.map((param, i) => (
+
+ updateTypeParam(i, "name", e.target.value)}
+ placeholder="Parameter"
+ className={inputClass}
+ />
+ updateTypeParam(i, "unit", e.target.value)}
+ placeholder="Unit"
+ className={inputClass}
+ />
+
+ updateTypeParam(i, "default_value", e.target.value)}
+ placeholder="800"
+ className={inputClass}
+ />
+ updateTypeParam(i, "min_value", e.target.value)}
+ placeholder="Min"
+ className={cn(inputClass, !param.is_variable && "opacity-40")}
+ disabled={!param.is_variable}
+ />
+ updateTypeParam(i, "max_value", e.target.value)}
+ placeholder="Max"
+ className={cn(inputClass, !param.is_variable && "opacity-40")}
+ disabled={!param.is_variable}
+ />
+
+
+ ))}
+
+
+
+
+ );
+
+ /* ─── Step 2: Experiment Definition (Plan) ─── */
+ case 2:
+ return (
+
+
+
Experiment Definition
+
+ Bind an experiment type to specific equipment and define the planned parameter values.
+
+
+
+
+
+
+
+
+
+
+ updateField("experiment_name", e.target.value)}
+ placeholder="e.g., DOE_RF_vs_Pressure"
+ className={inputClass}
+ />
+
+
+
+
+
+
+
+ {formData.type_name || "—"} (defined in Step 1)
+
+
+
+
+ updateField("planned_date", e.target.value)}
+ className={inputClass}
+ />
+
+
+
+ {/* Planned Parameter Values */}
+
+
+
+
Planned Parameters
+
+ Set the actual values for this specific experiment run.
+
+
+ {formData.type_parameters.some((p) => p.is_variable && p.name) && formData.planned_parameters.length === 0 && (
+
+ )}
+
+
+
+ Parameter Name
+ Planned Value
+ Unit
+
+
+
+ {formData.planned_parameters.map((param, i) => (
+
+ updatePlannedParam(i, "name", e.target.value)}
+ placeholder="e.g., Gas Flow"
+ className={inputClass}
+ />
+ updatePlannedParam(i, "planned_value", e.target.value)}
+ placeholder="55"
+ className={inputClass}
+ />
+ updatePlannedParam(i, "unit", e.target.value)}
+ placeholder="sccm"
+ className={cn(inputClass, "text-[hsl(var(--muted-foreground))]")}
+ />
+
+
+ ))}
+
+
+
+
+ );
+
+ /* ─── Step 3: Review & Submit ─── */
+ case 3:
+ return (
+
+
Review & Submit
+
+ Confirm the experiment plan before submitting.
+
+
+ {/* Two-column summary */}
+
+ {/* Experiment Type summary */}
+
+
Experiment Type
+
+
Name: {formData.type_name || "—"}
+
Objective: {formData.scientific_objective || "—"}
+
+ {formData.type_parameters.filter(p => p.name).length > 0 && (
+
+
+
+
+ | Param |
+ Variable? |
+ Default |
+ Range |
+
+
+
+ {formData.type_parameters.filter(p => p.name).map((p, i) => (
+
+ | {p.name} ({p.unit}) |
+
+
+ {p.is_variable ? "Yes" : "Fixed"}
+
+ |
+ {p.default_value} |
+ {p.is_variable ? `${p.min_value}–${p.max_value}` : "—"} |
+
+ ))}
+
+
+
+ )}
+
+
+ {/* Experiment Definition summary */}
+
+
Experiment Definition
+
+
Name: {formData.experiment_name || "—"}
+
Equipment: {formData.selected_equipment || "—"}
+
Date: {formData.planned_date || "—"}
+
Owner: {user.name}
+
+ {formData.planned_parameters.filter(p => p.name).length > 0 && (
+
+
+
+
+ | Parameter |
+ Planned Value |
+ Unit |
+
+
+
+ {formData.planned_parameters.filter(p => p.name).map((p, i) => (
+
+ | {p.name} |
+ {p.planned_value} |
+ {p.unit} |
+
+ ))}
+
+
+
+ )}
+
+
+
+ {/* Submission feedback */}
+ {submitResult && (
+
+ {submitResult.status === "success" ? (
+
+ ) : (
+
+ )}
+
{submitResult.message}
+
+ )}
+
+ {!submitResult && (
+
+
+
+ This will create an experiment definition. Actual runs are triggered separately.
+
+
+ )}
+
+ );
+ }
+ };
+
+ const totalSteps = STEPS.length;
+
+ return (
+
+
+
+ New Experiment
+
+
+ Stages 1–2 — Define an experiment type (recipe template) and create a specific experiment plan.
+
+
+
+ {/* Step Indicator */}
+
+ {STEPS.map((step, i) => (
+
+
+ {i < STEPS.length - 1 && (
+
+ )}
+
+ ))}
+
+
+ {/* Step Content */}
+
+ {renderStep()}
+
+
+ {/* Navigation Buttons */}
+
+
+ {currentStep < totalSteps ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
diff --git a/web/components/sidebar.tsx b/web/components/sidebar.tsx
index 33c42ef..d05629d 100644
--- a/web/components/sidebar.tsx
+++ b/web/components/sidebar.tsx
@@ -57,7 +57,7 @@ const NAV_SECTIONS = [
label: "ML & Viz",
items: [
{ id: "parity", label: "Parity Plots", href: "/ml/parity", icon: ScatterChart },
- { id: "importance", label: "Feature Import.", href: "/ml/importance", icon: BarChart3 },
+ { id: "importance", label: "Feature Signif.", href: "/ml/importance", icon: BarChart3 },
{ id: "convergence", label: "Convergence", href: "/ml/convergence", icon: TrendingUp },
{ id: "proposals", label: "Proposals", href: "/ml/proposals", icon: Beaker },
],
@@ -68,7 +68,8 @@ const NAV_SECTIONS = [
items: [
{ id: "users", label: "Users", href: "/admin/users", icon: Users },
{ id: "reviews", label: "Reviews", href: "/admin/reviews", icon: ClipboardCheck },
- { id: "register", label: "Register", href: "/equipment", icon: List },
+ { id: "register", label: "Register Equip.", href: "/equipment/register", icon: List },
+ { id: "new-experiment", label: "New Experiment", href: "/experiment/new", icon: FlaskConical },
{ id: "settings", label: "Settings", href: "/settings", icon: Settings },
],
},
diff --git a/web/lib/auth-context.tsx b/web/lib/auth-context.tsx
index 1871766..0b8e873 100644
--- a/web/lib/auth-context.tsx
+++ b/web/lib/auth-context.tsx
@@ -24,7 +24,7 @@ export const ROLE_PERMISSIONS: Record