phase 5: edge transforms, soleprint-ui rename, infra fixes
- pipeline edge transforms: stages can declare accepted_transforms, edges carry a transform dict, runner injects per-stage and nodes apply (e.g. invert_mask before edge detection); editable from UI via PUT /config/edge-transform - rename mpr-ui-framework -> soleprint-ui (now an external package synced via .spr from /home/mariano/wdir/spr); add @vue-flow/core and uplot to detection-app so linked package resolves them - Tiltfile guards kubectl context, k8s commands pin --context kind-mpr - kind-config: gateway on hostPort 30080 (Caddy fronts mpr.local.ar) - modelgen: pyproject.toml, .spr marker, dict default_factory support
This commit is contained in:
@@ -38,6 +38,14 @@ class StageOutputHintInfo(BaseModel):
|
|||||||
src_format: str = "png"
|
src_format: str = "png"
|
||||||
|
|
||||||
|
|
||||||
|
class TransformOptionInfo(BaseModel):
|
||||||
|
key: str
|
||||||
|
type: str
|
||||||
|
default: object = False
|
||||||
|
label: str = ""
|
||||||
|
description: str = ""
|
||||||
|
|
||||||
|
|
||||||
class StageConfigInfo(BaseModel):
|
class StageConfigInfo(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
label: str
|
label: str
|
||||||
@@ -45,6 +53,7 @@ class StageConfigInfo(BaseModel):
|
|||||||
category: str
|
category: str
|
||||||
config_fields: list[dict]
|
config_fields: list[dict]
|
||||||
output_hints: list[StageOutputHintInfo] = []
|
output_hints: list[StageOutputHintInfo] = []
|
||||||
|
accepted_transforms: list[TransformOptionInfo] = []
|
||||||
reads: list[str]
|
reads: list[str]
|
||||||
writes: list[str]
|
writes: list[str]
|
||||||
|
|
||||||
@@ -87,6 +96,51 @@ def get_pipeline_config(profile_name: str):
|
|||||||
return profile["pipeline"]
|
return profile["pipeline"]
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateEdgeTransformRequest(BaseModel):
|
||||||
|
profile_name: str = "soccer_broadcast"
|
||||||
|
source_stage: str
|
||||||
|
target_stage: str
|
||||||
|
transform: dict
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/config/edge-transform")
|
||||||
|
def update_edge_transform(req: UpdateEdgeTransformRequest):
|
||||||
|
"""Update the transform on an edge in a profile's pipeline config."""
|
||||||
|
from uuid import UUID
|
||||||
|
from core.db.models import Profile
|
||||||
|
from core.db.connection import get_session
|
||||||
|
from sqlmodel import select
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
with get_session() as session:
|
||||||
|
stmt = select(Profile).where(Profile.name == req.profile_name)
|
||||||
|
profile = session.exec(stmt).first()
|
||||||
|
if not profile:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Profile not found: {req.profile_name}")
|
||||||
|
|
||||||
|
pipeline = dict(profile.pipeline)
|
||||||
|
edges = pipeline.get("edges", [])
|
||||||
|
|
||||||
|
found = False
|
||||||
|
for edge in edges:
|
||||||
|
if edge.get("source") == req.source_stage and edge.get("target") == req.target_stage:
|
||||||
|
edge["transform"] = req.transform
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail=f"Edge not found: {req.source_stage} → {req.target_stage}",
|
||||||
|
)
|
||||||
|
|
||||||
|
pipeline["edges"] = edges
|
||||||
|
profile.pipeline = pipeline
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
return {"status": "updated", "edge": f"{req.source_stage} → {req.target_stage}", "transform": req.transform}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/config/stages", response_model=list[StageConfigInfo])
|
@router.get("/config/stages", response_model=list[StageConfigInfo])
|
||||||
def list_stage_configs():
|
def list_stage_configs():
|
||||||
"""Return the stage palette with config field metadata for the editor."""
|
"""Return the stage palette with config field metadata for the editor."""
|
||||||
@@ -137,6 +191,13 @@ def _stage_to_info(stage) -> StageConfigInfo:
|
|||||||
)
|
)
|
||||||
for h in getattr(stage, "output_hints", [])
|
for h in getattr(stage, "output_hints", [])
|
||||||
],
|
],
|
||||||
|
accepted_transforms=[
|
||||||
|
TransformOptionInfo(
|
||||||
|
key=t.key, type=t.type, default=t.default,
|
||||||
|
label=t.label, description=t.description,
|
||||||
|
)
|
||||||
|
for t in getattr(stage, "accepted_transforms", [])
|
||||||
|
],
|
||||||
reads=stage.io.reads,
|
reads=stage.io.reads,
|
||||||
writes=stage.io.writes,
|
writes=stage.io.writes,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -54,7 +54,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "field_segmentation",
|
"source": "field_segmentation",
|
||||||
"target": "detect_edges"
|
"target": "detect_edges",
|
||||||
|
"transform": {"invert_mask": true}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": "field_segmentation",
|
"source": "field_segmentation",
|
||||||
|
|||||||
@@ -162,6 +162,16 @@ def node_detect_edges(state: DetectState) -> dict:
|
|||||||
field_masks = state.get("field_masks", {})
|
field_masks = state.get("field_masks", {})
|
||||||
job_id = state.get("job_id")
|
job_id = state.get("job_id")
|
||||||
|
|
||||||
|
# Apply edge transforms from upstream connections
|
||||||
|
edge_transforms = state.get("_edge_transforms", {})
|
||||||
|
for source_stage, transform in edge_transforms.items():
|
||||||
|
if transform.get("invert_mask") and field_masks:
|
||||||
|
import numpy as np
|
||||||
|
field_masks = {
|
||||||
|
seq: np.bitwise_not(mask) if mask is not None else None
|
||||||
|
for seq, mask in field_masks.items()
|
||||||
|
}
|
||||||
|
|
||||||
regions = detect_edge_regions(
|
regions = detect_edge_regions(
|
||||||
frames, config, inference_url=INFERENCE_URL, job_id=job_id,
|
frames, config, inference_url=INFERENCE_URL, job_id=job_id,
|
||||||
field_masks=field_masks,
|
field_masks=field_masks,
|
||||||
|
|||||||
@@ -213,6 +213,13 @@ class PipelineRunner:
|
|||||||
self.config = config
|
self.config = config
|
||||||
self.do_checkpoint = checkpoint
|
self.do_checkpoint = checkpoint
|
||||||
self.stage_sequence = _flatten_config(config, start_from)
|
self.stage_sequence = _flatten_config(config, start_from)
|
||||||
|
# Build edge transform lookup: {target_stage: {source_stage: transform_dict}}
|
||||||
|
self._edge_transforms: dict[str, dict[str, dict]] = {}
|
||||||
|
for edge in config.edges:
|
||||||
|
if edge.transform:
|
||||||
|
if edge.target not in self._edge_transforms:
|
||||||
|
self._edge_transforms[edge.target] = {}
|
||||||
|
self._edge_transforms[edge.target][edge.source] = edge.transform
|
||||||
|
|
||||||
def invoke(self, state: DetectState) -> DetectState:
|
def invoke(self, state: DetectState) -> DetectState:
|
||||||
"""Run the pipeline on the given state. Returns final state."""
|
"""Run the pipeline on the given state. Returns final state."""
|
||||||
@@ -224,6 +231,14 @@ class PipelineRunner:
|
|||||||
if check and check():
|
if check and check():
|
||||||
raise PipelineCancelled(f"Cancelled before {stage_name}")
|
raise PipelineCancelled(f"Cancelled before {stage_name}")
|
||||||
|
|
||||||
|
# Inject edge transforms into state so the stage can read them.
|
||||||
|
# Compatible with LangGraph — just a state dict key.
|
||||||
|
transforms = self._edge_transforms.get(stage_name, {})
|
||||||
|
if transforms:
|
||||||
|
state["_edge_transforms"] = transforms
|
||||||
|
elif "_edge_transforms" in state:
|
||||||
|
del state["_edge_transforms"]
|
||||||
|
|
||||||
# 2. Run node function
|
# 2. Run node function
|
||||||
node_fn = _NODE_FN_MAP.get(stage_name)
|
node_fn = _NODE_FN_MAP.get(stage_name)
|
||||||
if node_fn is None:
|
if node_fn is None:
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from core.detect.stages.models import (
|
|||||||
StageDefinition,
|
StageDefinition,
|
||||||
StageIO,
|
StageIO,
|
||||||
StageOutputHint,
|
StageOutputHint,
|
||||||
|
TransformOption,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -54,6 +55,9 @@ class FieldSegmentationStage(Stage):
|
|||||||
output_hints=[
|
output_hints=[
|
||||||
StageOutputHint(key="mask_overlay_b64", type="overlay", label="Field mask", default_opacity=0.5, src_format="png"),
|
StageOutputHint(key="mask_overlay_b64", type="overlay", label="Field mask", default_opacity=0.5, src_format="png"),
|
||||||
],
|
],
|
||||||
|
accepted_transforms=[
|
||||||
|
TransformOption(key="invert_mask", type="bool", default=False, label="Invert selection", description="Invert the mask so downstream stages look outside the detected area"),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,14 @@ class StageOutputHint(BaseModel):
|
|||||||
default_opacity: float = 0.5
|
default_opacity: float = 0.5
|
||||||
src_format: str = "png"
|
src_format: str = "png"
|
||||||
|
|
||||||
|
class TransformOption(BaseModel):
|
||||||
|
"""A transform the stage accepts on its incoming edges."""
|
||||||
|
key: str
|
||||||
|
type: str
|
||||||
|
default: Any = False
|
||||||
|
label: str = ""
|
||||||
|
description: str = ""
|
||||||
|
|
||||||
class StageDefinition(BaseModel):
|
class StageDefinition(BaseModel):
|
||||||
"""Complete metadata for a pipeline stage."""
|
"""Complete metadata for a pipeline stage."""
|
||||||
name: str
|
name: str
|
||||||
@@ -44,6 +52,7 @@ class StageDefinition(BaseModel):
|
|||||||
io: StageIO
|
io: StageIO
|
||||||
config_fields: List[StageConfigField] = Field(default_factory=list)
|
config_fields: List[StageConfigField] = Field(default_factory=list)
|
||||||
output_hints: List[StageOutputHint] = Field(default_factory=list)
|
output_hints: List[StageOutputHint] = Field(default_factory=list)
|
||||||
|
accepted_transforms: List[TransformOption] = Field(default_factory=list)
|
||||||
tracks_element: Optional[str] = None
|
tracks_element: Optional[str] = None
|
||||||
|
|
||||||
class FrameExtractionConfig(BaseModel):
|
class FrameExtractionConfig(BaseModel):
|
||||||
@@ -105,6 +114,7 @@ class Edge(BaseModel):
|
|||||||
source: str
|
source: str
|
||||||
target: str
|
target: str
|
||||||
condition: str = ""
|
condition: str = ""
|
||||||
|
transform: Dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
class PipelineConfig(BaseModel):
|
class PipelineConfig(BaseModel):
|
||||||
"""Pipeline graph topology + routing rules."""
|
"""Pipeline graph topology + routing rules."""
|
||||||
@@ -112,4 +122,4 @@ class PipelineConfig(BaseModel):
|
|||||||
profile_name: str
|
profile_name: str
|
||||||
stages: List[StageRef] = Field(default_factory=list)
|
stages: List[StageRef] = Field(default_factory=list)
|
||||||
edges: List[Edge] = Field(default_factory=list)
|
edges: List[Edge] = Field(default_factory=list)
|
||||||
routing_rules: Dict[str, Any]
|
routing_rules: Dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|||||||
@@ -35,6 +35,16 @@ class StageIO:
|
|||||||
optional_reads: List[str] = field(default_factory=list)
|
optional_reads: List[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TransformOption:
|
||||||
|
"""A transform the stage accepts on its incoming edges."""
|
||||||
|
key: str
|
||||||
|
type: str # "bool", "float", "int", "str"
|
||||||
|
default: Any = False
|
||||||
|
label: str = ""
|
||||||
|
description: str = ""
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class StageOutputHint:
|
class StageOutputHint:
|
||||||
"""How to render a stage output in the compare/editor views."""
|
"""How to render a stage output in the compare/editor views."""
|
||||||
@@ -55,6 +65,7 @@ class StageDefinition:
|
|||||||
io: StageIO = field(default_factory=StageIO)
|
io: StageIO = field(default_factory=StageIO)
|
||||||
config_fields: List[StageConfigField] = field(default_factory=list)
|
config_fields: List[StageConfigField] = field(default_factory=list)
|
||||||
output_hints: List[StageOutputHint] = field(default_factory=list)
|
output_hints: List[StageOutputHint] = field(default_factory=list)
|
||||||
|
accepted_transforms: List[TransformOption] = field(default_factory=list)
|
||||||
tracks_element: Optional[str] = None
|
tracks_element: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@@ -129,10 +140,16 @@ class StageRef:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Edge:
|
class Edge:
|
||||||
"""Connection between stages in the graph."""
|
"""Connection between stages in the graph.
|
||||||
|
|
||||||
|
transform: per-edge data transformation spec. Flexible JSONB blob
|
||||||
|
type-checked by the consuming stage. E.g. {"invert_mask": true}
|
||||||
|
tells edge detection to invert the field segmentation mask.
|
||||||
|
"""
|
||||||
source: str
|
source: str
|
||||||
target: str
|
target: str
|
||||||
condition: str = ""
|
condition: str = ""
|
||||||
|
transform: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -151,6 +168,7 @@ STAGE_VIEWS = [
|
|||||||
StageConfigField,
|
StageConfigField,
|
||||||
StageIO,
|
StageIO,
|
||||||
StageOutputHint,
|
StageOutputHint,
|
||||||
|
TransformOption,
|
||||||
StageDefinition,
|
StageDefinition,
|
||||||
FrameExtractionConfig,
|
FrameExtractionConfig,
|
||||||
SceneFilterConfig,
|
SceneFilterConfig,
|
||||||
|
|||||||
@@ -4,9 +4,15 @@
|
|||||||
|
|
||||||
allow_k8s_contexts('kind-mpr')
|
allow_k8s_contexts('kind-mpr')
|
||||||
|
|
||||||
|
# Hard guard: Tilt deploys to whatever the shell's current kubectl context is,
|
||||||
|
# and allow_k8s_contexts auto-permits any local-looking (kind-*) cluster.
|
||||||
|
# Fail early instead of silently landing the workload in the wrong cluster.
|
||||||
|
if k8s_context() != 'kind-mpr':
|
||||||
|
fail("Wrong kubectl context: '%s'. Run: kubectl config use-context kind-mpr" % k8s_context())
|
||||||
|
|
||||||
# Create namespace first — kustomize includes it but Tilt may apply
|
# Create namespace first — kustomize includes it but Tilt may apply
|
||||||
# all resources in parallel, causing "namespace not found" races
|
# all resources in parallel, causing "namespace not found" races (pin cluster explicitly)
|
||||||
local('kubectl create namespace mpr --dry-run=client -o yaml | kubectl apply -f -')
|
local('kubectl --context kind-mpr create namespace mpr --dry-run=client -o yaml | kubectl --context kind-mpr apply -f -')
|
||||||
|
|
||||||
# Apply k8s manifests via kustomize (dev overlay)
|
# Apply k8s manifests via kustomize (dev overlay)
|
||||||
k8s_yaml(kustomize('k8s/overlays/dev'))
|
k8s_yaml(kustomize('k8s/overlays/dev'))
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ services:
|
|||||||
- ../ui/detection-app/src:/app/src
|
- ../ui/detection-app/src:/app/src
|
||||||
- ../ui/detection-app/vite.config.ts:/app/vite.config.ts
|
- ../ui/detection-app/vite.config.ts:/app/vite.config.ts
|
||||||
- ../ui/detection-app/index.html:/app/index.html
|
- ../ui/detection-app/index.html:/app/index.html
|
||||||
- ../ui/framework:/app/node_modules/mpr-ui-framework
|
- ../ui/framework:/app/node_modules/soleprint-ui
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ name: mpr
|
|||||||
nodes:
|
nodes:
|
||||||
- role: control-plane
|
- role: control-plane
|
||||||
extraPortMappings:
|
extraPortMappings:
|
||||||
# Gateway → http://k8s.mpr.local.ar (bind to 127.0.0.2 to avoid conflict with docker-compose on 127.0.0.1:80)
|
# Gateway → routed via Caddy on port 80 (mpr.local.ar, k8s.mpr.local.ar)
|
||||||
- containerPort: 30080
|
- containerPort: 30080
|
||||||
hostPort: 80
|
hostPort: 30080
|
||||||
listenAddress: "127.0.0.2"
|
listenAddress: "127.0.0.1"
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
# Redis
|
# Redis
|
||||||
- containerPort: 30379
|
- containerPort: 30379
|
||||||
|
|||||||
7
modelgen/.spr
Normal file
7
modelgen/.spr
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
name=soleprint-modelgen
|
||||||
|
version=0.2.0
|
||||||
|
type=pip
|
||||||
|
sha=0822761
|
||||||
|
mode=published
|
||||||
|
updated=2026-04-12T00:22:10Z
|
||||||
|
source=/home/mariano/wdir/spr/soleprint/station/tools/modelgen
|
||||||
@@ -328,7 +328,12 @@ class PydanticGenerator(BaseGenerator):
|
|||||||
if isinstance(default, Enum):
|
if isinstance(default, Enum):
|
||||||
return f" = {default.__class__.__name__}.{default.name}"
|
return f" = {default.__class__.__name__}.{default.name}"
|
||||||
if callable(default):
|
if callable(default):
|
||||||
return " = Field(default_factory=list)" if "list" in str(default) else ""
|
default_str = str(default)
|
||||||
|
if "list" in default_str:
|
||||||
|
return " = Field(default_factory=list)"
|
||||||
|
if "dict" in default_str:
|
||||||
|
return " = Field(default_factory=dict)"
|
||||||
|
return ""
|
||||||
return f" = {default!r}"
|
return f" = {default!r}"
|
||||||
|
|
||||||
def _generate_from_config(self, config) -> str:
|
def _generate_from_config(self, config) -> str:
|
||||||
|
|||||||
16
modelgen/pyproject.toml
Normal file
16
modelgen/pyproject.toml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=68.0"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "soleprint-modelgen"
|
||||||
|
version = "0.2.0"
|
||||||
|
description = "Multi-source, multi-target model code generator"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
modelgen = "modelgen.__main__:main"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
include = ["modelgen*"]
|
||||||
@@ -11,8 +11,10 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@techstark/opencv-js": "4.12.0-release.1",
|
"@techstark/opencv-js": "4.12.0-release.1",
|
||||||
"mpr-ui-framework": "link:../framework",
|
"@vue-flow/core": "^1.48.2",
|
||||||
|
"soleprint-ui": "link:../framework",
|
||||||
"pinia": "^2.2",
|
"pinia": "^2.2",
|
||||||
|
"uplot": "^1.6",
|
||||||
"vue": "^3.5"
|
"vue": "^3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
6
ui/detection-app/pnpm-lock.yaml
generated
6
ui/detection-app/pnpm-lock.yaml
generated
@@ -11,12 +11,12 @@ importers:
|
|||||||
'@techstark/opencv-js':
|
'@techstark/opencv-js':
|
||||||
specifier: 4.12.0-release.1
|
specifier: 4.12.0-release.1
|
||||||
version: 4.12.0-release.1
|
version: 4.12.0-release.1
|
||||||
mpr-ui-framework:
|
|
||||||
specifier: link:../framework
|
|
||||||
version: link:../framework
|
|
||||||
pinia:
|
pinia:
|
||||||
specifier: ^2.2
|
specifier: ^2.2
|
||||||
version: 2.3.1(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))
|
version: 2.3.1(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))
|
||||||
|
soleprint-ui:
|
||||||
|
specifier: link:../framework
|
||||||
|
version: link:../framework
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.5
|
specifier: ^3.5
|
||||||
version: 3.5.30(typescript@5.9.3)
|
version: 3.5.30(typescript@5.9.3)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { Panel, ResizeHandle, SplitPane } from 'mpr-ui-framework'
|
import { Panel, ResizeHandle, SplitPane } from 'soleprint-ui'
|
||||||
import 'mpr-ui-framework/src/tokens.css'
|
import 'soleprint-ui/src/tokens.css'
|
||||||
import LogPanel from './panels/LogPanel.vue'
|
import LogPanel from './panels/LogPanel.vue'
|
||||||
import FunnelPanel from './panels/FunnelPanel.vue'
|
import FunnelPanel from './panels/FunnelPanel.vue'
|
||||||
import PipelineGraphPanel from './panels/PipelineGraphPanel.vue'
|
import PipelineGraphPanel from './panels/PipelineGraphPanel.vue'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FrameOverlay } from 'mpr-ui-framework'
|
import type { FrameOverlay } from 'soleprint-ui'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
overlays: FrameOverlay[]
|
overlays: FrameOverlay[]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import { ParameterEditor, useEditorExecution } from 'mpr-ui-framework'
|
import { ParameterEditor, useEditorExecution } from 'soleprint-ui'
|
||||||
import type { ConfigField } from 'mpr-ui-framework'
|
import type { ConfigField } from 'soleprint-ui'
|
||||||
import {
|
import {
|
||||||
runEdgeDetectionTs,
|
runEdgeDetectionTs,
|
||||||
runEdgeDetectionTsDebug,
|
runEdgeDetectionTsDebug,
|
||||||
@@ -40,6 +40,9 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const fields = ref<ConfigField[]>([])
|
const fields = ref<ConfigField[]>([])
|
||||||
const values = ref<Record<string, unknown>>({})
|
const values = ref<Record<string, unknown>>({})
|
||||||
|
const transforms = ref<{ key: string; type: string; default: unknown; label: string; description: string }[]>([])
|
||||||
|
const transformValues = ref<Record<string, unknown>>({})
|
||||||
|
const edgeTargets = ref<string[]>([])
|
||||||
const statusText = ref<string | null>(null)
|
const statusText = ref<string | null>(null)
|
||||||
const debugEnabled = ref(true)
|
const debugEnabled = ref(true)
|
||||||
const processingIndex = ref<number | null>(null)
|
const processingIndex = ref<number | null>(null)
|
||||||
@@ -159,6 +162,98 @@ const {
|
|||||||
apply: applyDetection, onParameterChange,
|
apply: applyDetection, onParameterChange,
|
||||||
} = useEditorExecution(executeDetection)
|
} = useEditorExecution(executeDetection)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load cached overlays from DB stage output + S3 overlay cache.
|
||||||
|
* Returns true if data was found and emitted, false if nothing cached.
|
||||||
|
*/
|
||||||
|
async function loadCachedOverlays(): Promise<boolean> {
|
||||||
|
if (!props.jobId || !props.frames?.length) return false
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Load job detail to get stage outputs (inline overlays like field masks)
|
||||||
|
const jobResp = await fetch(`/api/detect/jobs/${props.jobId}`)
|
||||||
|
if (!jobResp.ok) return false
|
||||||
|
const jobDetail = await jobResp.json()
|
||||||
|
const stageOutput = jobDetail.stage_outputs?.[props.stage] ?? {}
|
||||||
|
|
||||||
|
// Check for inline overlay data (e.g. mask_overlays_by_frame)
|
||||||
|
const overlays_by_frame: Record<string, Record<string, string>> = {}
|
||||||
|
const regions_by_frame: Record<string, unknown[]> = {}
|
||||||
|
let foundData = false
|
||||||
|
|
||||||
|
// Find box data
|
||||||
|
for (const [key, val] of Object.entries(stageOutput)) {
|
||||||
|
if (key.endsWith('_by_frame') && !key.includes('overlay') && typeof val === 'object') {
|
||||||
|
for (const [seq, boxes] of Object.entries(val as Record<string, unknown>)) {
|
||||||
|
if (Array.isArray(boxes)) {
|
||||||
|
regions_by_frame[seq] = boxes
|
||||||
|
foundData = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find inline overlay data
|
||||||
|
for (const [key, val] of Object.entries(stageOutput)) {
|
||||||
|
if (key.includes('overlay') && key.endsWith('_by_frame') && typeof val === 'object') {
|
||||||
|
for (const [seq, b64] of Object.entries(val as Record<string, string>)) {
|
||||||
|
if (!overlays_by_frame[seq]) overlays_by_frame[seq] = {}
|
||||||
|
const overlayKey = key.replace('s_by_frame', '_b64')
|
||||||
|
overlays_by_frame[seq][overlayKey] = b64
|
||||||
|
foundData = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also try S3 overlay cache for each frame
|
||||||
|
if (!foundData || Object.keys(overlays_by_frame).length === 0) {
|
||||||
|
const { timelineId } = await import('../stores/pipeline').then(m => m.usePipelineStore())
|
||||||
|
if (timelineId) {
|
||||||
|
for (const frame of props.frames) {
|
||||||
|
try {
|
||||||
|
const oResp = await fetch(`/api/detect/overlays/${timelineId}/${props.jobId}/${props.stage}/${frame.seq}`)
|
||||||
|
if (oResp.ok) {
|
||||||
|
const oData = await oResp.json()
|
||||||
|
if (oData.overlays && Object.keys(oData.overlays).length > 0) {
|
||||||
|
overlays_by_frame[String(frame.seq)] = oData.overlays
|
||||||
|
foundData = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundData) {
|
||||||
|
emit('replay-result', {
|
||||||
|
regions_by_frame: Object.keys(regions_by_frame).length > 0 ? regions_by_frame : undefined,
|
||||||
|
overlays_by_frame: Object.keys(overlays_by_frame).length > 0 ? overlays_by_frame : undefined,
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTransformChange(key: string, value: unknown) {
|
||||||
|
transformValues.value[key] = value
|
||||||
|
// Save to all outgoing edges from this stage
|
||||||
|
const transform = { ...transformValues.value }
|
||||||
|
for (const target of edgeTargets.value) {
|
||||||
|
fetch('/api/detect/config/edge-transform', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
profile_name: 'soccer_broadcast',
|
||||||
|
source_stage: props.stage,
|
||||||
|
target_stage: target,
|
||||||
|
transform,
|
||||||
|
}),
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Init ---
|
// --- Init ---
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -167,15 +262,49 @@ onMounted(async () => {
|
|||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
const data = await resp.json()
|
const data = await resp.json()
|
||||||
fields.value = data.config_fields ?? []
|
fields.value = data.config_fields ?? []
|
||||||
|
transforms.value = data.accepted_transforms ?? []
|
||||||
}
|
}
|
||||||
} catch { /* use empty fields */ }
|
} catch { /* use empty fields */ }
|
||||||
|
|
||||||
for (const f of fields.value) {
|
for (const f of fields.value) {
|
||||||
values.value[f.name] = f.default
|
values.value[f.name] = f.default
|
||||||
}
|
}
|
||||||
|
for (const t of transforms.value) {
|
||||||
|
transformValues.value[t.key] = t.default
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load current edge transform values from pipeline config
|
||||||
|
// Transforms are on outgoing edges (source === this stage)
|
||||||
|
if (transforms.value.length > 0) {
|
||||||
|
try {
|
||||||
|
const pResp = await fetch(`/api/detect/config/profiles/soccer_broadcast/pipeline`)
|
||||||
|
if (pResp.ok) {
|
||||||
|
const pipeline = await pResp.json()
|
||||||
|
const edges = pipeline.edges ?? []
|
||||||
|
// Find outgoing edges from this stage and merge their transforms
|
||||||
|
for (const edge of edges) {
|
||||||
|
if (edge.source === props.stage && edge.transform) {
|
||||||
|
for (const [k, v] of Object.entries(edge.transform)) {
|
||||||
|
if (k in transformValues.value) {
|
||||||
|
transformValues.value[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remember which targets we're connected to
|
||||||
|
if (!edgeTargets.value.includes(edge.target)) {
|
||||||
|
edgeTargets.value.push(edge.target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
if (props.frameImage) {
|
if (props.frameImage) {
|
||||||
applyDetection()
|
// Try loading cached overlays first (from stage output or S3)
|
||||||
|
const loaded = await loadCachedOverlays()
|
||||||
|
if (!loaded) {
|
||||||
|
applyDetection()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -220,7 +349,10 @@ function getTargetFrames() {
|
|||||||
(props.selectionEnd ?? props.frames.length - 1) + 1,
|
(props.selectionEnd ?? props.frames.length - 1) + 1,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return [{ seq: props.frameRef ?? 0, jpeg_b64: props.frameImage! }]
|
if (props.frameImage) {
|
||||||
|
return [{ seq: props.frameRef ?? 0, jpeg_b64: props.frameImage }]
|
||||||
|
}
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
function edgeParams(): Partial<EdgeDetectionParams> {
|
function edgeParams(): Partial<EdgeDetectionParams> {
|
||||||
@@ -482,6 +614,20 @@ async function runGenericServer() {
|
|||||||
@update="onFieldUpdate"
|
@update="onFieldUpdate"
|
||||||
@reset="resetDefaults"
|
@reset="resetDefaults"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div v-if="transforms.length > 0" class="transform-section">
|
||||||
|
<div class="transform-header">Connection</div>
|
||||||
|
<label v-for="t in transforms" :key="t.key" class="transform-option">
|
||||||
|
<input
|
||||||
|
v-if="t.type === 'bool'"
|
||||||
|
type="checkbox"
|
||||||
|
:checked="!!transformValues[t.key]"
|
||||||
|
@change="onTransformChange(t.key, !transformValues[t.key])"
|
||||||
|
/>
|
||||||
|
<span class="transform-label">{{ t.label || t.key }}</span>
|
||||||
|
<span v-if="t.description" class="transform-desc">{{ t.description }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sliders-footer">
|
<div class="sliders-footer">
|
||||||
@@ -631,4 +777,43 @@ async function runGenericServer() {
|
|||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.transform-section {
|
||||||
|
margin-top: var(--space-3);
|
||||||
|
padding-top: var(--space-2);
|
||||||
|
border-top: 1px solid var(--surface-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transform-header {
|
||||||
|
font-size: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--text-dim);
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transform-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--space-2);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: var(--space-1) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transform-option input[type="checkbox"] {
|
||||||
|
accent-color: #00bcd4;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transform-label {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transform-desc {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
import type { DataSource } from 'mpr-ui-framework'
|
import type { DataSource } from 'soleprint-ui'
|
||||||
import { usePipelineStore } from '../stores/pipeline'
|
import { usePipelineStore } from '../stores/pipeline'
|
||||||
import { useStageRegistry } from './useStageRegistry'
|
import { useStageRegistry } from './useStageRegistry'
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
import type { FrameOverlay, FrameBBox } from 'mpr-ui-framework'
|
import type { FrameOverlay, FrameBBox } from 'soleprint-ui'
|
||||||
import type { StageResult } from '@/components/StageConfig.vue'
|
import type { StageResult } from '@/components/StageConfig.vue'
|
||||||
|
|
||||||
export type RegionBox = {
|
export type RegionBox = {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { SSEDataSource } from 'mpr-ui-framework'
|
import { SSEDataSource } from 'soleprint-ui'
|
||||||
import type { DataSource } from 'mpr-ui-framework'
|
import type { DataSource } from 'soleprint-ui'
|
||||||
import type { StatsUpdate, RunContext } from '../types/sse-contract'
|
import type { StatsUpdate, RunContext } from '../types/sse-contract'
|
||||||
import { usePipelineStore } from '../stores/pipeline'
|
import { usePipelineStore } from '../stores/pipeline'
|
||||||
|
|
||||||
@@ -24,8 +24,12 @@ export function useSSEConnection() {
|
|||||||
const sseConnected = ref(false)
|
const sseConnected = ref(false)
|
||||||
|
|
||||||
// No job selected and no hash route → open source selector
|
// No job selected and no hash route → open source selector
|
||||||
if (!jobParam && !window.location.hash.replace(/^#\/?/, '')) {
|
// Job selected but no hash route → go to compare (dashboard is for live runs)
|
||||||
|
const hashPath = window.location.hash.replace(/^#\/?/, '')
|
||||||
|
if (!jobParam && !hashPath) {
|
||||||
pipeline.openSourceSelector()
|
pipeline.openSourceSelector()
|
||||||
|
} else if (jobParam && !hashPath) {
|
||||||
|
pipeline.openCompare()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve timeline_id from job on init
|
// Resolve timeline_id from job on init
|
||||||
@@ -137,6 +141,10 @@ export function useSSEConnection() {
|
|||||||
if (!resp.ok) return
|
if (!resp.ok) return
|
||||||
const data = await resp.json()
|
const data = await resp.json()
|
||||||
paused.value = data.status === 'paused'
|
paused.value = data.status === 'paused'
|
||||||
|
// Stop polling if the job is no longer active
|
||||||
|
if (data.status === 'idle') {
|
||||||
|
stopStatusPoll()
|
||||||
|
}
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRegistry } from 'mpr-ui-framework'
|
import { useRegistry } from 'soleprint-ui'
|
||||||
|
|
||||||
export interface StageConfigField {
|
export interface StageConfigField {
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { Panel, TableRenderer } from 'mpr-ui-framework'
|
import { Panel, TableRenderer } from 'soleprint-ui'
|
||||||
import type { TableColumn, DataSource } from 'mpr-ui-framework'
|
import type { TableColumn, DataSource } from 'soleprint-ui'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
source: DataSource
|
source: DataSource
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, onMounted } from 'vue'
|
import { ref, computed, watch, onMounted } from 'vue'
|
||||||
import { Panel, SplitPane, FrameRenderer } from 'mpr-ui-framework'
|
import { Panel, SplitPane, FrameRenderer } from 'soleprint-ui'
|
||||||
import type { FrameBBox, FrameOverlay } from 'mpr-ui-framework'
|
import type { FrameBBox, FrameOverlay } from 'soleprint-ui'
|
||||||
import type { Job, Checkpoint } from '@common/types/generated'
|
import type { Job, Checkpoint } from '@common/types/generated'
|
||||||
import { usePipelineStore } from '../stores/pipeline'
|
import { usePipelineStore } from '../stores/pipeline'
|
||||||
import { useStageRegistry } from '../composables/useStageRegistry'
|
import { useStageRegistry } from '../composables/useStageRegistry'
|
||||||
@@ -222,12 +222,23 @@ function refreshOverlays() {
|
|||||||
watch([currentSeq, selectedStage], refreshOverlays)
|
watch([currentSeq, selectedStage], refreshOverlays)
|
||||||
|
|
||||||
// Sync overlay visibility/opacity from A controls → B overlays
|
// Sync overlay visibility/opacity from A controls → B overlays
|
||||||
|
// Also share src if one side has it and the other doesn't
|
||||||
watch(overlaysA, (aList) => {
|
watch(overlaysA, (aList) => {
|
||||||
for (const a of aList) {
|
for (const a of aList) {
|
||||||
const b = overlaysB.value.find(o => o.label === a.label)
|
const b = overlaysB.value.find(o => o.label === a.label)
|
||||||
if (b) {
|
if (b) {
|
||||||
b.visible = a.visible
|
b.visible = a.visible
|
||||||
b.opacity = a.opacity
|
b.opacity = a.opacity
|
||||||
|
if (a.src && !b.src) b.src = a.src
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
watch(overlaysB, (bList) => {
|
||||||
|
for (const b of bList) {
|
||||||
|
const a = overlaysA.value.find(o => o.label === b.label)
|
||||||
|
if (a) {
|
||||||
|
if (b.src && !a.src) a.src = b.src
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { Panel } from 'mpr-ui-framework'
|
import { Panel } from 'soleprint-ui'
|
||||||
import type { DataSource } from 'mpr-ui-framework'
|
import type { DataSource } from 'soleprint-ui'
|
||||||
import type { StatsUpdate, Detection } from '../types/sse-contract'
|
import type { StatsUpdate, Detection } from '../types/sse-contract'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { Panel, FrameRenderer } from 'mpr-ui-framework'
|
import { Panel, FrameRenderer } from 'soleprint-ui'
|
||||||
import type { FrameBBox, FrameOverlay, DataSource } from 'mpr-ui-framework'
|
import type { FrameBBox, FrameOverlay, DataSource } from 'soleprint-ui'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
source: DataSource
|
source: DataSource
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { Panel, TimeSeriesRenderer } from 'mpr-ui-framework'
|
import { Panel, TimeSeriesRenderer } from 'soleprint-ui'
|
||||||
import type { DataSource } from 'mpr-ui-framework'
|
import type { DataSource } from 'soleprint-ui'
|
||||||
import type { StatsUpdate } from '../types/sse-contract'
|
import type { StatsUpdate } from '../types/sse-contract'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { Panel, LogRenderer } from 'mpr-ui-framework'
|
import { Panel, LogRenderer } from 'soleprint-ui'
|
||||||
import type { LogEntry, DataSource } from 'mpr-ui-framework'
|
import type { LogEntry, DataSource } from 'soleprint-ui'
|
||||||
import type { LogEvent } from '../types/sse-contract'
|
import type { LogEvent } from '../types/sse-contract'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, computed, onMounted } from 'vue'
|
import { ref, watch, computed, onMounted } from 'vue'
|
||||||
import { Panel, GraphRenderer } from 'mpr-ui-framework'
|
import { Panel, GraphRenderer } from 'soleprint-ui'
|
||||||
import type { GraphNode, GraphMode, DataSource } from 'mpr-ui-framework'
|
import type { GraphNode, GraphMode, DataSource } from 'soleprint-ui'
|
||||||
import { usePipelineStore } from '../stores/pipeline'
|
import { usePipelineStore } from '../stores/pipeline'
|
||||||
import { useStageRegistry } from '../composables/useStageRegistry'
|
import { useStageRegistry } from '../composables/useStageRegistry'
|
||||||
|
|
||||||
@@ -43,20 +43,13 @@ const graphMode = computed<GraphMode>(() => {
|
|||||||
return 'observe'
|
return 'observe'
|
||||||
})
|
})
|
||||||
|
|
||||||
// Initialize nodes from pipeline config when it loads
|
// Initialize nodes from pipeline config — single source of truth for stage order
|
||||||
watch(pipelineConfig, (config) => {
|
watch(pipelineConfig, (config) => {
|
||||||
if (config && config.stages.length > 0 && nodes.value.length === 0) {
|
if (config && config.stages.length > 0 && nodes.value.length === 0) {
|
||||||
nodes.value = config.stages.map((s) => ({ id: s.name, status: 'pending' }))
|
nodes.value = config.stages.map((s) => ({ id: s.name, status: 'pending' }))
|
||||||
}
|
}
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
// Fallback: init from registry if no config loaded
|
|
||||||
watch(stageNames, (names) => {
|
|
||||||
if (names.length > 0 && nodes.value.length === 0) {
|
|
||||||
nodes.value = names.map((id) => ({ id, status: 'pending' }))
|
|
||||||
}
|
|
||||||
}, { immediate: true })
|
|
||||||
|
|
||||||
props.source.on<{ nodes: GraphNode[] }>('graph_update', (e) => {
|
props.source.on<{ nodes: GraphNode[] }>('graph_update', (e) => {
|
||||||
nodes.value = e.nodes
|
nodes.value = e.nodes
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { Panel } from 'mpr-ui-framework'
|
import { Panel } from 'soleprint-ui'
|
||||||
import { usePipelineStore } from '../stores/pipeline'
|
import { usePipelineStore } from '../stores/pipeline'
|
||||||
import type { Timeline, Job } from '@common/types/generated'
|
import type { Timeline, Job } from '@common/types/generated'
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { Panel } from 'mpr-ui-framework'
|
import { Panel } from 'soleprint-ui'
|
||||||
import type { DataSource } from 'mpr-ui-framework'
|
import type { DataSource } from 'soleprint-ui'
|
||||||
import type { Detection } from '../types/sse-contract'
|
import type { Detection } from '../types/sse-contract'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@@ -86,9 +86,9 @@ export const usePipelineStore = defineStore('pipeline', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closeEditor() {
|
function closeEditor() {
|
||||||
// Reload to reinitialize panels with current job context
|
// Go to compare view — the dashboard is for live runs only
|
||||||
const url = new URL(window.location.href)
|
const url = new URL(window.location.href)
|
||||||
url.hash = ''
|
url.hash = '#/compare'
|
||||||
if (jobId.value) {
|
if (jobId.value) {
|
||||||
url.searchParams.set('job', jobId.value)
|
url.searchParams.set('job', jobId.value)
|
||||||
}
|
}
|
||||||
|
|||||||
7
ui/framework/.spr
Normal file
7
ui/framework/.spr
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
name=soleprint-ui
|
||||||
|
version=0.1.0
|
||||||
|
type=npm
|
||||||
|
sha=0822761
|
||||||
|
mode=published
|
||||||
|
updated=2026-04-12T00:22:10Z
|
||||||
|
source=/home/mariano/wdir/spr/soleprint/common/ui
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "mpr-ui-framework",
|
"name": "soleprint-ui",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
1692
ui/framework/pnpm-lock.yaml
generated
1692
ui/framework/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user