diff --git a/.agents/rules/pytc-client.md b/.agents/rules/pytc-client.md
new file mode 100644
index 00000000..846f1ae1
--- /dev/null
+++ b/.agents/rules/pytc-client.md
@@ -0,0 +1,5 @@
+---
+trigger: always_on
+glob:
+description:
+---
diff --git a/.logs/start/api-server.log b/.logs/start/api-server.log
new file mode 100644
index 00000000..356d2b55
--- /dev/null
+++ b/.logs/start/api-server.log
@@ -0,0 +1,17 @@
+INFO: Started server process [1802573]
+INFO: Waiting for application startup.
+INFO: Application startup complete.
+INFO: Uvicorn running on http://0.0.0.0:4242 (Press CTRL+C to quit)
+================================================================================
+EHTOOL ROUTER MODULE LOADED - VERSION: DEBUG v2
+================================================================================
+INFO: 127.0.0.1:52216 - "GET /health HTTP/1.1" 200 OK
+INFO: 127.0.0.1:39000 - "GET /api/pm/data HTTP/1.1" 200 OK
+INFO: 127.0.0.1:38992 - "GET /files?parent=root HTTP/1.1" 200 OK
+INFO: 127.0.0.1:39000 - "GET /api/pm/data HTTP/1.1" 200 OK
+INFO: 127.0.0.1:55914 - "OPTIONS /api/pm/login HTTP/1.1" 200 OK
+INFO: 127.0.0.1:55930 - "POST /api/pm/login HTTP/1.1" 200 OK
+INFO: Shutting down
+INFO: Waiting for application shutdown.
+INFO: Application shutdown complete.
+INFO: Finished server process [1802573]
diff --git a/.logs/start/data-server.log b/.logs/start/data-server.log
new file mode 100644
index 00000000..b7f71d64
--- /dev/null
+++ b/.logs/start/data-server.log
@@ -0,0 +1 @@
+127.0.0.1 - - [26/Mar/2026 14:48:47] "GET / HTTP/1.1" 200 -
diff --git a/.logs/start/pytc-server.log b/.logs/start/pytc-server.log
new file mode 100644
index 00000000..cfd53951
--- /dev/null
+++ b/.logs/start/pytc-server.log
@@ -0,0 +1,21 @@
+INFO: Started server process [1802650]
+INFO: Waiting for application startup.
+INFO: Application startup complete.
+INFO: Uvicorn running on http://0.0.0.0:4243 (Press CTRL+C to quit)
+
+================================================================================
+SERVER_PYTC STARTING UP
+Python executable: /home/sam/Workshop/pytc-client/.venv/bin/python3
+Working directory: /home/sam/Workshop/pytc-client
+================================================================================
+
+
+================================================================================
+SERVER_PYTC: Starting Uvicorn server on port 4243...
+================================================================================
+
+INFO: 127.0.0.1:50152 - "GET /hello HTTP/1.1" 200 OK
+INFO: Shutting down
+INFO: Waiting for application shutdown.
+INFO: Application shutdown complete.
+INFO: Finished server process [1802650]
diff --git a/.logs/start/react-build.log b/.logs/start/react-build.log
new file mode 100644
index 00000000..b675bfdf
--- /dev/null
+++ b/.logs/start/react-build.log
@@ -0,0 +1,82 @@
+
+> PyTC Client@0.1.0 build
+> cross-env CI=false react-scripts build
+
+[baseline-browser-mapping] The data in this module is over two months old. To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D`
+Creating an optimized production build...
+[baseline-browser-mapping] The data in this module is over two months old. To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D`
+Compiled with warnings.
+
+[eslint]
+src/components/FilePickerModal.js
+ Line 102:35: Function declared in a loop contains unsafe references to variable(s) 'curr' no-loop-func
+ Line 124:35: Function declared in a loop contains unsafe references to variable(s) 'currParentId' no-loop-func
+ Line 243:15: The href attribute is required for an anchor to be keyboard accessible. Provide a valid, navigable address as the href value. If you cannot provide an href, but still need the element to resemble a link, use a button and change it with appropriate styles. Learn more: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/HEAD/docs/rules/anchor-is-valid.md jsx-a11y/anchor-is-valid
+
+src/components/YamlFileUploader.js
+ Line 347:6: React Hook useEffect has missing dependencies: 'getCurrentConfig' and 'syncYamlContext'. Either include them or remove the dependency array react-hooks/exhaustive-deps
+ Line 361:6: React Hook useEffect has missing dependencies: 'getCurrentConfig', 'setCurrentConfig', and 'updateInputSelectorInformation'. Either include them or remove the dependency array react-hooks/exhaustive-deps
+ Line 374:6: React Hook useMemo has a missing dependency: 'getCurrentConfig'. Either include it or remove the dependency array react-hooks/exhaustive-deps
+
+src/views/FilesManager.js
+ Line 81:31: Function declared in a loop contains unsafe references to variable(s) 'current' no-loop-func
+ Line 420:27: Function declared in a loop contains unsafe references to variable(s) 'curr' no-loop-func
+ Line 772:32: Function declared in a loop contains unsafe references to variable(s) 'descendantSizeKb', 'descendantSizeKb', 'descendantSizeKb', 'descendantSizeKb' no-loop-func
+ Line 1069:6: React Hook useEffect has missing dependencies: 'finishCreateFolder', 'finishRename', 'handleCopy', 'handleCut', 'handleDelete', and 'handlePaste'. Either include them or remove the dependency array react-hooks/exhaustive-deps
+
+src/views/Views.js
+ Line 11:3: 'BarChartOutlined' is defined but never used no-unused-vars
+
+src/views/mask-proofreading/MaskProofreading.js
+ Line 5:10: 'ehToolSession' is assigned a value but never used no-unused-vars
+ Line 5:25: 'setEhToolSession' is assigned a value but never used no-unused-vars
+ Line 6:10: 'refreshTrigger' is assigned a value but never used no-unused-vars
+ Line 6:26: 'setRefreshTrigger' is assigned a value but never used no-unused-vars
+
+src/views/project-manager/AnnotationDashboard.js
+ Line 340:5: 'isAdmin' is assigned a value but never used no-unused-vars
+
+src/views/project-manager/ModelQualityDashboard.js
+ Line 40:7: 'CONFUSION_MATRIX' is assigned a value but never used no-unused-vars
+
+src/views/project-manager/ProjectManager.js
+ Line 19:3: 'UserOutlined' is defined but never used no-unused-vars
+ Line 20:3: 'CrownOutlined' is defined but never used no-unused-vars
+ Line 38:61: 'loading' is assigned a value but never used no-unused-vars
+
+src/views/project-manager/ProjectManagerLogin.js
+ Line 14:11: 'success' is assigned a value but never used no-unused-vars
+
+src/views/project-manager/ProofreaderProgress.js
+ Line 163:6: React Hook useMemo has a missing dependency: 'getStats'. Either include it or remove the dependency array react-hooks/exhaustive-deps
+
+src/views/project-manager/QuotaManagement.js
+ Line 29:3: 'SaveOutlined' is defined but never used no-unused-vars
+ Line 98:5: 'activeWorker' is assigned a value but never used no-unused-vars
+
+src/views/project-manager/VolumeTracker.js
+ Line 16:3: 'Popconfirm' is defined but never used no-unused-vars
+ Line 127:6: React Hook useMemo has an unnecessary dependency: 'isAdmin'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
+ Line 184:15: 'cfg' is assigned a value but never used no-unused-vars
+
+Search for the keywords to learn more about each warning.
+To ignore, add // eslint-disable-next-line to the line before.
+
+File sizes after gzip:
+
+ 553.16 kB build/static/js/main.be80df26.js
+ 465 B build/static/css/main.40821645.css
+
+The bundle size is significantly larger than recommended.
+Consider reducing it with code splitting: https://goo.gl/9VhYWB
+You can also analyze the project dependencies: https://goo.gl/LeUzfb
+
+The project was built assuming it is hosted at ./.
+You can control this with the homepage field in your package.json.
+
+The build folder is ready to be deployed.
+
+Find out more about deployment here:
+
+ https://cra.link/deployment
+
diff --git a/.logs/start/react-dev.log b/.logs/start/react-dev.log
new file mode 100644
index 00000000..aa502f28
--- /dev/null
+++ b/.logs/start/react-dev.log
@@ -0,0 +1,21 @@
+
+> PyTC Client@0.1.0 start
+> react-scripts start
+
+[baseline-browser-mapping] The data in this module is over two months old. To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D`
+(node:1802959) [DEP_WEBPACK_DEV_SERVER_ON_AFTER_SETUP_MIDDLEWARE] DeprecationWarning: 'onAfterSetupMiddleware' option is deprecated. Please use the 'setupMiddlewares' option.
+(Use `node --trace-deprecation ...` to show where the warning was created)
+(node:1802959) [DEP_WEBPACK_DEV_SERVER_ON_BEFORE_SETUP_MIDDLEWARE] DeprecationWarning: 'onBeforeSetupMiddleware' option is deprecated. Please use the 'setupMiddlewares' option.
+Starting the development server...
+
+Compiled successfully!
+
+You can now view PyTC Client in the browser.
+
+ Local: http://localhost:3000
+ On Your Network: http://10.101.188.107:3000
+
+Note that the development build is not optimized.
+To create a production build, use npm run build.
+
+webpack compiled successfully
diff --git a/client/package.json b/client/package.json
index 7a080180..0107480c 100644
--- a/client/package.json
+++ b/client/package.json
@@ -28,6 +28,7 @@
},
"scripts": {
"start": "react-scripts start",
+ "test": "react-scripts test",
"build": "cross-env CI=false react-scripts build",
"electron": "electron ."
},
diff --git a/client/src/App.js b/client/src/App.js
index 6d14652d..9c26a2c7 100644
--- a/client/src/App.js
+++ b/client/src/App.js
@@ -3,6 +3,7 @@ import "./App.css";
import Views from "./views/Views";
import { AppContext, ContextWrapper } from "./contexts/GlobalContext";
import { YamlContextWrapper } from "./contexts/YamlContext";
+import { WorkflowProvider } from "./contexts/WorkflowContext";
function CacheBootstrapper({ children }) {
const { resetFileState } = useContext(AppContext);
@@ -38,11 +39,13 @@ function App() {
return (
-
-
-
-
-
+
+
+
+
+
+
+
);
diff --git a/client/src/__tests__/agentProposalCards.test.js b/client/src/__tests__/agentProposalCards.test.js
new file mode 100644
index 00000000..afd415ea
--- /dev/null
+++ b/client/src/__tests__/agentProposalCards.test.js
@@ -0,0 +1,78 @@
+import React from "react";
+import { fireEvent, render, screen } from "@testing-library/react";
+
+import AgentProposalCard from "../components/chat/AgentProposalCard";
+
+jest.mock("antd", () => ({
+ Button: ({ children, ...props }) => (
+
+ {children}
+
+ ),
+ Space: ({ children }) =>
{isLoadingConvo ? (
diff --git a/client/src/components/RuntimeLogPanel.js b/client/src/components/RuntimeLogPanel.js
index 9a938a7e..1ce77453 100644
--- a/client/src/components/RuntimeLogPanel.js
+++ b/client/src/components/RuntimeLogPanel.js
@@ -117,7 +117,7 @@ function RuntimeLogPanel({ title, runtime, onRefresh }) {
background: "#fff",
border: "1px solid #f0f0f0",
fontFamily:
- 'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace',
+ "ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace",
fontSize: 12,
color: "#262626",
wordBreak: "break-word",
@@ -142,7 +142,7 @@ function RuntimeLogPanel({ title, runtime, onRefresh }) {
whiteSpace: "pre-wrap",
wordBreak: "break-word",
fontFamily:
- 'ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace',
+ "ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, monospace",
}}
>
{text || "No runtime logs captured yet."}
diff --git a/client/src/components/WorkflowTimeline.js b/client/src/components/WorkflowTimeline.js
new file mode 100644
index 00000000..f2569f9b
--- /dev/null
+++ b/client/src/components/WorkflowTimeline.js
@@ -0,0 +1,241 @@
+import React, { useMemo, useState } from "react";
+import { Button, Empty, Input, List, Select, Space, Tag, Typography } from "antd";
+import { useWorkflow } from "../contexts/WorkflowContext";
+import AgentProposalCard from "./chat/AgentProposalCard";
+import {
+ DEFAULT_TIMELINE_FILTERS,
+ filterTimelineEvents,
+ normalizeTimelineFilters,
+ TIMELINE_ACTOR_OPTIONS,
+} from "../contexts/workflow/timelineFilters";
+
+const { Text } = Typography;
+
+const STAGE_LABELS = {
+ setup: "Setup",
+ visualization: "Visualization",
+ inference: "Inference",
+ proofreading: "Proofreading",
+ retraining_staged: "Retraining staged",
+ evaluation: "Evaluation",
+};
+
+const SEVERITY_COLORS = {
+ low: "default",
+ medium: "orange",
+ high: "red",
+};
+
+function formatEventTime(value) {
+ if (!value) return "";
+ const date = new Date(value);
+ if (Number.isNaN(date.getTime())) return "";
+ return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
+}
+
+function WorkflowTimeline({ limit = 8 }) {
+ const workflowContext = useWorkflow();
+ const workflow = workflowContext?.workflow;
+ const events = workflowContext?.events;
+ const hotspots = workflowContext?.hotspots || [];
+ const impactPreview = workflowContext?.impactPreview;
+ const refreshInsights = workflowContext?.refreshInsights;
+ const approveAgentAction = workflowContext?.approveAgentAction;
+ const rejectAgentAction = workflowContext?.rejectAgentAction;
+ const [filters, setFilters] = useState(DEFAULT_TIMELINE_FILTERS);
+
+ const reversedEvents = useMemo(() => [...(events || [])].reverse(), [events]);
+ const visibleEvents = useMemo(() => {
+ return filterTimelineEvents(reversedEvents, filters).slice(0, limit);
+ }, [reversedEvents, filters, limit]);
+ const stageLabel = STAGE_LABELS[workflow?.stage] || workflow?.stage || "Loading";
+ const topHotspot = hotspots[0] || null;
+
+ if (!workflowContext) return null;
+
+ return (
+
+
+
+
+ Workflow
+
+
+ {stageLabel}
+
+
+
+
+ {workflow?.title || "Segmentation Workflow"}
+
+ refreshInsights?.()}
+ style={{ paddingInline: 4 }}
+ >
+ Refresh Insights
+
+
+
+
+ {(topHotspot || impactPreview) && (
+
+ {topHotspot && (
+
+
+ Hotspot
+
+
+ {topHotspot.severity}
+
+ {topHotspot.summary}
+
+ )}
+ {impactPreview && (
+
+
+ Impact
+
+
+ {` ${impactPreview.summary} (confidence: ${impactPreview.confidence})`}
+
+
+ )}
+
+ )}
+
+
+ ({
+ value,
+ label: value === "all" ? "All actors" : value,
+ }))}
+ onChange={(value) =>
+ setFilters((prev) => normalizeTimelineFilters({ ...prev, actor: value }))
+ }
+ />
+
+ setFilters((prev) =>
+ normalizeTimelineFilters({ ...prev, eventType: event.target.value }),
+ )
+ }
+ />
+ setFilters(DEFAULT_TIMELINE_FILTERS)}
+ >
+ Clear
+
+
+
+ {visibleEvents.length === 0 ? (
+
+ ) : (
+
{
+ const isPendingProposal =
+ event.event_type === "agent.proposal_created" &&
+ event.approval_status === "pending";
+ return (
+
+ Needs review
+ ,
+ ]
+ : []
+ }
+ >
+
+ {event.summary}
+ {event.approval_status !== "not_required" && (
+
+ {event.approval_status}
+
+ )}
+
+ }
+ description={
+
+
+
+ {event.actor}
+
+
+ {event.event_type}
+
+
+ {formatEventTime(event.created_at)}
+
+
+ {isPendingProposal && (
+
approveAgentAction?.(event.id)}
+ onReject={() => rejectAgentAction?.(event.id)}
+ />
+ )}
+
+ }
+ />
+
+ );
+ }}
+ />
+ )}
+
+ );
+}
+
+export default WorkflowTimeline;
diff --git a/client/src/components/WorkflowTimeline.test.js b/client/src/components/WorkflowTimeline.test.js
new file mode 100644
index 00000000..4792681f
--- /dev/null
+++ b/client/src/components/WorkflowTimeline.test.js
@@ -0,0 +1,228 @@
+import React from "react";
+import { fireEvent, render, screen } from "@testing-library/react";
+
+import WorkflowTimeline from "./WorkflowTimeline";
+import { WorkflowContext } from "../contexts/WorkflowContext";
+
+jest.mock("antd", () => {
+ const React = require("react");
+ const List = ({ dataSource = [], renderItem }) => (
+
+ {dataSource.map((item, index) => (
+
+ {renderItem(item, index)}
+
+ ))}
+
+ );
+ List.Item = ({ children, actions = [] }) => (
+
+ {children}
+ {actions.map((action, index) => (
+ {action}
+ ))}
+
+ );
+ List.Item.Meta = ({ title, description }) => (
+
+
{title}
+
{description}
+
+ );
+
+ const Empty = ({ description }) =>
{description}
;
+ Empty.PRESENTED_IMAGE_SIMPLE = "simple";
+
+ return {
+ Button: ({ children, icon, ...props }) => (
+
+ {icon}
+ {children}
+
+ ),
+ Input: ({ ...props }) =>
,
+ Empty,
+ List,
+ Select: ({ options = [], value, onChange, ...props }) => (
+
onChange?.(event.target.value)}
+ {...props}
+ >
+ {options.map((option) => (
+
+ {option.label}
+
+ ))}
+
+ ),
+ Space: ({ children }) =>
{children} ,
+ Tag: ({ children }) =>
{children} ,
+ Typography: {
+ Text: ({ children }) =>
{children} ,
+ },
+ };
+});
+
+jest.mock("@ant-design/icons", () => {
+ const Icon = () =>
;
+ return {
+ CheckOutlined: Icon,
+ CloseOutlined: Icon,
+ };
+});
+
+jest.mock("../contexts/WorkflowContext", () => {
+ const React = require("react");
+ const WorkflowContext = React.createContext(null);
+ return {
+ WorkflowContext,
+ useWorkflow: () => React.useContext(WorkflowContext),
+ };
+});
+
+const workflow = {
+ id: 1,
+ title: "Test Workflow",
+ stage: "proofreading",
+};
+
+function renderTimeline(overrides = {}) {
+ const value = {
+ workflow,
+ events: [],
+ hotspots: [],
+ impactPreview: null,
+ refreshInsights: jest.fn(),
+ approveAgentAction: jest.fn(),
+ rejectAgentAction: jest.fn(),
+ ...overrides,
+ };
+ render(
+
+
+ ,
+ );
+ return value;
+}
+
+describe("WorkflowTimeline", () => {
+ it("renders workflow stage and chronological evidence", () => {
+ renderTimeline({
+ events: [
+ {
+ id: 1,
+ actor: "user",
+ event_type: "dataset.loaded",
+ stage: "proofreading",
+ summary: "Loaded dataset.",
+ approval_status: "not_required",
+ created_at: "2026-04-12T12:00:00Z",
+ },
+ ],
+ });
+
+ expect(screen.getByText("Proofreading")).toBeTruthy();
+ expect(screen.getByText("Loaded dataset.")).toBeTruthy();
+ expect(screen.getByText("dataset.loaded")).toBeTruthy();
+ });
+
+ it("exposes approve and reject controls for pending agent proposals", () => {
+ const approveAgentAction = jest.fn();
+ const rejectAgentAction = jest.fn();
+ renderTimeline({
+ approveAgentAction,
+ rejectAgentAction,
+ events: [
+ {
+ id: 7,
+ actor: "agent",
+ event_type: "agent.proposal_created",
+ stage: "proofreading",
+ summary: "Stage corrected masks.",
+ approval_status: "pending",
+ created_at: "2026-04-12T12:00:00Z",
+ },
+ ],
+ });
+
+ fireEvent.click(screen.getByText("Approve"));
+ fireEvent.click(screen.getByText("Reject"));
+
+ expect(approveAgentAction).toHaveBeenCalledWith(7);
+ expect(rejectAgentAction).toHaveBeenCalledWith(7);
+ });
+
+ it("renders hotspot + impact summary and refresh action", () => {
+ const refreshInsights = jest.fn();
+ renderTimeline({
+ refreshInsights,
+ hotspots: [
+ {
+ rank: 1,
+ region_key: "z:12",
+ score: 9.5,
+ severity: "high",
+ summary: "z:12 has repeated failures.",
+ recommended_action: "Open proofreading.",
+ evidence: {},
+ },
+ ],
+ impactPreview: {
+ confidence: "high",
+ summary: "Corrections are ready to stage.",
+ },
+ });
+
+ expect(screen.getByText("z:12 has repeated failures.")).toBeTruthy();
+ expect(
+ screen.getByText(
+ /Corrections are ready to stage\. \(confidence: high\)/,
+ ),
+ ).toBeTruthy();
+
+ fireEvent.click(screen.getByText("Refresh Insights"));
+ expect(refreshInsights).toHaveBeenCalledTimes(1);
+ });
+
+ it("filters timeline entries by actor and event type", () => {
+ renderTimeline({
+ events: [
+ {
+ id: 1,
+ actor: "user",
+ event_type: "dataset.loaded",
+ stage: "proofreading",
+ summary: "Loaded dataset.",
+ approval_status: "not_required",
+ created_at: "2026-04-12T12:00:00Z",
+ },
+ {
+ id: 2,
+ actor: "agent",
+ event_type: "agent.proposal_created",
+ stage: "proofreading",
+ summary: "Stage corrected masks.",
+ approval_status: "pending",
+ payload: {
+ action: "stage_retraining_from_corrections",
+ params: { corrected_mask_path: "/tmp/corrected.tif" },
+ },
+ created_at: "2026-04-12T12:02:00Z",
+ },
+ ],
+ });
+
+ fireEvent.change(screen.getByLabelText("Actor filter"), {
+ target: { value: "agent" },
+ });
+ expect(screen.queryByText("Loaded dataset.")).toBeNull();
+ expect(screen.getAllByText("Stage corrected masks.").length).toBeGreaterThan(0);
+
+ fireEvent.change(screen.getByLabelText("Event type filter"), {
+ target: { value: "proposal" },
+ });
+ expect(screen.getAllByText("Stage corrected masks.").length).toBeGreaterThan(0);
+ expect(screen.getByText("Stage Retraining From Corrections")).toBeTruthy();
+ });
+});
diff --git a/client/src/components/YamlFileUploader.js b/client/src/components/YamlFileUploader.js
index e7a33c5a..8fec2dbd 100644
--- a/client/src/components/YamlFileUploader.js
+++ b/client/src/components/YamlFileUploader.js
@@ -490,7 +490,11 @@ const YamlFileUploader = (props) => {
{yamlContent ? (
{sliderData.map((param, index) => {
- const sliderValue = getSliderValue(currentYamlData, type, param.key);
+ const sliderValue = getSliderValue(
+ currentYamlData,
+ type,
+ param.key,
+ );
const sliderSupported = isSliderSupported(
currentYamlData,
type,
@@ -507,7 +511,11 @@ const YamlFileUploader = (props) => {
min={param.min}
max={param.max}
marks={param.marks}
- value={typeof sliderValue === "number" ? sliderValue : param.value}
+ value={
+ typeof sliderValue === "number"
+ ? sliderValue
+ : param.value
+ }
disabled={!sliderSupported}
onChange={(newValue) =>
handleSliderChange(param.key, newValue)
diff --git a/client/src/components/chat/AgentProposalCard.js b/client/src/components/chat/AgentProposalCard.js
new file mode 100644
index 00000000..d6f06ab2
--- /dev/null
+++ b/client/src/components/chat/AgentProposalCard.js
@@ -0,0 +1,60 @@
+import React from "react";
+import { Button, Space, Tag, Typography } from "antd";
+
+import { getProposalCardContent } from "../../contexts/workflow/proposalCardConfig";
+
+const { Text } = Typography;
+
+function AgentProposalCard({ proposal, onApprove, onReject, disabled = false }) {
+ const content = getProposalCardContent(proposal || {});
+
+ return (
+
+
+
+
+ {content.title}
+
+ {content.type}
+
+ {content.rationale}
+ {content.fields?.length > 0 && (
+
+ {content.fields.map((field) => (
+
+
+ {field.label}
+
+ {field.value}
+
+ ))}
+
+ )}
+
+ onApprove?.(proposal)}
+ disabled={disabled}
+ >
+ Approve
+
+ onReject?.(proposal)} disabled={disabled}>
+ Reject
+
+
+
+
+ );
+}
+
+export default AgentProposalCard;
diff --git a/client/src/configSchema.js b/client/src/configSchema.js
index 23e6ca86..f1473e0d 100644
--- a/client/src/configSchema.js
+++ b/client/src/configSchema.js
@@ -135,14 +135,26 @@ export function setTrainingOutputPath(configObj, outputPath) {
}
const checkpointsPath = joinPath(outputPath, "checkpoints");
if (hasPath(configObj, ["train", "monitor", "checkpoint", "dirpath"])) {
- setPathValue(configObj, ["train", "monitor", "checkpoint", "dirpath"], checkpointsPath);
+ setPathValue(
+ configObj,
+ ["train", "monitor", "checkpoint", "dirpath"],
+ checkpointsPath,
+ );
return;
}
if (hasPath(configObj, ["monitor", "checkpoint", "dirpath"])) {
- setPathValue(configObj, ["monitor", "checkpoint", "dirpath"], checkpointsPath);
+ setPathValue(
+ configObj,
+ ["monitor", "checkpoint", "dirpath"],
+ checkpointsPath,
+ );
return;
}
- setPathValue(configObj, ["monitor", "checkpoint", "dirpath"], checkpointsPath);
+ setPathValue(
+ configObj,
+ ["monitor", "checkpoint", "dirpath"],
+ checkpointsPath,
+ );
}
export function setInferenceOutputPath(configObj, outputPath) {
@@ -152,7 +164,11 @@ export function setInferenceOutputPath(configObj, outputPath) {
setPathValue(configObj, ["INFERENCE", "OUTPUT_PATH"], outputPath);
return;
}
- setPathValue(configObj, ["inference", "save_prediction", "output_path"], outputPath);
+ setPathValue(
+ configObj,
+ ["inference", "save_prediction", "output_path"],
+ outputPath,
+ );
}
export function setInferenceExecutionDefaults(configObj) {
@@ -212,16 +228,14 @@ export function applyInputPaths(
}
if (mode === "training") {
- const imagePath =
- pickFirstExistingPath(configObj, [
- ["train", "data", "train", "image"],
- ["data", "train", "image"],
- ]) || ["train", "data", "train", "image"];
- const labelPath =
- pickFirstExistingPath(configObj, [
- ["train", "data", "train", "label"],
- ["data", "train", "label"],
- ]) || ["train", "data", "train", "label"];
+ const imagePath = pickFirstExistingPath(configObj, [
+ ["train", "data", "train", "image"],
+ ["data", "train", "image"],
+ ]) || ["train", "data", "train", "image"];
+ const labelPath = pickFirstExistingPath(configObj, [
+ ["train", "data", "train", "label"],
+ ["data", "train", "label"],
+ ]) || ["train", "data", "train", "label"];
setPathValue(configObj, imagePath, inputImagePath);
setPathValue(configObj, labelPath, inputLabelPath);
if (outputPath) {
@@ -230,16 +244,14 @@ export function applyInputPaths(
return;
}
- const imagePath =
- pickFirstExistingPath(configObj, [
- ["test", "data", "test", "image"],
- ["data", "test", "image"],
- ]) || ["test", "data", "test", "image"];
- const labelPath =
- pickFirstExistingPath(configObj, [
- ["test", "data", "test", "label"],
- ["data", "test", "label"],
- ]) || ["test", "data", "test", "label"];
+ const imagePath = pickFirstExistingPath(configObj, [
+ ["test", "data", "test", "image"],
+ ["data", "test", "image"],
+ ]) || ["test", "data", "test", "image"];
+ const labelPath = pickFirstExistingPath(configObj, [
+ ["test", "data", "test", "label"],
+ ["data", "test", "label"],
+ ]) || ["test", "data", "test", "label"];
setPathValue(configObj, imagePath, inputImagePath);
if (hasLabelPath) {
setPathValue(configObj, labelPath, inputLabelPath);
diff --git a/client/src/contexts/ProjectManagerContext.js b/client/src/contexts/ProjectManagerContext.js
new file mode 100644
index 00000000..291ec723
--- /dev/null
+++ b/client/src/contexts/ProjectManagerContext.js
@@ -0,0 +1,287 @@
+import React, {
+ createContext,
+ useContext,
+ useState,
+ useEffect,
+ useCallback,
+ useRef,
+} from "react";
+import { message, Spin } from "antd";
+import { getPMData, savePMData, resetPMData } from "../api";
+import { apiClient } from "../api";
+
+// ── Context ───────────────────────────────────────────────────────────────────
+
+const ProjectManagerContext = createContext(null);
+
+export function useProjectManager() {
+ const ctx = useContext(ProjectManagerContext);
+ if (!ctx)
+ throw new Error(
+ "useProjectManager must be used inside ",
+ );
+ return ctx;
+}
+
+// ── Provider ──────────────────────────────────────────────────────────────────
+
+const DEBOUNCE_MS = 800;
+
+/**
+ * ProjectManagerProvider
+ *
+ * Props:
+ * role "admin" | "worker" — current RBAC role
+ * activeWorker worker key string — e.g. "alex" (relevant when role=worker)
+ */
+export function ProjectManagerProvider({ children }) {
+ const [pmState, setPmState] = useState(null); // null = not yet loaded
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [user, setUser] = useState(null); // Authenticated user object
+ const debounceRef = useRef(null);
+
+ // ── Session Restoration ──────────────────────────────────────────────────
+ useEffect(() => {
+ const saved = localStorage.getItem("pm_user");
+ if (saved) {
+ try {
+ setUser(JSON.parse(saved));
+ } catch (e) {
+ localStorage.removeItem("pm_user");
+ }
+ }
+ }, []);
+
+ // ── Fetch on mount (always load PM config/seeds) ──────────────────────────
+ useEffect(() => {
+ let cancelled = false;
+ setLoading(true);
+ getPMData()
+ .then((data) => {
+ if (!cancelled) setPmState(data);
+ })
+ .catch((err) => {
+ if (!cancelled) {
+ console.error("[PM] Failed to load data:", err);
+ message.error("Failed to load Project Manager data from server.");
+ }
+ })
+ .finally(() => {
+ if (!cancelled) setLoading(false);
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ // ── Auth Actions ──────────────────────────────────────────────────────────
+ const login = useCallback(async (username, password) => {
+ try {
+ const res = await apiClient.post("/api/pm/login", { username, password });
+ if (res.data.ok) {
+ const userData = res.data.user;
+ setUser(userData);
+ localStorage.setItem("pm_user", JSON.stringify(userData));
+ message.success(`Logged in as ${userData.name}`);
+ return true;
+ }
+ } catch (err) {
+ message.error(err.response?.data?.detail || "Login failed");
+ return false;
+ }
+ }, []);
+
+ const logout = useCallback(() => {
+ setUser(null);
+ localStorage.removeItem("pm_user");
+ message.info("Logged out.");
+ }, []);
+
+ // ── Debounced server save ─────────────────────────────────────────────────
+ const _scheduleSave = useCallback((nextState) => {
+ if (debounceRef.current) clearTimeout(debounceRef.current);
+ debounceRef.current = setTimeout(async () => {
+ setSaving(true);
+ try {
+ await savePMData(nextState);
+ message.success({
+ content: "Changes saved.",
+ key: "pm-save",
+ duration: 1.5,
+ });
+ } catch (err) {
+ console.error("[PM] Save failed:", err);
+ message.error({
+ content: "Failed to save changes to server.",
+ key: "pm-save",
+ });
+ } finally {
+ setSaving(false);
+ }
+ }, DEBOUNCE_MS);
+ }, []);
+
+ // ── Public state-mutators ─────────────────────────────────────────────────
+ const updateState = useCallback(
+ (patch) => {
+ setPmState((prev) => {
+ const next = { ...prev, ...patch };
+ _scheduleSave(next);
+ return next;
+ });
+ },
+ [_scheduleSave],
+ );
+
+ const resetData = useCallback(async () => {
+ setLoading(true);
+ try {
+ const seed = await resetPMData();
+ setPmState(seed);
+ message.success("Reset to default data.");
+ } catch (err) {
+ console.error("[PM] Reset failed:", err);
+ message.error("Failed to reset data on server.");
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ // ── Volume helpers ───────────────────────────────────────────────────────
+ const getVolumes = useCallback(async (params = {}) => {
+ try {
+ const res = await apiClient.get("/api/pm/volumes", { params });
+ return res.data;
+ } catch (err) {
+ message.error("Failed to load volumes from server.");
+ throw err;
+ }
+ }, []);
+
+ const updateVolumeStatus = useCallback(async (volumeId, status) => {
+ try {
+ const res = await apiClient.patch(`/api/pm/volumes/${volumeId}`, {
+ status,
+ });
+ if (res.data?.global_progress) {
+ setPmState((prev) =>
+ prev ? { ...prev, global_progress: res.data.global_progress } : prev,
+ );
+ }
+ return res.data;
+ } catch (err) {
+ message.error("Failed to update volume status.");
+ throw err;
+ }
+ }, []);
+
+ // ── Convenience setters ───────────────────────────────────────────────────
+ const setQuotaData = useCallback(
+ (v) => updateState({ quota_data: v }),
+ [updateState],
+ );
+ const setProofreaderData = useCallback(
+ (v) => updateState({ proofreader_data: v }),
+ [updateState],
+ );
+ const setThroughputData = useCallback(
+ (v) => updateState({ throughput_data: v }),
+ [updateState],
+ );
+ const setMsgPreview = useCallback(
+ (v) => updateState({ msg_preview: v }),
+ [updateState],
+ );
+
+ // ── Computed RBAC Values ──────────────────────────────────────────────────
+ const isAuthenticated = !!user;
+ const role = user?.role || "guest";
+ const isAdmin = role === "admin";
+ const isWorker = role === "worker";
+ const activeWorker = isWorker ? user?.key : null;
+
+ // ── Role-filtered convenience getters ─────────────────────────────────────
+ const allQuotaData = pmState?.quota_data ?? [];
+ const allProofreaderData = pmState?.proofreader_data ?? [];
+
+ const quotaData = isWorker
+ ? allQuotaData.filter((r) => r.worker_key === activeWorker)
+ : allQuotaData;
+ const proofreaderData = isWorker
+ ? allProofreaderData.filter((r) => r.worker_key === activeWorker)
+ : allProofreaderData;
+
+ // ── Loading gate ──────────────────────────────────────────────────────────
+ if (loading && pmState === null) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/client/src/contexts/WorkflowContext.js b/client/src/contexts/WorkflowContext.js
new file mode 100644
index 00000000..fc9f23d1
--- /dev/null
+++ b/client/src/contexts/WorkflowContext.js
@@ -0,0 +1,210 @@
+import React, {
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useState,
+} from "react";
+import { message } from "antd";
+import {
+ appendWorkflowEvent,
+ approveAgentAction as approveAgentActionApi,
+ createAgentAction,
+ getCurrentWorkflow,
+ getWorkflowHotspots,
+ getWorkflowImpactPreview,
+ listWorkflowEvents,
+ queryWorkflowAgent,
+ rejectAgentAction as rejectAgentActionApi,
+ updateWorkflow as updateWorkflowApi,
+} from "../api";
+import { AppContext } from "./GlobalContext";
+
+export const WorkflowContext = createContext(null);
+
+export function useWorkflow() {
+ return useContext(WorkflowContext);
+}
+
+export function WorkflowProvider({ children }) {
+ const appContext = useContext(AppContext);
+ const [workflow, setWorkflow] = useState(null);
+ const [events, setEvents] = useState([]);
+ const [hotspots, setHotspots] = useState([]);
+ const [impactPreview, setImpactPreview] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [lastClientEffects, setLastClientEffects] = useState(null);
+
+ const refreshWorkflow = useCallback(async () => {
+ setLoading(true);
+ try {
+ const data = await getCurrentWorkflow();
+ setWorkflow(data?.workflow || null);
+ setEvents(data?.events || []);
+ return data;
+ } catch (error) {
+ message.error("Failed to load workflow state.");
+ return null;
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ const refreshEvents = useCallback(async () => {
+ if (!workflow?.id) return [];
+ const nextEvents = await listWorkflowEvents(workflow.id);
+ setEvents(nextEvents || []);
+ return nextEvents || [];
+ }, [workflow?.id]);
+
+ const refreshInsights = useCallback(async () => {
+ if (!workflow?.id) {
+ setHotspots([]);
+ setImpactPreview(null);
+ return null;
+ }
+ try {
+ const [hotspotData, impactData] = await Promise.all([
+ getWorkflowHotspots(workflow.id),
+ getWorkflowImpactPreview(workflow.id),
+ ]);
+ setHotspots(hotspotData?.hotspots || []);
+ setImpactPreview(impactData || null);
+ return {
+ hotspots: hotspotData?.hotspots || [],
+ impactPreview: impactData || null,
+ };
+ } catch (_error) {
+ return null;
+ }
+ }, [workflow?.id]);
+
+ useEffect(() => {
+ refreshWorkflow();
+ }, [refreshWorkflow]);
+
+ useEffect(() => {
+ if (!workflow?.id) {
+ setHotspots([]);
+ setImpactPreview(null);
+ return;
+ }
+ refreshInsights();
+ }, [
+ workflow?.id,
+ workflow?.stage,
+ workflow?.corrected_mask_path,
+ events.length,
+ refreshInsights,
+ ]);
+
+ const updateWorkflow = useCallback(
+ async (patch) => {
+ if (!workflow?.id) return null;
+ const nextWorkflow = await updateWorkflowApi(workflow.id, patch);
+ setWorkflow(nextWorkflow);
+ return nextWorkflow;
+ },
+ [workflow?.id],
+ );
+
+ const appendEvent = useCallback(
+ async (event) => {
+ if (!workflow?.id) return null;
+ const nextEvent = await appendWorkflowEvent(workflow.id, event);
+ setEvents((prev) => [...prev, nextEvent]);
+ return nextEvent;
+ },
+ [workflow?.id],
+ );
+
+ const proposeAgentAction = useCallback(
+ async (action) => {
+ if (!workflow?.id) return null;
+ const proposal = await createAgentAction(workflow.id, action);
+ await refreshEvents();
+ return proposal;
+ },
+ [workflow?.id, refreshEvents],
+ );
+
+ const applyClientEffects = useCallback(
+ (effects) => {
+ if (!effects) return;
+ if (effects.set_training_label_path && appContext?.trainingState) {
+ appContext.trainingState.setInputLabel(effects.set_training_label_path);
+ }
+ setLastClientEffects(effects);
+ },
+ [appContext],
+ );
+
+ const approveAgentAction = useCallback(
+ async (eventId) => {
+ if (!workflow?.id) return null;
+ const result = await approveAgentActionApi(workflow.id, eventId);
+ if (result?.workflow) {
+ setWorkflow(result.workflow);
+ }
+ applyClientEffects(result?.client_effects);
+ await refreshEvents();
+ message.success("Agent proposal approved.");
+ return result;
+ },
+ [workflow?.id, refreshEvents, applyClientEffects],
+ );
+
+ const rejectAgentAction = useCallback(
+ async (eventId) => {
+ if (!workflow?.id) return null;
+ const result = await rejectAgentActionApi(workflow.id, eventId);
+ await refreshEvents();
+ message.info("Agent proposal rejected.");
+ return result;
+ },
+ [workflow?.id, refreshEvents],
+ );
+
+ const queryAgent = useCallback(
+ async (query) => {
+ if (!workflow?.id) return null;
+ const result = await queryWorkflowAgent(workflow.id, query);
+ if (result?.proposals?.length) {
+ await refreshEvents();
+ }
+ return result;
+ },
+ [workflow?.id, refreshEvents],
+ );
+
+ const consumeClientEffects = useCallback(() => {
+ const effects = lastClientEffects;
+ setLastClientEffects(null);
+ return effects;
+ }, [lastClientEffects]);
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/client/src/contexts/WorkflowContext.test.js b/client/src/contexts/WorkflowContext.test.js
new file mode 100644
index 00000000..57549833
--- /dev/null
+++ b/client/src/contexts/WorkflowContext.test.js
@@ -0,0 +1,118 @@
+import React from "react";
+import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+
+import { AppContext } from "./GlobalContext";
+import { WorkflowProvider, useWorkflow } from "./WorkflowContext";
+import {
+ approveAgentAction,
+ getCurrentWorkflow,
+ getWorkflowHotspots,
+ getWorkflowImpactPreview,
+ listWorkflowEvents,
+} from "../api";
+
+jest.mock("../api", () => ({
+ approveAgentAction: jest.fn(),
+ appendWorkflowEvent: jest.fn(),
+ createAgentAction: jest.fn(),
+ getCurrentWorkflow: jest.fn(),
+ getWorkflowHotspots: jest.fn(),
+ getWorkflowImpactPreview: jest.fn(),
+ listWorkflowEvents: jest.fn(),
+ queryWorkflowAgent: jest.fn(),
+ rejectAgentAction: jest.fn(),
+ updateWorkflow: jest.fn(),
+}));
+
+const baseWorkflow = {
+ id: 1,
+ title: "Segmentation Workflow",
+ stage: "setup",
+};
+
+function Probe() {
+ const workflowContext = useWorkflow();
+ return (
+
+
{workflowContext.workflow?.stage || "loading"}
+
{workflowContext.events.map((event) => event.event_type).join(",")}
+
{workflowContext.hotspots?.[0]?.summary || "no-hotspot"}
+
{workflowContext.impactPreview?.confidence || "no-impact"}
+
workflowContext.approveAgentAction(7)}
+ >
+ Approve proposal
+
+
+ );
+}
+
+function renderProvider(appContextValue) {
+ render(
+
+
+
+
+ ,
+ );
+}
+
+describe("WorkflowProvider", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ getCurrentWorkflow.mockResolvedValue({
+ workflow: baseWorkflow,
+ events: [{ id: 1, event_type: "workflow.created" }],
+ });
+ listWorkflowEvents.mockResolvedValue([]);
+ getWorkflowHotspots.mockResolvedValue({
+ workflow_id: 1,
+ hotspots: [{ region_key: "z:9", summary: "Top hotspot", severity: "high" }],
+ });
+ getWorkflowImpactPreview.mockResolvedValue({
+ workflow_id: 1,
+ confidence: "medium",
+ summary: "Impact summary",
+ });
+ });
+
+ it("loads the current workflow and events on startup", async () => {
+ renderProvider({ trainingState: { setInputLabel: jest.fn() } });
+
+ expect(await screen.findByText("setup")).toBeTruthy();
+ expect(screen.getByText("workflow.created")).toBeTruthy();
+ await waitFor(() => {
+ expect(getWorkflowHotspots).toHaveBeenCalledWith(1);
+ expect(getWorkflowImpactPreview).toHaveBeenCalledWith(1);
+ });
+ await waitFor(() => {
+ expect(screen.getByText("Top hotspot")).toBeTruthy();
+ expect(screen.getByText("medium")).toBeTruthy();
+ });
+ expect(getCurrentWorkflow).toHaveBeenCalledTimes(1);
+ });
+
+ it("applies client effects when an agent proposal is approved", async () => {
+ const setInputLabel = jest.fn();
+ approveAgentAction.mockResolvedValue({
+ workflow: { ...baseWorkflow, stage: "retraining_staged" },
+ client_effects: {
+ navigate_to: "training",
+ set_training_label_path: "/tmp/corrected.tif",
+ },
+ });
+
+ renderProvider({ trainingState: { setInputLabel } });
+ await screen.findByText("setup");
+
+ fireEvent.click(screen.getByText("Approve proposal"));
+
+ await waitFor(() => {
+ expect(setInputLabel).toHaveBeenCalledWith("/tmp/corrected.tif");
+ });
+ await waitFor(() => {
+ expect(screen.getByText("retraining_staged")).toBeTruthy();
+ });
+ });
+});
diff --git a/client/src/contexts/workflow/proposalCardConfig.js b/client/src/contexts/workflow/proposalCardConfig.js
new file mode 100644
index 00000000..febf263a
--- /dev/null
+++ b/client/src/contexts/workflow/proposalCardConfig.js
@@ -0,0 +1,92 @@
+const toDisplayValue = (value) => {
+ if (value === null || value === undefined || value === "") {
+ return "—";
+ }
+ if (Array.isArray(value)) {
+ return value.length ? value.join(", ") : "—";
+ }
+ if (typeof value === "object") {
+ return JSON.stringify(value);
+ }
+ return String(value);
+};
+
+const compactRationale = (rationale) => {
+ if (!rationale) return "No rationale provided.";
+ const trimmed = String(rationale).trim();
+ return trimmed.length > 160 ? `${trimmed.slice(0, 157)}…` : trimmed;
+};
+
+const pickEntries = (proposal, keys) =>
+ keys
+ .filter((key) => key in proposal)
+ .map((key) => ({
+ key,
+ label: key.replace(/_/g, " "),
+ value: toDisplayValue(proposal[key]),
+ }));
+
+export const getProposalCardContent = (proposal = {}) => {
+ const type = proposal.type || proposal.proposal_type || proposal.action || "proposal";
+
+ if (type === "prioritize_failure_hotspots") {
+ return {
+ type,
+ title: "Prioritize Failure Hotspots",
+ rationale: compactRationale(proposal.rationale || proposal.why),
+ fields: pickEntries(proposal, [
+ "target_dataset",
+ "hotspots",
+ "priority_metric",
+ "min_failure_rate",
+ ]),
+ };
+ }
+
+ if (type === "preview_correction_impact") {
+ return {
+ type,
+ title: "Preview Correction Impact",
+ rationale: compactRationale(proposal.rationale || proposal.why),
+ fields: pickEntries(proposal, [
+ "target_metric",
+ "expected_delta",
+ "sample_size",
+ "confidence",
+ ]),
+ };
+ }
+
+ if (type === "stage_retraining_from_corrections") {
+ return {
+ type,
+ title: "Stage Retraining From Corrections",
+ rationale: compactRationale(
+ proposal.rationale ||
+ proposal.why ||
+ "Stage corrected masks for the next model iteration.",
+ ),
+ fields: pickEntries(proposal, [
+ "corrected_mask_path",
+ "written_path",
+ "training_output_path",
+ ]),
+ };
+ }
+
+ const fallbackFields = Object.entries(proposal)
+ .filter(([key]) => !["type", "proposal_type", "rationale", "why"].includes(key))
+ .slice(0, 4)
+ .map(([key, value]) => ({
+ key,
+ label: key.replace(/_/g, " "),
+ value: toDisplayValue(value),
+ }));
+
+ return {
+ type,
+ title: "Agent Proposal",
+ rationale: compactRationale(proposal.rationale || proposal.why),
+ fields: fallbackFields,
+ };
+};
diff --git a/client/src/contexts/workflow/timelineFilters.js b/client/src/contexts/workflow/timelineFilters.js
new file mode 100644
index 00000000..5bfeee1a
--- /dev/null
+++ b/client/src/contexts/workflow/timelineFilters.js
@@ -0,0 +1,42 @@
+export const TIMELINE_ACTOR_OPTIONS = ["all", "user", "agent", "system"];
+
+export const DEFAULT_TIMELINE_FILTERS = {
+ actor: "all",
+ eventType: "",
+};
+
+export function normalizeTimelineFilters(filters = {}) {
+ const actor = TIMELINE_ACTOR_OPTIONS.includes(filters.actor)
+ ? filters.actor
+ : DEFAULT_TIMELINE_FILTERS.actor;
+ const eventType = (filters.eventType || "").trim();
+ return { actor, eventType };
+}
+
+export function eventMatchesTimelineFilters(
+ event,
+ filters = DEFAULT_TIMELINE_FILTERS,
+) {
+ const normalized = normalizeTimelineFilters(filters);
+ const actor = (event?.actor || "").toLowerCase();
+ const eventType = (event?.event_type || event?.eventType || "").toLowerCase();
+
+ if (normalized.actor !== "all" && actor !== normalized.actor) {
+ return false;
+ }
+ if (!normalized.eventType) {
+ return true;
+ }
+ return eventType.includes(normalized.eventType.toLowerCase());
+}
+
+export function filterTimelineEvents(events = [], filters = DEFAULT_TIMELINE_FILTERS) {
+ if (!Array.isArray(events) || events.length === 0) {
+ return [];
+ }
+ const normalized = normalizeTimelineFilters(filters);
+ if (normalized.actor === "all" && !normalized.eventType) {
+ return events;
+ }
+ return events.filter((event) => eventMatchesTimelineFilters(event, normalized));
+}
diff --git a/client/src/views/EHTool.js b/client/src/views/EHTool.js
index 5d546fd7..3cb38e32 100644
--- a/client/src/views/EHTool.js
+++ b/client/src/views/EHTool.js
@@ -13,6 +13,7 @@ function EHTool({
onSessionChange,
refreshTrigger,
savedSessionId,
+ workflowId,
}) {
// Initialize with saved session if available
const [sessionId, setSessionId] = useState(savedSessionId || null);
@@ -37,6 +38,7 @@ function EHTool({
diff --git a/client/src/views/ModelInference.js b/client/src/views/ModelInference.js
index 8ea3bf44..8de45350 100644
--- a/client/src/views/ModelInference.js
+++ b/client/src/views/ModelInference.js
@@ -11,13 +11,18 @@ import Configurator from "../components/Configurator";
import { applyInputPaths } from "../configSchema";
import RuntimeLogPanel from "../components/RuntimeLogPanel";
import { AppContext } from "../contexts/GlobalContext";
+import { useWorkflow } from "../contexts/WorkflowContext";
function ModelInference({ isInferring, setIsInferring }) {
const context = useContext(AppContext);
+ const workflowContext = useWorkflow();
+ const appendWorkflowEvent = workflowContext?.appendEvent;
+ const workflowId = workflowContext?.workflow?.id;
const inference = context.inferenceState;
const [inferenceStatus, setInferenceStatus] = useState("");
const [inferenceRuntime, setInferenceRuntime] = useState(null);
const pollingIntervalRef = useRef(null);
+ const terminalLoggedRef = useRef(false);
const getPath = (val) => {
if (!val) return "";
@@ -60,7 +65,10 @@ function ModelInference({ isInferring, setIsInferring }) {
});
return yaml.dump(yamlData, { indent: 2 }).replace(/^\s*\n/gm, "");
} catch (error) {
- console.warn("Failed to prepare inference config from current inputs:", error);
+ console.warn(
+ "Failed to prepare inference config from current inputs:",
+ error,
+ );
return inferenceConfig;
}
};
@@ -81,9 +89,32 @@ function ModelInference({ isInferring, setIsInferring }) {
if (!status.isRunning) {
setIsInferring(false);
+ if (!terminalLoggedRef.current && appendWorkflowEvent) {
+ terminalLoggedRef.current = true;
+ const succeeded = status.exitCode === 0;
+ await appendWorkflowEvent({
+ actor: "system",
+ event_type: succeeded
+ ? "inference.completed"
+ : "inference.failed",
+ stage: "inference",
+ summary: succeeded
+ ? "Inference completed successfully."
+ : "Inference finished without a successful exit.",
+ payload: {
+ exitCode: status.exitCode,
+ phase: status.phase,
+ lastError: status.lastError,
+ outputPath: getPath(inference.outputPath),
+ },
+ });
+ }
if (status.exitCode === 0) {
setInferenceStatus("Inference completed successfully! ✓");
- } else if (status.exitCode !== null && status.exitCode !== undefined) {
+ } else if (
+ status.exitCode !== null &&
+ status.exitCode !== undefined
+ ) {
setInferenceStatus(
`Inference finished with exit code: ${status.exitCode}`,
);
@@ -96,6 +127,19 @@ function ModelInference({ isInferring, setIsInferring }) {
} catch (error) {
console.error("Error polling inference status:", error);
setIsInferring(false);
+ if (!terminalLoggedRef.current && appendWorkflowEvent) {
+ terminalLoggedRef.current = true;
+ await appendWorkflowEvent({
+ actor: "system",
+ event_type: "inference.failed",
+ stage: "inference",
+ summary: "Inference status polling failed.",
+ payload: {
+ error: error.message || "unknown error",
+ outputPath: getPath(inference.outputPath),
+ },
+ });
+ }
setInferenceStatus(
`Inference status polling failed: ${error.message || "unknown error"}`,
);
@@ -109,9 +153,10 @@ function ModelInference({ isInferring, setIsInferring }) {
pollingIntervalRef.current = null;
}
};
- }, [isInferring, setIsInferring]);
+ }, [isInferring, setIsInferring, appendWorkflowEvent, inference.outputPath]);
const handleStartButton = async () => {
+ let checkpointPath = "";
try {
const inferenceConfig = context.inferenceConfig;
if (!inferenceConfig) {
@@ -121,7 +166,7 @@ function ModelInference({ isInferring, setIsInferring }) {
return;
}
- const checkpointPath = getPath(inference.checkpointPath);
+ checkpointPath = getPath(inference.checkpointPath);
if (!checkpointPath) {
setInferenceStatus("Error: Please set checkpoint path first.");
return;
@@ -129,14 +174,17 @@ function ModelInference({ isInferring, setIsInferring }) {
setIsInferring(true);
setInferenceStatus("Starting inference...");
+ terminalLoggedRef.current = false;
- const preparedInferenceConfig = getPreparedInferenceConfig(inferenceConfig);
+ const preparedInferenceConfig =
+ getPreparedInferenceConfig(inferenceConfig);
const res = await startModelInference(
preparedInferenceConfig,
getPath(inference.outputPath),
checkpointPath,
getConfigOriginPath(),
+ workflowId,
);
console.log(res);
await refreshInferenceLogs();
@@ -144,6 +192,20 @@ function ModelInference({ isInferring, setIsInferring }) {
} catch (e) {
console.log(e);
setIsInferring(false);
+ if (!terminalLoggedRef.current && appendWorkflowEvent) {
+ terminalLoggedRef.current = true;
+ await appendWorkflowEvent({
+ actor: "system",
+ event_type: "inference.failed",
+ stage: "inference",
+ summary: "Inference failed to start.",
+ payload: {
+ error: e.message || "unknown error",
+ outputPath: getPath(inference.outputPath),
+ checkpointPath,
+ },
+ });
+ }
await refreshInferenceLogs();
setInferenceStatus(
`Inference error: ${e.message || "Please check console for details."}`,
diff --git a/client/src/views/ModelTraining.js b/client/src/views/ModelTraining.js
index 312724dc..cd848579 100644
--- a/client/src/views/ModelTraining.js
+++ b/client/src/views/ModelTraining.js
@@ -12,9 +12,11 @@ import Configurator from "../components/Configurator";
import { applyInputPaths } from "../configSchema";
import RuntimeLogPanel from "../components/RuntimeLogPanel";
import { AppContext } from "../contexts/GlobalContext";
+import { useWorkflow } from "../contexts/WorkflowContext";
function ModelTraining() {
const context = useContext(AppContext);
+ const workflowContext = useWorkflow();
const training = context.trainingState;
const [isTraining, setIsTraining] = useState(false);
const [trainingStatus, setTrainingStatus] = useState("");
@@ -61,7 +63,9 @@ function ModelTraining() {
if (status.exitCode === 0) {
setTrainingStatus("Training completed successfully! ✓");
} else if (status.exitCode !== null) {
- setTrainingStatus(`Training finished with exit code: ${status.exitCode}`);
+ setTrainingStatus(
+ `Training finished with exit code: ${status.exitCode}`,
+ );
} else if (status.phase === "failed" && status.lastError) {
setTrainingStatus(`Training failed: ${status.lastError}`);
} else {
@@ -91,7 +95,10 @@ function ModelTraining() {
});
return yaml.dump(yamlData, { indent: 2 }).replace(/^\s*\n/gm, "");
} catch (error) {
- console.warn("Failed to prepare training config from current inputs:", error);
+ console.warn(
+ "Failed to prepare training config from current inputs:",
+ error,
+ );
return trainingConfig;
}
};
@@ -161,6 +168,7 @@ function ModelTraining() {
getPath(training.logPath) || getPath(training.outputPath),
getPath(training.outputPath),
getConfigOriginPath(),
+ workflowContext?.workflow?.id,
);
console.log(res);
await refreshTrainingLogs();
diff --git a/client/src/views/Views.js b/client/src/views/Views.js
index e442adf5..fab8c0a7 100644
--- a/client/src/views/Views.js
+++ b/client/src/views/Views.js
@@ -8,14 +8,17 @@ import {
DashboardOutlined,
BugOutlined,
MessageOutlined,
+ ProjectOutlined,
} from "@ant-design/icons";
import FilesManager from "./FilesManager";
import Visualization from "./Visualization";
import ModelTraining from "./ModelTraining";
import ModelInference from "./ModelInference";
import Monitoring from "./Monitoring";
-import MaskProofreading from "./MaskProofreading";
+import MaskProofreading from "./mask-proofreading/MaskProofreading";
+import ProjectManager from "./project-manager/ProjectManager";
import Chatbot from "../components/Chatbot";
+import { useWorkflow } from "../contexts/WorkflowContext";
const { Content } = Layout;
@@ -34,11 +37,21 @@ const MODULE_ITEMS = [
key: "mask-proofreading",
icon: ,
},
+ {
+ label: "Project Manager",
+ key: "project-manager",
+ icon: ,
+ },
];
function Views() {
- const [current, setCurrent] = useState("files");
- const [visitedTabs, setVisitedTabs] = useState(new Set(["files"]));
+ const workflowContext = useWorkflow();
+ const lastClientEffects = workflowContext?.lastClientEffects;
+ const consumeClientEffects = workflowContext?.consumeClientEffects;
+ const [current, setCurrent] = useState("project-manager");
+ const [visitedTabs, setVisitedTabs] = useState(
+ new Set(["project-manager", "files"]),
+ );
const [isChatOpen, setIsChatOpen] = useState(false);
const [chatWidth, setChatWidth] = useState(560);
const isResizing = useRef(false);
@@ -46,6 +59,33 @@ function Views() {
const [viewers, setViewers] = useState([]);
const [isInferring, setIsInferring] = useState(false);
+/*
+ const allItems = [
+ { label: "File Management", key: "files", icon: },
+ { label: "Visualization", key: "visualization", icon: },
+ { label: "Model Training", key: "training", icon: },
+ {
+ label: "Model Inference",
+ key: "inference",
+ icon: ,
+ },
+ { label: "Tensorboard", key: "monitoring", icon: },
+ { label: "SynAnno", key: "synanno", icon: },
+ {
+ label: "Worm Error Handling",
+ key: "worm-error-handling",
+ icon: ,
+ },
+ {
+ label: "Project Manager",
+ key: "project-manager",
+ icon: ,
+ },
+ ];
+
+ const items = allItems.filter((item) => visibleTabs.has(item.key));
+*/
+
const onClick = (e) => {
setCurrent(e.key);
setVisitedTabs((prev) => new Set(prev).add(e.key));
@@ -78,6 +118,15 @@ function Views() {
};
}, [resize, stopResizing]);
+ useEffect(() => {
+ const target = lastClientEffects?.navigate_to;
+ if (!target) return;
+ const targetKey = target === "model-training" ? "training" : target;
+ setCurrent(targetKey);
+ setVisitedTabs((prev) => new Set(prev).add(targetKey));
+ consumeClientEffects?.();
+ }, [lastClientEffects, consumeClientEffects]);
+
const renderTabContent = (key, component) => {
if (!visitedTabs.has(key)) return null;
return (
@@ -141,7 +190,10 @@ function Views() {
/>,
)}
{renderTabContent("mask-proofreading", )}
-
+ {/* {renderTabContent("synanno", )} */}
+ {/* {renderTabContent("worm-error-handling", )} */}
+ {renderTabContent("project-manager", )}
+
setIsChatOpen(false)} />
-
+
);
}
diff --git a/client/src/views/Views.test.js b/client/src/views/Views.test.js
index 1f08242b..979bbde3 100644
--- a/client/src/views/Views.test.js
+++ b/client/src/views/Views.test.js
@@ -3,6 +3,10 @@ import { fireEvent, render, screen } from "@testing-library/react";
import Views from "./Views";
+jest.mock("../contexts/WorkflowContext", () => ({
+ useWorkflow: () => null,
+}));
+
jest.mock("antd", () => {
const React = require("react");
@@ -41,6 +45,7 @@ jest.mock("@ant-design/icons", () => {
DashboardOutlined: Icon,
BugOutlined: Icon,
MessageOutlined: Icon,
+ ProjectOutlined: Icon,
};
});
@@ -49,7 +54,12 @@ jest.mock("./Visualization", () => () => Visualization Content
);
jest.mock("./ModelTraining", () => () => Training Content
);
jest.mock("./ModelInference", () => () => Inference Content
);
jest.mock("./Monitoring", () => () => Monitoring Content
);
-jest.mock("./MaskProofreading", () => () => Mask Proofreading Content
);
+jest.mock("./mask-proofreading/MaskProofreading", () => () => (
+ Mask Proofreading Content
+));
+jest.mock("./project-manager/ProjectManager", () => () => (
+ Project Manager Content
+));
jest.mock("../components/Chatbot", () => () => Chatbot
);
describe("Views", () => {
diff --git a/client/src/views/Visualization.js b/client/src/views/Visualization.js
index ab2c58e7..4c2de865 100644
--- a/client/src/views/Visualization.js
+++ b/client/src/views/Visualization.js
@@ -7,11 +7,13 @@ import {
} from "@ant-design/icons";
import { getNeuroglancerViewer } from "../api";
import UnifiedFileInput from "../components/UnifiedFileInput";
+import { useWorkflow } from "../contexts/WorkflowContext";
const { Title } = Typography;
function Visualization(props) {
const { viewers, setViewers } = props;
+ const workflowContext = useWorkflow();
const [activeKey, setActiveKey] = useState(
viewers.length > 0 ? viewers[0].key : null,
);
@@ -77,6 +79,7 @@ function Visualization(props) {
imagePath,
labelPath,
scalesArray,
+ workflowContext?.workflow?.id,
);
console.log("Current Viewer at ", res);
diff --git a/client/src/views/ehtool/DetectionWorkflow.js b/client/src/views/ehtool/DetectionWorkflow.js
index 2207f60c..48ee921c 100644
--- a/client/src/views/ehtool/DetectionWorkflow.js
+++ b/client/src/views/ehtool/DetectionWorkflow.js
@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useMemo, useRef } from "react";
+import React, { useContext, useState, useEffect, useMemo, useRef } from "react";
import {
Layout,
message,
@@ -20,6 +20,8 @@ import InstanceNavigator from "./InstanceNavigator";
import ProofreadingEditor from "./ProofreadingEditor";
import SliceScheduler from "./SliceScheduler";
import { apiClient } from "../../api";
+import { AppContext } from "../../contexts/GlobalContext";
+import { useWorkflow } from "../../contexts/WorkflowContext";
const { Sider, Content } = Layout;
const { Title, Text } = Typography;
@@ -55,7 +57,14 @@ const SCRUB_IDLE_MS = parsePositiveInt(
120,
);
-function DetectionWorkflow({ sessionId, setSessionId, refreshTrigger }) {
+function DetectionWorkflow({
+ sessionId,
+ setSessionId,
+ refreshTrigger,
+ workflowId,
+}) {
+ const appContext = useContext(AppContext);
+ const workflowContext = useWorkflow();
const [projectName, setProjectName] = useState("");
const [totalLayers, setTotalLayers] = useState(0);
const [instances, setInstances] = useState([]);
@@ -108,6 +117,7 @@ function DetectionWorkflow({ sessionId, setSessionId, refreshTrigger }) {
const [persistence, setPersistence] = useState(null);
const [showExportModal, setShowExportModal] = useState(false);
const [exportPath, setExportPath] = useState("");
+ const [lastExportPath, setLastExportPath] = useState("");
const [exportingMasks, setExportingMasks] = useState(false);
const [overwritingSource, setOverwritingSource] = useState(false);
const lastPersistenceErrorRef = useRef(null);
@@ -448,6 +458,7 @@ function DetectionWorkflow({ sessionId, setSessionId, refreshTrigger }) {
dataset_path: datasetPath,
mask_path: maskPath || null,
project_name: projectName,
+ workflow_id: workflowId || null,
});
setSessionId(response.data.session_id);
@@ -1767,6 +1778,7 @@ function DetectionWorkflow({ sessionId, setSessionId, refreshTrigger }) {
create_backup: true,
});
setShowExportModal(false);
+ setLastExportPath(response.data.written_path);
message.success(`Exported masks to ${response.data.written_path}`);
if (response.data.backup_path) {
message.info(`Backup created at ${response.data.backup_path}`);
@@ -1805,6 +1817,7 @@ function DetectionWorkflow({ sessionId, setSessionId, refreshTrigger }) {
if (response.data.backup_path) {
message.info(`Backup created at ${response.data.backup_path}`);
}
+ setLastExportPath(response.data.written_path);
refreshPersistenceStatus();
} catch (error) {
Modal.error({
@@ -1822,6 +1835,44 @@ function DetectionWorkflow({ sessionId, setSessionId, refreshTrigger }) {
});
};
+ const handleStageForRetraining = async () => {
+ const correctedMaskPath = lastExportPath || persistence?.last_export_path;
+ if (!correctedMaskPath) {
+ message.warning("Export corrected masks before staging retraining.");
+ return;
+ }
+ if (!workflowContext?.workflow?.id) {
+ message.warning("Workflow state is not available yet.");
+ return;
+ }
+
+ try {
+ await workflowContext.updateWorkflow({
+ stage: "retraining_staged",
+ corrected_mask_path: correctedMaskPath,
+ });
+ await workflowContext.appendEvent({
+ actor: "user",
+ event_type: "retraining.staged",
+ stage: "retraining_staged",
+ summary: "Staged corrected masks for retraining.",
+ payload: {
+ corrected_mask_path: correctedMaskPath,
+ ehtool_session_id: sessionId,
+ source: "proofreading_export",
+ },
+ });
+ if (appContext?.trainingState?.setInputLabel) {
+ appContext.trainingState.setInputLabel(correctedMaskPath);
+ }
+ message.success("Corrected masks staged for retraining.");
+ } catch (error) {
+ message.error(
+ getErrorMessage(error, "Failed to stage corrected masks for retraining"),
+ );
+ }
+ };
+
if (!sessionId) {
return (
@@ -1834,6 +1885,7 @@ function DetectionWorkflow({ sessionId, setSessionId, refreshTrigger }) {
sliderZ ?? viewState.zIndex,
axisTotal || totalLayers,
);
+ const exportedMaskPath = lastExportPath || persistence?.last_export_path || "";
const reviewControls =
activeInstance && instanceMode !== "none" ? (
@@ -1929,6 +1981,15 @@ function DetectionWorkflow({ sessionId, setSessionId, refreshTrigger }) {
Next unreviewed
+ {exportedMaskPath && (
+
+ Stage for retraining
+
+ )}
{
// This prop is now nominally used to trigger internal modal
diff --git a/client/src/views/mask-proofreading/MaskProofreading.test.js b/client/src/views/mask-proofreading/MaskProofreading.test.js
new file mode 100644
index 00000000..4fb7fa95
--- /dev/null
+++ b/client/src/views/mask-proofreading/MaskProofreading.test.js
@@ -0,0 +1,26 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+
+import MaskProofreading from "./MaskProofreading";
+
+jest.mock("../EHTool", () => (props) => (
+
+));
+
+jest.mock("../../contexts/WorkflowContext", () => ({
+ useWorkflow: () => ({ workflow: { id: 42 } }),
+}));
+
+describe("MaskProofreading", () => {
+ it("renders EHTool and passes the active workflow id", () => {
+ render( );
+
+ const ehTool = screen.getByTestId("eh-tool");
+ expect(ehTool).toBeTruthy();
+ expect(ehTool.getAttribute("data-workflow-id")).toBe("42");
+ });
+});
diff --git a/client/src/views/project-manager/AnnotationDashboard.js b/client/src/views/project-manager/AnnotationDashboard.js
new file mode 100644
index 00000000..ca66a3eb
--- /dev/null
+++ b/client/src/views/project-manager/AnnotationDashboard.js
@@ -0,0 +1,768 @@
+import React, { useState, useMemo } from "react";
+import {
+ Card,
+ Statistic,
+ Table,
+ Tag,
+ Progress,
+ Button,
+ Space,
+ Typography,
+ Tooltip,
+ Row,
+ Col,
+ Divider,
+ Badge,
+} from "antd";
+import {
+ CheckCircleOutlined,
+ ClockCircleOutlined,
+ StopOutlined,
+ MinusCircleOutlined,
+ WarningOutlined,
+ RiseOutlined,
+ TeamOutlined,
+ FileTextOutlined,
+ ThunderboltOutlined,
+ DatabaseOutlined,
+} from "@ant-design/icons";
+import dayjs from "dayjs";
+
+import { useProjectManager } from "../../contexts/ProjectManagerContext";
+
+const { Title, Text } = Typography;
+
+const PROJECT_START = dayjs("2025-10-01");
+const PROJECT_END = dayjs("2026-03-31");
+
+// ─── Helpers ─────────────────────────────────────────────────────────────────
+
+const STATUS_META = {
+ in_progress: { label: "In Progress", color: "processing" },
+ done: { label: "Done", color: "success" },
+ blocked: { label: "Blocked", color: "error" },
+ not_started: { label: "Not Started", color: "default" },
+};
+
+function statusTag(status) {
+ const { label, color } = STATUS_META[status] || {};
+ return ;
+}
+
+// Convert a date to a 0-1 fraction along the 6-month timeline
+function dateToFrac(d) {
+ const total = PROJECT_END.diff(PROJECT_START, "day");
+ const elapsed = dayjs(d).diff(PROJECT_START, "day");
+ return Math.min(1, Math.max(0, elapsed / total));
+}
+
+// ─── Sub-components ──────────────────────────────────────────────────────────
+
+/** Custom SVG 6-month horizontal timeline */
+function SixMonthTimeline({ milestones }) {
+ const W = 900;
+ const H = 80;
+ const PAD = 40;
+ const trackY = 48;
+ const trackW = W - PAD * 2;
+
+ const months = [];
+ let cur = PROJECT_START.startOf("month");
+ while (cur.isBefore(PROJECT_END) || cur.isSame(PROJECT_END, "month")) {
+ months.push(cur);
+ cur = cur.add(1, "month");
+ }
+
+ const todayFrac = dateToFrac(dayjs("2026-03-20"));
+
+ return (
+
+
+
+ {months.map((m) => {
+ const x = PAD + dateToFrac(m) * trackW;
+ return (
+
+
+
+ {m.format("MMM")}
+
+
+ );
+ })}
+ {milestones.map((ms) => {
+ const x = PAD + dateToFrac(ms.date) * trackW;
+ return (
+
+
+
+
+ {ms.label}
+
+
+
+ );
+ })}
+
+
+ Today
+
+
+ );
+}
+
+/** Cumulative progress SVG line chart */
+function CumulativeChart({ cumulativeData, cumulativeTarget }) {
+ const W = 800;
+ const H = 180;
+ const PADL = 52;
+ const PADR = 16;
+ const PADT = 16;
+ const PADB = 32;
+ const chartW = W - PADL - PADR;
+ const chartH = H - PADT - PADB;
+ const maxVal = Math.max(...cumulativeData, ...cumulativeTarget) * 1.05;
+ const weeks = cumulativeData.length;
+ const toX = (i) => PADL + (i / (weeks - 1)) * chartW;
+ const toY = (v) => PADT + chartH - (v / maxVal) * chartH;
+ const pathD = (data) =>
+ data
+ .map(
+ (v, i) =>
+ `${i === 0 ? "M" : "L"} ${toX(i).toFixed(1)},${toY(v).toFixed(1)}`,
+ )
+ .join(" ");
+ const areaD = (data) =>
+ `${pathD(data)} L ${toX(weeks - 1).toFixed(1)},${(PADT + chartH).toFixed(1)} L ${PADL},${(PADT + chartH).toFixed(1)} Z`;
+ const yTicks = [0, 10000, 20000, 30000];
+ const xTicks = [0, 4, 8, 12, 16, 20, 24];
+ return (
+
+ {yTicks.map((v) => (
+
+
+
+ {v === 0 ? "0" : `${v / 1000}k`}
+
+
+ ))}
+ {xTicks.map((i) => (
+
+ Wk {i + 1}
+
+ ))}
+
+
+
+
+
+
+
+ Actual
+
+
+
+ Target
+
+
+
+ );
+}
+
+// ─── Column definitions ───────────────────────────────────────────────────────
+
+const DATASET_COLUMNS = [
+ {
+ title: "Dataset Name",
+ dataIndex: "name",
+ key: "name",
+ render: (name) => {name} ,
+ },
+ {
+ title: "Experiment",
+ dataIndex: "experiment",
+ key: "experiment",
+ render: (t) => {t} ,
+ },
+ {
+ title: "Total Samples",
+ dataIndex: "total",
+ key: "total",
+ align: "right",
+ render: (n) => n.toLocaleString(),
+ },
+ {
+ title: "% Proofread",
+ dataIndex: "proofread",
+ key: "proofread",
+ width: 180,
+ render: (pct) => (
+
+ = 80 ? "#52c41a" : pct >= 40 ? "#faad14" : "#ff4d4f"
+ }
+ showInfo={false}
+ />
+ {pct}%
+
+ ),
+ },
+ { title: "Status", dataIndex: "status", key: "status", render: statusTag },
+ {
+ title: "ETA",
+ dataIndex: "eta",
+ key: "eta",
+ render: (eta) => {eta} ,
+ },
+];
+
+// ─── Main Component ───────────────────────────────────────────────────────────
+
+function AnnotationDashboard() {
+ const {
+ quotaData,
+ allQuotaData,
+ proofreaderData,
+ datasets,
+ milestones,
+ cumulativeData,
+ cumulativeTarget,
+ atRisk,
+ upcomingMilestones,
+ globalProgress,
+ isAdmin,
+ isWorker,
+ activeWorker,
+ } = useProjectManager();
+
+ const [filter, setFilter] = useState("all");
+
+ // ── KPI calculations ──────────────────────────────────────────────────────
+ // Weekly quota progress (uses role-filtered quotaData)
+ const totalActual = useMemo(
+ () =>
+ quotaData.reduce(
+ (sum, row) =>
+ sum +
+ row.actualMon +
+ row.actualTue +
+ row.actualWed +
+ row.actualThu +
+ row.actualFri +
+ row.actualSat +
+ row.actualSun,
+ 0,
+ ),
+ [quotaData],
+ );
+ const totalTarget = useMemo(
+ () =>
+ quotaData.reduce(
+ (sum, row) =>
+ sum +
+ row.mon +
+ row.tue +
+ row.wed +
+ row.thu +
+ row.fri +
+ row.sat +
+ row.sun,
+ 0,
+ ),
+ [quotaData],
+ );
+
+ const totalSamplesCount = useMemo(
+ () => datasets.reduce((s, d) => s + d.total, 0),
+ [datasets],
+ );
+ const overallPct =
+ totalTarget > 0 ? Math.round((totalActual / totalTarget) * 100) : 0;
+ const activeDatasetsCount = datasets.filter(
+ (d) => d.status === "in_progress",
+ ).length;
+ const activeProofreadersCount = proofreaderData.filter(
+ (p) => p.status === "online" || p.status === "away",
+ ).length;
+
+ // Volume progress — admin = global 1000, worker = their 250
+ const volProgress = isWorker
+ ? (globalProgress?.by_worker?.[activeWorker] ?? {
+ done: 0,
+ total: 250,
+ pct: 0,
+ })
+ : (globalProgress ?? { done: 0, total: 1000, pct: 0 });
+
+ // Filtered datasets
+ const filteredDatasets = useMemo(() => {
+ if (filter === "all") return datasets;
+ if (filter === "high") return datasets.filter((d) => d.priority === "high");
+ if (filter === "blocked")
+ return datasets.filter((d) => d.status === "blocked");
+ return datasets;
+ }, [filter, datasets]);
+
+ const filterBtns = [
+ { key: "all", label: "All" },
+ { key: "high", label: "High Priority" },
+ { key: "blocked", label: "Blocked" },
+ ];
+
+ return (
+
+ {/* ── Header ── */}
+
+
+ {isWorker ? "My Dashboard" : "Neural Dataset Proofreading – 6 Months"}
+
+
+ {PROJECT_START.format("MMM D, YYYY")} →{" "}
+ {PROJECT_END.format("MMM D, YYYY")} · Real-time Context Sync
+
+
+
+ {/* ── KPI Cards ── */}
+
+ {/* ① Volume Progress — derived from actual done status of 1000 volumes */}
+
+
+
+ {isWorker ? "My Volumes Done" : "Global Volume Progress"}
+
+ }
+ value={volProgress.done}
+ suffix={`/ ${volProgress.total}`}
+ prefix={ }
+ valueStyle={{
+ color: "#722ed1",
+ fontSize: 20,
+ fontWeight: "bold",
+ }}
+ />
+
+
+
+ {volProgress.pct ?? 0}% of{" "}
+ {isWorker ? "250 assigned" : "1,000 total"} volumes done
+
+
+
+
+
+ {/* ② Weekly Points */}
+
+
+
+ Weekly Points
+
+ }
+ value={totalActual.toLocaleString()}
+ suffix={`/ ${totalTarget.toLocaleString()}`}
+ prefix={ }
+ valueStyle={{
+ color: "#52c41a",
+ fontSize: 20,
+ fontWeight: "bold",
+ }}
+ />
+
+
+
+
+ {/* ③ Active Datasets (admin only, for worker show accuracy) */}
+
+
+
+ Active Datasets
+
+ }
+ value={activeDatasetsCount}
+ suffix={`/ ${datasets.length}`}
+ prefix={ }
+ valueStyle={{
+ color: "#1890ff",
+ fontSize: 20,
+ fontWeight: "bold",
+ }}
+ />
+
+ {totalSamplesCount.toLocaleString()} total samples
+
+
+
+
+ {/* ④ Team status */}
+
+
+
+ {isWorker ? "My Weekly Points" : "Proofreaders Online"}
+
+ }
+ value={isWorker ? totalActual : activeProofreadersCount}
+ suffix={isWorker ? undefined : `/ ${allQuotaData.length}`}
+ prefix={
+ isWorker ? (
+
+ ) : (
+
+ )
+ }
+ valueStyle={{
+ color: "#fa8c16",
+ fontSize: 20,
+ fontWeight: "bold",
+ }}
+ />
+
+ {isWorker ? "Points this week" : "Team-wide active status"}
+
+
+
+
+
+ {/* ── Timeline ── */}
+
6-Month Project Timeline}
+ style={{ marginBottom: 20, borderRadius: 8 }}
+ bodyStyle={{ paddingTop: 8 }}
+ >
+
+
+
+ {/* ── Main content: Table + Right Sidebar ── */}
+
+ {/* Datasets Table */}
+
+ Datasets}
+ extra={
+
+ {filterBtns.map((b) => (
+ setFilter(b.key)}
+ >
+ {b.label}
+
+ ))}
+
+ }
+ style={{ marginBottom: 20, borderRadius: 8 }}
+ >
+
+
+
+
+ {/* Right Sidebar */}
+
+ {/* This Week */}
+
+
+ Weekly Progress
+
+ }
+ style={{ marginBottom: 12, borderRadius: 8 }}
+ >
+
+ Total items annotated vs. target
+
+
+
+ Completed
+
+ {totalActual.toLocaleString()}
+
+
+
+
+
+ Target: {totalTarget.toLocaleString()}
+
+
+ {overallPct}%
+
+
+
+
+
+ {/* At Risk */}
+
+
+ At Risk
+
+ }
+ style={{ marginBottom: 12, borderRadius: 8 }}
+ >
+ {atRisk.map((item, idx) => (
+
+ {idx > 0 &&
}
+
+ {item.icon === "clock" ? (
+
+ ) : item.icon === "blocked" ? (
+
+ ) : (
+
+ )}
+
+
+ {item.label}
+
+
+
+ {item.reason}
+
+
+
+
+ ))}
+
+
+ {/* Upcoming Milestones */}
+
+
+ Upcoming Milestones
+
+ }
+ style={{ borderRadius: 8 }}
+ >
+ {upcomingMilestones.map((ms, idx) => (
+
+ {idx > 0 &&
}
+
+ {ms.label}
+
+ {ms.date}
+
+
+
+ ))}
+
+
+
+
+ {/* ── Cumulative Progress Chart ── */}
+
Cumulative Items Proofread over 6 Months}
+ style={{ marginBottom: 8, borderRadius: 8 }}
+ bodyStyle={{ paddingTop: 8 }}
+ >
+
+
+
+ );
+}
+
+export default AnnotationDashboard;
diff --git a/client/src/views/project-manager/ModelQualityDashboard.js b/client/src/views/project-manager/ModelQualityDashboard.js
new file mode 100644
index 00000000..048adf31
--- /dev/null
+++ b/client/src/views/project-manager/ModelQualityDashboard.js
@@ -0,0 +1,481 @@
+import React, { useState } from "react";
+import {
+ Card,
+ Row,
+ Col,
+ Statistic,
+ Progress,
+ Table,
+ Tag,
+ Button,
+ Typography,
+ Space,
+ Select,
+ Divider,
+} from "antd";
+import {
+ LineChartOutlined,
+ CheckCircleOutlined,
+ WarningOutlined,
+ HistoryOutlined,
+ ThunderboltOutlined,
+ SlidersOutlined,
+} from "@ant-design/icons";
+
+const { Title, Text } = Typography;
+const { Option } = Select;
+
+// ─── Seed Data ───────────────────────────────────────────────────────────────
+
+const QUALITY_METRICS = {
+ accuracy: 94.2,
+ precision: 92.5,
+ recall: 91.8,
+ f1: 92.1,
+ agreement: 88.5,
+ disagreement: 4.2,
+ uncertainty: 2.1,
+};
+
+const CONFUSION_MATRIX = [
+ { key: "1", actual: "Synapse", predSynapse: 1420, predNonSynapse: 85 },
+ { key: "2", actual: "Non-Synapse", predSynapse: 112, predNonSynapse: 3200 },
+];
+
+const BATCH_QUALITY = [
+ {
+ key: "1",
+ batch: "Batch #104",
+ dataset: "Hippocampus_CA3",
+ agreement: 91.2,
+ flagged: 12,
+ revisionNeeded: 5,
+ status: "verified",
+ },
+ {
+ key: "2",
+ batch: "Batch #105",
+ dataset: "Motor_Cortex_M1",
+ agreement: 84.5,
+ flagged: 42,
+ revisionNeeded: 18,
+ status: "needs_review",
+ },
+ {
+ key: "3",
+ batch: "Batch #106",
+ dataset: "Cerebellum_PC",
+ agreement: 89.1,
+ flagged: 8,
+ revisionNeeded: 2,
+ status: "verified",
+ },
+ {
+ key: "4",
+ batch: "Batch #107",
+ dataset: "Retina_GCL",
+ agreement: 72.0,
+ flagged: 5,
+ revisionNeeded: 22,
+ status: "blocked",
+ },
+];
+
+const TREND_DATA = [82, 84, 85, 83, 86, 88, 87, 89, 91, 93, 94.2];
+
+// ─── Sub-components ──────────────────────────────────────────────────────────
+
+function QualityTrendChart() {
+ const W = 800;
+ const H = 200;
+ const PAD = 40;
+ const chartW = W - PAD * 2;
+ const chartH = H - PAD;
+
+ const maxVal = 100;
+ const minVal = 80; // Zoomed in to show subtle improvements
+ const range = maxVal - minVal;
+
+ const toX = (i) => PAD + (i / (TREND_DATA.length - 1)) * chartW;
+ const toY = (v) => PAD / 2 + chartH - ((v - minVal) / range) * chartH;
+
+ const pathD = TREND_DATA.map(
+ (v, i) => `${i === 0 ? "M" : "L"} ${toX(i)},${toY(v)}`,
+ ).join(" ");
+
+ return (
+
+ {/* Grid lines */}
+ {[80, 85, 90, 95, 100].map((v) => (
+
+
+
+ {v}%
+
+
+ ))}
+
+ {/* The Line */}
+
+
+ {/* Area fill */}
+
+
+ {/* Points */}
+ {TREND_DATA.map((v, i) => (
+
+ ))}
+
+ {/* Labels */}
+ {TREND_DATA.map(
+ (v, i) =>
+ i % 2 === 0 && (
+
+ v{i + 1}
+
+ ),
+ )}
+
+ );
+}
+
+// ─── Column definitions ───────────────────────────────────────────────────────
+
+const BATCH_COLUMNS = [
+ {
+ title: "Batch",
+ dataIndex: "batch",
+ key: "batch",
+ render: (text) => {text} ,
+ },
+ {
+ title: "Dataset",
+ dataIndex: "dataset",
+ key: "dataset",
+ render: (text) => {text} ,
+ },
+ {
+ title: "IAA Score",
+ dataIndex: "agreement",
+ key: "agreement",
+ render: (val) => (
+
+ 85 ? "#52c41a" : "#faad14"}
+ />
+ {val}%
+
+ ),
+ },
+ {
+ title: "Flagged",
+ dataIndex: "flagged",
+ key: "flagged",
+ render: (val) => 20 ? "red" : "blue"}>{val} items ,
+ },
+ {
+ title: "Revision Rate",
+ dataIndex: "revisionNeeded",
+ key: "revisionNeeded",
+ render: (val) => (
+ 10 ? "danger" : "secondary"}>{val}%
+ ),
+ },
+ {
+ title: "Status",
+ dataIndex: "status",
+ key: "status",
+ render: (status) => (
+
+ {status.toUpperCase().replace("_", " ")}
+
+ ),
+ },
+];
+
+// ─── Main Component ───────────────────────────────────────────────────────────
+
+function ModelQualityDashboard() {
+ const [modelVersion, setModelVersion] = useState("v3.2");
+
+ return (
+
+ {/* ── Header ── */}
+
+
+
+ Model Performance & Data Quality
+
+
+ Monitor model accuracy and annotation consistency
+
+
+
+
+ Model Version:
+
+ v3.2 (Latest)
+ v3.1
+ v2.8
+
+ }>
+ Trigger Retrain
+
+
+
+
+
+ {/* ── Main Metrics Gauges ── */}
+
+
+
+ (
+
+ )}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ />
+
+
+ }
+ />
+
+
+ }
+ />
+
+
+
+
+
+
+
+ {/* Quality Trend */}
+
+
+
+ Model Performance Trend (Last 12 Iterations)
+
+ }
+ extra={
+
+ View Detailed Logs
+
+ }
+ style={{ marginBottom: 20 }}
+ >
+
+
+
+
+ {/* Confusion Matrix */}
+
+ Confusion Matrix (v3.2)}
+ style={{ marginBottom: 20 }}
+ >
+
+
+
+
+ Pred Syn
+
+
+ Pred Non
+
+
+
+ Act Syn
+
+
+
+
+
+ Act Non
+
+
+
+
+
+
+ *Based on evaluation dataset: 4,817 samples
+
+
+
+
+
+
+
+ {/* Batch Data Quality Table */}
+
+
+ Data Quality Log (Per Batch)
+
+ }
+ >
+
+
+
+ );
+}
+
+export default ModelQualityDashboard;
diff --git a/client/src/views/project-manager/ProjectManager.js b/client/src/views/project-manager/ProjectManager.js
new file mode 100644
index 00000000..09a0b3a3
--- /dev/null
+++ b/client/src/views/project-manager/ProjectManager.js
@@ -0,0 +1,205 @@
+import React, { useState } from "react";
+import {
+ Layout,
+ Menu,
+ Typography,
+ ConfigProvider,
+ Space,
+ Button,
+ Tag,
+} from "antd";
+import {
+ DashboardOutlined,
+ TeamOutlined,
+ ScheduleOutlined,
+ LineChartOutlined,
+ BugOutlined,
+ SettingOutlined,
+ DatabaseOutlined,
+ UserOutlined,
+ CrownOutlined,
+} from "@ant-design/icons";
+
+import AnnotationDashboard from "./AnnotationDashboard";
+import ProofreaderProgress from "./ProofreaderProgress";
+import QuotaManagement from "./QuotaManagement";
+import ModelQualityDashboard from "./ModelQualityDashboard";
+import VolumeTracker from "./VolumeTracker";
+import ProjectManagerLogin from "./ProjectManagerLogin";
+import {
+ ProjectManagerProvider,
+ useProjectManager,
+} from "../../contexts/ProjectManagerContext";
+
+const { Sider, Content } = Layout;
+const { Title, Text } = Typography;
+
+const ProjectManagerInner = () => {
+ const { isAuthenticated, user, logout, isAdmin, isWorker, loading } =
+ useProjectManager();
+
+ const [selectedKey, setSelectedKey] = useState("dashboard");
+
+ // ── Login Gate ────────────────────────────────────────────────────────────
+ if (!isAuthenticated) {
+ return ;
+ }
+
+ // ── Menu items ────────────────────────────────────────────────────────────
+ const menuItems = [
+ { key: "dashboard", icon: , label: "Dashboard" },
+ {
+ key: "progress",
+ icon: ,
+ label: isAdmin ? "Team Progress" : "My Progress",
+ },
+ {
+ key: "quotas",
+ icon: ,
+ label: isAdmin ? "Weekly Quotas" : "My Quota",
+ },
+ {
+ key: "volumes",
+ icon: ,
+ label: isWorker ? "My Volumes" : "Volume Tracker",
+ },
+ // Admin-only items
+ ...(isAdmin
+ ? [
+ {
+ key: "model-quality",
+ icon: ,
+ label: "Model Quality",
+ },
+ ]
+ : []),
+ { type: "divider" },
+ {
+ key: "issues",
+ icon: ,
+ label: "Issues & Flags",
+ disabled: true,
+ },
+ {
+ key: "settings",
+ icon: ,
+ label: "Settings",
+ disabled: true,
+ },
+ ];
+
+ const renderSubView = () => {
+ switch (selectedKey) {
+ case "dashboard":
+ return ;
+ case "progress":
+ return ;
+ case "quotas":
+ return ;
+ case "volumes":
+ return ;
+ case "model-quality":
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ return (
+
+
+ {/* ── Brand header ── */}
+
+
+ Project Manager
+
+
+ v2.0 · seg.bio
+
+
+
+ {/* ── User Profile & Role Tag ── */}
+
+
+
+
+ {user?.name}
+
+
+
+ {isAdmin ? "ADMIN" : "WORKER"}
+
+
+ Sign Out
+
+
+
+
+
+
+ {/* ── Navigation ── */}
+ setSelectedKey(e.key)}
+ items={menuItems}
+ style={{ borderRight: 0, flex: 1 }}
+ />
+
+
+
+ {renderSubView()}
+
+
+ );
+};
+
+const ProjectManager = () => (
+
+
+
+
+
+);
+
+export default ProjectManager;
diff --git a/client/src/views/project-manager/ProjectManagerLogin.js b/client/src/views/project-manager/ProjectManagerLogin.js
new file mode 100644
index 00000000..aa9229da
--- /dev/null
+++ b/client/src/views/project-manager/ProjectManagerLogin.js
@@ -0,0 +1,134 @@
+import React, { useState } from "react";
+import { Card, Form, Input, Button, Typography, Space, Divider } from "antd";
+import { UserOutlined, LockOutlined, RocketOutlined } from "@ant-design/icons";
+import { useProjectManager } from "../../contexts/ProjectManagerContext";
+
+const { Title, Text } = Typography;
+
+export default function ProjectManagerLogin() {
+ const { login } = useProjectManager();
+ const [loading, setLoading] = useState(false);
+
+ const onFinish = async (values) => {
+ setLoading(true);
+ const success = await login(values.username, values.password);
+ setLoading(false);
+ };
+
+ return (
+
+
+
+
+
+
+
+ Project Manager
+
+
Sign in to manage EM connectomics tasks
+
+
+
+ }
+ placeholder="Username"
+ style={{ borderRadius: 8 }}
+ />
+
+
+
+ }
+ placeholder="Password"
+ style={{ borderRadius: 8 }}
+ />
+
+
+
+
+ Login
+
+
+
+
+
+
+ Credentials Template
+
+
+
+
+
+
+ Admin: admin / admin123
+
+
+ Worker: alex / alex123
+
+
+
+
+
+ );
+}
diff --git a/client/src/views/project-manager/ProofreaderProgress.js b/client/src/views/project-manager/ProofreaderProgress.js
new file mode 100644
index 00000000..8d356999
--- /dev/null
+++ b/client/src/views/project-manager/ProofreaderProgress.js
@@ -0,0 +1,484 @@
+import React, { useMemo } from "react";
+import {
+ Card,
+ Table,
+ Avatar,
+ Progress,
+ Typography,
+ Row,
+ Col,
+ Tag,
+ Space,
+ Badge,
+ Tooltip,
+ Divider,
+ Button,
+ Popconfirm,
+ Spin,
+} from "antd";
+import {
+ UserOutlined,
+ CheckCircleOutlined,
+ ClockCircleOutlined,
+ ThunderboltOutlined,
+ RiseOutlined,
+ ReloadOutlined,
+} from "@ant-design/icons";
+import { useProjectManager } from "../../contexts/ProjectManagerContext";
+
+const { Title, Text } = Typography;
+
+// ─── Read-only display pill (for metrics that must not look editable) ─────────
+const ReadOnlyPill = ({ children, color = "#595959", bg = "#fafafa" }) => (
+
+ {children}
+
+);
+
+// ─── Sub-components ──────────────────────────────────────────────────────────
+
+function WeeklyThroughputChart({ throughput }) {
+ const W = 800;
+ const H = 150;
+ const PAD = 40;
+ const chartW = W - PAD * 2;
+ const chartH = H - PAD;
+
+ if (!throughput || throughput.length === 0) return null;
+ const maxVal = Math.max(...throughput.map((d) => d.count)) * 1.1;
+ const barWidth = (chartW / throughput.length) * 0.6;
+ const gap = (chartW / throughput.length) * 0.4;
+
+ return (
+
+ {[0, 0.5, 1].map((f) => (
+
+ ))}
+ {throughput.map((d, i) => {
+ const x = PAD + i * (barWidth + gap) + gap / 2;
+ const barH = (d.count / maxVal) * chartH;
+ const y = PAD / 2 + chartH - barH;
+ return (
+
+
+
+
+
+ {d.day}
+
+
+ {d.count.toLocaleString()}
+
+
+ );
+ })}
+
+ );
+}
+
+// ─── Main Component ───────────────────────────────────────────────────────────
+
+function ProofreaderProgress() {
+ const {
+ quotaData,
+ proofreaderData,
+ throughputData,
+ saving,
+ resetData,
+ isWorker,
+ } = useProjectManager();
+
+ // Helper to get calculated stats for a proofreader from quotaData
+ const getStats = (key) => {
+ const quota = quotaData.find((q) => q.key === key);
+ if (!quota) return { target: 1500, actual: 0 };
+ const target =
+ quota.mon +
+ quota.tue +
+ quota.wed +
+ quota.thu +
+ quota.fri +
+ quota.sat +
+ quota.sun;
+ const actual =
+ (quota.actualMon || 0) +
+ (quota.actualTue || 0) +
+ (quota.actualWed || 0) +
+ (quota.actualThu || 0) +
+ (quota.actualFri || 0) +
+ (quota.actualSat || 0) +
+ (quota.actualSun || 0);
+ return { target, actual };
+ };
+
+ const topPerformers = useMemo(() => {
+ const withPoints = proofreaderData.map((p) => ({
+ ...p,
+ weeklyPoints: getStats(p.key).actual,
+ }));
+ return [...withPoints]
+ .sort((a, b) => b.weeklyPoints - a.weeklyPoints)
+ .slice(0, 3);
+ }, [proofreaderData, quotaData]);
+
+ const teamAccuracy =
+ proofreaderData.length > 0
+ ? (
+ proofreaderData.reduce((sum, p) => sum + p.accuracy, 0) /
+ proofreaderData.length
+ ).toFixed(1)
+ : 0;
+
+ const COLUMNS = [
+ {
+ title: "Proofreader",
+ dataIndex: "name",
+ key: "name",
+ render: (name, record) => (
+
+ }
+ size="small"
+ />
+
+
+ {name}
+
+
+ {record.role}
+
+
+
+ ),
+ },
+ {
+ title: "Total Points",
+ dataIndex: "totalPoints",
+ key: "totalPoints",
+ align: "right",
+ render: (pts) => {pts.toLocaleString()} ,
+ },
+ {
+ title: "This Week",
+ key: "weeklyProgress",
+ align: "right",
+ render: (_, record) => {
+ const { target, actual } = getStats(record.key);
+ const pct = target > 0 ? Math.round((actual / target) * 100) : 0;
+ return (
+
+
+ {actual.toLocaleString()}{" "}
+
+ / {target.toLocaleString()}
+
+
+ = target ? "#52c41a" : "#1890ff"}
+ />
+
+ );
+ },
+ },
+ {
+ title: "Accuracy",
+ dataIndex: "accuracy",
+ key: "accuracy",
+ align: "center",
+ render: (pct) => (
+ = 95 ? "#52c41a" : pct >= 90 ? "#faad14" : "#f5222d"}
+ bg={pct >= 95 ? "#f6ffed" : pct >= 90 ? "#fffbe6" : "#fff2f0"}
+ >
+ {pct}%
+
+ ),
+ },
+ {
+ title: "Last Active",
+ dataIndex: "lastActive",
+ key: "lastActive",
+ render: (time, record) => (
+
+
+ {time}
+
+ ),
+ },
+ ];
+
+ return (
+
+ {/* ── Header ── */}
+
+
+
+ {isWorker ? "My Performance" : "Proofreader Performance"}
+
+
+ {isWorker
+ ? "Your personal throughput and accuracy · auto-saved to server"
+ : "Real-time throughput and accuracy tracking · auto-saved to server"}
+ {saving && (
+ <>
+ {" "}
+ · saving…
+ >
+ )}
+
+
+
+ } danger>
+ Reset to Defaults
+
+
+
+
+ {/* ── Top Row: Individual Cards ── */}
+
+ {proofreaderData.map((pr) => {
+ const { target, actual } = getStats(pr.key);
+ const pct = target > 0 ? Math.round((actual / target) * 100) : 0;
+ return (
+
+
+
+
+
+
+ {pr.name}
+
+
+ {pr.role}
+
+
+
+
+
+ Weekly Goal
+
+
+ {pct}%
+
+
+
= target ? "#52c41a" : "#1890ff"}
+ />
+
+ {actual.toLocaleString()} / {target.toLocaleString()} pts
+
+
+
+
+
+ );
+ })}
+
+
+ {/* ── Main Section: Table + Insights ── */}
+
+
+
+ Active Session Metrics
+
+ }
+ >
+
+
+
+
+
+
+ Top Performers
+
+ }
+ style={{ height: "100%" }}
+ >
+
+ {topPerformers.map((pr, idx) => (
+
+
+ {idx + 1}
+
+
+
+ {pr.name}
+
+
+
+ {pr.weeklyPoints.toLocaleString()} pts this week
+
+
+
+ +{Math.round(pr.accuracy)}% acc
+
+
+ ))}
+
+
+
+
+
+ Team
+ Accuracy
+
+
+
+
+
+
+
+
+ {/* ── Bottom Section: Team Throughput ── */}
+
+ Team Throughput (Last 7 Days)
+
+ }
+ >
+
+
+
+ );
+}
+
+export default ProofreaderProgress;
diff --git a/client/src/views/project-manager/QuotaManagement.js b/client/src/views/project-manager/QuotaManagement.js
new file mode 100644
index 00000000..e3d227df
--- /dev/null
+++ b/client/src/views/project-manager/QuotaManagement.js
@@ -0,0 +1,474 @@
+import React, { useMemo } from "react";
+import {
+ Card,
+ Table,
+ DatePicker,
+ Button,
+ Input,
+ InputNumber,
+ Row,
+ Col,
+ Typography,
+ Space,
+ Tag,
+ Divider,
+ Progress,
+ Tooltip,
+ message,
+ Popconfirm,
+ Spin,
+} from "antd";
+import {
+ ScheduleOutlined,
+ SendOutlined,
+ ThunderboltOutlined,
+ LeftOutlined,
+ RightOutlined,
+ EditOutlined,
+ ReloadOutlined,
+ SaveOutlined,
+} from "@ant-design/icons";
+import dayjs from "dayjs";
+import weekday from "dayjs/plugin/weekday";
+import localeData from "dayjs/plugin/localeData";
+import { useState } from "react";
+import { useProjectManager } from "../../contexts/ProjectManagerContext";
+
+dayjs.extend(weekday);
+dayjs.extend(localeData);
+
+const { Title, Text } = Typography;
+const { TextArea } = Input;
+
+// ─── Sparkline trend data for the last 8 weeks (attainment percentages) ──────
+
+const PERFORMANCE_TRENDS = {
+ 1: [98, 102, 100, 95, 105, 101, 100, 99],
+ 2: [90, 92, 88, 85, 90, 94, 91, 89],
+ 3: [70, 75, 65, 80, 72, 60, 55, 62],
+ 4: [100, 105, 110, 100, 102, 108, 104, 106],
+};
+
+// ─── Helpers ─────────────────────────────────────────────────────────────────
+
+function getAttainmentColor(percent) {
+ if (percent >= 100) return "#52c41a";
+ if (percent >= 75) return "#faad14";
+ return "#f5222d";
+}
+
+// ─── Sub-components ──────────────────────────────────────────────────────────
+
+function Sparkline({ data }) {
+ const W = 100;
+ const H = 20;
+ const gap = 2;
+ const barW = (W - (data.length - 1) * gap) / data.length;
+ return (
+
+ {data.map((v, i) => {
+ const h = (v / 120) * H;
+ return (
+
+ );
+ })}
+
+ );
+}
+
+// ─── Main Component ───────────────────────────────────────────────────────────
+
+function QuotaManagement() {
+ const {
+ quotaData,
+ msgPreview,
+ setQuotaData,
+ setMsgPreview,
+ saving,
+ resetData,
+ isWorker,
+ activeWorker,
+ } = useProjectManager();
+
+ const [selectedWeek, setSelectedWeek] = useState(dayjs().startOf("week"));
+
+ const handleUpdateQuota = (key, day, val) => {
+ const updated = quotaData.map((item) =>
+ item.key === key ? { ...item, [day]: val } : item,
+ );
+ setQuotaData(updated);
+ };
+
+ const handleAutoAllocate = () => {
+ message.loading("Calculating optimal distribution...", 1.5).then(() => {
+ message.success(
+ "Quotas auto-allocated based on user capacity and dataset priority.",
+ );
+ });
+ };
+
+ const handleSendQuotas = () => {
+ message.success("Weekly quotas dispatched to proofreaders.");
+ };
+
+ // Day columns
+ const dayCols = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"].map(
+ (day) => ({
+ title: day.charAt(0).toUpperCase() + day.slice(1),
+ dataIndex: day,
+ key: day,
+ width: 100,
+ render: (val, record) => {
+ const actual =
+ record[`actual${day.charAt(0).toUpperCase() + day.slice(1)}`];
+ const attainment = val > 0 ? Math.round((actual / val) * 100) : 100;
+ return (
+
+
+
+ Target
+
+ handleUpdateQuota(record.key, day, v)}
+ style={{
+ width: "100%",
+ fontWeight: "bold",
+ background: "#e6f7ff",
+ borderRadius: 4,
+ border: "1px solid #91d5ff",
+ }}
+ controls={false}
+ />
+
+
+
+ Actual
+
+
+
+ {actual}
+
+
+
+
+ );
+ },
+ }),
+ );
+
+ const finalColumns = [
+ {
+ title: "Proofreader",
+ dataIndex: "name",
+ key: "name",
+ fixed: "left",
+ width: 160,
+ render: (text, record) => (
+
+
{text}
+
+ {record.datasets.join(", ")}
+
+
+ ),
+ },
+ ...dayCols,
+ {
+ title: "Weekly Summary",
+ key: "total",
+ width: 140,
+ fixed: "right",
+ render: (_, record) => {
+ const targetTotal =
+ record.mon +
+ record.tue +
+ record.wed +
+ record.thu +
+ record.fri +
+ record.sat +
+ record.sun;
+ const actualTotal =
+ record.actualMon +
+ record.actualTue +
+ record.actualWed +
+ record.actualThu +
+ record.actualFri +
+ record.actualSat +
+ record.actualSun;
+ const attainment = Math.round((actualTotal / targetTotal) * 100);
+ return (
+
+
+ {actualTotal}
+ / {targetTotal}
+
+
+
+ );
+ },
+ },
+ {
+ title: "8-Wk Trend",
+ key: "trend",
+ width: 120,
+ fixed: "right",
+ render: (_, record) => (
+
+ ),
+ },
+ ];
+
+ // Dynamic Calculations for Summary
+ const totals = useMemo(() => {
+ return quotaData.reduce(
+ (acc, row) => {
+ acc.target +=
+ row.mon + row.tue + row.wed + row.thu + row.fri + row.sat + row.sun;
+ acc.actual +=
+ (row.actualMon || 0) +
+ (row.actualTue || 0) +
+ (row.actualWed || 0) +
+ (row.actualThu || 0) +
+ (row.actualFri || 0) +
+ (row.actualSat || 0) +
+ (row.actualSun || 0);
+ return acc;
+ },
+ { target: 0, actual: 0 },
+ );
+ }, [quotaData]);
+
+ const globalUtilization =
+ totals.target > 0 ? Math.round((totals.actual / totals.target) * 100) : 0;
+ const remainingBuffer = Math.max(0, totals.target - totals.actual);
+
+ return (
+
+ {/* ── Header ── */}
+
+
+
+ {isWorker ? "My Weekly Quota" : "Weekly Quota Management"}
+
+
+ Plan targets and track capacity utilization · Real-time Sync
+ {saving && (
+ <>
+ {" "}
+ · saving…
+ >
+ )}
+
+
+
+
+ }
+ onClick={() => setSelectedWeek(selectedWeek.subtract(1, "week"))}
+ />
+
+ }
+ onClick={() => setSelectedWeek(selectedWeek.add(1, "week"))}
+ />
+ }
+ onClick={handleAutoAllocate}
+ style={{ borderRadius: 6 }}
+ >
+ Auto-Allocate
+
+
+ }
+ danger
+ style={{ borderRadius: 6 }}
+ >
+ Reset to Defaults
+
+
+
+
+
+
+ {/* ── Quota Table ── */}
+
+
+ Targets vs Actuals
+
+ Week 10 (Current)
+
+
+ }
+ extra={
+
+ Input numbers in blue cells to update targets instantly.
+
+ }
+ style={{ marginBottom: 20, borderRadius: 8, overflow: "hidden" }}
+ >
+
+
+
+
+ {/* Capacity Summary */}
+
+ Capacity Summary}
+ style={{ borderRadius: 8 }}
+ >
+
+
+
+ Global Utilization
+
+
+
+
+
+ Total Weekly Target
+ {totals.target.toLocaleString()} points
+
+
+ Actual points so far
+ {totals.actual.toLocaleString()} points
+
+
+ Remaining to target
+ 0 ? "#faad14" : "#52c41a" }}
+ >
+ {remainingBuffer.toLocaleString()}
+
+
+
+
+
+
+ {/* Communication Preview */}
+
+
+ Team Communication Preview
+
+ }
+ extra={
+ }
+ onClick={handleSendQuotas}
+ >
+ Send Quotas
+
+ }
+ >
+
+
+
+
+ );
+}
+
+export default QuotaManagement;
diff --git a/client/src/views/project-manager/VolumeTracker.js b/client/src/views/project-manager/VolumeTracker.js
new file mode 100644
index 00000000..1ecf8e93
--- /dev/null
+++ b/client/src/views/project-manager/VolumeTracker.js
@@ -0,0 +1,381 @@
+import React, { useState, useEffect, useCallback, useMemo } from "react";
+import {
+ Card,
+ Table,
+ Tag,
+ Select,
+ Button,
+ Progress,
+ Typography,
+ Row,
+ Col,
+ Statistic,
+ Tooltip,
+ Space,
+ Badge,
+ Popconfirm,
+} from "antd";
+import {
+ DatabaseOutlined,
+ CheckCircleOutlined,
+ SyncOutlined,
+ ClockCircleOutlined,
+ FilterOutlined,
+ ReloadOutlined,
+} from "@ant-design/icons";
+import { useProjectManager } from "../../contexts/ProjectManagerContext";
+
+const { Title, Text } = Typography;
+
+// ── Status helpers ────────────────────────────────────────────────────────────
+const STATUS_CONFIG = {
+ todo: { label: "To Do", color: "default", badgeColor: "#d9d9d9" },
+ in_progress: {
+ label: "In Progress",
+ color: "processing",
+ badgeColor: "#1890ff",
+ },
+ done: { label: "Done", color: "success", badgeColor: "#52c41a" },
+};
+
+const STATUS_OPTIONS = Object.entries(STATUS_CONFIG).map(
+ ([value, { label }]) => ({ value, label }),
+);
+const ALL_STATUSES = [{ value: "", label: "All statuses" }, ...STATUS_OPTIONS];
+
+// ── Workers list (mirrors backend) ────────────────────────────────────────────
+const WORKER_OPTIONS_ALL = [
+ { value: "", label: "All workers" },
+ { value: "alex", label: "Alex Rivera" },
+ { value: "jordan", label: "Jordan Smith" },
+ { value: "taylor", label: "Sam Taylor" },
+ { value: "morgan", label: "Morgan Lee" },
+];
+
+// ── Main Component ─────────────────────────────────────────────────────────────
+function VolumeTracker() {
+ const {
+ isAdmin,
+ isWorker,
+ activeWorker,
+ globalProgress,
+ updateVolumeStatus,
+ } = useProjectManager();
+
+ const [volumes, setVolumes] = useState([]);
+ const [total, setTotal] = useState(0);
+ const [page, setPage] = useState(1);
+ const [pageSize] = useState(50);
+ const [loadingVols, setLoadingVols] = useState(false);
+ const [statusFilter, setStatusFilter] = useState("");
+ const [workerFilter, setWorkerFilter] = useState(
+ isWorker ? activeWorker : "",
+ );
+ const [updatingId, setUpdatingId] = useState(null);
+
+ // When role/activeWorker changes, reset worker filter
+ useEffect(() => {
+ setWorkerFilter(isWorker ? activeWorker : "");
+ setPage(1);
+ }, [isWorker, activeWorker]);
+
+ // ── Fetch volumes ─────────────────────────────────────────────────────────
+ const fetchVolumes = useCallback(async () => {
+ setLoadingVols(true);
+ try {
+ const params = { page, page_size: pageSize };
+ if (workerFilter) params.assignee = workerFilter;
+ if (statusFilter) params.status = statusFilter;
+ const res = await fetch(
+ `http://localhost:4242/api/pm/volumes?${new URLSearchParams(params)}`,
+ { credentials: "include" },
+ );
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ const data = await res.json();
+ setVolumes(data.items ?? []);
+ setTotal(data.total ?? 0);
+ } catch (err) {
+ console.error("[VolumeTracker] fetch failed:", err);
+ } finally {
+ setLoadingVols(false);
+ }
+ }, [page, pageSize, workerFilter, statusFilter]);
+
+ useEffect(() => {
+ fetchVolumes();
+ }, [fetchVolumes]);
+
+ // ── Status update handler ─────────────────────────────────────────────────
+ const handleStatusChange = async (volumeId, newStatus) => {
+ setUpdatingId(volumeId);
+ try {
+ await updateVolumeStatus(volumeId, newStatus);
+ setVolumes((prev) =>
+ prev.map((v) => (v.id === volumeId ? { ...v, status: newStatus } : v)),
+ );
+ } finally {
+ setUpdatingId(null);
+ }
+ };
+
+ // ── Stats from globalProgress for this view ───────────────────────────────
+ const workerProgress = useMemo(() => {
+ if (isWorker && globalProgress?.by_worker) {
+ return globalProgress.by_worker[activeWorker] ?? null;
+ }
+ return null;
+ }, [isAdmin, isWorker, activeWorker, globalProgress]);
+
+ const displayPct =
+ isWorker && workerProgress
+ ? workerProgress.pct
+ : (globalProgress?.pct ?? 0);
+ const displayDone =
+ isWorker && workerProgress
+ ? workerProgress.done
+ : (globalProgress?.done ?? 0);
+ const displayTotal =
+ isWorker && workerProgress
+ ? workerProgress.total
+ : (globalProgress?.total ?? 1000);
+ const displayIP =
+ isWorker && workerProgress
+ ? workerProgress.in_progress
+ : (globalProgress?.in_progress ?? 0);
+
+ // ── Columns ───────────────────────────────────────────────────────────────
+ const columns = [
+ {
+ title: "Volume ID",
+ dataIndex: "id",
+ key: "id",
+ width: 180,
+ render: (id) => (
+
+ {id}
+
+ ),
+ },
+ // Admin-only: assignee column
+ ...(isAdmin
+ ? [
+ {
+ title: "Assignee",
+ dataIndex: "assignee",
+ key: "assignee",
+ width: 130,
+ render: (key) => {
+ const opt = WORKER_OPTIONS_ALL.find((w) => w.value === key);
+ return (
+
+ {opt?.label ?? key}
+
+ );
+ },
+ },
+ ]
+ : []),
+ {
+ title: "Status",
+ dataIndex: "status",
+ key: "status",
+ width: 160,
+ render: (status, record) => {
+ const cfg = STATUS_CONFIG[status] ?? STATUS_CONFIG.todo;
+ return (
+ handleStatusChange(record.id, val)}
+ style={{ width: 140 }}
+ options={STATUS_OPTIONS.map(({ value, label }) => ({
+ value,
+ label: (
+
+
+ {label}
+
+ ),
+ }))}
+ />
+ );
+ },
+ },
+ ];
+
+ return (
+
+ {/* ── Header ── */}
+
+
+
+ {isWorker ? "My Volumes" : "Volume Tracker"}
+
+
+ {isWorker
+ ? `Showing your 250 assigned volumes · click status to update`
+ : `All 1,000 volumes · click status to update`}
+
+
+
+ {/* ── KPI Strip ── */}
+
+
+
+
+ Done
+
+ }
+ value={displayDone}
+ suffix={`/ ${displayTotal}`}
+ prefix={ }
+ valueStyle={{
+ color: "#52c41a",
+ fontSize: 18,
+ fontWeight: "bold",
+ }}
+ />
+
+
+
+
+
+ In Progress
+
+ }
+ value={displayIP}
+ prefix={
+ 0}
+ />
+ }
+ valueStyle={{
+ color: "#1890ff",
+ fontSize: 18,
+ fontWeight: "bold",
+ }}
+ />
+
+
+
+
+
+ To Do
+
+ }
+ value={displayTotal - displayDone - displayIP}
+ prefix={ }
+ valueStyle={{
+ color: "#fa8c16",
+ fontSize: 18,
+ fontWeight: "bold",
+ }}
+ />
+
+
+
+
+
+ {isWorker ? "My Progress" : "Global Progress"}
+
+ (
+ {p}%
+ )}
+ />
+
+
+
+
+ {/* ── Table with Filters ── */}
+
+
+ Volume List
+ {total.toLocaleString()} matching
+
+ }
+ extra={
+
+ {/* Assignee filter — admin only */}
+ {isAdmin && (
+ {
+ setWorkerFilter(v);
+ setPage(1);
+ }}
+ options={WORKER_OPTIONS_ALL}
+ />
+ )}
+ {
+ setStatusFilter(v);
+ setPage(1);
+ }}
+ options={ALL_STATUSES}
+ />
+
+ }
+ onClick={fetchVolumes}
+ loading={loadingVols}
+ />
+
+
+ }
+ >
+ `${s}–${e} of ${t}`,
+ onChange: (p) => setPage(p),
+ }}
+ scroll={{ x: isAdmin ? 480 : 360, y: "calc(100vh - 440px)" }}
+ />
+
+
+ );
+}
+
+export default VolumeTracker;
diff --git a/docs/research/workflow-evidence-export.md b/docs/research/workflow-evidence-export.md
new file mode 100644
index 00000000..6ca60b8a
--- /dev/null
+++ b/docs/research/workflow-evidence-export.md
@@ -0,0 +1,28 @@
+# Workflow Evidence Export
+
+Use the workflow utilities to generate stable evidence summaries for paper notes and analysis.
+
+## Primary endpoints
+
+- `GET /api/workflows/{workflow_id}/metrics`
+- `POST /api/workflows/{workflow_id}/export-bundle`
+
+## Bundle schema highlights
+
+`export-bundle` returns:
+
+- `schema_version` (`workflow-export-bundle/v1`)
+- `workflow_id`
+- `session_snapshot` (current workflow state)
+- `events` (chronologically ordered)
+- `artifact_paths` (`[{ path, exists }]`)
+
+## Paper-note utility
+
+`server_api.workflows.evidence_export` provides a deterministic summary shape:
+
+- `stage_progression_summary`
+- `agent_proposal_approval_summary`
+- `key_event_timeline_snippet`
+
+Use this when exporting workflow evidence into local research logs for manuscript drafting.
diff --git a/docs/seg.bio_job_management.pdf b/docs/seg.bio_job_management.pdf
new file mode 100644
index 00000000..59f4fb7f
Binary files /dev/null and b/docs/seg.bio_job_management.pdf differ
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 00000000..081f5cd9
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,8 @@
+[pytest]
+testpaths = tests
+pythonpath = .
+norecursedirs =
+ .git
+ .venv
+ client
+ pytorch_connectomics
diff --git a/pytorch_connectomics b/pytorch_connectomics
index 0a0dceb8..20ccfde6 160000
--- a/pytorch_connectomics
+++ b/pytorch_connectomics
@@ -1 +1 @@
-Subproject commit 0a0dceb8bc03e0afbb843aef17181c9f854209da
+Subproject commit 20ccfde6f85868351b00d7b795d4cf89a251d6be
diff --git a/scripts/dev.sh b/scripts/dev.sh
index 455ed869..46823600 100755
--- a/scripts/dev.sh
+++ b/scripts/dev.sh
@@ -4,8 +4,6 @@ set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CLIENT_DIR="${ROOT_DIR}/client"
-DEFAULT_OLLAMA_BASE_URL="http://cscigpu08.bc.edu:4443"
-DEFAULT_OLLAMA_MODEL="llama3.1:8b"
if ! command -v uv >/dev/null 2>&1; then
echo "uv is required. Run scripts/bootstrap.sh first." >&2
@@ -37,14 +35,17 @@ cleanup() {
trap cleanup EXIT INT TERM
+if [[ -z "${OLLAMA_BASE_URL:-}" || -z "${OLLAMA_MODEL:-}" || -z "${OLLAMA_EMBED_MODEL:-}" ]]; then
+ echo "AI assistant LLM environment is incomplete. Export OLLAMA_BASE_URL, OLLAMA_MODEL, and OLLAMA_EMBED_MODEL before using chat."
+ echo "Example: export OLLAMA_BASE_URL=http://:"
+fi
+
echo "Starting Data Server (port 8000)..."
uv run --directory "${ROOT_DIR}" python server_api/scripts/serve_data.py &
DATA_SERVER_PID=$!
echo "Starting API server (port 4242)..."
-OLLAMA_BASE_URL="${OLLAMA_BASE_URL:-${DEFAULT_OLLAMA_BASE_URL}}" \
- OLLAMA_MODEL="${OLLAMA_MODEL:-${DEFAULT_OLLAMA_MODEL}}" \
- PYTHONDONTWRITEBYTECODE=1 uv run --directory "${ROOT_DIR}" python -m server_api.main &
+PYTHONDONTWRITEBYTECODE=1 uv run --directory "${ROOT_DIR}" python -m server_api.main &
API_PID=$!
echo "Starting PyTC server (port 4243)..."
diff --git a/scripts/kill.sh b/scripts/kill.sh
new file mode 100755
index 00000000..dac45082
--- /dev/null
+++ b/scripts/kill.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+for port in 3000 4242 4243 8000; do
+ fuser -k ${port}/tcp
+done
+
+pkill -f "uvicorn"
+pkill -f "serve_data.py"
+pkill -f "electron"
+
+echo "Ports and processes cleared."
diff --git a/scripts/start.sh b/scripts/start.sh
index 2311eae6..2f1be365 100755
--- a/scripts/start.sh
+++ b/scripts/start.sh
@@ -4,9 +4,6 @@ set -euo pipefail
# Prefer Homebrew's Node over nvm to avoid version conflicts
export PATH="/opt/homebrew/bin:$PATH"
-export OLLAMA_BASE_URL="http://cscigpu08.bc.edu:4443"
-export OLLAMA_MODEL="gpt-oss:20b"
-export OLLAMA_EMBED_MODEL="qwen3-embedding:8b"
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CLIENT_DIR="${ROOT_DIR}/client"
@@ -103,6 +100,11 @@ cleanup() {
trap cleanup EXIT INT TERM
+if [[ -z "${OLLAMA_BASE_URL:-}" || -z "${OLLAMA_MODEL:-}" || -z "${OLLAMA_EMBED_MODEL:-}" ]]; then
+ echo "AI assistant LLM environment is incomplete. Export OLLAMA_BASE_URL, OLLAMA_MODEL, and OLLAMA_EMBED_MODEL before using chat."
+ echo "Example: export OLLAMA_BASE_URL=http://:"
+fi
+
start_service \
"Data server" \
8000 \
diff --git a/server_api/chatbot/chatbot.py b/server_api/chatbot/chatbot.py
index 337e42f8..0b443169 100644
--- a/server_api/chatbot/chatbot.py
+++ b/server_api/chatbot/chatbot.py
@@ -7,6 +7,7 @@
# - RAG: Documentation search via FAISS vector store
import os
+from urllib.parse import urlparse
from pathlib import Path
from langchain_ollama import OllamaEmbeddings, ChatOllama
@@ -21,6 +22,62 @@
list_checkpoints,
)
+
+class LLMConfigurationError(RuntimeError):
+ """Raised when the configured LLM endpoint is missing or malformed."""
+
+
+def _format_admin_llm_error(error: Exception) -> str:
+ return (
+ "The AI assistant could not connect to its configured language model. "
+ "Please contact your system administrator with this error: "
+ f"{str(error).strip() or error.__class__.__name__}"
+ )
+
+
+def _validate_ollama_base_url(base_url: str) -> str:
+ parsed = urlparse(base_url)
+ if not parsed.scheme or not parsed.netloc:
+ raise LLMConfigurationError(
+ "OLLAMA_BASE_URL must be a full URL, for example "
+ "http://:."
+ )
+ try:
+ port = parsed.port
+ except ValueError as exc:
+ raise LLMConfigurationError(
+ f"OLLAMA_BASE_URL has an invalid port: {base_url}"
+ ) from exc
+ if port is None:
+ raise LLMConfigurationError(
+ "OLLAMA_BASE_URL must include the LLM service port, for example "
+ "http://:11434."
+ )
+ return base_url.rstrip("/")
+
+
+def get_ollama_config():
+ """Read required Ollama configuration from the launch environment."""
+ base_url = os.getenv("OLLAMA_BASE_URL")
+ model = os.getenv("OLLAMA_MODEL")
+ embed_model = os.getenv("OLLAMA_EMBED_MODEL")
+ missing = [
+ name
+ for name, value in {
+ "OLLAMA_BASE_URL": base_url,
+ "OLLAMA_MODEL": model,
+ "OLLAMA_EMBED_MODEL": embed_model,
+ }.items()
+ if not value
+ ]
+ if missing:
+ raise LLMConfigurationError(
+ "Missing required LLM environment variable(s): "
+ f"{', '.join(missing)}. Export them before starting PyTC Client. "
+ "Example: export OLLAMA_BASE_URL=http://:"
+ )
+ return _validate_ollama_base_url(base_url), model, embed_model
+
TRAINING_AGENT_PROMPT = """You are a **Training Agent** for PyTorch Connectomics.
You help users set up and configure training jobs for biomedical image segmentation.
@@ -119,9 +176,7 @@
def build_chain():
"""Build the multi-agent system with supervisor, training, and inference agents."""
- ollama_base_url = os.getenv("OLLAMA_BASE_URL", "http://cscigpu08.bc.edu:4443")
- ollama_model = os.getenv("OLLAMA_MODEL", "gpt-oss:20b")
- ollama_embed_model = os.getenv("OLLAMA_EMBED_MODEL", "qwen3-embedding:8b")
+ ollama_base_url, ollama_model, ollama_embed_model = get_ollama_config()
llm = ChatOllama(model=ollama_model, base_url=ollama_base_url, temperature=0)
embeddings = OllamaEmbeddings(model=ollama_embed_model, base_url=ollama_base_url)
faiss_path = process_path("server_api/chatbot/faiss_index")
@@ -148,6 +203,7 @@ def build_chain():
# Call counter to prevent infinite search loops (reset before each user message)
_search_call_count = [0]
+ _rag_enabled = [True]
def reset_search_counter():
_search_call_count[0] = 0
@@ -184,12 +240,22 @@ def search_documentation(query: str) -> str:
print("[TOOL] search limit reached (max 2 per question)")
return "Search limit reached. Please answer based on the documentation already retrieved."
- # Primary: FAISS semantic search (chunked embeddings)
- docs = retriever.invoke(query)
- if docs:
- sources = [d.metadata.get("source", "?") for d in docs]
- print(f"[TOOL] RAG → {len(docs)} chunks: {sources}")
- return "\n\n".join([doc.page_content for doc in docs])
+ # Primary: FAISS semantic search (chunked embeddings).
+ # If embeddings/index dimensions do not match (or any retrieval error occurs),
+ # disable RAG for the current process and use keyword fallback.
+ if _rag_enabled[0]:
+ try:
+ docs = retriever.invoke(query)
+ if docs:
+ sources = [d.metadata.get("source", "?") for d in docs]
+ print(f"[TOOL] RAG → {len(docs)} chunks: {sources}")
+ return "\n\n".join([doc.page_content for doc in docs])
+ except Exception as exc:
+ _rag_enabled[0] = False
+ print(
+ "[TOOL] RAG retrieval failed; disabling RAG and falling back "
+ f"to keyword search ({exc.__class__.__name__}: {exc!r})"
+ )
# Fallback: keyword scoring against full docs
print("[TOOL] RAG returned nothing, trying keyword fallback")
@@ -301,9 +367,7 @@ def build_helper_chain():
chatbot but has NO access to training/inference sub-agents.
Returns ``(agent, reset_search_counter)`` — same interface as ``build_chain``.
"""
- ollama_base_url = os.getenv("OLLAMA_BASE_URL", "http://cscigpu08.bc.edu:4443")
- ollama_model = os.getenv("OLLAMA_MODEL", "gpt-oss:20b")
- ollama_embed_model = os.getenv("OLLAMA_EMBED_MODEL", "qwen3-embedding:8b")
+ ollama_base_url, ollama_model, ollama_embed_model = get_ollama_config()
llm = ChatOllama(model=ollama_model, base_url=ollama_base_url, temperature=0)
embeddings = OllamaEmbeddings(model=ollama_embed_model, base_url=ollama_base_url)
faiss_path = process_path("server_api/chatbot/faiss_index")
@@ -326,6 +390,7 @@ def build_helper_chain():
_all_docs[md_file.name] = md_file.read_text(encoding="utf-8")
_search_call_count = [0]
+ _rag_enabled = [True]
def reset_search_counter():
_search_call_count[0] = 0
@@ -346,9 +411,17 @@ def search_documentation(query: str) -> str:
"Search limit reached. Answer with the documentation already retrieved."
)
- docs = retriever.invoke(query)
- if docs:
- return "\n\n".join([doc.page_content for doc in docs])
+ if _rag_enabled[0]:
+ try:
+ docs = retriever.invoke(query)
+ if docs:
+ return "\n\n".join([doc.page_content for doc in docs])
+ except Exception as exc:
+ _rag_enabled[0] = False
+ print(
+ "[HELPER TOOL] RAG retrieval failed; disabling RAG and "
+ f"falling back to keyword search ({exc.__class__.__name__}: {exc!r})"
+ )
# Keyword fallback
query_lower = query.lower()
diff --git a/server_api/chatbot/logging_utils.py b/server_api/chatbot/logging_utils.py
new file mode 100644
index 00000000..41dcdfe9
--- /dev/null
+++ b/server_api/chatbot/logging_utils.py
@@ -0,0 +1,29 @@
+import logging
+import time
+import uuid
+from typing import Any, Optional
+
+logger = logging.getLogger(__name__)
+
+
+def request_id_from_request(request: Any) -> str:
+ return request.headers.get("x-request-id") or str(uuid.uuid4())
+
+
+def log_request_summary(
+ *,
+ request_id: str,
+ endpoint: str,
+ start_time: float,
+ status: str,
+ error_type: Optional[str] = None,
+) -> None:
+ latency_ms = round((time.perf_counter() - start_time) * 1000, 2)
+ logger.info(
+ "request_summary request_id=%s endpoint=%s latency_ms=%s status=%s error_type=%s",
+ request_id,
+ endpoint,
+ latency_ms,
+ status,
+ error_type or "none",
+ )
diff --git a/server_api/data_store/project_manager_data.json b/server_api/data_store/project_manager_data.json
new file mode 100644
index 00000000..88d4ed4d
--- /dev/null
+++ b/server_api/data_store/project_manager_data.json
@@ -0,0 +1,6364 @@
+{
+ "volumes": [
+ {
+ "id": "vol_001_em.h5",
+ "filename": "vol_001_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_002_em.h5",
+ "filename": "vol_002_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_003_em.h5",
+ "filename": "vol_003_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_004_em.h5",
+ "filename": "vol_004_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_005_em.h5",
+ "filename": "vol_005_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_006_em.h5",
+ "filename": "vol_006_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_007_em.h5",
+ "filename": "vol_007_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_008_em.h5",
+ "filename": "vol_008_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_009_em.h5",
+ "filename": "vol_009_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_010_em.h5",
+ "filename": "vol_010_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_011_em.h5",
+ "filename": "vol_011_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_012_em.h5",
+ "filename": "vol_012_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_013_em.h5",
+ "filename": "vol_013_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_014_em.h5",
+ "filename": "vol_014_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_015_em.h5",
+ "filename": "vol_015_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_016_em.h5",
+ "filename": "vol_016_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_017_em.h5",
+ "filename": "vol_017_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_018_em.h5",
+ "filename": "vol_018_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_019_em.h5",
+ "filename": "vol_019_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_020_em.h5",
+ "filename": "vol_020_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_021_em.h5",
+ "filename": "vol_021_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_022_em.h5",
+ "filename": "vol_022_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_023_em.h5",
+ "filename": "vol_023_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_024_em.h5",
+ "filename": "vol_024_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_025_em.h5",
+ "filename": "vol_025_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_026_em.h5",
+ "filename": "vol_026_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_027_em.h5",
+ "filename": "vol_027_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_028_em.h5",
+ "filename": "vol_028_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_029_em.h5",
+ "filename": "vol_029_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_030_em.h5",
+ "filename": "vol_030_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_031_em.h5",
+ "filename": "vol_031_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_032_em.h5",
+ "filename": "vol_032_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_033_em.h5",
+ "filename": "vol_033_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_034_em.h5",
+ "filename": "vol_034_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_035_em.h5",
+ "filename": "vol_035_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_036_em.h5",
+ "filename": "vol_036_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_037_em.h5",
+ "filename": "vol_037_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_038_em.h5",
+ "filename": "vol_038_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_039_em.h5",
+ "filename": "vol_039_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_040_em.h5",
+ "filename": "vol_040_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_041_em.h5",
+ "filename": "vol_041_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_042_em.h5",
+ "filename": "vol_042_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_043_em.h5",
+ "filename": "vol_043_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_044_em.h5",
+ "filename": "vol_044_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_045_em.h5",
+ "filename": "vol_045_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_046_em.h5",
+ "filename": "vol_046_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_047_em.h5",
+ "filename": "vol_047_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_048_em.h5",
+ "filename": "vol_048_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_049_em.h5",
+ "filename": "vol_049_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_050_em.h5",
+ "filename": "vol_050_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_051_em.h5",
+ "filename": "vol_051_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_052_em.h5",
+ "filename": "vol_052_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_053_em.h5",
+ "filename": "vol_053_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_054_em.h5",
+ "filename": "vol_054_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_055_em.h5",
+ "filename": "vol_055_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_056_em.h5",
+ "filename": "vol_056_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_057_em.h5",
+ "filename": "vol_057_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_058_em.h5",
+ "filename": "vol_058_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_059_em.h5",
+ "filename": "vol_059_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_060_em.h5",
+ "filename": "vol_060_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_061_em.h5",
+ "filename": "vol_061_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_062_em.h5",
+ "filename": "vol_062_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_063_em.h5",
+ "filename": "vol_063_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_064_em.h5",
+ "filename": "vol_064_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_065_em.h5",
+ "filename": "vol_065_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_066_em.h5",
+ "filename": "vol_066_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_067_em.h5",
+ "filename": "vol_067_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_068_em.h5",
+ "filename": "vol_068_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_069_em.h5",
+ "filename": "vol_069_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_070_em.h5",
+ "filename": "vol_070_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_071_em.h5",
+ "filename": "vol_071_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_072_em.h5",
+ "filename": "vol_072_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_073_em.h5",
+ "filename": "vol_073_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_074_em.h5",
+ "filename": "vol_074_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_075_em.h5",
+ "filename": "vol_075_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_076_em.h5",
+ "filename": "vol_076_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_077_em.h5",
+ "filename": "vol_077_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_078_em.h5",
+ "filename": "vol_078_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_079_em.h5",
+ "filename": "vol_079_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_080_em.h5",
+ "filename": "vol_080_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_081_em.h5",
+ "filename": "vol_081_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_082_em.h5",
+ "filename": "vol_082_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_083_em.h5",
+ "filename": "vol_083_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_084_em.h5",
+ "filename": "vol_084_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_085_em.h5",
+ "filename": "vol_085_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_086_em.h5",
+ "filename": "vol_086_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_087_em.h5",
+ "filename": "vol_087_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_088_em.h5",
+ "filename": "vol_088_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_089_em.h5",
+ "filename": "vol_089_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_090_em.h5",
+ "filename": "vol_090_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_091_em.h5",
+ "filename": "vol_091_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_092_em.h5",
+ "filename": "vol_092_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_093_em.h5",
+ "filename": "vol_093_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_094_em.h5",
+ "filename": "vol_094_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_095_em.h5",
+ "filename": "vol_095_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_096_em.h5",
+ "filename": "vol_096_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_097_em.h5",
+ "filename": "vol_097_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_098_em.h5",
+ "filename": "vol_098_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_099_em.h5",
+ "filename": "vol_099_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_100_em.h5",
+ "filename": "vol_100_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_101_em.h5",
+ "filename": "vol_101_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_102_em.h5",
+ "filename": "vol_102_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_103_em.h5",
+ "filename": "vol_103_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_104_em.h5",
+ "filename": "vol_104_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_105_em.h5",
+ "filename": "vol_105_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_106_em.h5",
+ "filename": "vol_106_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_107_em.h5",
+ "filename": "vol_107_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_108_em.h5",
+ "filename": "vol_108_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_109_em.h5",
+ "filename": "vol_109_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_110_em.h5",
+ "filename": "vol_110_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_111_em.h5",
+ "filename": "vol_111_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_112_em.h5",
+ "filename": "vol_112_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_113_em.h5",
+ "filename": "vol_113_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_114_em.h5",
+ "filename": "vol_114_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_115_em.h5",
+ "filename": "vol_115_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_116_em.h5",
+ "filename": "vol_116_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_117_em.h5",
+ "filename": "vol_117_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_118_em.h5",
+ "filename": "vol_118_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_119_em.h5",
+ "filename": "vol_119_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_120_em.h5",
+ "filename": "vol_120_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_121_em.h5",
+ "filename": "vol_121_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_122_em.h5",
+ "filename": "vol_122_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_123_em.h5",
+ "filename": "vol_123_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_124_em.h5",
+ "filename": "vol_124_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_125_em.h5",
+ "filename": "vol_125_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_126_em.h5",
+ "filename": "vol_126_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_127_em.h5",
+ "filename": "vol_127_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_128_em.h5",
+ "filename": "vol_128_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_129_em.h5",
+ "filename": "vol_129_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_130_em.h5",
+ "filename": "vol_130_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_131_em.h5",
+ "filename": "vol_131_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_132_em.h5",
+ "filename": "vol_132_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_133_em.h5",
+ "filename": "vol_133_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_134_em.h5",
+ "filename": "vol_134_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_135_em.h5",
+ "filename": "vol_135_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_136_em.h5",
+ "filename": "vol_136_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_137_em.h5",
+ "filename": "vol_137_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_138_em.h5",
+ "filename": "vol_138_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_139_em.h5",
+ "filename": "vol_139_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_140_em.h5",
+ "filename": "vol_140_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_141_em.h5",
+ "filename": "vol_141_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_142_em.h5",
+ "filename": "vol_142_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_143_em.h5",
+ "filename": "vol_143_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_144_em.h5",
+ "filename": "vol_144_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_145_em.h5",
+ "filename": "vol_145_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_146_em.h5",
+ "filename": "vol_146_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_147_em.h5",
+ "filename": "vol_147_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_148_em.h5",
+ "filename": "vol_148_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_149_em.h5",
+ "filename": "vol_149_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_150_em.h5",
+ "filename": "vol_150_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_151_em.h5",
+ "filename": "vol_151_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_152_em.h5",
+ "filename": "vol_152_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_153_em.h5",
+ "filename": "vol_153_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_154_em.h5",
+ "filename": "vol_154_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_155_em.h5",
+ "filename": "vol_155_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_156_em.h5",
+ "filename": "vol_156_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_157_em.h5",
+ "filename": "vol_157_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_158_em.h5",
+ "filename": "vol_158_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_159_em.h5",
+ "filename": "vol_159_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_160_em.h5",
+ "filename": "vol_160_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_161_em.h5",
+ "filename": "vol_161_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_162_em.h5",
+ "filename": "vol_162_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_163_em.h5",
+ "filename": "vol_163_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_164_em.h5",
+ "filename": "vol_164_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_165_em.h5",
+ "filename": "vol_165_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_166_em.h5",
+ "filename": "vol_166_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_167_em.h5",
+ "filename": "vol_167_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_168_em.h5",
+ "filename": "vol_168_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_169_em.h5",
+ "filename": "vol_169_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_170_em.h5",
+ "filename": "vol_170_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_171_em.h5",
+ "filename": "vol_171_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_172_em.h5",
+ "filename": "vol_172_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_173_em.h5",
+ "filename": "vol_173_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_174_em.h5",
+ "filename": "vol_174_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_175_em.h5",
+ "filename": "vol_175_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_176_em.h5",
+ "filename": "vol_176_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_177_em.h5",
+ "filename": "vol_177_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_178_em.h5",
+ "filename": "vol_178_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_179_em.h5",
+ "filename": "vol_179_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_180_em.h5",
+ "filename": "vol_180_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_181_em.h5",
+ "filename": "vol_181_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_182_em.h5",
+ "filename": "vol_182_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_183_em.h5",
+ "filename": "vol_183_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_184_em.h5",
+ "filename": "vol_184_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_185_em.h5",
+ "filename": "vol_185_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_186_em.h5",
+ "filename": "vol_186_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_187_em.h5",
+ "filename": "vol_187_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_188_em.h5",
+ "filename": "vol_188_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_189_em.h5",
+ "filename": "vol_189_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_190_em.h5",
+ "filename": "vol_190_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_191_em.h5",
+ "filename": "vol_191_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_192_em.h5",
+ "filename": "vol_192_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_193_em.h5",
+ "filename": "vol_193_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_194_em.h5",
+ "filename": "vol_194_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_195_em.h5",
+ "filename": "vol_195_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_196_em.h5",
+ "filename": "vol_196_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_197_em.h5",
+ "filename": "vol_197_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_198_em.h5",
+ "filename": "vol_198_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_199_em.h5",
+ "filename": "vol_199_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_200_em.h5",
+ "filename": "vol_200_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_201_em.h5",
+ "filename": "vol_201_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_202_em.h5",
+ "filename": "vol_202_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_203_em.h5",
+ "filename": "vol_203_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_204_em.h5",
+ "filename": "vol_204_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_205_em.h5",
+ "filename": "vol_205_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_206_em.h5",
+ "filename": "vol_206_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_207_em.h5",
+ "filename": "vol_207_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_208_em.h5",
+ "filename": "vol_208_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_209_em.h5",
+ "filename": "vol_209_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_210_em.h5",
+ "filename": "vol_210_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_211_em.h5",
+ "filename": "vol_211_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_212_em.h5",
+ "filename": "vol_212_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_213_em.h5",
+ "filename": "vol_213_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_214_em.h5",
+ "filename": "vol_214_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_215_em.h5",
+ "filename": "vol_215_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_216_em.h5",
+ "filename": "vol_216_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_217_em.h5",
+ "filename": "vol_217_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_218_em.h5",
+ "filename": "vol_218_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_219_em.h5",
+ "filename": "vol_219_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_220_em.h5",
+ "filename": "vol_220_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_221_em.h5",
+ "filename": "vol_221_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_222_em.h5",
+ "filename": "vol_222_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_223_em.h5",
+ "filename": "vol_223_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_224_em.h5",
+ "filename": "vol_224_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_225_em.h5",
+ "filename": "vol_225_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_226_em.h5",
+ "filename": "vol_226_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_227_em.h5",
+ "filename": "vol_227_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_228_em.h5",
+ "filename": "vol_228_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_229_em.h5",
+ "filename": "vol_229_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_230_em.h5",
+ "filename": "vol_230_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_231_em.h5",
+ "filename": "vol_231_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_232_em.h5",
+ "filename": "vol_232_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_233_em.h5",
+ "filename": "vol_233_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_234_em.h5",
+ "filename": "vol_234_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_235_em.h5",
+ "filename": "vol_235_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_236_em.h5",
+ "filename": "vol_236_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_237_em.h5",
+ "filename": "vol_237_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_238_em.h5",
+ "filename": "vol_238_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_239_em.h5",
+ "filename": "vol_239_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_240_em.h5",
+ "filename": "vol_240_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_241_em.h5",
+ "filename": "vol_241_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_242_em.h5",
+ "filename": "vol_242_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_243_em.h5",
+ "filename": "vol_243_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_244_em.h5",
+ "filename": "vol_244_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_245_em.h5",
+ "filename": "vol_245_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_246_em.h5",
+ "filename": "vol_246_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_247_em.h5",
+ "filename": "vol_247_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_248_em.h5",
+ "filename": "vol_248_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_249_em.h5",
+ "filename": "vol_249_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_250_em.h5",
+ "filename": "vol_250_em.h5",
+ "assignee": "alex",
+ "status": "todo"
+ },
+ {
+ "id": "vol_251_em.h5",
+ "filename": "vol_251_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_252_em.h5",
+ "filename": "vol_252_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_253_em.h5",
+ "filename": "vol_253_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_254_em.h5",
+ "filename": "vol_254_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_255_em.h5",
+ "filename": "vol_255_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_256_em.h5",
+ "filename": "vol_256_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_257_em.h5",
+ "filename": "vol_257_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_258_em.h5",
+ "filename": "vol_258_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_259_em.h5",
+ "filename": "vol_259_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_260_em.h5",
+ "filename": "vol_260_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_261_em.h5",
+ "filename": "vol_261_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_262_em.h5",
+ "filename": "vol_262_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_263_em.h5",
+ "filename": "vol_263_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_264_em.h5",
+ "filename": "vol_264_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_265_em.h5",
+ "filename": "vol_265_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_266_em.h5",
+ "filename": "vol_266_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_267_em.h5",
+ "filename": "vol_267_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_268_em.h5",
+ "filename": "vol_268_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_269_em.h5",
+ "filename": "vol_269_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_270_em.h5",
+ "filename": "vol_270_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_271_em.h5",
+ "filename": "vol_271_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_272_em.h5",
+ "filename": "vol_272_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_273_em.h5",
+ "filename": "vol_273_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_274_em.h5",
+ "filename": "vol_274_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_275_em.h5",
+ "filename": "vol_275_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_276_em.h5",
+ "filename": "vol_276_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_277_em.h5",
+ "filename": "vol_277_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_278_em.h5",
+ "filename": "vol_278_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_279_em.h5",
+ "filename": "vol_279_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_280_em.h5",
+ "filename": "vol_280_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_281_em.h5",
+ "filename": "vol_281_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_282_em.h5",
+ "filename": "vol_282_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_283_em.h5",
+ "filename": "vol_283_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_284_em.h5",
+ "filename": "vol_284_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_285_em.h5",
+ "filename": "vol_285_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_286_em.h5",
+ "filename": "vol_286_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_287_em.h5",
+ "filename": "vol_287_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_288_em.h5",
+ "filename": "vol_288_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_289_em.h5",
+ "filename": "vol_289_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_290_em.h5",
+ "filename": "vol_290_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_291_em.h5",
+ "filename": "vol_291_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_292_em.h5",
+ "filename": "vol_292_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_293_em.h5",
+ "filename": "vol_293_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_294_em.h5",
+ "filename": "vol_294_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_295_em.h5",
+ "filename": "vol_295_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_296_em.h5",
+ "filename": "vol_296_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_297_em.h5",
+ "filename": "vol_297_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_298_em.h5",
+ "filename": "vol_298_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_299_em.h5",
+ "filename": "vol_299_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_300_em.h5",
+ "filename": "vol_300_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_301_em.h5",
+ "filename": "vol_301_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_302_em.h5",
+ "filename": "vol_302_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_303_em.h5",
+ "filename": "vol_303_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_304_em.h5",
+ "filename": "vol_304_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_305_em.h5",
+ "filename": "vol_305_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_306_em.h5",
+ "filename": "vol_306_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_307_em.h5",
+ "filename": "vol_307_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_308_em.h5",
+ "filename": "vol_308_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_309_em.h5",
+ "filename": "vol_309_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_310_em.h5",
+ "filename": "vol_310_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_311_em.h5",
+ "filename": "vol_311_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_312_em.h5",
+ "filename": "vol_312_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_313_em.h5",
+ "filename": "vol_313_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_314_em.h5",
+ "filename": "vol_314_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_315_em.h5",
+ "filename": "vol_315_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_316_em.h5",
+ "filename": "vol_316_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_317_em.h5",
+ "filename": "vol_317_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_318_em.h5",
+ "filename": "vol_318_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_319_em.h5",
+ "filename": "vol_319_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_320_em.h5",
+ "filename": "vol_320_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_321_em.h5",
+ "filename": "vol_321_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_322_em.h5",
+ "filename": "vol_322_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_323_em.h5",
+ "filename": "vol_323_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_324_em.h5",
+ "filename": "vol_324_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_325_em.h5",
+ "filename": "vol_325_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_326_em.h5",
+ "filename": "vol_326_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_327_em.h5",
+ "filename": "vol_327_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_328_em.h5",
+ "filename": "vol_328_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_329_em.h5",
+ "filename": "vol_329_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_330_em.h5",
+ "filename": "vol_330_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_331_em.h5",
+ "filename": "vol_331_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_332_em.h5",
+ "filename": "vol_332_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_333_em.h5",
+ "filename": "vol_333_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_334_em.h5",
+ "filename": "vol_334_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_335_em.h5",
+ "filename": "vol_335_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_336_em.h5",
+ "filename": "vol_336_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_337_em.h5",
+ "filename": "vol_337_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_338_em.h5",
+ "filename": "vol_338_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_339_em.h5",
+ "filename": "vol_339_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_340_em.h5",
+ "filename": "vol_340_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_341_em.h5",
+ "filename": "vol_341_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_342_em.h5",
+ "filename": "vol_342_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_343_em.h5",
+ "filename": "vol_343_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_344_em.h5",
+ "filename": "vol_344_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_345_em.h5",
+ "filename": "vol_345_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_346_em.h5",
+ "filename": "vol_346_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_347_em.h5",
+ "filename": "vol_347_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_348_em.h5",
+ "filename": "vol_348_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_349_em.h5",
+ "filename": "vol_349_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_350_em.h5",
+ "filename": "vol_350_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_351_em.h5",
+ "filename": "vol_351_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_352_em.h5",
+ "filename": "vol_352_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_353_em.h5",
+ "filename": "vol_353_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_354_em.h5",
+ "filename": "vol_354_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_355_em.h5",
+ "filename": "vol_355_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_356_em.h5",
+ "filename": "vol_356_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_357_em.h5",
+ "filename": "vol_357_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_358_em.h5",
+ "filename": "vol_358_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_359_em.h5",
+ "filename": "vol_359_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_360_em.h5",
+ "filename": "vol_360_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_361_em.h5",
+ "filename": "vol_361_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_362_em.h5",
+ "filename": "vol_362_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_363_em.h5",
+ "filename": "vol_363_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_364_em.h5",
+ "filename": "vol_364_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_365_em.h5",
+ "filename": "vol_365_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_366_em.h5",
+ "filename": "vol_366_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_367_em.h5",
+ "filename": "vol_367_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_368_em.h5",
+ "filename": "vol_368_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_369_em.h5",
+ "filename": "vol_369_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_370_em.h5",
+ "filename": "vol_370_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_371_em.h5",
+ "filename": "vol_371_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_372_em.h5",
+ "filename": "vol_372_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_373_em.h5",
+ "filename": "vol_373_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_374_em.h5",
+ "filename": "vol_374_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_375_em.h5",
+ "filename": "vol_375_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_376_em.h5",
+ "filename": "vol_376_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_377_em.h5",
+ "filename": "vol_377_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_378_em.h5",
+ "filename": "vol_378_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_379_em.h5",
+ "filename": "vol_379_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_380_em.h5",
+ "filename": "vol_380_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_381_em.h5",
+ "filename": "vol_381_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_382_em.h5",
+ "filename": "vol_382_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_383_em.h5",
+ "filename": "vol_383_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_384_em.h5",
+ "filename": "vol_384_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_385_em.h5",
+ "filename": "vol_385_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_386_em.h5",
+ "filename": "vol_386_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_387_em.h5",
+ "filename": "vol_387_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_388_em.h5",
+ "filename": "vol_388_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_389_em.h5",
+ "filename": "vol_389_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_390_em.h5",
+ "filename": "vol_390_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_391_em.h5",
+ "filename": "vol_391_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_392_em.h5",
+ "filename": "vol_392_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_393_em.h5",
+ "filename": "vol_393_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_394_em.h5",
+ "filename": "vol_394_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_395_em.h5",
+ "filename": "vol_395_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_396_em.h5",
+ "filename": "vol_396_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_397_em.h5",
+ "filename": "vol_397_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_398_em.h5",
+ "filename": "vol_398_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_399_em.h5",
+ "filename": "vol_399_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_400_em.h5",
+ "filename": "vol_400_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_401_em.h5",
+ "filename": "vol_401_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_402_em.h5",
+ "filename": "vol_402_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_403_em.h5",
+ "filename": "vol_403_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_404_em.h5",
+ "filename": "vol_404_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_405_em.h5",
+ "filename": "vol_405_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_406_em.h5",
+ "filename": "vol_406_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_407_em.h5",
+ "filename": "vol_407_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_408_em.h5",
+ "filename": "vol_408_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_409_em.h5",
+ "filename": "vol_409_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_410_em.h5",
+ "filename": "vol_410_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_411_em.h5",
+ "filename": "vol_411_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_412_em.h5",
+ "filename": "vol_412_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_413_em.h5",
+ "filename": "vol_413_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_414_em.h5",
+ "filename": "vol_414_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_415_em.h5",
+ "filename": "vol_415_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_416_em.h5",
+ "filename": "vol_416_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_417_em.h5",
+ "filename": "vol_417_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_418_em.h5",
+ "filename": "vol_418_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_419_em.h5",
+ "filename": "vol_419_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_420_em.h5",
+ "filename": "vol_420_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_421_em.h5",
+ "filename": "vol_421_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_422_em.h5",
+ "filename": "vol_422_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_423_em.h5",
+ "filename": "vol_423_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_424_em.h5",
+ "filename": "vol_424_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_425_em.h5",
+ "filename": "vol_425_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_426_em.h5",
+ "filename": "vol_426_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_427_em.h5",
+ "filename": "vol_427_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_428_em.h5",
+ "filename": "vol_428_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_429_em.h5",
+ "filename": "vol_429_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_430_em.h5",
+ "filename": "vol_430_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_431_em.h5",
+ "filename": "vol_431_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_432_em.h5",
+ "filename": "vol_432_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_433_em.h5",
+ "filename": "vol_433_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_434_em.h5",
+ "filename": "vol_434_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_435_em.h5",
+ "filename": "vol_435_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_436_em.h5",
+ "filename": "vol_436_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_437_em.h5",
+ "filename": "vol_437_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_438_em.h5",
+ "filename": "vol_438_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_439_em.h5",
+ "filename": "vol_439_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_440_em.h5",
+ "filename": "vol_440_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_441_em.h5",
+ "filename": "vol_441_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_442_em.h5",
+ "filename": "vol_442_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_443_em.h5",
+ "filename": "vol_443_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_444_em.h5",
+ "filename": "vol_444_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_445_em.h5",
+ "filename": "vol_445_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_446_em.h5",
+ "filename": "vol_446_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_447_em.h5",
+ "filename": "vol_447_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_448_em.h5",
+ "filename": "vol_448_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_449_em.h5",
+ "filename": "vol_449_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_450_em.h5",
+ "filename": "vol_450_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_451_em.h5",
+ "filename": "vol_451_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_452_em.h5",
+ "filename": "vol_452_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_453_em.h5",
+ "filename": "vol_453_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_454_em.h5",
+ "filename": "vol_454_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_455_em.h5",
+ "filename": "vol_455_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_456_em.h5",
+ "filename": "vol_456_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_457_em.h5",
+ "filename": "vol_457_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_458_em.h5",
+ "filename": "vol_458_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_459_em.h5",
+ "filename": "vol_459_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_460_em.h5",
+ "filename": "vol_460_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_461_em.h5",
+ "filename": "vol_461_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_462_em.h5",
+ "filename": "vol_462_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_463_em.h5",
+ "filename": "vol_463_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_464_em.h5",
+ "filename": "vol_464_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_465_em.h5",
+ "filename": "vol_465_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_466_em.h5",
+ "filename": "vol_466_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_467_em.h5",
+ "filename": "vol_467_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_468_em.h5",
+ "filename": "vol_468_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_469_em.h5",
+ "filename": "vol_469_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_470_em.h5",
+ "filename": "vol_470_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_471_em.h5",
+ "filename": "vol_471_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_472_em.h5",
+ "filename": "vol_472_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_473_em.h5",
+ "filename": "vol_473_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_474_em.h5",
+ "filename": "vol_474_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_475_em.h5",
+ "filename": "vol_475_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_476_em.h5",
+ "filename": "vol_476_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_477_em.h5",
+ "filename": "vol_477_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_478_em.h5",
+ "filename": "vol_478_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_479_em.h5",
+ "filename": "vol_479_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_480_em.h5",
+ "filename": "vol_480_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_481_em.h5",
+ "filename": "vol_481_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_482_em.h5",
+ "filename": "vol_482_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_483_em.h5",
+ "filename": "vol_483_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_484_em.h5",
+ "filename": "vol_484_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_485_em.h5",
+ "filename": "vol_485_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_486_em.h5",
+ "filename": "vol_486_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_487_em.h5",
+ "filename": "vol_487_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_488_em.h5",
+ "filename": "vol_488_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_489_em.h5",
+ "filename": "vol_489_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_490_em.h5",
+ "filename": "vol_490_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_491_em.h5",
+ "filename": "vol_491_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_492_em.h5",
+ "filename": "vol_492_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_493_em.h5",
+ "filename": "vol_493_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_494_em.h5",
+ "filename": "vol_494_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_495_em.h5",
+ "filename": "vol_495_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_496_em.h5",
+ "filename": "vol_496_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_497_em.h5",
+ "filename": "vol_497_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_498_em.h5",
+ "filename": "vol_498_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_499_em.h5",
+ "filename": "vol_499_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_500_em.h5",
+ "filename": "vol_500_em.h5",
+ "assignee": "jordan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_501_em.h5",
+ "filename": "vol_501_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_502_em.h5",
+ "filename": "vol_502_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_503_em.h5",
+ "filename": "vol_503_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_504_em.h5",
+ "filename": "vol_504_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_505_em.h5",
+ "filename": "vol_505_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_506_em.h5",
+ "filename": "vol_506_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_507_em.h5",
+ "filename": "vol_507_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_508_em.h5",
+ "filename": "vol_508_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_509_em.h5",
+ "filename": "vol_509_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_510_em.h5",
+ "filename": "vol_510_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_511_em.h5",
+ "filename": "vol_511_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_512_em.h5",
+ "filename": "vol_512_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_513_em.h5",
+ "filename": "vol_513_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_514_em.h5",
+ "filename": "vol_514_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_515_em.h5",
+ "filename": "vol_515_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_516_em.h5",
+ "filename": "vol_516_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_517_em.h5",
+ "filename": "vol_517_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_518_em.h5",
+ "filename": "vol_518_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_519_em.h5",
+ "filename": "vol_519_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_520_em.h5",
+ "filename": "vol_520_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_521_em.h5",
+ "filename": "vol_521_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_522_em.h5",
+ "filename": "vol_522_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_523_em.h5",
+ "filename": "vol_523_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_524_em.h5",
+ "filename": "vol_524_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_525_em.h5",
+ "filename": "vol_525_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_526_em.h5",
+ "filename": "vol_526_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_527_em.h5",
+ "filename": "vol_527_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_528_em.h5",
+ "filename": "vol_528_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_529_em.h5",
+ "filename": "vol_529_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_530_em.h5",
+ "filename": "vol_530_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_531_em.h5",
+ "filename": "vol_531_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_532_em.h5",
+ "filename": "vol_532_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_533_em.h5",
+ "filename": "vol_533_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_534_em.h5",
+ "filename": "vol_534_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_535_em.h5",
+ "filename": "vol_535_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_536_em.h5",
+ "filename": "vol_536_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_537_em.h5",
+ "filename": "vol_537_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_538_em.h5",
+ "filename": "vol_538_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_539_em.h5",
+ "filename": "vol_539_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_540_em.h5",
+ "filename": "vol_540_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_541_em.h5",
+ "filename": "vol_541_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_542_em.h5",
+ "filename": "vol_542_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_543_em.h5",
+ "filename": "vol_543_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_544_em.h5",
+ "filename": "vol_544_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_545_em.h5",
+ "filename": "vol_545_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_546_em.h5",
+ "filename": "vol_546_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_547_em.h5",
+ "filename": "vol_547_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_548_em.h5",
+ "filename": "vol_548_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_549_em.h5",
+ "filename": "vol_549_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_550_em.h5",
+ "filename": "vol_550_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_551_em.h5",
+ "filename": "vol_551_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_552_em.h5",
+ "filename": "vol_552_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_553_em.h5",
+ "filename": "vol_553_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_554_em.h5",
+ "filename": "vol_554_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_555_em.h5",
+ "filename": "vol_555_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_556_em.h5",
+ "filename": "vol_556_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_557_em.h5",
+ "filename": "vol_557_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_558_em.h5",
+ "filename": "vol_558_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_559_em.h5",
+ "filename": "vol_559_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_560_em.h5",
+ "filename": "vol_560_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_561_em.h5",
+ "filename": "vol_561_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_562_em.h5",
+ "filename": "vol_562_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_563_em.h5",
+ "filename": "vol_563_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_564_em.h5",
+ "filename": "vol_564_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_565_em.h5",
+ "filename": "vol_565_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_566_em.h5",
+ "filename": "vol_566_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_567_em.h5",
+ "filename": "vol_567_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_568_em.h5",
+ "filename": "vol_568_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_569_em.h5",
+ "filename": "vol_569_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_570_em.h5",
+ "filename": "vol_570_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_571_em.h5",
+ "filename": "vol_571_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_572_em.h5",
+ "filename": "vol_572_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_573_em.h5",
+ "filename": "vol_573_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_574_em.h5",
+ "filename": "vol_574_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_575_em.h5",
+ "filename": "vol_575_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_576_em.h5",
+ "filename": "vol_576_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_577_em.h5",
+ "filename": "vol_577_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_578_em.h5",
+ "filename": "vol_578_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_579_em.h5",
+ "filename": "vol_579_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_580_em.h5",
+ "filename": "vol_580_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_581_em.h5",
+ "filename": "vol_581_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_582_em.h5",
+ "filename": "vol_582_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_583_em.h5",
+ "filename": "vol_583_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_584_em.h5",
+ "filename": "vol_584_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_585_em.h5",
+ "filename": "vol_585_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_586_em.h5",
+ "filename": "vol_586_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_587_em.h5",
+ "filename": "vol_587_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_588_em.h5",
+ "filename": "vol_588_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_589_em.h5",
+ "filename": "vol_589_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_590_em.h5",
+ "filename": "vol_590_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_591_em.h5",
+ "filename": "vol_591_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_592_em.h5",
+ "filename": "vol_592_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_593_em.h5",
+ "filename": "vol_593_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_594_em.h5",
+ "filename": "vol_594_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_595_em.h5",
+ "filename": "vol_595_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_596_em.h5",
+ "filename": "vol_596_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_597_em.h5",
+ "filename": "vol_597_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_598_em.h5",
+ "filename": "vol_598_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_599_em.h5",
+ "filename": "vol_599_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_600_em.h5",
+ "filename": "vol_600_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_601_em.h5",
+ "filename": "vol_601_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_602_em.h5",
+ "filename": "vol_602_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_603_em.h5",
+ "filename": "vol_603_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_604_em.h5",
+ "filename": "vol_604_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_605_em.h5",
+ "filename": "vol_605_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_606_em.h5",
+ "filename": "vol_606_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_607_em.h5",
+ "filename": "vol_607_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_608_em.h5",
+ "filename": "vol_608_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_609_em.h5",
+ "filename": "vol_609_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_610_em.h5",
+ "filename": "vol_610_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_611_em.h5",
+ "filename": "vol_611_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_612_em.h5",
+ "filename": "vol_612_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_613_em.h5",
+ "filename": "vol_613_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_614_em.h5",
+ "filename": "vol_614_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_615_em.h5",
+ "filename": "vol_615_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_616_em.h5",
+ "filename": "vol_616_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_617_em.h5",
+ "filename": "vol_617_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_618_em.h5",
+ "filename": "vol_618_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_619_em.h5",
+ "filename": "vol_619_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_620_em.h5",
+ "filename": "vol_620_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_621_em.h5",
+ "filename": "vol_621_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_622_em.h5",
+ "filename": "vol_622_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_623_em.h5",
+ "filename": "vol_623_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_624_em.h5",
+ "filename": "vol_624_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_625_em.h5",
+ "filename": "vol_625_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_626_em.h5",
+ "filename": "vol_626_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_627_em.h5",
+ "filename": "vol_627_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_628_em.h5",
+ "filename": "vol_628_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_629_em.h5",
+ "filename": "vol_629_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_630_em.h5",
+ "filename": "vol_630_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_631_em.h5",
+ "filename": "vol_631_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_632_em.h5",
+ "filename": "vol_632_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_633_em.h5",
+ "filename": "vol_633_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_634_em.h5",
+ "filename": "vol_634_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_635_em.h5",
+ "filename": "vol_635_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_636_em.h5",
+ "filename": "vol_636_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_637_em.h5",
+ "filename": "vol_637_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_638_em.h5",
+ "filename": "vol_638_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_639_em.h5",
+ "filename": "vol_639_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_640_em.h5",
+ "filename": "vol_640_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_641_em.h5",
+ "filename": "vol_641_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_642_em.h5",
+ "filename": "vol_642_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_643_em.h5",
+ "filename": "vol_643_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_644_em.h5",
+ "filename": "vol_644_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_645_em.h5",
+ "filename": "vol_645_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_646_em.h5",
+ "filename": "vol_646_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_647_em.h5",
+ "filename": "vol_647_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_648_em.h5",
+ "filename": "vol_648_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_649_em.h5",
+ "filename": "vol_649_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_650_em.h5",
+ "filename": "vol_650_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_651_em.h5",
+ "filename": "vol_651_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_652_em.h5",
+ "filename": "vol_652_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_653_em.h5",
+ "filename": "vol_653_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_654_em.h5",
+ "filename": "vol_654_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_655_em.h5",
+ "filename": "vol_655_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_656_em.h5",
+ "filename": "vol_656_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_657_em.h5",
+ "filename": "vol_657_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_658_em.h5",
+ "filename": "vol_658_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_659_em.h5",
+ "filename": "vol_659_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_660_em.h5",
+ "filename": "vol_660_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_661_em.h5",
+ "filename": "vol_661_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_662_em.h5",
+ "filename": "vol_662_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_663_em.h5",
+ "filename": "vol_663_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_664_em.h5",
+ "filename": "vol_664_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_665_em.h5",
+ "filename": "vol_665_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_666_em.h5",
+ "filename": "vol_666_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_667_em.h5",
+ "filename": "vol_667_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_668_em.h5",
+ "filename": "vol_668_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_669_em.h5",
+ "filename": "vol_669_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_670_em.h5",
+ "filename": "vol_670_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_671_em.h5",
+ "filename": "vol_671_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_672_em.h5",
+ "filename": "vol_672_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_673_em.h5",
+ "filename": "vol_673_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_674_em.h5",
+ "filename": "vol_674_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_675_em.h5",
+ "filename": "vol_675_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_676_em.h5",
+ "filename": "vol_676_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_677_em.h5",
+ "filename": "vol_677_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_678_em.h5",
+ "filename": "vol_678_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_679_em.h5",
+ "filename": "vol_679_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_680_em.h5",
+ "filename": "vol_680_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_681_em.h5",
+ "filename": "vol_681_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_682_em.h5",
+ "filename": "vol_682_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_683_em.h5",
+ "filename": "vol_683_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_684_em.h5",
+ "filename": "vol_684_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_685_em.h5",
+ "filename": "vol_685_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_686_em.h5",
+ "filename": "vol_686_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_687_em.h5",
+ "filename": "vol_687_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_688_em.h5",
+ "filename": "vol_688_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_689_em.h5",
+ "filename": "vol_689_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_690_em.h5",
+ "filename": "vol_690_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_691_em.h5",
+ "filename": "vol_691_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_692_em.h5",
+ "filename": "vol_692_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_693_em.h5",
+ "filename": "vol_693_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_694_em.h5",
+ "filename": "vol_694_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_695_em.h5",
+ "filename": "vol_695_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_696_em.h5",
+ "filename": "vol_696_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_697_em.h5",
+ "filename": "vol_697_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_698_em.h5",
+ "filename": "vol_698_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_699_em.h5",
+ "filename": "vol_699_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_700_em.h5",
+ "filename": "vol_700_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_701_em.h5",
+ "filename": "vol_701_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_702_em.h5",
+ "filename": "vol_702_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_703_em.h5",
+ "filename": "vol_703_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_704_em.h5",
+ "filename": "vol_704_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_705_em.h5",
+ "filename": "vol_705_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_706_em.h5",
+ "filename": "vol_706_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_707_em.h5",
+ "filename": "vol_707_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_708_em.h5",
+ "filename": "vol_708_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_709_em.h5",
+ "filename": "vol_709_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_710_em.h5",
+ "filename": "vol_710_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_711_em.h5",
+ "filename": "vol_711_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_712_em.h5",
+ "filename": "vol_712_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_713_em.h5",
+ "filename": "vol_713_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_714_em.h5",
+ "filename": "vol_714_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_715_em.h5",
+ "filename": "vol_715_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_716_em.h5",
+ "filename": "vol_716_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_717_em.h5",
+ "filename": "vol_717_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_718_em.h5",
+ "filename": "vol_718_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_719_em.h5",
+ "filename": "vol_719_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_720_em.h5",
+ "filename": "vol_720_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_721_em.h5",
+ "filename": "vol_721_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_722_em.h5",
+ "filename": "vol_722_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_723_em.h5",
+ "filename": "vol_723_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_724_em.h5",
+ "filename": "vol_724_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_725_em.h5",
+ "filename": "vol_725_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_726_em.h5",
+ "filename": "vol_726_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_727_em.h5",
+ "filename": "vol_727_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_728_em.h5",
+ "filename": "vol_728_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_729_em.h5",
+ "filename": "vol_729_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_730_em.h5",
+ "filename": "vol_730_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_731_em.h5",
+ "filename": "vol_731_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_732_em.h5",
+ "filename": "vol_732_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_733_em.h5",
+ "filename": "vol_733_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_734_em.h5",
+ "filename": "vol_734_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_735_em.h5",
+ "filename": "vol_735_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_736_em.h5",
+ "filename": "vol_736_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_737_em.h5",
+ "filename": "vol_737_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_738_em.h5",
+ "filename": "vol_738_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_739_em.h5",
+ "filename": "vol_739_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_740_em.h5",
+ "filename": "vol_740_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_741_em.h5",
+ "filename": "vol_741_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_742_em.h5",
+ "filename": "vol_742_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_743_em.h5",
+ "filename": "vol_743_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_744_em.h5",
+ "filename": "vol_744_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_745_em.h5",
+ "filename": "vol_745_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_746_em.h5",
+ "filename": "vol_746_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_747_em.h5",
+ "filename": "vol_747_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_748_em.h5",
+ "filename": "vol_748_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_749_em.h5",
+ "filename": "vol_749_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_750_em.h5",
+ "filename": "vol_750_em.h5",
+ "assignee": "taylor",
+ "status": "todo"
+ },
+ {
+ "id": "vol_751_em.h5",
+ "filename": "vol_751_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_752_em.h5",
+ "filename": "vol_752_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_753_em.h5",
+ "filename": "vol_753_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_754_em.h5",
+ "filename": "vol_754_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_755_em.h5",
+ "filename": "vol_755_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_756_em.h5",
+ "filename": "vol_756_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_757_em.h5",
+ "filename": "vol_757_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_758_em.h5",
+ "filename": "vol_758_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_759_em.h5",
+ "filename": "vol_759_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_760_em.h5",
+ "filename": "vol_760_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_761_em.h5",
+ "filename": "vol_761_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_762_em.h5",
+ "filename": "vol_762_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_763_em.h5",
+ "filename": "vol_763_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_764_em.h5",
+ "filename": "vol_764_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_765_em.h5",
+ "filename": "vol_765_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_766_em.h5",
+ "filename": "vol_766_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_767_em.h5",
+ "filename": "vol_767_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_768_em.h5",
+ "filename": "vol_768_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_769_em.h5",
+ "filename": "vol_769_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_770_em.h5",
+ "filename": "vol_770_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_771_em.h5",
+ "filename": "vol_771_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_772_em.h5",
+ "filename": "vol_772_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_773_em.h5",
+ "filename": "vol_773_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_774_em.h5",
+ "filename": "vol_774_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_775_em.h5",
+ "filename": "vol_775_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_776_em.h5",
+ "filename": "vol_776_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_777_em.h5",
+ "filename": "vol_777_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_778_em.h5",
+ "filename": "vol_778_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_779_em.h5",
+ "filename": "vol_779_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_780_em.h5",
+ "filename": "vol_780_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_781_em.h5",
+ "filename": "vol_781_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_782_em.h5",
+ "filename": "vol_782_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_783_em.h5",
+ "filename": "vol_783_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_784_em.h5",
+ "filename": "vol_784_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_785_em.h5",
+ "filename": "vol_785_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_786_em.h5",
+ "filename": "vol_786_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_787_em.h5",
+ "filename": "vol_787_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_788_em.h5",
+ "filename": "vol_788_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_789_em.h5",
+ "filename": "vol_789_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_790_em.h5",
+ "filename": "vol_790_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_791_em.h5",
+ "filename": "vol_791_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_792_em.h5",
+ "filename": "vol_792_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_793_em.h5",
+ "filename": "vol_793_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_794_em.h5",
+ "filename": "vol_794_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_795_em.h5",
+ "filename": "vol_795_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_796_em.h5",
+ "filename": "vol_796_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_797_em.h5",
+ "filename": "vol_797_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_798_em.h5",
+ "filename": "vol_798_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_799_em.h5",
+ "filename": "vol_799_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_800_em.h5",
+ "filename": "vol_800_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_801_em.h5",
+ "filename": "vol_801_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_802_em.h5",
+ "filename": "vol_802_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_803_em.h5",
+ "filename": "vol_803_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_804_em.h5",
+ "filename": "vol_804_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_805_em.h5",
+ "filename": "vol_805_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_806_em.h5",
+ "filename": "vol_806_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_807_em.h5",
+ "filename": "vol_807_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_808_em.h5",
+ "filename": "vol_808_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_809_em.h5",
+ "filename": "vol_809_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_810_em.h5",
+ "filename": "vol_810_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_811_em.h5",
+ "filename": "vol_811_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_812_em.h5",
+ "filename": "vol_812_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_813_em.h5",
+ "filename": "vol_813_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_814_em.h5",
+ "filename": "vol_814_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_815_em.h5",
+ "filename": "vol_815_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_816_em.h5",
+ "filename": "vol_816_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_817_em.h5",
+ "filename": "vol_817_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_818_em.h5",
+ "filename": "vol_818_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_819_em.h5",
+ "filename": "vol_819_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_820_em.h5",
+ "filename": "vol_820_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_821_em.h5",
+ "filename": "vol_821_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_822_em.h5",
+ "filename": "vol_822_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_823_em.h5",
+ "filename": "vol_823_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_824_em.h5",
+ "filename": "vol_824_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_825_em.h5",
+ "filename": "vol_825_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_826_em.h5",
+ "filename": "vol_826_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_827_em.h5",
+ "filename": "vol_827_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_828_em.h5",
+ "filename": "vol_828_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_829_em.h5",
+ "filename": "vol_829_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_830_em.h5",
+ "filename": "vol_830_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_831_em.h5",
+ "filename": "vol_831_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_832_em.h5",
+ "filename": "vol_832_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_833_em.h5",
+ "filename": "vol_833_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_834_em.h5",
+ "filename": "vol_834_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_835_em.h5",
+ "filename": "vol_835_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_836_em.h5",
+ "filename": "vol_836_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_837_em.h5",
+ "filename": "vol_837_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_838_em.h5",
+ "filename": "vol_838_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_839_em.h5",
+ "filename": "vol_839_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_840_em.h5",
+ "filename": "vol_840_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_841_em.h5",
+ "filename": "vol_841_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_842_em.h5",
+ "filename": "vol_842_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_843_em.h5",
+ "filename": "vol_843_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_844_em.h5",
+ "filename": "vol_844_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_845_em.h5",
+ "filename": "vol_845_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_846_em.h5",
+ "filename": "vol_846_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_847_em.h5",
+ "filename": "vol_847_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_848_em.h5",
+ "filename": "vol_848_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_849_em.h5",
+ "filename": "vol_849_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_850_em.h5",
+ "filename": "vol_850_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_851_em.h5",
+ "filename": "vol_851_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_852_em.h5",
+ "filename": "vol_852_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_853_em.h5",
+ "filename": "vol_853_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_854_em.h5",
+ "filename": "vol_854_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_855_em.h5",
+ "filename": "vol_855_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_856_em.h5",
+ "filename": "vol_856_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_857_em.h5",
+ "filename": "vol_857_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_858_em.h5",
+ "filename": "vol_858_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_859_em.h5",
+ "filename": "vol_859_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_860_em.h5",
+ "filename": "vol_860_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_861_em.h5",
+ "filename": "vol_861_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_862_em.h5",
+ "filename": "vol_862_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_863_em.h5",
+ "filename": "vol_863_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_864_em.h5",
+ "filename": "vol_864_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_865_em.h5",
+ "filename": "vol_865_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_866_em.h5",
+ "filename": "vol_866_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_867_em.h5",
+ "filename": "vol_867_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_868_em.h5",
+ "filename": "vol_868_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_869_em.h5",
+ "filename": "vol_869_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_870_em.h5",
+ "filename": "vol_870_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_871_em.h5",
+ "filename": "vol_871_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_872_em.h5",
+ "filename": "vol_872_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_873_em.h5",
+ "filename": "vol_873_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_874_em.h5",
+ "filename": "vol_874_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_875_em.h5",
+ "filename": "vol_875_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_876_em.h5",
+ "filename": "vol_876_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_877_em.h5",
+ "filename": "vol_877_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_878_em.h5",
+ "filename": "vol_878_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_879_em.h5",
+ "filename": "vol_879_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_880_em.h5",
+ "filename": "vol_880_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_881_em.h5",
+ "filename": "vol_881_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_882_em.h5",
+ "filename": "vol_882_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_883_em.h5",
+ "filename": "vol_883_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_884_em.h5",
+ "filename": "vol_884_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_885_em.h5",
+ "filename": "vol_885_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_886_em.h5",
+ "filename": "vol_886_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_887_em.h5",
+ "filename": "vol_887_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_888_em.h5",
+ "filename": "vol_888_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_889_em.h5",
+ "filename": "vol_889_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_890_em.h5",
+ "filename": "vol_890_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_891_em.h5",
+ "filename": "vol_891_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_892_em.h5",
+ "filename": "vol_892_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_893_em.h5",
+ "filename": "vol_893_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_894_em.h5",
+ "filename": "vol_894_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_895_em.h5",
+ "filename": "vol_895_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_896_em.h5",
+ "filename": "vol_896_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_897_em.h5",
+ "filename": "vol_897_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_898_em.h5",
+ "filename": "vol_898_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_899_em.h5",
+ "filename": "vol_899_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_900_em.h5",
+ "filename": "vol_900_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_901_em.h5",
+ "filename": "vol_901_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_902_em.h5",
+ "filename": "vol_902_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_903_em.h5",
+ "filename": "vol_903_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_904_em.h5",
+ "filename": "vol_904_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_905_em.h5",
+ "filename": "vol_905_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_906_em.h5",
+ "filename": "vol_906_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_907_em.h5",
+ "filename": "vol_907_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_908_em.h5",
+ "filename": "vol_908_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_909_em.h5",
+ "filename": "vol_909_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_910_em.h5",
+ "filename": "vol_910_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_911_em.h5",
+ "filename": "vol_911_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_912_em.h5",
+ "filename": "vol_912_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_913_em.h5",
+ "filename": "vol_913_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_914_em.h5",
+ "filename": "vol_914_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_915_em.h5",
+ "filename": "vol_915_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_916_em.h5",
+ "filename": "vol_916_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_917_em.h5",
+ "filename": "vol_917_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_918_em.h5",
+ "filename": "vol_918_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_919_em.h5",
+ "filename": "vol_919_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_920_em.h5",
+ "filename": "vol_920_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_921_em.h5",
+ "filename": "vol_921_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_922_em.h5",
+ "filename": "vol_922_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_923_em.h5",
+ "filename": "vol_923_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_924_em.h5",
+ "filename": "vol_924_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_925_em.h5",
+ "filename": "vol_925_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_926_em.h5",
+ "filename": "vol_926_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_927_em.h5",
+ "filename": "vol_927_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_928_em.h5",
+ "filename": "vol_928_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_929_em.h5",
+ "filename": "vol_929_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_930_em.h5",
+ "filename": "vol_930_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_931_em.h5",
+ "filename": "vol_931_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_932_em.h5",
+ "filename": "vol_932_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_933_em.h5",
+ "filename": "vol_933_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_934_em.h5",
+ "filename": "vol_934_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_935_em.h5",
+ "filename": "vol_935_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_936_em.h5",
+ "filename": "vol_936_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_937_em.h5",
+ "filename": "vol_937_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_938_em.h5",
+ "filename": "vol_938_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_939_em.h5",
+ "filename": "vol_939_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_940_em.h5",
+ "filename": "vol_940_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_941_em.h5",
+ "filename": "vol_941_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_942_em.h5",
+ "filename": "vol_942_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_943_em.h5",
+ "filename": "vol_943_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_944_em.h5",
+ "filename": "vol_944_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_945_em.h5",
+ "filename": "vol_945_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_946_em.h5",
+ "filename": "vol_946_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_947_em.h5",
+ "filename": "vol_947_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_948_em.h5",
+ "filename": "vol_948_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_949_em.h5",
+ "filename": "vol_949_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_950_em.h5",
+ "filename": "vol_950_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_951_em.h5",
+ "filename": "vol_951_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_952_em.h5",
+ "filename": "vol_952_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_953_em.h5",
+ "filename": "vol_953_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_954_em.h5",
+ "filename": "vol_954_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_955_em.h5",
+ "filename": "vol_955_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_956_em.h5",
+ "filename": "vol_956_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_957_em.h5",
+ "filename": "vol_957_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_958_em.h5",
+ "filename": "vol_958_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_959_em.h5",
+ "filename": "vol_959_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_960_em.h5",
+ "filename": "vol_960_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_961_em.h5",
+ "filename": "vol_961_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_962_em.h5",
+ "filename": "vol_962_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_963_em.h5",
+ "filename": "vol_963_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_964_em.h5",
+ "filename": "vol_964_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_965_em.h5",
+ "filename": "vol_965_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_966_em.h5",
+ "filename": "vol_966_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_967_em.h5",
+ "filename": "vol_967_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_968_em.h5",
+ "filename": "vol_968_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_969_em.h5",
+ "filename": "vol_969_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_970_em.h5",
+ "filename": "vol_970_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_971_em.h5",
+ "filename": "vol_971_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_972_em.h5",
+ "filename": "vol_972_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_973_em.h5",
+ "filename": "vol_973_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_974_em.h5",
+ "filename": "vol_974_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_975_em.h5",
+ "filename": "vol_975_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_976_em.h5",
+ "filename": "vol_976_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_977_em.h5",
+ "filename": "vol_977_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_978_em.h5",
+ "filename": "vol_978_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_979_em.h5",
+ "filename": "vol_979_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_980_em.h5",
+ "filename": "vol_980_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_981_em.h5",
+ "filename": "vol_981_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_982_em.h5",
+ "filename": "vol_982_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_983_em.h5",
+ "filename": "vol_983_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_984_em.h5",
+ "filename": "vol_984_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_985_em.h5",
+ "filename": "vol_985_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_986_em.h5",
+ "filename": "vol_986_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_987_em.h5",
+ "filename": "vol_987_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_988_em.h5",
+ "filename": "vol_988_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_989_em.h5",
+ "filename": "vol_989_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_990_em.h5",
+ "filename": "vol_990_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_991_em.h5",
+ "filename": "vol_991_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_992_em.h5",
+ "filename": "vol_992_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_993_em.h5",
+ "filename": "vol_993_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_994_em.h5",
+ "filename": "vol_994_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_995_em.h5",
+ "filename": "vol_995_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_996_em.h5",
+ "filename": "vol_996_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_997_em.h5",
+ "filename": "vol_997_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_998_em.h5",
+ "filename": "vol_998_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_999_em.h5",
+ "filename": "vol_999_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ },
+ {
+ "id": "vol_1000_em.h5",
+ "filename": "vol_1000_em.h5",
+ "assignee": "morgan",
+ "status": "todo"
+ }
+ ],
+ "quota_data": [
+ {
+ "key": "1",
+ "name": "Alex Rivera",
+ "worker_key": "alex",
+ "datasets": ["Hippocampus_CA3", "Motor_Cortex_M1"],
+ "mon": 300,
+ "tue": 300,
+ "wed": 300,
+ "thu": 300,
+ "fri": 300,
+ "sat": 150,
+ "sun": 100,
+ "actualMon": 310,
+ "actualTue": 290,
+ "actualWed": 320,
+ "actualThu": 280,
+ "actualFri": 300,
+ "actualSat": 160,
+ "actualSun": 90
+ },
+ {
+ "key": "2",
+ "name": "Jordan Smith",
+ "worker_key": "jordan",
+ "datasets": ["Cerebellum_PC"],
+ "mon": 250,
+ "tue": 250,
+ "wed": 250,
+ "thu": 250,
+ "fri": 250,
+ "sat": 0,
+ "sun": 0,
+ "actualMon": 240,
+ "actualTue": 260,
+ "actualWed": 230,
+ "actualThu": 250,
+ "actualFri": 220,
+ "actualSat": 0,
+ "actualSun": 0
+ },
+ {
+ "key": "3",
+ "name": "Sam Taylor",
+ "worker_key": "taylor",
+ "datasets": ["Retina_GCL"],
+ "mon": 200,
+ "tue": 200,
+ "wed": 200,
+ "thu": 200,
+ "fri": 200,
+ "sat": 250,
+ "sun": 250,
+ "actualMon": 180,
+ "actualTue": 190,
+ "actualWed": 170,
+ "actualThu": 210,
+ "actualFri": 200,
+ "actualSat": 100,
+ "actualSun": 0
+ },
+ {
+ "key": "4",
+ "name": "Morgan Lee",
+ "worker_key": "morgan",
+ "datasets": ["Visual_Cortex_V1"],
+ "mon": 240,
+ "tue": 240,
+ "wed": 240,
+ "thu": 240,
+ "fri": 240,
+ "sat": 0,
+ "sun": 0,
+ "actualMon": 250,
+ "actualTue": 260,
+ "actualWed": 270,
+ "actualThu": 240,
+ "actualFri": 230,
+ "actualSat": 0,
+ "actualSun": 0
+ }
+ ],
+ "proofreader_data": [
+ {
+ "key": "1",
+ "name": "Alex Rivera",
+ "worker_key": "alex",
+ "avatarColor": "#1890ff",
+ "role": "Senior Annotator",
+ "totalPoints": 12450,
+ "weeklyPoints": 1420,
+ "weeklyQuota": 1750,
+ "accuracy": 98.5,
+ "lastActive": "2 mins ago",
+ "status": "online"
+ },
+ {
+ "key": "2",
+ "name": "Jordan Smith",
+ "worker_key": "jordan",
+ "avatarColor": "#52c41a",
+ "role": "Proofreader",
+ "totalPoints": 8900,
+ "weeklyPoints": 1100,
+ "weeklyQuota": 1500,
+ "accuracy": 94.2,
+ "lastActive": "15 mins ago",
+ "status": "online"
+ },
+ {
+ "key": "3",
+ "name": "Sam Taylor",
+ "worker_key": "taylor",
+ "avatarColor": "#fadb14",
+ "role": "Proofreader",
+ "totalPoints": 5600,
+ "weeklyPoints": 850,
+ "weeklyQuota": 1500,
+ "accuracy": 92.1,
+ "lastActive": "1 hour ago",
+ "status": "away"
+ },
+ {
+ "key": "4",
+ "name": "Morgan Lee",
+ "worker_key": "morgan",
+ "avatarColor": "#eb2f96",
+ "role": "Junior Annotator",
+ "totalPoints": 3200,
+ "weeklyPoints": 1250,
+ "weeklyQuota": 1200,
+ "accuracy": 96.8,
+ "lastActive": "3 hours ago",
+ "status": "offline"
+ }
+ ],
+ "throughput_data": [
+ {
+ "day": "Mon",
+ "count": 4200
+ },
+ {
+ "day": "Tue",
+ "count": 3800
+ },
+ {
+ "day": "Wed",
+ "count": 5100
+ },
+ {
+ "day": "Thu",
+ "count": 4700
+ },
+ {
+ "day": "Fri",
+ "count": 5300
+ },
+ {
+ "day": "Sat",
+ "count": 1200
+ },
+ {
+ "day": "Sun",
+ "count": 900
+ }
+ ],
+ "datasets": [
+ {
+ "key": "1",
+ "name": "Hippocampus_CA3",
+ "experiment": "Synapse detection",
+ "total": 12450,
+ "proofread": 38,
+ "status": "in_progress",
+ "eta": "Jan 18",
+ "priority": "high"
+ },
+ {
+ "key": "2",
+ "name": "Motor_Cortex_M1",
+ "experiment": "Axon tracing",
+ "total": 9800,
+ "proofread": 72,
+ "status": "in_progress",
+ "eta": "Jan 22",
+ "priority": "high"
+ },
+ {
+ "key": "3",
+ "name": "Cerebellum_PC",
+ "experiment": "Dendrite segmentation",
+ "total": 6200,
+ "proofread": 91,
+ "status": "done",
+ "eta": "Complete",
+ "priority": "normal"
+ },
+ {
+ "key": "4",
+ "name": "Retina_GCL",
+ "experiment": "Cell classification",
+ "total": 3300,
+ "proofread": 5,
+ "status": "blocked",
+ "eta": "TBD",
+ "priority": "high"
+ },
+ {
+ "key": "5",
+ "name": "Olfactory_Bulb",
+ "experiment": "Glomeruli mapping",
+ "total": 7650,
+ "proofread": 0,
+ "status": "not_started",
+ "eta": "Feb 28",
+ "priority": "normal"
+ },
+ {
+ "key": "6",
+ "name": "Visual_Cortex_V1",
+ "experiment": "Spine detection",
+ "total": 15000,
+ "proofread": 55,
+ "status": "in_progress",
+ "eta": "Feb 10",
+ "priority": "normal"
+ }
+ ],
+ "milestones": [
+ {
+ "label": "Model v1",
+ "date": "2025-11-15",
+ "color": "#1890ff"
+ },
+ {
+ "label": "Mid-Review",
+ "date": "2025-12-20",
+ "color": "#722ed1"
+ },
+ {
+ "label": "Model v2",
+ "date": "2026-01-28",
+ "color": "#1890ff"
+ },
+ {
+ "label": "Data Freeze",
+ "date": "2026-02-15",
+ "color": "#fa8c16"
+ },
+ {
+ "label": "Model v3",
+ "date": "2026-03-20",
+ "color": "#1890ff"
+ }
+ ],
+ "cumulative_data": [
+ 0, 480, 1100, 1950, 2800, 3900, 5100, 6200, 7450, 8600, 9900, 11200, 12600,
+ 13900, 15400, 17000, 18500, 20100, 21800, 23400, 25000, 26700, 28500, 30200,
+ 32000, 33800
+ ],
+ "cumulative_target": [
+ 0, 600, 1200, 1800, 2400, 3000, 3600, 4800, 6000, 7200, 8400, 9600, 10800,
+ 12000, 13200, 14400, 15600, 16800, 18000, 19200, 20400, 21600, 22800, 24000,
+ 25200, 26400
+ ],
+ "at_risk": [
+ {
+ "label": "Hippocampus_CA3",
+ "reason": "Low progress (38%)",
+ "icon": "progress"
+ },
+ {
+ "label": "Motor_Cortex_M1",
+ "reason": "Deadline approaching",
+ "icon": "clock"
+ },
+ {
+ "label": "Retina_GCL",
+ "reason": "Blocked — awaiting data",
+ "icon": "blocked"
+ }
+ ],
+ "upcoming_milestones": [
+ {
+ "label": "Model v2 data freeze",
+ "date": "JAN 28"
+ },
+ {
+ "label": "Mid-project review",
+ "date": "FEB 5"
+ },
+ {
+ "label": "Final data freeze",
+ "date": "FEB 15"
+ },
+ {
+ "label": "Model v3 launch",
+ "date": "MAR 20"
+ }
+ ],
+ "msg_preview": "Hi Team,\n\nI've just assigned the quotas for the upcoming week. Please review your targets in the dashboard. Our goal for this week is to maintain 95%+ accuracy while meeting the sample volume targets.\n\nGood luck!",
+ "workers": [
+ {
+ "key": "alex",
+ "name": "Alex Rivera",
+ "avatarColor": "#1890ff"
+ },
+ {
+ "key": "jordan",
+ "name": "Jordan Smith",
+ "avatarColor": "#52c41a"
+ },
+ {
+ "key": "taylor",
+ "name": "Sam Taylor",
+ "avatarColor": "#fadb14"
+ },
+ {
+ "key": "morgan",
+ "name": "Morgan Lee",
+ "avatarColor": "#eb2f96"
+ }
+ ],
+ "users": [
+ {
+ "username": "admin",
+ "password": "admin123",
+ "name": "Project Manager",
+ "role": "admin",
+ "key": "admin"
+ },
+ {
+ "username": "alex",
+ "password": "alex123",
+ "name": "Alex Rivera",
+ "role": "worker",
+ "key": "alex"
+ },
+ {
+ "username": "jordan",
+ "password": "jordan123",
+ "name": "Jordan Smith",
+ "role": "worker",
+ "key": "jordan"
+ },
+ {
+ "username": "taylor",
+ "password": "taylor123",
+ "name": "Sam Taylor",
+ "role": "worker",
+ "key": "taylor"
+ },
+ {
+ "username": "morgan",
+ "password": "morgan123",
+ "name": "Morgan Lee",
+ "role": "worker",
+ "key": "morgan"
+ }
+ ]
+}
diff --git a/server_api/ehtool/db_models.py b/server_api/ehtool/db_models.py
index 475d4653..38daab4b 100644
--- a/server_api/ehtool/db_models.py
+++ b/server_api/ehtool/db_models.py
@@ -19,6 +19,7 @@ class EHToolSession(Base):
dataset_path = Column(String) # Path to uploaded dataset
mask_path = Column(String, nullable=True) # Path to mask dataset (optional)
total_layers = Column(Integer, default=0)
+ workflow_id = Column(Integer, nullable=True, index=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
diff --git a/server_api/ehtool/models.py b/server_api/ehtool/models.py
index d1b88c45..d1ec7ac0 100644
--- a/server_api/ehtool/models.py
+++ b/server_api/ehtool/models.py
@@ -15,6 +15,7 @@ class DetectionLoadRequest(BaseModel):
dataset_path: str
mask_path: Optional[str] = None
project_name: str = "Untitled Project"
+ workflow_id: Optional[int] = None
@field_validator("dataset_path", "mask_path")
@classmethod
diff --git a/server_api/ehtool/router.py b/server_api/ehtool/router.py
index 2a93923d..567ce397 100644
--- a/server_api/ehtool/router.py
+++ b/server_api/ehtool/router.py
@@ -36,6 +36,12 @@
from .db_models import EHToolSession, EHToolLayer
from .data_manager import DataManager
from .utils import array_to_base64, glasbey_color
+from server_api.workflows.service import (
+ append_event_for_workflow_if_present,
+ append_workflow_event,
+ get_user_workflow_or_404,
+ update_workflow_fields,
+)
router = APIRouter()
@@ -86,6 +92,12 @@ async def load_detection_dataset(
db: Session = Depends(get_db),
):
try:
+ workflow = None
+ if request.workflow_id:
+ workflow = get_user_workflow_or_404(
+ db, workflow_id=request.workflow_id, user_id=current_user.id
+ )
+
# Create DataManager and load dataset
data_manager = DataManager()
dataset_info = data_manager.load_dataset(
@@ -101,6 +113,7 @@ async def load_detection_dataset(
dataset_path=request.dataset_path,
mask_path=request.mask_path,
total_layers=dataset_info["total_layers"],
+ workflow_id=request.workflow_id,
)
db.add(db_session)
db.commit()
@@ -122,6 +135,47 @@ async def load_detection_dataset(
# Cache DataManager
_data_managers[db_session.id] = data_manager
+ if workflow:
+ update_workflow_fields(
+ db,
+ workflow,
+ {
+ "stage": "proofreading",
+ "title": request.project_name or workflow.title,
+ "dataset_path": request.dataset_path,
+ "image_path": request.dataset_path,
+ "mask_path": request.mask_path,
+ "proofreading_session_id": db_session.id,
+ },
+ commit=True,
+ )
+ append_workflow_event(
+ db,
+ workflow_id=workflow.id,
+ actor="user",
+ event_type="dataset.loaded",
+ stage="proofreading",
+ summary=f"Loaded dataset for proofreading: {request.project_name}",
+ payload={
+ "dataset_path": request.dataset_path,
+ "mask_path": request.mask_path,
+ "total_layers": dataset_info["total_layers"],
+ "ehtool_session_id": db_session.id,
+ },
+ )
+ append_workflow_event(
+ db,
+ workflow_id=workflow.id,
+ actor="system",
+ event_type="proofreading.session_loaded",
+ stage="proofreading",
+ summary="Mask proofreading session linked to workflow.",
+ payload={
+ "ehtool_session_id": db_session.id,
+ "project_name": request.project_name,
+ },
+ )
+
return DetectionLoadResponse(
session_id=db_session.id,
total_layers=dataset_info["total_layers"],
@@ -613,6 +667,20 @@ async def classify_instances(
ui_state = request.ui_state.dict() if request.ui_state else None
data_manager.save_progress(ui_state=ui_state)
+ append_event_for_workflow_if_present(
+ db,
+ workflow_id=db_session.workflow_id,
+ actor="user",
+ event_type="proofreading.instance_classified",
+ stage="proofreading",
+ summary=f"Classified {updated} instance(s) as {request.classification}.",
+ payload={
+ "ehtool_session_id": request.session_id,
+ "instance_ids": request.instance_ids,
+ "classification": request.classification,
+ "updated_count": updated,
+ },
+ )
return ClassifyResponse(
updated_count=updated,
@@ -754,6 +822,20 @@ async def save_instance_mask(
)
if request.ui_state:
data_manager.save_progress(ui_state=request.ui_state.dict())
+ append_event_for_workflow_if_present(
+ db,
+ workflow_id=db_session.workflow_id,
+ actor="user",
+ event_type="proofreading.mask_saved",
+ stage="proofreading",
+ summary=f"Saved mask edit for instance {request.instance_id}.",
+ payload={
+ "ehtool_session_id": request.session_id,
+ "instance_id": request.instance_id,
+ "axis": request.axis,
+ "z_index": request.z_index,
+ },
+ )
return {"message": "Instance mask saved successfully"}
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@@ -794,6 +876,21 @@ async def export_masks(
output_path=request.output_path,
create_backup=request.create_backup,
)
+ append_event_for_workflow_if_present(
+ db,
+ workflow_id=db_session.workflow_id,
+ actor="user",
+ event_type="proofreading.masks_exported",
+ stage="proofreading",
+ summary=f"Exported edited masks to {result['written_path']}.",
+ payload={
+ "ehtool_session_id": request.session_id,
+ "mode": request.mode,
+ "written_path": result["written_path"],
+ "backup_path": result.get("backup_path"),
+ "timestamp": result["timestamp"],
+ },
+ )
return ExportMasksResponse(
message=result["message"],
written_path=result["written_path"],
diff --git a/server_api/main.py b/server_api/main.py
index a97153f8..38fe9ac7 100644
--- a/server_api/main.py
+++ b/server_api/main.py
@@ -3,6 +3,9 @@
import re
import shutil
import tempfile
+import traceback
+import time
+from concurrent.futures import ThreadPoolExecutor, TimeoutError
from typing import List, Optional
from urllib.parse import urlsplit, urlunsplit
@@ -10,6 +13,7 @@
import uvicorn
from fastapi import Depends, FastAPI, HTTPException, Request, UploadFile
from fastapi.middleware.cors import CORSMiddleware
+from sqlalchemy import inspect, text
from sqlalchemy.orm import Session
from runtime_settings import (
get_allowed_origins,
@@ -21,6 +25,17 @@
from server_api.auth.database import get_db
from server_api.auth.router import get_current_user
from server_api.ehtool import router as ehtool_router
+from server_api.project_manager import router as pm_router
+from server_api.chatbot.logging_utils import (
+ log_request_summary,
+ request_id_from_request,
+)
+from server_api.workflows import router as workflow_router
+from server_api.workflows.service import (
+ append_event_for_workflow_if_present,
+ get_user_workflow_or_404,
+ update_workflow_fields,
+)
from fastapi.staticfiles import StaticFiles
import os
@@ -28,12 +43,26 @@
# Chatbot is optional; keep the server running if dependencies or model endpoints
# are unavailable. We initialize lazily on demand.
try:
- from server_api.chatbot.chatbot import build_chain, build_helper_chain
+ from server_api.chatbot.chatbot import (
+ build_chain,
+ build_helper_chain,
+ _format_admin_llm_error,
+ )
except Exception as exc: # pragma: no cover - exercised indirectly via endpoints
build_chain = None
build_helper_chain = None
_chatbot_error = exc
+ def _format_admin_llm_error(error):
+ return (
+ "The AI assistant could not connect to its configured language model. "
+ "Please contact your system administrator with this error: "
+ f"{str(error).strip() or error.__class__.__name__}"
+ )
+
+else:
+ _chatbot_error = None
+
chain = None
_reset_search = None
@@ -50,25 +79,80 @@
def _ensure_chatbot():
global chain, _reset_search, _chatbot_error
if chain is not None and _reset_search is not None:
+ print("[CHATBOT] Reusing initialized main chat chain")
return True
if build_chain is None:
+ print("[CHATBOT] build_chain is unavailable; chatbot backend not configured")
return False
+ start_time = time.perf_counter()
+ print("[CHATBOT] Initializing main chat chain...")
try:
chain, _reset_search = build_chain()
_chatbot_error = None
+ elapsed = time.perf_counter() - start_time
+ print(f"[CHATBOT] Main chat chain ready in {elapsed:.2f}s")
return True
except Exception as exc: # pragma: no cover - runtime config issue
chain = None
_reset_search = None
_chatbot_error = exc
+ print(
+ "[CHATBOT] Failed to initialize LLM backend: "
+ f"{exc.__class__.__name__}: {exc!r}"
+ )
+ traceback.print_exc()
return False
+def _llm_unavailable_detail(error):
+ return {
+ "user_message": _format_admin_llm_error(error),
+ "error": str(error),
+ "reason": "llm_unavailable",
+ }
+
+
+def _invoke_with_progress(invoke_fn, *, label: str, request_id: str, poll_seconds=5.0):
+ start_time = time.perf_counter()
+ with ThreadPoolExecutor(max_workers=1) as executor:
+ future = executor.submit(invoke_fn)
+ while True:
+ try:
+ result = future.result(timeout=poll_seconds)
+ elapsed = time.perf_counter() - start_time
+ print(
+ f"[CHATBOT][{request_id}] {label} completed in {elapsed:.2f}s"
+ )
+ return result
+ except TimeoutError:
+ elapsed = time.perf_counter() - start_time
+ print(
+ f"[CHATBOT][{request_id}] {label} still running "
+ f"after {elapsed:.2f}s..."
+ )
+
+
REACT_APP_SERVER_PROTOCOL = "http"
REACT_APP_SERVER_URL = "localhost:4243"
models.Base.metadata.create_all(bind=database.engine)
+
+def _ensure_sqlite_column(table_name: str, column_name: str, ddl: str) -> None:
+ if database.engine.dialect.name != "sqlite":
+ return
+ inspector = inspect(database.engine)
+ if table_name not in inspector.get_table_names():
+ return
+ existing = {column["name"] for column in inspector.get_columns(table_name)}
+ if column_name in existing:
+ return
+ with database.engine.begin() as connection:
+ connection.execute(text(f"ALTER TABLE {table_name} ADD COLUMN {ddl}"))
+
+
+_ensure_sqlite_column("ehtool_sessions", "workflow_id", "workflow_id INTEGER")
+
app = FastAPI()
# Ensure uploads directory exists
@@ -77,6 +161,8 @@ def _ensure_chatbot():
app.include_router(auth_router.router)
app.include_router(ehtool_router.router, prefix="/eh", tags=["ehtool"])
+app.include_router(pm_router.router, prefix="/api/pm", tags=["project-manager"])
+app.include_router(workflow_router.router, prefix="/api/workflows", tags=["workflows"])
app.add_middleware(
CORSMiddleware,
@@ -363,7 +449,11 @@ def _is_probable_label_volume(image_array) -> bool:
@app.post("/neuroglancer")
-async def neuroglancer(req: Request):
+async def neuroglancer(
+ req: Request,
+ current_user: models.User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
import neuroglancer
cleanup_paths: List[pathlib.Path] = []
@@ -383,6 +473,8 @@ async def neuroglancer(req: Request):
raise HTTPException(
status_code=400, detail="Scales payload is invalid."
)
+ workflow_id_raw = form.get("workflow_id")
+ workflow_id = int(workflow_id_raw) if workflow_id_raw else None
image = save_upload_to_tempfile(image_upload)
cleanup_paths.append(image)
@@ -397,6 +489,7 @@ async def neuroglancer(req: Request):
image = process_path(payload["image"])
label = process_path(payload.get("label"))
scales = payload["scales"]
+ workflow_id = payload.get("workflow_id") or payload.get("workflowId")
print(image, label, scales)
@@ -433,6 +526,35 @@ def ngLayer(data, res, oo=[0, 0, 0], tt="segmentation"):
s.layers.append(name="gt", layer=ngLayer(gt, res, tt="segmentation"))
public_url = _build_neuroglancer_public_url(str(viewer), req)
+ if workflow_id:
+ workflow = get_user_workflow_or_404(
+ db, workflow_id=int(workflow_id), user_id=current_user.id
+ )
+ update_workflow_fields(
+ db,
+ workflow,
+ {
+ "stage": "visualization",
+ "image_path": str(image),
+ "label_path": str(label) if label else None,
+ "neuroglancer_url": public_url,
+ },
+ commit=True,
+ )
+ append_event_for_workflow_if_present(
+ db,
+ workflow_id=workflow.id,
+ actor="user",
+ event_type="viewer.created",
+ stage="visualization",
+ summary="Created Neuroglancer viewer.",
+ payload={
+ "image_path": str(image),
+ "label_path": str(label) if label else None,
+ "scales": scales,
+ "neuroglancer_url": public_url,
+ },
+ )
print(public_url)
return public_url
finally:
@@ -446,8 +568,39 @@ def ngLayer(data, res, oo=[0, 0, 0], tt="segmentation"):
@app.post("/start_model_training")
-async def start_model_training(req: Request):
+async def start_model_training(
+ req: Request,
+ current_user: models.User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
body = await req.json()
+ workflow_id = body.get("workflow_id") or body.get("workflowId")
+ if workflow_id:
+ workflow = get_user_workflow_or_404(
+ db, workflow_id=int(workflow_id), user_id=current_user.id
+ )
+ update_workflow_fields(
+ db,
+ workflow,
+ {
+ "stage": "retraining_staged",
+ "training_output_path": body.get("outputPath"),
+ },
+ commit=True,
+ )
+ append_event_for_workflow_if_present(
+ db,
+ workflow_id=workflow.id,
+ actor="user",
+ event_type="training.started",
+ stage=workflow.stage,
+ summary="Started model training from the workflow.",
+ payload={
+ "outputPath": body.get("outputPath"),
+ "logPath": body.get("logPath"),
+ "configOriginPath": body.get("configOriginPath"),
+ },
+ )
worker_data = _proxy_to_worker(
"post",
"/start_model_training",
@@ -481,8 +634,42 @@ async def get_training_logs():
@app.post("/start_model_inference")
-async def start_model_inference(req: Request):
+async def start_model_inference(
+ req: Request,
+ current_user: models.User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
body = await req.json()
+ workflow_id = body.get("workflow_id") or body.get("workflowId")
+ if workflow_id:
+ workflow = get_user_workflow_or_404(
+ db, workflow_id=int(workflow_id), user_id=current_user.id
+ )
+ update_workflow_fields(
+ db,
+ workflow,
+ {
+ "stage": "inference",
+ "inference_output_path": body.get("outputPath"),
+ "checkpoint_path": (body.get("arguments") or {}).get("checkpoint")
+ or body.get("checkpointPath"),
+ },
+ commit=True,
+ )
+ append_event_for_workflow_if_present(
+ db,
+ workflow_id=workflow.id,
+ actor="user",
+ event_type="inference.started",
+ stage="inference",
+ summary="Started model inference from the workflow.",
+ payload={
+ "outputPath": body.get("outputPath"),
+ "checkpointPath": (body.get("arguments") or {}).get("checkpoint")
+ or body.get("checkpointPath"),
+ "configOriginPath": body.get("configOriginPath"),
+ },
+ )
worker_data = _proxy_to_worker(
"post",
"/start_model_inference",
@@ -709,16 +896,31 @@ async def chat_query(
user: models.User = Depends(get_current_user),
db: Session = Depends(get_db),
):
+ global _chatbot_error
+ request_id = request_id_from_request(req).replace("-", "")[:8]
+ request_start = time.perf_counter()
+ print(f"[CHATBOT][{request_id}] Incoming /chat/query request from user={user.id}")
if not _ensure_chatbot():
- detail = "Chatbot is not configured"
- if "_chatbot_error" in globals():
- detail = f"{detail}: {_chatbot_error}"
- raise HTTPException(status_code=503, detail=detail)
+ print(f"[CHATBOT][{request_id}] Main chain unavailable before request")
+ log_request_summary(
+ request_id=request_id,
+ endpoint="/chat/query",
+ start_time=request_start,
+ status="error",
+ error_type="llm_unavailable",
+ )
+ raise HTTPException(
+ status_code=503, detail=_llm_unavailable_detail(_chatbot_error)
+ )
body = await req.json()
query = body.get("query")
convo_id = body.get("conversationId")
if not isinstance(query, str) or not query.strip():
raise HTTPException(status_code=400, detail="Query must be a non-empty string.")
+ print(
+ f"[CHATBOT][{request_id}] Parsed request: convo_id={convo_id}, "
+ f"query_len={len(query.strip())}"
+ )
# Auto-create a conversation if none supplied
if not convo_id:
@@ -741,13 +943,48 @@ async def chat_query(
# Rebuild in-memory history from DB when switching conversations
_load_history_for_convo(convo_id, db)
+ print(
+ f"[CHATBOT][{request_id}] Loaded history messages={len(_chat_history)} "
+ f"for convo_id={convo_id}"
+ )
if _reset_search is not None:
_reset_search()
+ print(f"[CHATBOT][{request_id}] Reset documentation search call counter")
all_messages = _chat_history + [{"role": "user", "content": query}]
- result = chain.invoke({"messages": all_messages})
+ print(
+ f"[CHATBOT][{request_id}] Invoking main chain with "
+ f"{len(all_messages)} message(s)"
+ )
+ try:
+ result = _invoke_with_progress(
+ lambda: chain.invoke({"messages": all_messages}),
+ label="main chain invoke",
+ request_id=request_id,
+ )
+ except Exception as exc:
+ _chatbot_error = exc
+ print(
+ "[CHATBOT] LLM request failed: "
+ f"{exc.__class__.__name__}: {exc!r}"
+ )
+ traceback.print_exc()
+ log_request_summary(
+ request_id=request_id,
+ endpoint="/chat/query",
+ start_time=request_start,
+ status="error",
+ error_type=exc.__class__.__name__,
+ )
+ raise HTTPException(
+ status_code=503, detail=_llm_unavailable_detail(exc)
+ ) from exc
messages = result.get("messages", [])
response = messages[-1].content if messages else "No response generated"
+ print(
+ f"[CHATBOT][{request_id}] Chain returned messages={len(messages)}, "
+ f"response_len={len(response) if isinstance(response, str) else 0}"
+ )
# Persist to DB
db.add(models.ChatMessage(conversation_id=convo_id, role="user", content=query))
@@ -764,6 +1001,17 @@ async def chat_query(
# Update in-memory history
_chat_history.append({"role": "user", "content": query})
_chat_history.append({"role": "assistant", "content": response})
+ total_elapsed = time.perf_counter() - request_start
+ print(
+ f"[CHATBOT][{request_id}] /chat/query completed in {total_elapsed:.2f}s "
+ f"(convo_id={convo_id})"
+ )
+ log_request_summary(
+ request_id=request_id,
+ endpoint="/chat/query",
+ start_time=request_start,
+ status="ok",
+ )
return {"response": response, "conversationId": convo_id}
@@ -775,10 +1023,9 @@ async def clear_chat(
"""Reset the in-memory LangChain context (does NOT delete DB messages)."""
global _active_convo_id, _chat_history
if not _ensure_chatbot():
- detail = "Chatbot is not configured"
- if "_chatbot_error" in globals():
- detail = f"{detail}: {_chatbot_error}"
- raise HTTPException(status_code=503, detail=detail)
+ raise HTTPException(
+ status_code=503, detail=_llm_unavailable_detail(_chatbot_error)
+ )
if _reset_search is not None:
_reset_search()
_chat_history.clear()
@@ -804,21 +1051,36 @@ def _ensure_helper_chat(task_key: str):
"""Lazily build a helper agent for *task_key*, reusing it on subsequent calls."""
global _chatbot_error
if task_key in _helper_chains:
+ print(f"[CHATBOT] Reusing helper chain for task_key={task_key}")
return True
if build_helper_chain is None:
+ print("[CHATBOT] build_helper_chain is unavailable")
return False
+ start_time = time.perf_counter()
+ print(f"[CHATBOT] Initializing helper chain for task_key={task_key}...")
try:
agent, reset_fn = build_helper_chain()
_helper_chains[task_key] = (agent, reset_fn)
_helper_histories[task_key] = []
+ elapsed = time.perf_counter() - start_time
+ print(
+ f"[CHATBOT] Helper chain ready for task_key={task_key} in {elapsed:.2f}s"
+ )
return True
except Exception as exc:
_chatbot_error = exc
+ print(
+ "[CHATBOT] Failed to initialize helper LLM backend: "
+ f"{exc.__class__.__name__}: {exc!r}"
+ )
+ traceback.print_exc()
return False
@app.post("/chat/helper/query")
async def chat_helper_query(req: Request):
+ request_id = request_id_from_request(req).replace("-", "")[:8]
+ request_start = time.perf_counter()
body = await req.json()
task_key = body.get("taskKey")
query = body.get("query")
@@ -828,12 +1090,23 @@ async def chat_helper_query(req: Request):
raise HTTPException(status_code=400, detail="taskKey is required")
if not isinstance(query, str) or not query.strip():
raise HTTPException(status_code=400, detail="query must be a non-empty string.")
+ print(
+ f"[CHATBOT][{request_id}] Incoming /chat/helper/query "
+ f"task_key={task_key} query_len={len(query.strip())}"
+ )
if not _ensure_helper_chat(task_key):
- detail = "Helper chatbot is not configured"
- if "_chatbot_error" in globals():
- detail = f"{detail}: {_chatbot_error}"
- raise HTTPException(status_code=503, detail=detail)
+ print(f"[CHATBOT][{request_id}] Helper chain unavailable for task_key={task_key}")
+ log_request_summary(
+ request_id=request_id,
+ endpoint="/chat/helper/query",
+ start_time=request_start,
+ status="error",
+ error_type="llm_unavailable",
+ )
+ raise HTTPException(
+ status_code=503, detail=_llm_unavailable_detail(_chatbot_error)
+ )
agent, reset_fn = _helper_chains[task_key]
history = _helper_histories[task_key]
@@ -845,12 +1118,48 @@ async def chat_helper_query(req: Request):
)
reset_fn()
+ print(
+ f"[CHATBOT][{request_id}] Helper history_len={len(history)}; invoking helper "
+ f"with context_len={len(user_content)}"
+ )
all_messages = history + [{"role": "user", "content": user_content}]
- result = agent.invoke({"messages": all_messages})
+ try:
+ result = _invoke_with_progress(
+ lambda: agent.invoke({"messages": all_messages}),
+ label="helper chain invoke",
+ request_id=request_id,
+ )
+ except Exception as exc:
+ print(
+ "[CHATBOT] Helper LLM request failed: "
+ f"{exc.__class__.__name__}: {exc!r}"
+ )
+ traceback.print_exc()
+ log_request_summary(
+ request_id=request_id,
+ endpoint="/chat/helper/query",
+ start_time=request_start,
+ status="error",
+ error_type=exc.__class__.__name__,
+ )
+ raise HTTPException(
+ status_code=503, detail=_llm_unavailable_detail(exc)
+ ) from exc
messages = result.get("messages", [])
response = messages[-1].content if messages else "No response generated"
history.append({"role": "user", "content": user_content})
history.append({"role": "assistant", "content": response})
+ total_elapsed = time.perf_counter() - request_start
+ print(
+ f"[CHATBOT][{request_id}] /chat/helper/query completed in {total_elapsed:.2f}s "
+ f"response_len={len(response) if isinstance(response, str) else 0}"
+ )
+ log_request_summary(
+ request_id=request_id,
+ endpoint="/chat/helper/query",
+ start_time=request_start,
+ status="ok",
+ )
return {"response": response}
diff --git a/server_api/project_manager/__init__.py b/server_api/project_manager/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/server_api/project_manager/router.py b/server_api/project_manager/router.py
new file mode 100644
index 00000000..c2358ff2
--- /dev/null
+++ b/server_api/project_manager/router.py
@@ -0,0 +1,616 @@
+"""
+Project Manager — FastAPI router (v2 — Industrial Grade)
+
+Endpoints
+---------
+GET /api/pm/data Full PM state + computed global_progress
+POST /api/pm/data Overwrite full state (bulk save)
+POST /api/pm/data/reset Revert to seed data
+GET /api/pm/volumes Paginated volume list with optional filters
+PATCH /api/pm/volumes/{volume_id} Update a single volume's status
+"""
+
+from __future__ import annotations
+
+import json
+import math
+import pathlib
+from typing import Any, Dict, List, Optional
+
+from fastapi import APIRouter, HTTPException, Query, Request
+from pydantic import BaseModel
+
+router = APIRouter()
+
+# ── Paths ─────────────────────────────────────────────────────────────────────
+_HERE = pathlib.Path(__file__).resolve().parent
+_API_DIR = _HERE.parent
+_DATA_DIR = _API_DIR / "data_store"
+_DATA_FILE = _DATA_DIR / "project_manager_data.json"
+
+# ── Worker definitions (single source of truth) ───────────────────────────────
+_WORKERS = [
+ {"key": "alex", "name": "Alex Rivera", "avatarColor": "#1890ff"},
+ {"key": "jordan", "name": "Jordan Smith", "avatarColor": "#52c41a"},
+ {"key": "taylor", "name": "Sam Taylor", "avatarColor": "#fadb14"},
+ {"key": "morgan", "name": "Morgan Lee", "avatarColor": "#eb2f96"},
+]
+_WORKER_KEYS = [w["key"] for w in _WORKERS] # ["alex","jordan","taylor","morgan"]
+VOLUMES_PER_WORKER = 250
+TOTAL_VOLUMES = len(_WORKER_KEYS) * VOLUMES_PER_WORKER # 1 000
+
+
+def _generate_volumes() -> List[Dict[str, Any]]:
+ """Return 1 000 volume records distributed evenly across 4 workers."""
+ vols: List[Dict[str, Any]] = []
+ for idx in range(1, TOTAL_VOLUMES + 1):
+ worker_key = _WORKER_KEYS[(idx - 1) // VOLUMES_PER_WORKER]
+ vols.append(
+ {
+ "id": f"vol_{idx:03d}_em.h5",
+ "filename": f"vol_{idx:03d}_em.h5",
+ "assignee": worker_key,
+ "status": "todo", # todo | in_progress | done
+ }
+ )
+ return vols
+
+
+# ── Seed Data ─────────────────────────────────────────────────────────────────
+def _build_seed() -> Dict[str, Any]:
+ return {
+ # ── Volume catalogue ──────────────────────────────────────────────────
+ "volumes": _generate_volumes(),
+ # ── Weekly quota table (one row per worker) ───────────────────────────
+ "quota_data": [
+ {
+ "key": "1",
+ "name": "Alex Rivera",
+ "worker_key": "alex",
+ "datasets": ["Hippocampus_CA3", "Motor_Cortex_M1"],
+ "mon": 300,
+ "tue": 300,
+ "wed": 300,
+ "thu": 300,
+ "fri": 300,
+ "sat": 150,
+ "sun": 100,
+ "actualMon": 310,
+ "actualTue": 290,
+ "actualWed": 320,
+ "actualThu": 280,
+ "actualFri": 300,
+ "actualSat": 160,
+ "actualSun": 90,
+ },
+ {
+ "key": "2",
+ "name": "Jordan Smith",
+ "worker_key": "jordan",
+ "datasets": ["Cerebellum_PC"],
+ "mon": 250,
+ "tue": 250,
+ "wed": 250,
+ "thu": 250,
+ "fri": 250,
+ "sat": 0,
+ "sun": 0,
+ "actualMon": 240,
+ "actualTue": 260,
+ "actualWed": 230,
+ "actualThu": 250,
+ "actualFri": 220,
+ "actualSat": 0,
+ "actualSun": 0,
+ },
+ {
+ "key": "3",
+ "name": "Sam Taylor",
+ "worker_key": "taylor",
+ "datasets": ["Retina_GCL"],
+ "mon": 200,
+ "tue": 200,
+ "wed": 200,
+ "thu": 200,
+ "fri": 200,
+ "sat": 250,
+ "sun": 250,
+ "actualMon": 180,
+ "actualTue": 190,
+ "actualWed": 170,
+ "actualThu": 210,
+ "actualFri": 200,
+ "actualSat": 100,
+ "actualSun": 0,
+ },
+ {
+ "key": "4",
+ "name": "Morgan Lee",
+ "worker_key": "morgan",
+ "datasets": ["Visual_Cortex_V1"],
+ "mon": 240,
+ "tue": 240,
+ "wed": 240,
+ "thu": 240,
+ "fri": 240,
+ "sat": 0,
+ "sun": 0,
+ "actualMon": 250,
+ "actualTue": 260,
+ "actualWed": 270,
+ "actualThu": 240,
+ "actualFri": 230,
+ "actualSat": 0,
+ "actualSun": 0,
+ },
+ ],
+ # ── Proofreader progress cards (one per worker) ───────────────────────
+ "proofreader_data": [
+ {
+ "key": "1",
+ "name": "Alex Rivera",
+ "worker_key": "alex",
+ "avatarColor": "#1890ff",
+ "role": "Senior Annotator",
+ "totalPoints": 12450,
+ "weeklyPoints": 1420,
+ "weeklyQuota": 1750,
+ "accuracy": 98.5,
+ "lastActive": "2 mins ago",
+ "status": "online",
+ },
+ {
+ "key": "2",
+ "name": "Jordan Smith",
+ "worker_key": "jordan",
+ "avatarColor": "#52c41a",
+ "role": "Proofreader",
+ "totalPoints": 8900,
+ "weeklyPoints": 1100,
+ "weeklyQuota": 1500,
+ "accuracy": 94.2,
+ "lastActive": "15 mins ago",
+ "status": "online",
+ },
+ {
+ "key": "3",
+ "name": "Sam Taylor",
+ "worker_key": "taylor",
+ "avatarColor": "#fadb14",
+ "role": "Proofreader",
+ "totalPoints": 5600,
+ "weeklyPoints": 850,
+ "weeklyQuota": 1500,
+ "accuracy": 92.1,
+ "lastActive": "1 hour ago",
+ "status": "away",
+ },
+ {
+ "key": "4",
+ "name": "Morgan Lee",
+ "worker_key": "morgan",
+ "avatarColor": "#eb2f96",
+ "role": "Junior Annotator",
+ "totalPoints": 3200,
+ "weeklyPoints": 1250,
+ "weeklyQuota": 1200,
+ "accuracy": 96.8,
+ "lastActive": "3 hours ago",
+ "status": "offline",
+ },
+ ],
+ # ── Team throughput (last 7 days) ─────────────────────────────────────
+ "throughput_data": [
+ {"day": "Mon", "count": 4200},
+ {"day": "Tue", "count": 3800},
+ {"day": "Wed", "count": 5100},
+ {"day": "Thu", "count": 4700},
+ {"day": "Fri", "count": 5300},
+ {"day": "Sat", "count": 1200},
+ {"day": "Sun", "count": 900},
+ ],
+ # ── Dashboard datasets ────────────────────────────────────────────────
+ "datasets": [
+ {
+ "key": "1",
+ "name": "Hippocampus_CA3",
+ "experiment": "Synapse detection",
+ "total": 12450,
+ "proofread": 38,
+ "status": "in_progress",
+ "eta": "Jan 18",
+ "priority": "high",
+ },
+ {
+ "key": "2",
+ "name": "Motor_Cortex_M1",
+ "experiment": "Axon tracing",
+ "total": 9800,
+ "proofread": 72,
+ "status": "in_progress",
+ "eta": "Jan 22",
+ "priority": "high",
+ },
+ {
+ "key": "3",
+ "name": "Cerebellum_PC",
+ "experiment": "Dendrite segmentation",
+ "total": 6200,
+ "proofread": 91,
+ "status": "done",
+ "eta": "Complete",
+ "priority": "normal",
+ },
+ {
+ "key": "4",
+ "name": "Retina_GCL",
+ "experiment": "Cell classification",
+ "total": 3300,
+ "proofread": 5,
+ "status": "blocked",
+ "eta": "TBD",
+ "priority": "high",
+ },
+ {
+ "key": "5",
+ "name": "Olfactory_Bulb",
+ "experiment": "Glomeruli mapping",
+ "total": 7650,
+ "proofread": 0,
+ "status": "not_started",
+ "eta": "Feb 28",
+ "priority": "normal",
+ },
+ {
+ "key": "6",
+ "name": "Visual_Cortex_V1",
+ "experiment": "Spine detection",
+ "total": 15000,
+ "proofread": 55,
+ "status": "in_progress",
+ "eta": "Feb 10",
+ "priority": "normal",
+ },
+ ],
+ # ── Timeline / chart data ─────────────────────────────────────────────
+ "milestones": [
+ {"label": "Model v1", "date": "2025-11-15", "color": "#1890ff"},
+ {"label": "Mid-Review", "date": "2025-12-20", "color": "#722ed1"},
+ {"label": "Model v2", "date": "2026-01-28", "color": "#1890ff"},
+ {"label": "Data Freeze", "date": "2026-02-15", "color": "#fa8c16"},
+ {"label": "Model v3", "date": "2026-03-20", "color": "#1890ff"},
+ ],
+ "cumulative_data": [
+ 0,
+ 480,
+ 1100,
+ 1950,
+ 2800,
+ 3900,
+ 5100,
+ 6200,
+ 7450,
+ 8600,
+ 9900,
+ 11200,
+ 12600,
+ 13900,
+ 15400,
+ 17000,
+ 18500,
+ 20100,
+ 21800,
+ 23400,
+ 25000,
+ 26700,
+ 28500,
+ 30200,
+ 32000,
+ 33800,
+ ],
+ "cumulative_target": [
+ 0,
+ 600,
+ 1200,
+ 1800,
+ 2400,
+ 3000,
+ 3600,
+ 4800,
+ 6000,
+ 7200,
+ 8400,
+ 9600,
+ 10800,
+ 12000,
+ 13200,
+ 14400,
+ 15600,
+ 16800,
+ 18000,
+ 19200,
+ 20400,
+ 21600,
+ 22800,
+ 24000,
+ 25200,
+ 26400,
+ ],
+ "at_risk": [
+ {
+ "label": "Hippocampus_CA3",
+ "reason": "Low progress (38%)",
+ "icon": "progress",
+ },
+ {
+ "label": "Motor_Cortex_M1",
+ "reason": "Deadline approaching",
+ "icon": "clock",
+ },
+ {
+ "label": "Retina_GCL",
+ "reason": "Blocked — awaiting data",
+ "icon": "blocked",
+ },
+ ],
+ "upcoming_milestones": [
+ {"label": "Model v2 data freeze", "date": "JAN 28"},
+ {"label": "Mid-project review", "date": "FEB 5"},
+ {"label": "Final data freeze", "date": "FEB 15"},
+ {"label": "Model v3 launch", "date": "MAR 20"},
+ ],
+ "msg_preview": (
+ "Hi Team,\n\nI've just assigned the quotas for the upcoming week. "
+ "Please review your targets in the dashboard. Our goal for this week "
+ "is to maintain 95%+ accuracy while meeting the sample volume targets.\n\nGood luck!"
+ ),
+ # Workers list (mirrors _WORKERS, stored so frontend can read it)
+ "workers": _WORKERS,
+ # ── Users for login ───────────────────────────────────────────────────
+ "users": [
+ {
+ "username": "admin",
+ "password": "admin123",
+ "name": "Project Manager",
+ "role": "admin",
+ "key": "admin",
+ },
+ {
+ "username": "alex",
+ "password": "alex123",
+ "name": "Alex Rivera",
+ "role": "worker",
+ "key": "alex",
+ },
+ {
+ "username": "jordan",
+ "password": "jordan123",
+ "name": "Jordan Smith",
+ "role": "worker",
+ "key": "jordan",
+ },
+ {
+ "username": "taylor",
+ "password": "taylor123",
+ "name": "Sam Taylor",
+ "role": "worker",
+ "key": "taylor",
+ },
+ {
+ "username": "morgan",
+ "password": "morgan123",
+ "name": "Morgan Lee",
+ "role": "worker",
+ "key": "morgan",
+ },
+ ],
+ }
+
+
+_SEED: Dict[str, Any] = _build_seed()
+
+
+# ── Helpers ───────────────────────────────────────────────────────────────────
+
+
+def _ensure_data_dir() -> None:
+ _DATA_DIR.mkdir(parents=True, exist_ok=True)
+
+
+def _read_data() -> Dict[str, Any]:
+ _ensure_data_dir()
+ if not _DATA_FILE.exists():
+ _write_data(_SEED)
+ return _SEED
+ try:
+ return json.loads(_DATA_FILE.read_text(encoding="utf-8"))
+ except (json.JSONDecodeError, OSError) as exc:
+ raise HTTPException(
+ status_code=500, detail=f"Failed to read data store: {exc}"
+ ) from exc
+
+
+def _write_data(payload: Dict[str, Any]) -> None:
+ _ensure_data_dir()
+ tmp = _DATA_FILE.with_suffix(".tmp")
+ try:
+ tmp.write_text(
+ json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8"
+ )
+ tmp.replace(_DATA_FILE)
+ except OSError as exc:
+ raise HTTPException(
+ status_code=500, detail=f"Failed to write data store: {exc}"
+ ) from exc
+
+
+def _compute_global_progress(volumes: List[Dict[str, Any]]) -> Dict[str, Any]:
+ total = len(volumes)
+ done = sum(1 for v in volumes if v["status"] == "done")
+ in_progress = sum(1 for v in volumes if v["status"] == "in_progress")
+ todo = total - done - in_progress
+ pct = round((done / total) * 100, 1) if total else 0
+ # Per-worker breakdown
+ by_worker: Dict[str, Dict[str, int]] = {}
+ for wk in _WORKER_KEYS:
+ subset = [v for v in volumes if v["assignee"] == wk]
+ w_done = sum(1 for v in subset if v["status"] == "done")
+ w_ip = sum(1 for v in subset if v["status"] == "in_progress")
+ by_worker[wk] = {
+ "total": len(subset),
+ "done": w_done,
+ "in_progress": w_ip,
+ "todo": len(subset) - w_done - w_ip,
+ "pct": round((w_done / len(subset)) * 100, 1) if subset else 0,
+ }
+ return {
+ "total": total,
+ "done": done,
+ "in_progress": in_progress,
+ "todo": todo,
+ "pct": pct,
+ "by_worker": by_worker,
+ }
+
+
+# ── Pydantic models ───────────────────────────────────────────────────────────
+
+
+class VolumeStatusUpdate(BaseModel):
+ status: str # todo | in_progress | done
+
+
+class LoginRequest(BaseModel):
+ username: str
+ password: str
+
+
+# ── Endpoints ─────────────────────────────────────────────────────────────────
+
+
+@router.post("/login")
+async def login(body: LoginRequest):
+ """Simple credential matching for Project Manager login."""
+ data = _read_data()
+ users = data.get("users", [])
+
+ for user in users:
+ if user["username"] == body.username and user["password"] == body.password:
+ # Return user metadata without password
+ return {
+ "ok": True,
+ "user": {
+ "username": user["username"],
+ "name": user["name"],
+ "role": user["role"],
+ "key": user["key"],
+ },
+ }
+
+ raise HTTPException(status_code=401, detail="Invalid username or password")
+
+
+@router.get("/data")
+def get_pm_data():
+ """Return full PM state + computed global_progress (volumes excluded from body)."""
+ data = _read_data()
+ volumes = data.get("volumes", [])
+ # Attach computed progress but omit the large volumes array from this response
+ # (volumes are served via /api/pm/volumes with pagination)
+ result = {k: v for k, v in data.items() if k != "volumes"}
+ result["global_progress"] = _compute_global_progress(volumes)
+ return result
+
+
+@router.post("/data")
+async def save_pm_data(req: Request):
+ """Overwrite the PM state (bulk save from frontend debounced write)."""
+ try:
+ payload = await req.json()
+ except Exception as exc:
+ raise HTTPException(
+ status_code=400, detail=f"Invalid JSON body: {exc}"
+ ) from exc
+ # Preserve volumes from current file if payload doesn't include them
+ current = _read_data()
+ if "volumes" not in payload:
+ payload["volumes"] = current.get("volumes", _SEED["volumes"])
+ _write_data(payload)
+ return {"ok": True}
+
+
+@router.post("/data/reset")
+def reset_pm_data():
+ """Revert the data file to original seed and return state (without volumes)."""
+ _write_data(_SEED)
+ result = {k: v for k, v in _SEED.items() if k != "volumes"}
+ result["global_progress"] = _compute_global_progress(_SEED["volumes"])
+ return result
+
+
+# ── Volume endpoints ──────────────────────────────────────────────────────────
+
+
+@router.get("/volumes")
+def get_volumes(
+ assignee: Optional[str] = Query(None, description="Filter by worker key"),
+ status: Optional[str] = Query(
+ None, description="Filter by status: todo|in_progress|done"
+ ),
+ page: int = Query(1, ge=1),
+ page_size: int = Query(50, ge=1, le=200),
+):
+ """Return paginated volume list with optional assignee/status filters."""
+ data = _read_data()
+ volumes: List[Dict[str, Any]] = data.get("volumes", [])
+
+ # Apply filters
+ if assignee:
+ volumes = [v for v in volumes if v["assignee"] == assignee]
+ if status:
+ volumes = [v for v in volumes if v["status"] == status]
+
+ total = len(volumes)
+ total_pages = max(1, math.ceil(total / page_size))
+ start = (page - 1) * page_size
+ items = volumes[start : start + page_size]
+
+ return {
+ "total": total,
+ "page": page,
+ "page_size": page_size,
+ "total_pages": total_pages,
+ "items": items,
+ }
+
+
+@router.patch("/volumes/{volume_id}")
+async def update_volume_status(volume_id: str, body: VolumeStatusUpdate):
+ """Update a single volume's status atomically."""
+ valid_statuses = {"todo", "in_progress", "done"}
+ if body.status not in valid_statuses:
+ raise HTTPException(
+ status_code=400, detail=f"status must be one of {valid_statuses}"
+ )
+
+ data = _read_data()
+ volumes: List[Dict[str, Any]] = data.get("volumes", [])
+
+ for vol in volumes:
+ if vol["id"] == volume_id:
+ vol["status"] = body.status
+ data["volumes"] = volumes
+ _write_data(data)
+ return {
+ "ok": True,
+ "volume": vol,
+ "global_progress": _compute_global_progress(volumes),
+ }
+
+ raise HTTPException(status_code=404, detail=f"Volume '{volume_id}' not found")
+
+
+@router.get("/workers")
+def get_workers():
+ """Return the list of defined workers."""
+ return {"workers": _WORKERS}
diff --git a/server_api/workflows/__init__.py b/server_api/workflows/__init__.py
new file mode 100644
index 00000000..d03ab027
--- /dev/null
+++ b/server_api/workflows/__init__.py
@@ -0,0 +1,2 @@
+"""Workflow spine package for iterative segmentation sessions."""
+
diff --git a/server_api/workflows/bundle_export.py b/server_api/workflows/bundle_export.py
new file mode 100644
index 00000000..1abe8be1
--- /dev/null
+++ b/server_api/workflows/bundle_export.py
@@ -0,0 +1,72 @@
+from __future__ import annotations
+
+import os
+from datetime import datetime, timezone
+from typing import Any, Dict, Iterable, List, Tuple
+
+from .db_models import WorkflowEvent, WorkflowSession
+from .service import event_to_dict, workflow_to_dict
+
+
+def _parse_timestamp(value: Any) -> datetime:
+ if isinstance(value, datetime):
+ return value
+ text = str(value or "1970-01-01T00:00:00+00:00")
+ if text.endswith("Z"):
+ text = text.replace("Z", "+00:00")
+ return datetime.fromisoformat(text)
+
+
+def _event_sort_key(event: Dict[str, Any]) -> Tuple[datetime, str]:
+ timestamp = event.get("created_at")
+ event_id = str(event.get("id") or "")
+ return (_parse_timestamp(timestamp), event_id)
+
+
+def _normalize_value(value: Any) -> Any:
+ if isinstance(value, datetime):
+ return value.isoformat()
+ if isinstance(value, dict):
+ return {k: _normalize_value(v) for k, v in value.items()}
+ if isinstance(value, list):
+ return [_normalize_value(item) for item in value]
+ return value
+
+
+def _collect_paths(value: Any) -> Iterable[str]:
+ if isinstance(value, dict):
+ for key, inner in value.items():
+ if isinstance(inner, str) and key.endswith("_path"):
+ yield inner
+ if key == "path" and isinstance(inner, str):
+ yield inner
+ yield from _collect_paths(inner)
+ elif isinstance(value, list):
+ for item in value:
+ yield from _collect_paths(item)
+
+
+def build_export_bundle(
+ workflow: WorkflowSession,
+ events: List[WorkflowEvent],
+) -> Dict[str, Any]:
+ session_snapshot = _normalize_value(workflow_to_dict(workflow))
+ ordered_events = sorted(
+ (_normalize_value(event_to_dict(event)) for event in events),
+ key=_event_sort_key,
+ )
+
+ discovered = set(_collect_paths(session_snapshot))
+ for event in ordered_events:
+ discovered.update(_collect_paths(event))
+ artifact_paths = sorted(path for path in discovered if path)
+ artifacts = [{"path": path, "exists": os.path.exists(path)} for path in artifact_paths]
+
+ return {
+ "schema_version": "workflow-export-bundle/v1",
+ "exported_at": datetime.now(timezone.utc).isoformat(),
+ "workflow_id": workflow.id,
+ "session_snapshot": session_snapshot,
+ "events": ordered_events,
+ "artifact_paths": artifacts,
+ }
diff --git a/server_api/workflows/db_models.py b/server_api/workflows/db_models.py
new file mode 100644
index 00000000..f7d2b56b
--- /dev/null
+++ b/server_api/workflows/db_models.py
@@ -0,0 +1,55 @@
+from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text
+from sqlalchemy.orm import relationship
+from sqlalchemy.sql import func
+
+from server_api.auth.database import Base
+
+
+class WorkflowSession(Base):
+ __tablename__ = "workflow_sessions"
+
+ id = Column(Integer, primary_key=True, index=True)
+ user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
+ title = Column(String, default="Segmentation Workflow")
+ stage = Column(String, default="setup", index=True)
+ dataset_path = Column(String, nullable=True)
+ image_path = Column(String, nullable=True)
+ label_path = Column(String, nullable=True)
+ mask_path = Column(String, nullable=True)
+ neuroglancer_url = Column(Text, nullable=True)
+ inference_output_path = Column(String, nullable=True)
+ checkpoint_path = Column(String, nullable=True)
+ proofreading_session_id = Column(Integer, nullable=True, index=True)
+ corrected_mask_path = Column(String, nullable=True)
+ training_output_path = Column(String, nullable=True)
+ metadata_json = Column(Text, nullable=True)
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
+ updated_at = Column(
+ DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
+ )
+
+ events = relationship(
+ "WorkflowEvent",
+ back_populates="workflow",
+ cascade="all, delete-orphan",
+ order_by="WorkflowEvent.created_at",
+ )
+
+
+class WorkflowEvent(Base):
+ __tablename__ = "workflow_events"
+
+ id = Column(Integer, primary_key=True, index=True)
+ workflow_id = Column(
+ Integer, ForeignKey("workflow_sessions.id"), nullable=False, index=True
+ )
+ actor = Column(String, default="system", index=True)
+ event_type = Column(String, nullable=False, index=True)
+ stage = Column(String, nullable=True, index=True)
+ summary = Column(Text, nullable=False)
+ payload_json = Column(Text, nullable=True)
+ approval_status = Column(String, default="not_required", index=True)
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
+
+ workflow = relationship("WorkflowSession", back_populates="events")
+
diff --git a/server_api/workflows/evidence_export.py b/server_api/workflows/evidence_export.py
new file mode 100644
index 00000000..ca51394f
--- /dev/null
+++ b/server_api/workflows/evidence_export.py
@@ -0,0 +1,102 @@
+from __future__ import annotations
+
+from collections import Counter
+from typing import Any, Dict, List
+
+from .db_models import WorkflowEvent, WorkflowSession
+from .service import decode_json
+
+EVIDENCE_EXPORT_VERSION = "1.0"
+
+
+def _iso(value: Any) -> str | None:
+ if value is None:
+ return None
+ if hasattr(value, "isoformat"):
+ return value.isoformat()
+ return str(value)
+
+
+def _stage_progression(events: List[WorkflowEvent]) -> Dict[str, Any]:
+ observed: List[str] = []
+ entered_at: Dict[str, str] = {}
+ transition_count = 0
+ previous_stage: str | None = None
+
+ for event in events:
+ stage = event.stage
+ if not stage:
+ continue
+ if stage not in observed:
+ observed.append(stage)
+ if stage not in entered_at:
+ entered_at[stage] = _iso(event.created_at) or ""
+ if previous_stage and previous_stage != stage:
+ transition_count += 1
+ previous_stage = stage
+
+ return {
+ "observed_stages": observed,
+ "entered_at": entered_at,
+ "transition_count": transition_count,
+ }
+
+
+def _proposal_approval_summary(events: List[WorkflowEvent]) -> Dict[str, Any]:
+ proposals_by_actor: Counter[str] = Counter()
+ decisions_by_actor: Counter[str] = Counter()
+ approved = 0
+ rejected = 0
+
+ for event in events:
+ if event.event_type == "agent.proposal_created":
+ proposals_by_actor[event.actor or "unknown"] += 1
+ elif event.event_type == "agent.proposal_approved":
+ decisions_by_actor[event.actor or "unknown"] += 1
+ approved += 1
+ elif event.event_type == "agent.proposal_rejected":
+ decisions_by_actor[event.actor or "unknown"] += 1
+ rejected += 1
+
+ return {
+ "proposal_count": int(sum(proposals_by_actor.values())),
+ "approved_count": approved,
+ "rejected_count": rejected,
+ "proposals_by_actor": dict(proposals_by_actor),
+ "decisions_by_actor": dict(decisions_by_actor),
+ }
+
+
+def _timeline_snippet(events: List[WorkflowEvent], max_events: int = 20) -> List[Dict[str, Any]]:
+ snippet: List[Dict[str, Any]] = []
+ for event in events[:max_events]:
+ payload = decode_json(event.payload_json)
+ snippet.append(
+ {
+ "timestamp": _iso(event.created_at),
+ "event_type": event.event_type,
+ "stage": event.stage,
+ "actor": event.actor,
+ "summary": event.summary,
+ "payload_keys": sorted(payload.keys()),
+ }
+ )
+ return snippet
+
+
+def build_workflow_evidence_export(
+ workflow: WorkflowSession,
+ events: List[WorkflowEvent],
+) -> Dict[str, Any]:
+ stage_progression = _stage_progression(events)
+ proposal_summary = _proposal_approval_summary(events)
+ timeline = _timeline_snippet(events)
+
+ return {
+ "version": EVIDENCE_EXPORT_VERSION,
+ "workflow_id": workflow.id,
+ "workflow_stage": workflow.stage,
+ "stage_progression_summary": stage_progression,
+ "agent_proposal_approval_summary": proposal_summary,
+ "key_event_timeline_snippet": timeline,
+ }
diff --git a/server_api/workflows/metrics.py b/server_api/workflows/metrics.py
new file mode 100644
index 00000000..8012d1ad
--- /dev/null
+++ b/server_api/workflows/metrics.py
@@ -0,0 +1,68 @@
+from __future__ import annotations
+
+from collections import Counter
+from datetime import datetime
+from typing import Any, Dict, List
+
+from .db_models import WorkflowEvent
+
+
+def _to_iso8601(value: Any) -> str | None:
+ if value is None:
+ return None
+ if isinstance(value, datetime):
+ return value.isoformat()
+ return str(value)
+
+
+def compute_workflow_metrics(events: List[WorkflowEvent]) -> Dict[str, Any]:
+ event_type_counts: Counter[str] = Counter()
+ stage_transition_counts: Counter[str] = Counter()
+
+ approvals = 0
+ rejections = 0
+ first_timestamp: Any = None
+ last_timestamp: Any = None
+ last_stage: str | None = None
+
+ for event in events:
+ event_type = event.event_type or "unknown"
+ event_type_counts[event_type] += 1
+
+ if event_type == "agent.proposal_approved":
+ approvals += 1
+ if event_type == "agent.proposal_rejected":
+ rejections += 1
+
+ stage = event.stage
+ if stage:
+ if last_stage and last_stage != stage:
+ stage_transition_counts[f"{last_stage}->{stage}"] += 1
+ last_stage = stage
+
+ timestamp = event.created_at
+ if first_timestamp is None or (timestamp and timestamp < first_timestamp):
+ first_timestamp = timestamp
+ if last_timestamp is None or (timestamp and timestamp > last_timestamp):
+ last_timestamp = timestamp
+
+ total_decisions = approvals + rejections
+ approval_rate = (approvals / total_decisions) if total_decisions else 0.0
+ rejection_rate = (rejections / total_decisions) if total_decisions else 0.0
+
+ return {
+ "event_counts": dict(event_type_counts),
+ "decision_metrics": {
+ "approvals": approvals,
+ "rejections": rejections,
+ "approval_rate": approval_rate,
+ "rejection_rate": rejection_rate,
+ "total_decisions": total_decisions,
+ },
+ "stage_transition_counts": dict(stage_transition_counts),
+ "timestamps": {
+ "first_event_at": _to_iso8601(first_timestamp),
+ "last_event_at": _to_iso8601(last_timestamp),
+ },
+ "total_events": len(events),
+ }
diff --git a/server_api/workflows/router.py b/server_api/workflows/router.py
new file mode 100644
index 00000000..5bcfe801
--- /dev/null
+++ b/server_api/workflows/router.py
@@ -0,0 +1,848 @@
+from __future__ import annotations
+
+from datetime import datetime, timezone
+from typing import Any, Dict, List, Optional
+
+from fastapi import APIRouter, Depends, HTTPException
+from pydantic import BaseModel, Field
+from sqlalchemy.orm import Session
+
+from server_api.auth import models as auth_models
+from server_api.auth.database import get_db
+from server_api.auth.router import get_current_user
+
+from .db_models import WorkflowEvent, WorkflowSession
+from .service import (
+ append_workflow_event,
+ decode_json,
+ event_to_dict,
+ get_current_or_create_workflow,
+ get_user_workflow_or_404,
+ update_workflow_fields,
+ validate_stage,
+ workflow_to_dict,
+)
+from .bundle_export import build_export_bundle
+from .metrics import compute_workflow_metrics
+
+router = APIRouter()
+
+
+class WorkflowResponse(BaseModel):
+ id: int
+ user_id: int
+ title: Optional[str] = None
+ stage: str
+ dataset_path: Optional[str] = None
+ image_path: Optional[str] = None
+ label_path: Optional[str] = None
+ mask_path: Optional[str] = None
+ neuroglancer_url: Optional[str] = None
+ inference_output_path: Optional[str] = None
+ checkpoint_path: Optional[str] = None
+ proofreading_session_id: Optional[int] = None
+ corrected_mask_path: Optional[str] = None
+ training_output_path: Optional[str] = None
+ metadata_json: Optional[str] = None
+ metadata: Dict[str, Any] = Field(default_factory=dict)
+ created_at: Any
+ updated_at: Any
+
+
+class WorkflowEventResponse(BaseModel):
+ id: int
+ workflow_id: int
+ actor: str
+ event_type: str
+ stage: Optional[str] = None
+ summary: str
+ payload_json: Optional[str] = None
+ payload: Dict[str, Any] = Field(default_factory=dict)
+ approval_status: str
+ created_at: Any
+
+
+class WorkflowDetailResponse(BaseModel):
+ workflow: WorkflowResponse
+ events: List[WorkflowEventResponse]
+
+
+class WorkflowUpdateRequest(BaseModel):
+ title: Optional[str] = None
+ stage: Optional[str] = None
+ dataset_path: Optional[str] = None
+ image_path: Optional[str] = None
+ label_path: Optional[str] = None
+ mask_path: Optional[str] = None
+ neuroglancer_url: Optional[str] = None
+ inference_output_path: Optional[str] = None
+ checkpoint_path: Optional[str] = None
+ proofreading_session_id: Optional[int] = None
+ corrected_mask_path: Optional[str] = None
+ training_output_path: Optional[str] = None
+ metadata: Optional[Dict[str, Any]] = None
+
+
+class WorkflowEventCreateRequest(BaseModel):
+ actor: str = "system"
+ event_type: str
+ stage: Optional[str] = None
+ summary: str
+ payload: Optional[Dict[str, Any]] = None
+ approval_status: str = "not_required"
+
+
+class AgentActionCreateRequest(BaseModel):
+ action: str
+ summary: Optional[str] = None
+ payload: Dict[str, Any] = Field(default_factory=dict)
+
+
+class AgentQueryRequest(BaseModel):
+ query: str
+
+
+class AgentQueryResponse(BaseModel):
+ response: str
+ proposals: List[WorkflowEventResponse] = Field(default_factory=list)
+
+
+class AgentActionResult(BaseModel):
+ workflow: WorkflowResponse
+ proposal: WorkflowEventResponse
+ events: List[WorkflowEventResponse]
+ client_effects: Dict[str, Any] = Field(default_factory=dict)
+
+
+class WorkflowHotspotItem(BaseModel):
+ rank: int
+ region_key: str
+ score: float
+ severity: str
+ summary: str
+ recommended_action: str
+ evidence: Dict[str, Any] = Field(default_factory=dict)
+
+
+class WorkflowHotspotsResponse(BaseModel):
+ workflow_id: int
+ generated_at: str
+ hotspots: List[WorkflowHotspotItem] = Field(default_factory=list)
+
+
+class WorkflowImpactPreviewResponse(BaseModel):
+ workflow_id: int
+ generated_at: str
+ can_stage_retraining: bool
+ recommended_stage: str
+ corrected_mask_path: Optional[str] = None
+ confidence: str
+ projected_improvement: float
+ summary: str
+ signals: Dict[str, int] = Field(default_factory=dict)
+ next_actions: List[str] = Field(default_factory=list)
+
+
+class WorkflowMetricsResponse(BaseModel):
+ workflow_id: int
+ metrics: Dict[str, Any] = Field(default_factory=dict)
+
+
+class WorkflowExportBundleResponse(BaseModel):
+ schema_version: str
+ exported_at: str
+ workflow_id: int
+ session_snapshot: Dict[str, Any] = Field(default_factory=dict)
+ events: List[Dict[str, Any]] = Field(default_factory=list)
+ artifact_paths: List[Dict[str, Any]] = Field(default_factory=list)
+
+
+def _workflow_response(workflow: WorkflowSession) -> WorkflowResponse:
+ return WorkflowResponse(**workflow_to_dict(workflow))
+
+
+def _event_response(event: WorkflowEvent) -> WorkflowEventResponse:
+ return WorkflowEventResponse(**event_to_dict(event))
+
+
+def _event_list(db: Session, workflow_id: int) -> List[WorkflowEventResponse]:
+ events = (
+ db.query(WorkflowEvent)
+ .filter(WorkflowEvent.workflow_id == workflow_id)
+ .order_by(WorkflowEvent.created_at.asc(), WorkflowEvent.id.asc())
+ .all()
+ )
+ return [_event_response(event) for event in events]
+
+
+def _event_rows(db: Session, workflow_id: int) -> List[WorkflowEvent]:
+ return (
+ db.query(WorkflowEvent)
+ .filter(WorkflowEvent.workflow_id == workflow_id)
+ .order_by(WorkflowEvent.created_at.asc(), WorkflowEvent.id.asc())
+ .all()
+ )
+
+
+def _get_pending_proposal_or_404(
+ db: Session, *, workflow_id: int, event_id: int
+) -> WorkflowEvent:
+ event = (
+ db.query(WorkflowEvent)
+ .filter(
+ WorkflowEvent.id == event_id,
+ WorkflowEvent.workflow_id == workflow_id,
+ WorkflowEvent.event_type == "agent.proposal_created",
+ )
+ .first()
+ )
+ if not event:
+ raise HTTPException(status_code=404, detail="Agent proposal not found")
+ if event.approval_status != "pending":
+ raise HTTPException(status_code=400, detail="Agent proposal is not pending")
+ return event
+
+
+def _latest_exported_mask_path(db: Session, workflow_id: int) -> Optional[str]:
+ event = (
+ db.query(WorkflowEvent)
+ .filter(
+ WorkflowEvent.workflow_id == workflow_id,
+ WorkflowEvent.event_type == "proofreading.masks_exported",
+ )
+ .order_by(WorkflowEvent.created_at.desc(), WorkflowEvent.id.desc())
+ .first()
+ )
+ if not event:
+ return None
+ payload = decode_json(event.payload_json)
+ return payload.get("written_path") or payload.get("output_path")
+
+
+def _proposal_action_payload(proposal: WorkflowEvent) -> Dict[str, Any]:
+ payload = decode_json(proposal.payload_json)
+ params = payload.get("params")
+ if not isinstance(params, dict):
+ params = {}
+ return {"action": payload.get("action"), "params": params}
+
+
+NEGATIVE_CLASSIFICATIONS = {
+ "incorrect",
+ "uncertain",
+ "error",
+ "false_positive",
+ "false_negative",
+ "needs_review",
+}
+
+
+def _region_key_from_payload(payload: Dict[str, Any]) -> Optional[str]:
+ for key in ("region_key", "region_id", "region"):
+ value = payload.get(key)
+ if isinstance(value, (str, int, float)):
+ return str(value)
+
+ instance_id = payload.get("instance_id")
+ if isinstance(instance_id, (str, int)):
+ return f"instance:{instance_id}"
+
+ instance_ids = payload.get("instance_ids")
+ if isinstance(instance_ids, list) and instance_ids:
+ first = instance_ids[0]
+ if isinstance(first, (str, int)):
+ return f"instance:{first}"
+
+ axis = payload.get("axis")
+ z_index = payload.get("z_index")
+ if axis is not None and z_index is not None:
+ return f"{axis}:{z_index}"
+ if z_index is not None:
+ return f"z:{z_index}"
+ return None
+
+
+def _hotspot_severity(score: float) -> str:
+ if score >= 8:
+ return "high"
+ if score >= 4:
+ return "medium"
+ return "low"
+
+
+def _default_region_action(workflow: WorkflowSession, severity: str) -> str:
+ if workflow.stage == "proofreading":
+ return "Open this region in proofreading and apply mask corrections."
+ if severity == "high":
+ return "Prioritize this region for proofreading before the next training iteration."
+ if workflow.stage in {"visualization", "inference"}:
+ return "Inspect this region in visualization and route it into proofreading."
+ return "Inspect this region and log whether model output is acceptable."
+
+
+def _compute_hotspots(
+ workflow: WorkflowSession,
+ events: List[WorkflowEvent],
+) -> List[WorkflowHotspotItem]:
+ region_stats: Dict[str, Dict[str, Any]] = {}
+
+ def ensure_region(region_key: str) -> Dict[str, Any]:
+ stat = region_stats.get(region_key)
+ if stat:
+ return stat
+ stat = {
+ "raw_score": 0.0,
+ "inference_failures": 0,
+ "classifications": 0,
+ "negative_classifications": 0,
+ "mask_saves": 0,
+ "exports": 0,
+ "last_event_index": -1,
+ }
+ region_stats[region_key] = stat
+ return stat
+
+ for index, event in enumerate(events):
+ payload = decode_json(event.payload_json)
+ region_key = _region_key_from_payload(payload)
+ stat = ensure_region(region_key) if region_key else None
+
+ if event.event_type == "inference.failed" and stat:
+ stat["raw_score"] += 4.0
+ stat["inference_failures"] += 1
+ stat["last_event_index"] = index
+
+ if event.event_type == "proofreading.instance_classified" and stat:
+ classification = str(payload.get("classification") or "").lower()
+ if classification in NEGATIVE_CLASSIFICATIONS:
+ stat["raw_score"] += 2.5
+ stat["negative_classifications"] += 1
+ else:
+ stat["raw_score"] += 1.0
+ stat["classifications"] += 1
+ stat["last_event_index"] = index
+
+ if event.event_type == "proofreading.mask_saved" and stat:
+ stat["raw_score"] += 3.0
+ stat["mask_saves"] += 1
+ stat["last_event_index"] = index
+
+ if event.event_type == "proofreading.masks_exported":
+ if stat:
+ stat["raw_score"] += 1.0
+ stat["exports"] += 1
+ stat["last_event_index"] = index
+ else:
+ # Export happened without region metadata; keep evidence visible.
+ global_region = ensure_region("current_view")
+ global_region["raw_score"] += 1.0
+ global_region["exports"] += 1
+ global_region["last_event_index"] = index
+
+ if not region_stats:
+ region_stats["current_view"] = {
+ "raw_score": 1.0,
+ "inference_failures": 0,
+ "classifications": 0,
+ "negative_classifications": 0,
+ "mask_saves": 0,
+ "exports": 0,
+ "last_event_index": len(events) - 1,
+ }
+
+ total_events = max(len(events), 1)
+ ranked: List[WorkflowHotspotItem] = []
+ for region_key, stat in region_stats.items():
+ distance = max(0, (total_events - 1) - stat["last_event_index"])
+ recency_bonus = max(0.0, 1.5 - (distance * 0.25))
+ score = round(float(stat["raw_score"] + recency_bonus), 2)
+ severity = _hotspot_severity(score)
+
+ summary = (
+ f"{region_key}: {stat['inference_failures']} inference failures, "
+ f"{stat['mask_saves']} mask edits, "
+ f"{stat['negative_classifications']} uncertain/incorrect classifications."
+ )
+ recommended_action = _default_region_action(workflow, severity)
+ if stat["exports"] > 0 and workflow.stage != "retraining_staged":
+ recommended_action = (
+ "Corrections already exist; stage this region's masks for retraining."
+ )
+
+ ranked.append(
+ WorkflowHotspotItem(
+ rank=0,
+ region_key=region_key,
+ score=score,
+ severity=severity,
+ summary=summary,
+ recommended_action=recommended_action,
+ evidence={
+ "inference_failures": stat["inference_failures"],
+ "classifications": stat["classifications"],
+ "negative_classifications": stat["negative_classifications"],
+ "mask_saves": stat["mask_saves"],
+ "exports": stat["exports"],
+ },
+ )
+ )
+
+ ranked.sort(key=lambda item: item.score, reverse=True)
+ return [
+ item.model_copy(update={"rank": index + 1}) for index, item in enumerate(ranked)
+ ]
+
+
+def _compute_impact_preview(
+ workflow: WorkflowSession,
+ events: List[WorkflowEvent],
+ hotspots: List[WorkflowHotspotItem],
+ corrected_mask_path: Optional[str],
+) -> WorkflowImpactPreviewResponse:
+ signals = {
+ "inference_started": 0,
+ "inference_completed": 0,
+ "inference_failed": 0,
+ "proofreading_classified": 0,
+ "proofreading_mask_saved": 0,
+ "proofreading_masks_exported": 0,
+ "pending_agent_proposals": 0,
+ }
+ for event in events:
+ if event.event_type == "inference.started":
+ signals["inference_started"] += 1
+ if event.event_type == "inference.completed":
+ signals["inference_completed"] += 1
+ if event.event_type == "inference.failed":
+ signals["inference_failed"] += 1
+ if event.event_type == "proofreading.instance_classified":
+ signals["proofreading_classified"] += 1
+ if event.event_type == "proofreading.mask_saved":
+ signals["proofreading_mask_saved"] += 1
+ if event.event_type == "proofreading.masks_exported":
+ signals["proofreading_masks_exported"] += 1
+ if (
+ event.event_type == "agent.proposal_created"
+ and event.approval_status == "pending"
+ ):
+ signals["pending_agent_proposals"] += 1
+
+ correction_signal = (
+ (signals["proofreading_classified"] * 2)
+ + (signals["proofreading_mask_saved"] * 3)
+ + (signals["proofreading_masks_exported"] * 5)
+ )
+ failure_signal = signals["inference_failed"] * 3
+ hotspot_signal = len([item for item in hotspots if item.severity == "high"]) * 4
+ projected_improvement = min(
+ 0.95,
+ round(
+ 0.05
+ + (correction_signal * 0.03)
+ + (failure_signal * 0.02)
+ + (hotspot_signal * 0.01),
+ 3,
+ ),
+ )
+
+ confidence = "low"
+ if correction_signal >= 8 and signals["proofreading_masks_exported"] > 0:
+ confidence = "high"
+ elif correction_signal >= 3:
+ confidence = "medium"
+
+ can_stage_retraining = (
+ bool(corrected_mask_path) and workflow.stage != "retraining_staged"
+ )
+ recommended_stage = workflow.stage
+ if can_stage_retraining:
+ recommended_stage = "retraining_staged"
+ elif workflow.stage == "retraining_staged":
+ recommended_stage = "evaluation"
+
+ next_actions: List[str] = []
+ if hotspots:
+ next_actions.append(hotspots[0].recommended_action)
+ if not corrected_mask_path:
+ next_actions.append(
+ "Export corrected masks from proofreading to create a retraining artifact."
+ )
+ elif can_stage_retraining:
+ next_actions.append("Approve or trigger retraining staging from corrected masks.")
+ if workflow.stage == "retraining_staged":
+ next_actions.append(
+ "Open Model Training and launch the next experiment using staged labels."
+ )
+
+ if confidence == "low":
+ summary = (
+ "Correction evidence is still sparse; prioritize proofreading edits before retraining."
+ )
+ elif can_stage_retraining:
+ summary = (
+ "Corrections are substantial enough to justify retraining staging for the next model iteration."
+ )
+ else:
+ summary = (
+ "Correction evidence is accumulating; compare outcomes after the next staged loop."
+ )
+
+ return WorkflowImpactPreviewResponse(
+ workflow_id=workflow.id,
+ generated_at=datetime.now(timezone.utc).isoformat(),
+ can_stage_retraining=can_stage_retraining,
+ recommended_stage=recommended_stage,
+ corrected_mask_path=corrected_mask_path,
+ confidence=confidence,
+ projected_improvement=projected_improvement,
+ summary=summary,
+ signals=signals,
+ next_actions=next_actions,
+ )
+
+
+def _recommendation_for_workflow(
+ workflow: WorkflowSession,
+ events: List[WorkflowEventResponse],
+) -> str:
+ if workflow.stage == "setup":
+ return "Start by loading an image volume and, if available, the current mask or label volume."
+ if workflow.stage == "visualization":
+ return "The next useful step is to run inference or open proofreading on the current result."
+ if workflow.stage == "inference":
+ return "Review the inference output and send likely failure regions into proofreading."
+ if workflow.stage == "proofreading":
+ has_export = any(
+ event.event_type == "proofreading.masks_exported" for event in events
+ )
+ if has_export or workflow.corrected_mask_path:
+ return "Corrected masks are available. Stage them for retraining so the next model iteration is linked to the edits."
+ return "Continue classifying instances and save or export corrected masks before retraining."
+ if workflow.stage == "retraining_staged":
+ return "The corrected masks are staged. Review the training configuration before launching retraining."
+ return "Review the workflow timeline and compare results before starting another iteration."
+
+
+@router.get("/current", response_model=WorkflowDetailResponse)
+def get_current_workflow(
+ user: auth_models.User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ workflow = get_current_or_create_workflow(db, user_id=user.id)
+ return {
+ "workflow": _workflow_response(workflow),
+ "events": _event_list(db, workflow.id),
+ }
+
+
+@router.patch("/{workflow_id}", response_model=WorkflowResponse)
+async def update_workflow(
+ workflow_id: int,
+ body: WorkflowUpdateRequest,
+ user: auth_models.User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ workflow = get_user_workflow_or_404(db, workflow_id=workflow_id, user_id=user.id)
+ updates = body.model_dump(exclude_unset=True)
+ workflow = update_workflow_fields(db, workflow, updates, commit=True)
+ return _workflow_response(workflow)
+
+
+@router.get("/{workflow_id}/events", response_model=List[WorkflowEventResponse])
+def list_workflow_events(
+ workflow_id: int,
+ user: auth_models.User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ get_user_workflow_or_404(db, workflow_id=workflow_id, user_id=user.id)
+ return _event_list(db, workflow_id)
+
+
+@router.get("/{workflow_id}/hotspots", response_model=WorkflowHotspotsResponse)
+def get_workflow_hotspots(
+ workflow_id: int,
+ user: auth_models.User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ workflow = get_user_workflow_or_404(db, workflow_id=workflow_id, user_id=user.id)
+ events = _event_rows(db, workflow.id)
+ hotspots = _compute_hotspots(workflow, events)
+ return WorkflowHotspotsResponse(
+ workflow_id=workflow.id,
+ generated_at=datetime.now(timezone.utc).isoformat(),
+ hotspots=hotspots,
+ )
+
+
+@router.get(
+ "/{workflow_id}/impact-preview",
+ response_model=WorkflowImpactPreviewResponse,
+)
+def get_workflow_impact_preview(
+ workflow_id: int,
+ user: auth_models.User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ workflow = get_user_workflow_or_404(db, workflow_id=workflow_id, user_id=user.id)
+ events = _event_rows(db, workflow.id)
+ hotspots = _compute_hotspots(workflow, events)
+ corrected_mask_path = workflow.corrected_mask_path or _latest_exported_mask_path(
+ db, workflow.id
+ )
+ return _compute_impact_preview(workflow, events, hotspots, corrected_mask_path)
+
+
+@router.get("/{workflow_id}/metrics", response_model=WorkflowMetricsResponse)
+def get_workflow_metrics(
+ workflow_id: int,
+ user: auth_models.User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ workflow = get_user_workflow_or_404(db, workflow_id=workflow_id, user_id=user.id)
+ events = _event_rows(db, workflow.id)
+ return WorkflowMetricsResponse(
+ workflow_id=workflow.id,
+ metrics=compute_workflow_metrics(events),
+ )
+
+
+@router.post(
+ "/{workflow_id}/export-bundle",
+ response_model=WorkflowExportBundleResponse,
+)
+def export_workflow_bundle(
+ workflow_id: int,
+ user: auth_models.User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ workflow = get_user_workflow_or_404(db, workflow_id=workflow_id, user_id=user.id)
+ events = _event_rows(db, workflow.id)
+ return WorkflowExportBundleResponse(**build_export_bundle(workflow, events))
+
+
+@router.post("/{workflow_id}/events", response_model=WorkflowEventResponse)
+async def create_workflow_event(
+ workflow_id: int,
+ body: WorkflowEventCreateRequest,
+ user: auth_models.User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ workflow = get_user_workflow_or_404(db, workflow_id=workflow_id, user_id=user.id)
+ stage = validate_stage(body.stage or workflow.stage)
+ event = append_workflow_event(
+ db,
+ workflow_id=workflow.id,
+ actor=body.actor,
+ event_type=body.event_type,
+ stage=stage,
+ summary=body.summary,
+ payload=body.payload,
+ approval_status=body.approval_status,
+ commit=True,
+ )
+ return _event_response(event)
+
+
+@router.post("/{workflow_id}/agent-actions", response_model=WorkflowEventResponse)
+async def create_agent_action(
+ workflow_id: int,
+ body: AgentActionCreateRequest,
+ user: auth_models.User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ workflow = get_user_workflow_or_404(db, workflow_id=workflow_id, user_id=user.id)
+ summary = body.summary or f"Agent proposed: {body.action}"
+ event = append_workflow_event(
+ db,
+ workflow_id=workflow.id,
+ actor="agent",
+ event_type="agent.proposal_created",
+ stage=workflow.stage,
+ summary=summary,
+ payload={"action": body.action, "params": body.payload},
+ approval_status="pending",
+ commit=True,
+ )
+ return _event_response(event)
+
+
+@router.post(
+ "/{workflow_id}/agent-actions/{event_id}/approve",
+ response_model=AgentActionResult,
+)
+async def approve_agent_action(
+ workflow_id: int,
+ event_id: int,
+ user: auth_models.User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ workflow = get_user_workflow_or_404(db, workflow_id=workflow_id, user_id=user.id)
+ proposal = _get_pending_proposal_or_404(
+ db, workflow_id=workflow.id, event_id=event_id
+ )
+ action_payload = _proposal_action_payload(proposal)
+ action = action_payload.get("action")
+ params = action_payload.get("params", {})
+
+ if action != "stage_retraining_from_corrections":
+ raise HTTPException(status_code=400, detail=f"Unsupported action: {action}")
+
+ corrected_mask_path = (
+ params.get("corrected_mask_path")
+ or params.get("written_path")
+ or workflow.corrected_mask_path
+ or _latest_exported_mask_path(db, workflow.id)
+ )
+ if not corrected_mask_path:
+ raise HTTPException(
+ status_code=400,
+ detail="No corrected mask artifact is available to stage for retraining.",
+ )
+
+ proposal.approval_status = "approved"
+ update_workflow_fields(
+ db,
+ workflow,
+ {
+ "stage": "retraining_staged",
+ "corrected_mask_path": corrected_mask_path,
+ "training_output_path": params.get("training_output_path")
+ or workflow.training_output_path,
+ },
+ commit=False,
+ )
+ db.commit()
+ db.refresh(workflow)
+ db.refresh(proposal)
+
+ approved = append_workflow_event(
+ db,
+ workflow_id=workflow.id,
+ actor="user",
+ event_type="agent.proposal_approved",
+ stage=workflow.stage,
+ summary=f"Approved agent proposal: {proposal.summary}",
+ payload={"proposal_event_id": proposal.id, "action": action},
+ commit=True,
+ )
+ staged = append_workflow_event(
+ db,
+ workflow_id=workflow.id,
+ actor="system",
+ event_type="retraining.staged",
+ stage=workflow.stage,
+ summary="Corrected masks staged for retraining.",
+ payload={
+ "corrected_mask_path": corrected_mask_path,
+ "source": "agent_action",
+ "proposal_event_id": proposal.id,
+ },
+ commit=True,
+ )
+ return AgentActionResult(
+ workflow=_workflow_response(workflow),
+ proposal=_event_response(proposal),
+ events=[_event_response(approved), _event_response(staged)],
+ client_effects={
+ "navigate_to": "training",
+ "set_training_label_path": corrected_mask_path,
+ "workflow_stage": workflow.stage,
+ },
+ )
+
+
+@router.post(
+ "/{workflow_id}/agent-actions/{event_id}/reject",
+ response_model=WorkflowEventResponse,
+)
+async def reject_agent_action(
+ workflow_id: int,
+ event_id: int,
+ user: auth_models.User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ workflow = get_user_workflow_or_404(db, workflow_id=workflow_id, user_id=user.id)
+ proposal = _get_pending_proposal_or_404(
+ db, workflow_id=workflow.id, event_id=event_id
+ )
+ proposal.approval_status = "rejected"
+ db.commit()
+ db.refresh(proposal)
+ event = append_workflow_event(
+ db,
+ workflow_id=workflow.id,
+ actor="user",
+ event_type="agent.proposal_rejected",
+ stage=workflow.stage,
+ summary=f"Rejected agent proposal: {proposal.summary}",
+ payload={"proposal_event_id": proposal.id},
+ commit=True,
+ )
+ return _event_response(event)
+
+
+@router.post("/{workflow_id}/agent/query", response_model=AgentQueryResponse)
+async def query_workflow_agent(
+ workflow_id: int,
+ body: AgentQueryRequest,
+ user: auth_models.User = Depends(get_current_user),
+ db: Session = Depends(get_db),
+):
+ if not body.query.strip():
+ raise HTTPException(status_code=400, detail="query must be non-empty")
+
+ workflow = get_user_workflow_or_404(db, workflow_id=workflow_id, user_id=user.id)
+ event_rows = _event_rows(db, workflow.id)
+ events = _event_list(db, workflow.id)
+ recommendation = _recommendation_for_workflow(workflow, events)
+ proposals: List[WorkflowEventResponse] = []
+ lower_query = body.query.lower()
+ wants_retraining = any(
+ term in lower_query for term in ["retrain", "training", "stage", "corrected"]
+ )
+ wants_failure_analysis = any(
+ term in lower_query for term in ["fail", "failure", "error", "hotspot", "where"]
+ )
+ corrected_mask_path = workflow.corrected_mask_path or _latest_exported_mask_path(
+ db, workflow.id
+ )
+ hotspots = _compute_hotspots(workflow, event_rows)
+ impact = _compute_impact_preview(
+ workflow,
+ event_rows,
+ hotspots,
+ corrected_mask_path,
+ )
+
+ if wants_retraining and corrected_mask_path:
+ proposal = append_workflow_event(
+ db,
+ workflow_id=workflow.id,
+ actor="agent",
+ event_type="agent.proposal_created",
+ stage=workflow.stage,
+ summary="Stage corrected masks for retraining.",
+ payload={
+ "action": "stage_retraining_from_corrections",
+ "params": {"corrected_mask_path": corrected_mask_path},
+ },
+ approval_status="pending",
+ commit=True,
+ )
+ proposals.append(_event_response(proposal))
+ response = (
+ "I found a corrected mask artifact and prepared a retraining-stage "
+ "proposal. Approve it when you want the app to link those corrections "
+ "to the next training configuration."
+ )
+ elif wants_failure_analysis and hotspots:
+ top_hotspot = hotspots[0]
+ response = (
+ f"Top hotspot: {top_hotspot.summary} "
+ f"Recommended action: {top_hotspot.recommended_action} "
+ f"Impact preview: {impact.summary}"
+ )
+ else:
+ response = recommendation
+
+ return AgentQueryResponse(response=response, proposals=proposals)
diff --git a/server_api/workflows/service.py b/server_api/workflows/service.py
new file mode 100644
index 00000000..850a5852
--- /dev/null
+++ b/server_api/workflows/service.py
@@ -0,0 +1,246 @@
+import json
+from typing import Any, Dict, Optional
+
+from fastapi import HTTPException
+from sqlalchemy.orm import Session
+
+from .db_models import WorkflowEvent, WorkflowSession
+
+ALLOWED_STAGES = {
+ "setup",
+ "visualization",
+ "inference",
+ "proofreading",
+ "retraining_staged",
+ "evaluation",
+}
+
+ALLOWED_ACTORS = {"user", "agent", "system"}
+ALLOWED_APPROVAL_STATUSES = {"not_required", "pending", "approved", "rejected"}
+
+
+def encode_json(value: Optional[Dict[str, Any]]) -> Optional[str]:
+ if value is None:
+ return None
+ return json.dumps(value, ensure_ascii=False, sort_keys=True)
+
+
+def decode_json(value: Optional[str]) -> Dict[str, Any]:
+ if not value:
+ return {}
+ try:
+ parsed = json.loads(value)
+ except (TypeError, json.JSONDecodeError):
+ return {}
+ return parsed if isinstance(parsed, dict) else {}
+
+
+def validate_stage(stage: Optional[str]) -> Optional[str]:
+ if stage is None:
+ return None
+ if stage not in ALLOWED_STAGES:
+ raise HTTPException(
+ status_code=400,
+ detail=f"stage must be one of: {', '.join(sorted(ALLOWED_STAGES))}",
+ )
+ return stage
+
+
+def validate_actor(actor: str) -> str:
+ if actor not in ALLOWED_ACTORS:
+ raise HTTPException(
+ status_code=400,
+ detail=f"actor must be one of: {', '.join(sorted(ALLOWED_ACTORS))}",
+ )
+ return actor
+
+
+def validate_approval_status(status: str) -> str:
+ if status not in ALLOWED_APPROVAL_STATUSES:
+ raise HTTPException(
+ status_code=400,
+ detail=(
+ "approval_status must be one of: "
+ f"{', '.join(sorted(ALLOWED_APPROVAL_STATUSES))}"
+ ),
+ )
+ return status
+
+
+def event_to_dict(event: WorkflowEvent) -> Dict[str, Any]:
+ return {
+ "id": event.id,
+ "workflow_id": event.workflow_id,
+ "actor": event.actor,
+ "event_type": event.event_type,
+ "stage": event.stage,
+ "summary": event.summary,
+ "payload_json": event.payload_json,
+ "payload": decode_json(event.payload_json),
+ "approval_status": event.approval_status,
+ "created_at": event.created_at,
+ }
+
+
+def workflow_to_dict(workflow: WorkflowSession) -> Dict[str, Any]:
+ return {
+ "id": workflow.id,
+ "user_id": workflow.user_id,
+ "title": workflow.title,
+ "stage": workflow.stage,
+ "dataset_path": workflow.dataset_path,
+ "image_path": workflow.image_path,
+ "label_path": workflow.label_path,
+ "mask_path": workflow.mask_path,
+ "neuroglancer_url": workflow.neuroglancer_url,
+ "inference_output_path": workflow.inference_output_path,
+ "checkpoint_path": workflow.checkpoint_path,
+ "proofreading_session_id": workflow.proofreading_session_id,
+ "corrected_mask_path": workflow.corrected_mask_path,
+ "training_output_path": workflow.training_output_path,
+ "metadata_json": workflow.metadata_json,
+ "metadata": decode_json(workflow.metadata_json),
+ "created_at": workflow.created_at,
+ "updated_at": workflow.updated_at,
+ }
+
+
+def get_user_workflow_or_404(
+ db: Session, *, workflow_id: int, user_id: int
+) -> WorkflowSession:
+ workflow = (
+ db.query(WorkflowSession)
+ .filter(WorkflowSession.id == workflow_id, WorkflowSession.user_id == user_id)
+ .first()
+ )
+ if not workflow:
+ raise HTTPException(status_code=404, detail="Workflow not found")
+ return workflow
+
+
+def get_current_or_create_workflow(db: Session, *, user_id: int) -> WorkflowSession:
+ workflow = (
+ db.query(WorkflowSession)
+ .filter(WorkflowSession.user_id == user_id)
+ .order_by(WorkflowSession.updated_at.desc(), WorkflowSession.id.desc())
+ .first()
+ )
+ if workflow:
+ return workflow
+
+ workflow = WorkflowSession(user_id=user_id, title="Segmentation Workflow")
+ db.add(workflow)
+ db.commit()
+ db.refresh(workflow)
+ append_workflow_event(
+ db,
+ workflow_id=workflow.id,
+ actor="system",
+ event_type="workflow.created",
+ stage=workflow.stage,
+ summary="Workflow session created.",
+ commit=True,
+ )
+ db.refresh(workflow)
+ return workflow
+
+
+WORKFLOW_PATCH_FIELDS = {
+ "title",
+ "stage",
+ "dataset_path",
+ "image_path",
+ "label_path",
+ "mask_path",
+ "neuroglancer_url",
+ "inference_output_path",
+ "checkpoint_path",
+ "proofreading_session_id",
+ "corrected_mask_path",
+ "training_output_path",
+}
+
+
+def update_workflow_fields(
+ db: Session,
+ workflow: WorkflowSession,
+ updates: Dict[str, Any],
+ *,
+ commit: bool = True,
+) -> WorkflowSession:
+ for key, value in updates.items():
+ if key == "metadata":
+ workflow.metadata_json = encode_json(value if isinstance(value, dict) else {})
+ continue
+ if key == "metadata_json":
+ workflow.metadata_json = value
+ continue
+ if key not in WORKFLOW_PATCH_FIELDS:
+ continue
+ if key == "stage":
+ if value is None:
+ continue
+ value = validate_stage(value)
+ setattr(workflow, key, value)
+
+ if commit:
+ db.commit()
+ db.refresh(workflow)
+ return workflow
+
+
+def append_workflow_event(
+ db: Session,
+ *,
+ workflow_id: Optional[int],
+ actor: str,
+ event_type: str,
+ summary: str,
+ stage: Optional[str] = None,
+ payload: Optional[Dict[str, Any]] = None,
+ approval_status: str = "not_required",
+ commit: bool = True,
+) -> Optional[WorkflowEvent]:
+ if not workflow_id:
+ return None
+ actor = validate_actor(actor)
+ approval_status = validate_approval_status(approval_status)
+ stage = validate_stage(stage) if stage else stage
+ event = WorkflowEvent(
+ workflow_id=workflow_id,
+ actor=actor,
+ event_type=event_type,
+ stage=stage,
+ summary=summary,
+ payload_json=encode_json(payload),
+ approval_status=approval_status,
+ )
+ db.add(event)
+ if commit:
+ db.commit()
+ db.refresh(event)
+ return event
+
+
+def append_event_for_workflow_if_present(
+ db: Session,
+ *,
+ workflow_id: Optional[int],
+ actor: str,
+ event_type: str,
+ summary: str,
+ stage: Optional[str] = None,
+ payload: Optional[Dict[str, Any]] = None,
+) -> Optional[WorkflowEvent]:
+ if not workflow_id:
+ return None
+ return append_workflow_event(
+ db,
+ workflow_id=workflow_id,
+ actor=actor,
+ event_type=event_type,
+ summary=summary,
+ stage=stage,
+ payload=payload,
+ commit=True,
+ )
diff --git a/server_pytc/services/model.py b/server_pytc/services/model.py
index 57303961..ee64b747 100644
--- a/server_pytc/services/model.py
+++ b/server_pytc/services/model.py
@@ -73,7 +73,9 @@ def _clear_runtime_process(kind: str, process=None):
_inference_process = None
-def _reset_runtime_state(kind: str, *, phase: str = "idle", metadata: dict | None = None):
+def _reset_runtime_state(
+ kind: str, *, phase: str = "idle", metadata: dict | None = None
+):
state = _new_runtime_state()
timestamp = _utc_now()
state["phase"] = phase
@@ -361,7 +363,9 @@ def start_training(payload: dict):
configPath=temp_filepath,
configOriginPath=config_origin_path,
)
- _append_runtime_event("training", f"Config origin: {config_origin_path or 'none'}")
+ _append_runtime_event(
+ "training", f"Config origin: {config_origin_path or 'none'}"
+ )
_append_runtime_event("training", f"Staged config path: {temp_filepath}")
command = [
@@ -380,7 +384,9 @@ def start_training(payload: dict):
)
print(f"[MODEL.PY] Final training command: {' '.join(command)}")
- _append_runtime_event("training", f"Final training command: {' '.join(command)}")
+ _append_runtime_event(
+ "training", f"Final training command: {' '.join(command)}"
+ )
_training_process = _start_logged_process(
command,
current_dir,
@@ -586,7 +592,9 @@ def start_inference(payload: dict):
)
print(f"[MODEL.PY] Final inference command: {' '.join(command)}")
- _append_runtime_event("inference", f"Final inference command: {' '.join(command)}")
+ _append_runtime_event(
+ "inference", f"Final inference command: {' '.join(command)}"
+ )
_inference_process = _start_logged_process(
command,
current_dir,
@@ -642,7 +650,8 @@ def stop_inference():
if _inference_process and _inference_process.poll() is None:
try:
_append_runtime_event(
- "inference", f"Terminating inference process PID: {_inference_process.pid}"
+ "inference",
+ f"Terminating inference process PID: {_inference_process.pid}",
)
print(f"Terminating inference process PID: {_inference_process.pid}")
_inference_process.terminate()
diff --git a/tests/test_chat_logging_fields.py b/tests/test_chat_logging_fields.py
new file mode 100644
index 00000000..71c02ee6
--- /dev/null
+++ b/tests/test_chat_logging_fields.py
@@ -0,0 +1,43 @@
+import logging
+import time
+
+from server_api.chatbot import logging_utils
+
+
+def test_standardized_summary_log_success_fields(caplog):
+ caplog.set_level(logging.INFO)
+
+ logging_utils.log_request_summary(
+ request_id="req-123",
+ endpoint="/chat/query",
+ start_time=time.perf_counter() - 0.01,
+ status="ok",
+ )
+
+ message = caplog.records[-1].getMessage()
+ assert "request_id=req-123" in message
+ assert "endpoint=/chat/query" in message
+ assert "latency_ms=" in message
+ assert "status=ok" in message
+ assert "error_type=none" in message
+
+
+def test_standardized_summary_log_error_fields_and_no_payload_leak(caplog):
+ caplog.set_level(logging.INFO)
+ sensitive_query = "my secret token is abc123"
+
+ logging_utils.log_request_summary(
+ request_id="req-456",
+ endpoint="/chat/helper/query",
+ start_time=time.perf_counter() - 0.02,
+ status="error",
+ error_type="HTTPException",
+ )
+
+ message = caplog.records[-1].getMessage()
+ assert "request_id=req-456" in message
+ assert "endpoint=/chat/helper/query" in message
+ assert "latency_ms=" in message
+ assert "status=error" in message
+ assert "error_type=HTTPException" in message
+ assert sensitive_query not in message
diff --git a/tests/test_chatbot_config.py b/tests/test_chatbot_config.py
new file mode 100644
index 00000000..93918863
--- /dev/null
+++ b/tests/test_chatbot_config.py
@@ -0,0 +1,40 @@
+import pytest
+
+from server_api.chatbot.chatbot import LLMConfigurationError, get_ollama_config
+
+
+def test_ollama_config_requires_explicit_environment(monkeypatch):
+ monkeypatch.delenv("OLLAMA_BASE_URL", raising=False)
+ monkeypatch.delenv("OLLAMA_MODEL", raising=False)
+ monkeypatch.delenv("OLLAMA_EMBED_MODEL", raising=False)
+
+ with pytest.raises(LLMConfigurationError) as exc_info:
+ get_ollama_config()
+
+ message = str(exc_info.value)
+ assert "OLLAMA_BASE_URL" in message
+ assert "OLLAMA_MODEL" in message
+ assert "OLLAMA_EMBED_MODEL" in message
+
+
+def test_ollama_base_url_must_include_port(monkeypatch):
+ monkeypatch.setenv("OLLAMA_BASE_URL", "http://localhost")
+ monkeypatch.setenv("OLLAMA_MODEL", "llama3.1:8b")
+ monkeypatch.setenv("OLLAMA_EMBED_MODEL", "qwen3-embedding:8b")
+
+ with pytest.raises(LLMConfigurationError) as exc_info:
+ get_ollama_config()
+
+ assert "must include the LLM service port" in str(exc_info.value)
+
+
+def test_ollama_config_accepts_url_model_and_embed_model(monkeypatch):
+ monkeypatch.setenv("OLLAMA_BASE_URL", "http://localhost:11434/")
+ monkeypatch.setenv("OLLAMA_MODEL", "llama3.1:8b")
+ monkeypatch.setenv("OLLAMA_EMBED_MODEL", "qwen3-embedding:8b")
+
+ assert get_ollama_config() == (
+ "http://localhost:11434",
+ "llama3.1:8b",
+ "qwen3-embedding:8b",
+ )
diff --git a/tests/test_pytc_runtime_routes.py b/tests/test_pytc_runtime_routes.py
index 63e6ddf9..68326c65 100644
--- a/tests/test_pytc_runtime_routes.py
+++ b/tests/test_pytc_runtime_routes.py
@@ -45,9 +45,7 @@ def test_start_tensorboard_without_log_path_returns_400(self):
def test_training_logs_route_returns_worker_payload(self):
payload = {"phase": "running", "text": "hello", "lines": ["hello"]}
- with patch(
- "server_pytc.main.get_training_process_logs", return_value=payload
- ):
+ with patch("server_pytc.main.get_training_process_logs", return_value=payload):
response = self.client.get("/training_logs")
self.assertEqual(response.status_code, 200)
@@ -129,11 +127,11 @@ def test_write_temp_config_uses_origin_parent_for_relative_bases(self):
config_path = model_service._write_temp_config(
"foo: bar\n",
"training",
- config_origin_path="tutorials/neuron_snemi.yaml",
+ config_origin_path="configs/Lucchi-Mitochondria.yaml",
)
written_path = pathlib.Path(config_path)
expected_parent = (
- model_service._project_root() / "pytorch_connectomics" / "tutorials"
+ model_service._project_root() / "pytorch_connectomics" / "configs"
)
self.assertTrue(written_path.exists())
diff --git a/tests/test_workflow_evidence_export.py b/tests/test_workflow_evidence_export.py
new file mode 100644
index 00000000..9792ff16
--- /dev/null
+++ b/tests/test_workflow_evidence_export.py
@@ -0,0 +1,98 @@
+import pathlib
+import tempfile
+import unittest
+
+import pytest
+pytest.importorskip("sqlalchemy")
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+
+pytest.importorskip("fastapi")
+from fastapi.testclient import TestClient
+
+from server_api.auth import database as auth_database
+from server_api.auth import models
+from server_api.main import app as server_api_app
+from server_api.workflows.db_models import WorkflowEvent, WorkflowSession
+from server_api.workflows.evidence_export import (
+ EVIDENCE_EXPORT_VERSION,
+ build_workflow_evidence_export,
+)
+
+
+class WorkflowEvidenceExportTests(unittest.TestCase):
+ def setUp(self):
+ self.temp_dir = tempfile.TemporaryDirectory()
+ self.db_path = pathlib.Path(self.temp_dir.name) / "workflow-evidence-test.db"
+ self.engine = create_engine(
+ f"sqlite:///{self.db_path}", connect_args={"check_same_thread": False}
+ )
+ self.SessionLocal = sessionmaker(
+ autocommit=False, autoflush=False, bind=self.engine
+ )
+ models.Base.metadata.create_all(bind=self.engine)
+
+ def override_get_db():
+ db = self.SessionLocal()
+ try:
+ yield db
+ finally:
+ db.close()
+
+ server_api_app.dependency_overrides[auth_database.get_db] = override_get_db
+ self.client = TestClient(server_api_app)
+
+ def tearDown(self):
+ server_api_app.dependency_overrides.clear()
+ self.engine.dispose()
+ self.temp_dir.cleanup()
+
+ def test_evidence_export_contains_required_sections(self):
+ response = self.client.get("/api/workflows/current")
+ self.assertEqual(response.status_code, 200)
+ workflow_id = response.json()["workflow"]["id"]
+
+ self.client.post(
+ f"/api/workflows/{workflow_id}/events",
+ json={
+ "actor": "agent",
+ "event_type": "agent.proposal_created",
+ "stage": "proofreading",
+ "summary": "Proposal created",
+ "approval_status": "pending",
+ },
+ )
+ self.client.post(
+ f"/api/workflows/{workflow_id}/events",
+ json={
+ "actor": "user",
+ "event_type": "agent.proposal_approved",
+ "stage": "retraining_staged",
+ "summary": "Proposal approved",
+ },
+ )
+
+ with self.SessionLocal() as db:
+ workflow = (
+ db.query(WorkflowSession).filter(WorkflowSession.id == workflow_id).first()
+ )
+ events = (
+ db.query(WorkflowEvent)
+ .filter(WorkflowEvent.workflow_id == workflow_id)
+ .order_by(WorkflowEvent.created_at.asc(), WorkflowEvent.id.asc())
+ .all()
+ )
+
+ export_payload = build_workflow_evidence_export(workflow, events)
+ self.assertEqual(export_payload["version"], EVIDENCE_EXPORT_VERSION)
+ self.assertEqual(export_payload["workflow_id"], workflow_id)
+ self.assertIn("stage_progression_summary", export_payload)
+ self.assertIn("agent_proposal_approval_summary", export_payload)
+ self.assertIn("key_event_timeline_snippet", export_payload)
+ self.assertGreaterEqual(
+ export_payload["agent_proposal_approval_summary"]["approved_count"], 1
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_workflow_export_bundle.py b/tests/test_workflow_export_bundle.py
new file mode 100644
index 00000000..d166e53f
--- /dev/null
+++ b/tests/test_workflow_export_bundle.py
@@ -0,0 +1,93 @@
+import pathlib
+import tempfile
+import unittest
+
+import pytest
+pytest.importorskip("sqlalchemy")
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+
+pytest.importorskip("fastapi")
+from fastapi.testclient import TestClient
+
+from server_api.auth import database as auth_database
+from server_api.auth import models
+from server_api.main import app as server_api_app
+
+
+class WorkflowExportBundleTests(unittest.TestCase):
+ def setUp(self):
+ self.temp_dir = tempfile.TemporaryDirectory()
+ self.db_path = pathlib.Path(self.temp_dir.name) / "workflow-export-test.db"
+ self.engine = create_engine(
+ f"sqlite:///{self.db_path}", connect_args={"check_same_thread": False}
+ )
+ self.SessionLocal = sessionmaker(
+ autocommit=False, autoflush=False, bind=self.engine
+ )
+ models.Base.metadata.create_all(bind=self.engine)
+
+ def override_get_db():
+ db = self.SessionLocal()
+ try:
+ yield db
+ finally:
+ db.close()
+
+ server_api_app.dependency_overrides[auth_database.get_db] = override_get_db
+ self.client = TestClient(server_api_app)
+
+ def tearDown(self):
+ server_api_app.dependency_overrides.clear()
+ self.engine.dispose()
+ self.temp_dir.cleanup()
+
+ def _current_workflow_id(self) -> int:
+ response = self.client.get("/api/workflows/current")
+ self.assertEqual(response.status_code, 200)
+ return response.json()["workflow"]["id"]
+
+ def test_export_bundle_sorts_events_and_reports_artifact_existence(self):
+ workflow_id = self._current_workflow_id()
+ existing_path = pathlib.Path(self.temp_dir.name) / "existing-mask.tif"
+ existing_path.write_text("ok", encoding="utf-8")
+ missing_path = pathlib.Path(self.temp_dir.name) / "missing-mask.tif"
+
+ self.client.patch(
+ f"/api/workflows/{workflow_id}",
+ json={
+ "image_path": str(existing_path),
+ "corrected_mask_path": str(missing_path),
+ "stage": "proofreading",
+ },
+ )
+
+ self.client.post(
+ f"/api/workflows/{workflow_id}/events",
+ json={
+ "actor": "system",
+ "event_type": "proofreading.masks_exported",
+ "stage": "proofreading",
+ "summary": "Masks exported",
+ "payload": {"output_path": str(existing_path)},
+ },
+ )
+
+ response = self.client.post(f"/api/workflows/{workflow_id}/export-bundle")
+ self.assertEqual(response.status_code, 200)
+ payload = response.json()
+
+ self.assertEqual(payload["schema_version"], "workflow-export-bundle/v1")
+ self.assertEqual(payload["workflow_id"], workflow_id)
+ self.assertIsInstance(payload["events"], list)
+ self.assertGreaterEqual(len(payload["events"]), 2) # includes workflow.created
+
+ artifacts = {entry["path"]: entry["exists"] for entry in payload["artifact_paths"]}
+ self.assertIn(str(existing_path), artifacts)
+ self.assertIn(str(missing_path), artifacts)
+ self.assertTrue(artifacts[str(existing_path)])
+ self.assertFalse(artifacts[str(missing_path)])
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_workflow_metrics.py b/tests/test_workflow_metrics.py
new file mode 100644
index 00000000..5f72479d
--- /dev/null
+++ b/tests/test_workflow_metrics.py
@@ -0,0 +1,120 @@
+import pathlib
+import tempfile
+import unittest
+
+import pytest
+pytest.importorskip("sqlalchemy")
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+
+pytest.importorskip("fastapi")
+from fastapi.testclient import TestClient
+
+from server_api.auth import database as auth_database
+from server_api.auth import models
+from server_api.main import app as server_api_app
+
+
+class WorkflowMetricsTests(unittest.TestCase):
+ def setUp(self):
+ self.temp_dir = tempfile.TemporaryDirectory()
+ self.db_path = pathlib.Path(self.temp_dir.name) / "workflow-metrics-test.db"
+ self.engine = create_engine(
+ f"sqlite:///{self.db_path}", connect_args={"check_same_thread": False}
+ )
+ self.SessionLocal = sessionmaker(
+ autocommit=False, autoflush=False, bind=self.engine
+ )
+ models.Base.metadata.create_all(bind=self.engine)
+
+ def override_get_db():
+ db = self.SessionLocal()
+ try:
+ yield db
+ finally:
+ db.close()
+
+ server_api_app.dependency_overrides[auth_database.get_db] = override_get_db
+ self.client = TestClient(server_api_app)
+
+ def tearDown(self):
+ server_api_app.dependency_overrides.clear()
+ self.engine.dispose()
+ self.temp_dir.cleanup()
+
+ def _current_workflow_id(self) -> int:
+ response = self.client.get("/api/workflows/current")
+ self.assertEqual(response.status_code, 200)
+ return response.json()["workflow"]["id"]
+
+ def test_metrics_empty_workflow_returns_stable_shape(self):
+ workflow_id = self._current_workflow_id()
+ response = self.client.get(f"/api/workflows/{workflow_id}/metrics")
+ self.assertEqual(response.status_code, 200)
+ payload = response.json()
+ metrics = payload["metrics"]
+ self.assertEqual(payload["workflow_id"], workflow_id)
+ self.assertIn("event_counts", metrics)
+ self.assertIn("decision_metrics", metrics)
+ self.assertIn("stage_transition_counts", metrics)
+ self.assertIn("timestamps", metrics)
+ self.assertIn("total_events", metrics)
+
+ def test_metrics_aggregates_events_and_decisions(self):
+ workflow_id = self._current_workflow_id()
+ self.client.patch(
+ f"/api/workflows/{workflow_id}",
+ json={"stage": "visualization"},
+ )
+ self.client.patch(
+ f"/api/workflows/{workflow_id}",
+ json={"stage": "inference"},
+ )
+
+ events = [
+ {
+ "actor": "system",
+ "event_type": "inference.started",
+ "stage": "inference",
+ "summary": "Inference started",
+ },
+ {
+ "actor": "agent",
+ "event_type": "agent.proposal_created",
+ "stage": "proofreading",
+ "summary": "Proposal created",
+ "approval_status": "pending",
+ },
+ {
+ "actor": "user",
+ "event_type": "agent.proposal_approved",
+ "stage": "retraining_staged",
+ "summary": "Proposal approved",
+ },
+ {
+ "actor": "user",
+ "event_type": "agent.proposal_rejected",
+ "stage": "proofreading",
+ "summary": "Proposal rejected",
+ },
+ ]
+ for event in events:
+ response = self.client.post(f"/api/workflows/{workflow_id}/events", json=event)
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.get(f"/api/workflows/{workflow_id}/metrics")
+ self.assertEqual(response.status_code, 200)
+ metrics = response.json()["metrics"]
+
+ self.assertEqual(metrics["event_counts"]["inference.started"], 1)
+ self.assertEqual(metrics["event_counts"]["agent.proposal_created"], 1)
+ self.assertEqual(metrics["decision_metrics"]["approvals"], 1)
+ self.assertEqual(metrics["decision_metrics"]["rejections"], 1)
+ self.assertEqual(metrics["decision_metrics"]["total_decisions"], 2)
+ self.assertGreaterEqual(metrics["decision_metrics"]["approval_rate"], 0.0)
+ self.assertGreaterEqual(metrics["decision_metrics"]["rejection_rate"], 0.0)
+ self.assertGreaterEqual(metrics["total_events"], 5) # includes workflow.created
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_workflow_routes.py b/tests/test_workflow_routes.py
new file mode 100644
index 00000000..da48d11f
--- /dev/null
+++ b/tests/test_workflow_routes.py
@@ -0,0 +1,308 @@
+import pathlib
+import tempfile
+import unittest
+
+import numpy as np
+import pytest
+pytest.importorskip("sqlalchemy")
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+
+pytest.importorskip("fastapi")
+tifffile = pytest.importorskip("tifffile")
+from fastapi.testclient import TestClient
+
+from server_api.auth import database as auth_database
+from server_api.auth import models
+import server_api.ehtool.router as ehtool_router_module
+from server_api.ehtool.utils import array_to_base64
+from server_api.main import app as server_api_app
+
+
+class WorkflowRouteTests(unittest.TestCase):
+ def setUp(self):
+ self.temp_dir = tempfile.TemporaryDirectory()
+ self.db_path = pathlib.Path(self.temp_dir.name) / "workflow-test.db"
+ self.engine = create_engine(
+ f"sqlite:///{self.db_path}", connect_args={"check_same_thread": False}
+ )
+ self.SessionLocal = sessionmaker(
+ autocommit=False, autoflush=False, bind=self.engine
+ )
+ models.Base.metadata.create_all(bind=self.engine)
+
+ def override_get_db():
+ db = self.SessionLocal()
+ try:
+ yield db
+ finally:
+ db.close()
+
+ server_api_app.dependency_overrides[auth_database.get_db] = override_get_db
+ self.client = TestClient(server_api_app)
+
+ def tearDown(self):
+ server_api_app.dependency_overrides.clear()
+ ehtool_router_module._data_managers.clear()
+ self.engine.dispose()
+ self.temp_dir.cleanup()
+
+ def _current_workflow(self):
+ response = self.client.get("/api/workflows/current")
+ self.assertEqual(response.status_code, 200)
+ payload = response.json()
+ return payload["workflow"], payload["events"]
+
+ def test_current_workflow_create_update_and_events(self):
+ workflow, events = self._current_workflow()
+
+ self.assertEqual(workflow["stage"], "setup")
+ self.assertEqual(len(events), 1)
+ self.assertEqual(events[0]["event_type"], "workflow.created")
+
+ second_workflow, second_events = self._current_workflow()
+ self.assertEqual(second_workflow["id"], workflow["id"])
+ self.assertEqual(len(second_events), 1)
+
+ patch_response = self.client.patch(
+ f"/api/workflows/{workflow['id']}",
+ json={"stage": "visualization", "image_path": "/tmp/image.tif"},
+ )
+ self.assertEqual(patch_response.status_code, 200)
+ self.assertEqual(patch_response.json()["stage"], "visualization")
+
+ event_response = self.client.post(
+ f"/api/workflows/{workflow['id']}/events",
+ json={
+ "actor": "user",
+ "event_type": "dataset.loaded",
+ "stage": "visualization",
+ "summary": "Loaded a test dataset.",
+ "payload": {"image_path": "/tmp/image.tif"},
+ },
+ )
+ self.assertEqual(event_response.status_code, 200)
+
+ events_response = self.client.get(f"/api/workflows/{workflow['id']}/events")
+ self.assertEqual(events_response.status_code, 200)
+ event_types = [event["event_type"] for event in events_response.json()]
+ self.assertEqual(event_types, ["workflow.created", "dataset.loaded"])
+
+ def test_agent_action_approve_and_reject_flow(self):
+ workflow, _ = self._current_workflow()
+ workflow_id = workflow["id"]
+
+ reject_proposal = self.client.post(
+ f"/api/workflows/{workflow_id}/agent-actions",
+ json={
+ "action": "stage_retraining_from_corrections",
+ "summary": "Rejectable staging proposal.",
+ "payload": {"corrected_mask_path": "/tmp/rejected.tif"},
+ },
+ )
+ self.assertEqual(reject_proposal.status_code, 200)
+ self.assertEqual(reject_proposal.json()["approval_status"], "pending")
+
+ reject_response = self.client.post(
+ f"/api/workflows/{workflow_id}/agent-actions/"
+ f"{reject_proposal.json()['id']}/reject"
+ )
+ self.assertEqual(reject_response.status_code, 200)
+ self.assertEqual(reject_response.json()["event_type"], "agent.proposal_rejected")
+
+ approve_proposal = self.client.post(
+ f"/api/workflows/{workflow_id}/agent-actions",
+ json={
+ "action": "stage_retraining_from_corrections",
+ "summary": "Stage corrected masks.",
+ "payload": {"corrected_mask_path": "/tmp/corrected.tif"},
+ },
+ )
+ self.assertEqual(approve_proposal.status_code, 200)
+
+ approve_response = self.client.post(
+ f"/api/workflows/{workflow_id}/agent-actions/"
+ f"{approve_proposal.json()['id']}/approve"
+ )
+ self.assertEqual(approve_response.status_code, 200)
+ approve_payload = approve_response.json()
+ self.assertEqual(approve_payload["workflow"]["stage"], "retraining_staged")
+ self.assertEqual(
+ approve_payload["workflow"]["corrected_mask_path"], "/tmp/corrected.tif"
+ )
+ self.assertEqual(
+ approve_payload["client_effects"]["set_training_label_path"],
+ "/tmp/corrected.tif",
+ )
+
+ events_response = self.client.get(f"/api/workflows/{workflow_id}/events")
+ event_types = [event["event_type"] for event in events_response.json()]
+ self.assertIn("agent.proposal_approved", event_types)
+ self.assertIn("agent.proposal_rejected", event_types)
+ self.assertIn("retraining.staged", event_types)
+
+ def test_hotspots_and_impact_preview(self):
+ workflow, _ = self._current_workflow()
+ workflow_id = workflow["id"]
+ patch_response = self.client.patch(
+ f"/api/workflows/{workflow_id}",
+ json={"stage": "proofreading"},
+ )
+ self.assertEqual(patch_response.status_code, 200)
+
+ self.client.post(
+ f"/api/workflows/{workflow_id}/events",
+ json={
+ "actor": "system",
+ "event_type": "inference.failed",
+ "stage": "inference",
+ "summary": "Inference failed on z:12",
+ "payload": {"region_id": "z:12"},
+ },
+ )
+ self.client.post(
+ f"/api/workflows/{workflow_id}/events",
+ json={
+ "actor": "user",
+ "event_type": "proofreading.instance_classified",
+ "stage": "proofreading",
+ "summary": "Classified uncertain instances.",
+ "payload": {
+ "region_id": "z:12",
+ "classification": "incorrect",
+ "instance_ids": [101, 202],
+ },
+ },
+ )
+ self.client.post(
+ f"/api/workflows/{workflow_id}/events",
+ json={
+ "actor": "user",
+ "event_type": "proofreading.mask_saved",
+ "stage": "proofreading",
+ "summary": "Saved corrected mask for z:12",
+ "payload": {"region_id": "z:12", "instance_id": 101},
+ },
+ )
+ self.client.post(
+ f"/api/workflows/{workflow_id}/events",
+ json={
+ "actor": "system",
+ "event_type": "proofreading.masks_exported",
+ "stage": "proofreading",
+ "summary": "Exported corrected masks.",
+ "payload": {"written_path": "/tmp/corrected-z12.tif"},
+ },
+ )
+
+ hotspot_response = self.client.get(f"/api/workflows/{workflow_id}/hotspots")
+ self.assertEqual(hotspot_response.status_code, 200)
+ hotspot_payload = hotspot_response.json()
+ self.assertEqual(hotspot_payload["workflow_id"], workflow_id)
+ self.assertGreaterEqual(len(hotspot_payload["hotspots"]), 1)
+ self.assertEqual(hotspot_payload["hotspots"][0]["region_key"], "z:12")
+ self.assertIn(
+ hotspot_payload["hotspots"][0]["severity"],
+ {"low", "medium", "high"},
+ )
+
+ impact_response = self.client.get(
+ f"/api/workflows/{workflow_id}/impact-preview"
+ )
+ self.assertEqual(impact_response.status_code, 200)
+ impact_payload = impact_response.json()
+ self.assertTrue(impact_payload["can_stage_retraining"])
+ self.assertEqual(
+ impact_payload["corrected_mask_path"], "/tmp/corrected-z12.tif"
+ )
+ self.assertIn(impact_payload["confidence"], {"low", "medium", "high"})
+ self.assertIn("proofreading_mask_saved", impact_payload["signals"])
+
+ def test_ehtool_load_classify_save_and_export_append_workflow_events(self):
+ workflow, _ = self._current_workflow()
+ workflow_id = workflow["id"]
+ data_root = pathlib.Path(self.temp_dir.name) / "volumes"
+ data_root.mkdir()
+ image_path = data_root / "image.tif"
+ mask_path = data_root / "mask.tif"
+ export_path = data_root / "corrected-mask.tif"
+
+ image = np.arange(2 * 6 * 6, dtype=np.uint8).reshape(2, 6, 6)
+ mask = np.zeros((2, 6, 6), dtype=np.uint16)
+ mask[0, 0:2, 0:2] = 1
+ mask[0, 2:4, 2:4] = 2
+ mask[1, 4:6, 4:6] = 3
+ tifffile.imwrite(str(image_path), image)
+ tifffile.imwrite(str(mask_path), mask)
+
+ load_response = self.client.post(
+ "/eh/detection/load",
+ json={
+ "dataset_path": str(image_path),
+ "mask_path": str(mask_path),
+ "project_name": "Workflow EHTool",
+ "workflow_id": workflow_id,
+ },
+ )
+ self.assertEqual(load_response.status_code, 200)
+ session_id = load_response.json()["session_id"]
+
+ workflow_response = self.client.get("/api/workflows/current")
+ updated_workflow = workflow_response.json()["workflow"]
+ self.assertEqual(updated_workflow["stage"], "proofreading")
+ self.assertEqual(updated_workflow["proofreading_session_id"], session_id)
+
+ instances_response = self.client.get(
+ "/eh/detection/instances", params={"session_id": session_id}
+ )
+ self.assertEqual(instances_response.status_code, 200)
+ instance_id = instances_response.json()["instances"][0]["id"]
+
+ classify_response = self.client.post(
+ "/eh/detection/instance-classify",
+ json={
+ "session_id": session_id,
+ "instance_ids": [instance_id],
+ "classification": "correct",
+ },
+ )
+ self.assertEqual(classify_response.status_code, 200)
+ self.assertEqual(classify_response.json()["updated_count"], 1)
+
+ edited_mask = np.ones((6, 6), dtype=np.uint8) * 255
+ save_response = self.client.post(
+ "/eh/detection/instance-mask",
+ json={
+ "session_id": session_id,
+ "instance_id": instance_id,
+ "axis": "xy",
+ "z_index": 0,
+ "mask_base64": array_to_base64(edited_mask, format="PNG"),
+ },
+ )
+ self.assertEqual(save_response.status_code, 200)
+
+ export_response = self.client.post(
+ "/eh/detection/export-masks",
+ json={
+ "session_id": session_id,
+ "mode": "new_file",
+ "output_path": str(export_path),
+ "create_backup": True,
+ },
+ )
+ self.assertEqual(export_response.status_code, 200)
+ self.assertEqual(export_response.json()["written_path"], str(export_path))
+
+ events_response = self.client.get(f"/api/workflows/{workflow_id}/events")
+ self.assertEqual(events_response.status_code, 200)
+ event_types = [event["event_type"] for event in events_response.json()]
+ self.assertIn("dataset.loaded", event_types)
+ self.assertIn("proofreading.session_loaded", event_types)
+ self.assertIn("proofreading.instance_classified", event_types)
+ self.assertIn("proofreading.mask_saved", event_types)
+ self.assertIn("proofreading.masks_exported", event_types)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_workflow_spine_smoke.py b/tests/test_workflow_spine_smoke.py
new file mode 100644
index 00000000..d46ace5c
--- /dev/null
+++ b/tests/test_workflow_spine_smoke.py
@@ -0,0 +1,103 @@
+import pathlib
+import tempfile
+import unittest
+
+import pytest
+pytest.importorskip("sqlalchemy")
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+
+pytest.importorskip("fastapi")
+from fastapi.testclient import TestClient
+
+from server_api.auth import database as auth_database
+from server_api.auth import models
+from server_api.main import app as server_api_app
+
+
+class WorkflowSpineSmokeTests(unittest.TestCase):
+ def setUp(self):
+ self.temp_dir = tempfile.TemporaryDirectory()
+ self.db_path = pathlib.Path(self.temp_dir.name) / "workflow-spine-smoke.db"
+ self.engine = create_engine(
+ f"sqlite:///{self.db_path}", connect_args={"check_same_thread": False}
+ )
+ self.SessionLocal = sessionmaker(
+ autocommit=False, autoflush=False, bind=self.engine
+ )
+ models.Base.metadata.create_all(bind=self.engine)
+
+ def override_get_db():
+ db = self.SessionLocal()
+ try:
+ yield db
+ finally:
+ db.close()
+
+ server_api_app.dependency_overrides[auth_database.get_db] = override_get_db
+ self.client = TestClient(server_api_app)
+
+ def tearDown(self):
+ server_api_app.dependency_overrides.clear()
+ self.engine.dispose()
+ self.temp_dir.cleanup()
+
+ def test_spine_loop_load_proofread_stage_and_export_evidence(self):
+ workflow_response = self.client.get("/api/workflows/current")
+ self.assertEqual(workflow_response.status_code, 200)
+ workflow = workflow_response.json()["workflow"]
+ workflow_id = workflow["id"]
+
+ export_path = pathlib.Path(self.temp_dir.name) / "corrected-mask.tif"
+ export_path.write_text("mask", encoding="utf-8")
+
+ self.client.patch(
+ f"/api/workflows/{workflow_id}",
+ json={"stage": "proofreading", "corrected_mask_path": str(export_path)},
+ )
+ self.client.post(
+ f"/api/workflows/{workflow_id}/events",
+ json={
+ "actor": "system",
+ "event_type": "proofreading.masks_exported",
+ "stage": "proofreading",
+ "summary": "Corrected masks exported.",
+ "payload": {"written_path": str(export_path), "region_id": "z:12"},
+ },
+ )
+
+ query_response = self.client.post(
+ f"/api/workflows/{workflow_id}/agent/query",
+ json={"query": "stage corrected masks for retraining"},
+ )
+ self.assertEqual(query_response.status_code, 200)
+ proposals = query_response.json()["proposals"]
+ self.assertEqual(len(proposals), 1)
+ proposal_id = proposals[0]["id"]
+
+ approve_response = self.client.post(
+ f"/api/workflows/{workflow_id}/agent-actions/{proposal_id}/approve"
+ )
+ self.assertEqual(approve_response.status_code, 200)
+ self.assertEqual(
+ approve_response.json()["workflow"]["stage"], "retraining_staged"
+ )
+
+ hotspots_response = self.client.get(f"/api/workflows/{workflow_id}/hotspots")
+ impact_response = self.client.get(f"/api/workflows/{workflow_id}/impact-preview")
+ metrics_response = self.client.get(f"/api/workflows/{workflow_id}/metrics")
+ bundle_response = self.client.post(
+ f"/api/workflows/{workflow_id}/export-bundle"
+ )
+
+ self.assertEqual(hotspots_response.status_code, 200)
+ self.assertEqual(impact_response.status_code, 200)
+ self.assertEqual(metrics_response.status_code, 200)
+ self.assertEqual(bundle_response.status_code, 200)
+ self.assertEqual(
+ bundle_response.json()["schema_version"], "workflow-export-bundle/v1"
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/uv.lock b/uv.lock
index 5a255c3f..f9229774 100644
--- a/uv.lock
+++ b/uv.lock
@@ -102,12 +102,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
]
-[[package]]
-name = "antlr4-python3-runtime"
-version = "4.9.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034, upload-time = "2021-11-06T17:52:23.524Z" }
-
[[package]]
name = "anyio"
version = "4.12.1"
@@ -122,6 +116,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
]
+[[package]]
+name = "appnope"
+version = "0.1.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" },
+]
+
[[package]]
name = "argon2-cffi"
version = "25.1.0"
@@ -160,12 +163,46 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/82/82745642d3c46e7cea25e1885b014b033f4693346ce46b7f47483cf5d448/argon2_cffi_bindings-25.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:da0c79c23a63723aa5d782250fbf51b768abca630285262fb5144ba5ae01e520", size = 29187, upload-time = "2025-07-30T10:02:03.674Z" },
]
+[[package]]
+name = "arrow"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "python-dateutil" },
+ { name = "tzdata" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" },
+]
+
[[package]]
name = "asciitree"
version = "0.3.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2d/6a/885bc91484e1aa8f618f6f0228d76d0e67000b0fdd6090673b777e311913/asciitree-0.3.3.tar.gz", hash = "sha256:4aa4b9b649f85e3fcb343363d97564aa1fb62e249677f2e18a96765145cc0f6e", size = 3951, upload-time = "2016-09-05T19:10:42.681Z" }
+[[package]]
+name = "asttokens"
+version = "3.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" },
+]
+
+[[package]]
+name = "async-lru"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e8/1f/989ecfef8e64109a489fff357450cb73fa73a865a92bd8c272170a6922c2/async_lru-2.3.0.tar.gz", hash = "sha256:89bdb258a0140d7313cf8f4031d816a042202faa61d0ab310a0a538baa1c24b6", size = 16332, upload-time = "2026-03-19T01:04:32.413Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl", hash = "sha256:eea27b01841909316f2cc739807acea1c623df2be8c5cfad7583286397bb8315", size = 8403, upload-time = "2026-03-19T01:04:30.883Z" },
+]
+
[[package]]
name = "async-timeout"
version = "4.0.3"
@@ -190,6 +227,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
]
+[[package]]
+name = "babel"
+version = "2.18.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" },
+]
+
[[package]]
name = "bcrypt"
version = "5.0.0"
@@ -232,6 +278,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/f8/972c96f5a2b6c4b3deca57009d93e946bbdbe2241dca9806d502f29dd3ee/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4", size = 273375, upload-time = "2025-09-25T19:50:45.43Z" },
]
+[[package]]
+name = "beautifulsoup4"
+version = "4.14.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "soupsieve" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" },
+]
+
[[package]]
name = "black"
version = "26.1.0"
@@ -261,6 +320,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/3d/51bdb3ecbfadfaf825ec0c75e1de6077422b4afa2091c6c9ba34fbfc0c2d/black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede", size = 204010, upload-time = "2026-01-18T04:50:09.978Z" },
]
+[[package]]
+name = "bleach"
+version = "6.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "webencodings" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/07/18/3c8523962314be6bf4c8989c79ad9531c825210dd13a8669f6b84336e8bd/bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22", size = 203533, upload-time = "2025-10-27T17:57:39.211Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6", size = 164437, upload-time = "2025-10-27T17:57:37.538Z" },
+]
+
+[package.optional-dependencies]
+css = [
+ { name = "tinycss2" },
+]
+
[[package]]
name = "certifi"
version = "2026.1.4"
@@ -369,26 +445,12 @@ wheels = [
]
[[package]]
-name = "connected-components-3d"
-version = "3.26.1"
+name = "comm"
+version = "0.2.3"
source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "numpy" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/db/f5/7d2f66ef47b8496460e0738fdb349c7daa88ea6d39047a840a57364f8bb3/connected_components_3d-3.26.1.tar.gz", hash = "sha256:2f558954c3f732d404fc9b380a36a934ba9216c54e387e3d32439fd35ed48bbb", size = 78353, upload-time = "2025-11-03T17:20:37.337Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/5f/ec/ad230171e96793b4a70696eef2c5d347029d3532cb443702fa087078b0cb/connected_components_3d-3.26.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:291a060c484c0b8b72a17ac1c17fe87e4cf8ace09b72f92e78ad1f3b4485b1b0", size = 791755, upload-time = "2025-11-03T17:19:32.106Z" },
- { url = "https://files.pythonhosted.org/packages/05/e8/a0fb6d241ef3cc6b51596af6052550f2704ee0ec7740615f55ab49f2379d/connected_components_3d-3.26.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f64e931a8e523814fd14609a4778ccbe027f39267852f76fc1419c2af655ac05", size = 688042, upload-time = "2025-11-03T17:19:34.325Z" },
- { url = "https://files.pythonhosted.org/packages/64/2f/99ec0706d73f34d7d645de0539e6089081d36102d88cea4c0daf62c0e172/connected_components_3d-3.26.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50859da852a8539993aa72fa01876b49e19585e7a1855ac8561919f261c97be6", size = 4257671, upload-time = "2025-11-03T17:19:35.927Z" },
- { url = "https://files.pythonhosted.org/packages/2e/66/a7898038932d26bb117205fef090ad2c4d7eb9347bd6099df65e0e34941e/connected_components_3d-3.26.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e889d1786b1ff6957f5f49db8619ab8069f62aa3224aa64439fcb62d8d7f299", size = 4588657, upload-time = "2025-11-03T17:19:38.27Z" },
- { url = "https://files.pythonhosted.org/packages/83/7c/04a41ee31d277383a254d86dcc07e243197163ac17a391e5da6b0809a87c/connected_components_3d-3.26.1-cp310-cp310-win32.whl", hash = "sha256:c385d6695f9995b4d126fd6c6e45eb30459630cdfe74e78f52fb44dbdb826df0", size = 569496, upload-time = "2025-11-03T17:19:41.693Z" },
- { url = "https://files.pythonhosted.org/packages/a8/c5/07ccaf426210cbd4e9954b77ec827ee2472f858a18880f3d450592350bd6/connected_components_3d-3.26.1-cp310-cp310-win_amd64.whl", hash = "sha256:f5ce2319b3d51eac11a6f430729935e01120dd281d0ef30269788d933fe1a0c4", size = 521247, upload-time = "2025-11-03T17:19:40.266Z" },
- { url = "https://files.pythonhosted.org/packages/d4/7f/d832e2f0cfe8476cfadb3f9d0e2e152c35d21f7dd6e30a96d865ddfbc189/connected_components_3d-3.26.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a1819fbcd034f96a995066d5020a5a29b99fb5f3d9b373e3e4e52f9abb0e4808", size = 789472, upload-time = "2025-11-03T17:19:43.165Z" },
- { url = "https://files.pythonhosted.org/packages/de/68/e8bd3a5434a6f2cec234446693e3f491c1cc6f704b2e76505c62f9a0fcd0/connected_components_3d-3.26.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9127d142dd8d161612fb0ca899cfc19d83fb14c67596446428932f4dce178094", size = 690644, upload-time = "2025-11-03T17:19:44.143Z" },
- { url = "https://files.pythonhosted.org/packages/2e/77/1f9528dc38cedcb1cd0036c8c2fc9ba951d8009c09fd0aa0738b3faa8075/connected_components_3d-3.26.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08c281f9eae37221ba8eeab4feb80aad8acd478461d104c7e052b21def7e4816", size = 4363718, upload-time = "2025-11-03T17:19:45.942Z" },
- { url = "https://files.pythonhosted.org/packages/d0/bd/6266ce161a2030c0173cfa0da4d3efe7cd599cc70e3758167cb3b7947180/connected_components_3d-3.26.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd0ffb1e5dd5f3a86f72241db45b670b3bbc7b06d1f1b3950e135091766d68be", size = 4685381, upload-time = "2025-11-03T17:19:47.974Z" },
- { url = "https://files.pythonhosted.org/packages/1f/38/6042c6d8c0118c6a53c83434fc6804a2d2bcb18485854368bb971ee2b584/connected_components_3d-3.26.1-cp311-cp311-win32.whl", hash = "sha256:497de338400befa1a60e4a27cca7cc1393a52140a4790aba248b5b21fbb1428d", size = 568133, upload-time = "2025-11-03T17:19:51.891Z" },
- { url = "https://files.pythonhosted.org/packages/bb/d7/0335fc9e969091e8c3e38bad059b622b0009c779aca44faa29095b78bc0d/connected_components_3d-3.26.1-cp311-cp311-win_amd64.whl", hash = "sha256:ae86c2e195836750facbbbe88414370e711cf17e68ae110f66b7bd98ea90611f", size = 518515, upload-time = "2025-11-03T17:19:50.181Z" },
+ { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" },
]
[[package]]
@@ -465,30 +527,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" },
]
-[[package]]
-name = "crackle-codec"
-version = "0.36.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "fastremap" },
- { name = "google-crc32c" },
- { name = "numpy" },
- { name = "pybind11" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/a0/a5/3eb3f748f2a9e80911511af967b8e69721288b56096af50982eea5e5476d/crackle_codec-0.36.0.tar.gz", hash = "sha256:d6e619853dd6debceecbbd252515da00fddb08c588fa64abacbf5950d34785d2", size = 145350, upload-time = "2025-10-31T22:46:37.182Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7e/90/d92b3d4bdc7c231991cd1534092447781e42b6ada71ede9e3a7d7d31af2d/crackle_codec-0.36.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b536a8ccf8830c05ca740888e30cd5f8d88724ec0f911b6df05dabc53fe6b779", size = 490611, upload-time = "2025-10-31T22:46:07.14Z" },
- { url = "https://files.pythonhosted.org/packages/98/fa/cfaa5e54d78022c787cdb4b143ddd8354dabf871301c5d3335767cf03e42/crackle_codec-0.36.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c480056d10249137b737541f889637b8017d06449d35721a74324774188bd4e8", size = 430113, upload-time = "2025-10-31T22:46:08.896Z" },
- { url = "https://files.pythonhosted.org/packages/43/82/6025c118d700be2c6e5bec4ec7444c10fd54dd36a06d3b7a0805eacc8d3b/crackle_codec-0.36.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:592f68b4bb4001c13a75e4f7ed6c073b78be2175c29665a82344d490dd130ad5", size = 457761, upload-time = "2025-10-31T22:46:10.247Z" },
- { url = "https://files.pythonhosted.org/packages/6d/a5/976085448663d057f59f4baa0afd06ab1348b3193eabfde093d8a603eb29/crackle_codec-0.36.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8df2097131f9fe143574b79c21982325f48459cac0ef2d2a015c0eb52e1db906", size = 473427, upload-time = "2025-10-31T22:46:11.533Z" },
- { url = "https://files.pythonhosted.org/packages/e3/9c/3c4d243147ffaeff3cac1e4e3bf05b3446c2bff96aa1155a88ffcdb07b88/crackle_codec-0.36.0-cp310-cp310-win_amd64.whl", hash = "sha256:57db09c1536df847788c1d8048f8979301d88194d8a3017e5a82f2b575b3e7e0", size = 310982, upload-time = "2025-10-31T22:46:12.781Z" },
- { url = "https://files.pythonhosted.org/packages/14/ac/f75225bb5be2828e9246686d71aaea3200345a47b6b644db2642f033c280/crackle_codec-0.36.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0b40719622e37501eb1c2c496a3d134a5f00fe663029cad42eb6a826ec2dfe30", size = 492609, upload-time = "2025-10-31T22:46:13.833Z" },
- { url = "https://files.pythonhosted.org/packages/70/db/f979460d39527f598d463267281eded150631f62365f3cc946b860cd27cc/crackle_codec-0.36.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8c5423887f92613d18a103560d1ef883c2a2db6ef9d5c3c95e979b12bd70bb82", size = 431340, upload-time = "2025-10-31T22:46:14.799Z" },
- { url = "https://files.pythonhosted.org/packages/a8/ff/0381a71f5c00fda001bab35bc6b48e91c86a389545cbeb5cf3fc38b0a8f4/crackle_codec-0.36.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b34e6fed19085f421dacd3533083e35965622dfa166b8a96ba4a82a2a67fd264", size = 458087, upload-time = "2025-10-31T22:46:16.26Z" },
- { url = "https://files.pythonhosted.org/packages/b3/70/92a9ce9a17536a411b3397e881ce90a3efff918515fcc47db6eb6709dbe9/crackle_codec-0.36.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8594a9b111eb2f2ab0fb99b657af2ce8d753b85844f085785e7a28aa23e8a2f0", size = 474205, upload-time = "2025-10-31T22:46:17.652Z" },
- { url = "https://files.pythonhosted.org/packages/ad/96/c24bc14609103b1bc22ca1031e5ba8edfbeb8e1b8464fe113332c124863f/crackle_codec-0.36.0-cp311-cp311-win_amd64.whl", hash = "sha256:aa99fbce3754dad6c081b8f522baf527bbc4811f437da4f0d2aa2f50a7d02b85", size = 312132, upload-time = "2025-10-31T22:46:19.042Z" },
-]
-
[[package]]
name = "cryptography"
version = "46.0.4"
@@ -604,36 +642,38 @@ wheels = [
]
[[package]]
-name = "dijkstra3d"
-version = "1.15.2"
+name = "debugpy"
+version = "1.8.20"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/4e/c5/f03b5c00d4fee861bc30b3bb7a412f601fb29784954f36ca0bc595696274/dijkstra3d-1.15.2.tar.gz", hash = "sha256:f5db0e21bca8a1d7bcedc103d55757b0c030d76d2d62228b6536550952c1e5db", size = 65198, upload-time = "2025-11-06T04:42:31.34Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207, upload-time = "2026-01-29T23:03:28.199Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/3c/94/7fb0fc4b46f8741b8cef1f10c84ec2e4f8aaa2c01d07e67f1891eb6afd17/dijkstra3d-1.15.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f636f62d0b01884b1189f61d2ecabb47f87190dc9e532dadbc6c7e8d9887101d", size = 340172, upload-time = "2025-11-06T04:41:06.222Z" },
- { url = "https://files.pythonhosted.org/packages/74/00/6048321db5fb4840e39821f5811d8ff9e94d30cdc98dc919c65d54eb8f49/dijkstra3d-1.15.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f44f630f9a87855dbfeefeb5ecc8ccce667528f067bb9e088e248aaf8a5dcf73", size = 297743, upload-time = "2025-11-06T04:41:08.431Z" },
- { url = "https://files.pythonhosted.org/packages/a7/28/d690de28c29f525c9d53798a7a5e36bcc39fac74f75fd49c30066952a41b/dijkstra3d-1.15.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c373dcbafe10ba5f9da1461bf6ae25c689bdcee3a4a2d9ef8a02037da6a67fa", size = 3486512, upload-time = "2025-11-06T04:41:09.828Z" },
- { url = "https://files.pythonhosted.org/packages/45/41/4c46ede55b320a7183bc45ffc0af98d1556fa1c6e2b5ae191415add5d137/dijkstra3d-1.15.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d432652d84f26609b2b1d36c226c25dc4a7d7562f39007c952b305969134cb2", size = 3398209, upload-time = "2025-11-06T04:41:11.644Z" },
- { url = "https://files.pythonhosted.org/packages/0b/1b/c269fb9398b0b5a2e7a1aea29a2909f3abdea7010990747b36133bdda927/dijkstra3d-1.15.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81de5a8e222d68002315e11c04f837533488c9c0e975c6ff1595f50a01140cba", size = 4309238, upload-time = "2025-11-06T04:41:13.301Z" },
- { url = "https://files.pythonhosted.org/packages/69/a4/2fc577f7c8a541be773e4d75279fcbcd1b4a56a4c7445280924bf5781be2/dijkstra3d-1.15.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:52321114974afd9f8340fc65f2ed234001c2a859c440397d35253184d98c7256", size = 4373361, upload-time = "2025-11-06T04:41:14.908Z" },
- { url = "https://files.pythonhosted.org/packages/87/ce/e24a0eba72affbec9d62783fe390bf09e1514f21917b0a2d19e3722fafe5/dijkstra3d-1.15.2-cp310-cp310-win32.whl", hash = "sha256:378a57e5ad57e603cd087ad4b1423dc45c48507e40ff9f5fdab4b11d32e48bbe", size = 262266, upload-time = "2025-11-06T04:41:18.153Z" },
- { url = "https://files.pythonhosted.org/packages/d4/95/0034db41d13ea655298baa815382e55f44da22bb09c8cafc3d28ca7ad67a/dijkstra3d-1.15.2-cp310-cp310-win_amd64.whl", hash = "sha256:9c17277a8ae863b4edccb9df49f7f29d182c452fb9afcdc33e70b18092bb8157", size = 266392, upload-time = "2025-11-06T04:41:17.037Z" },
- { url = "https://files.pythonhosted.org/packages/51/38/e30f2d8b359128e9fbadfed51e0aa5ca18890dd0bbbc5f9d69ab6ed6a13e/dijkstra3d-1.15.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3878120086015c4c903a60774b629f63ae48e935ae2c601bf60d16a9dae2071a", size = 338429, upload-time = "2025-11-06T04:41:19.578Z" },
- { url = "https://files.pythonhosted.org/packages/e0/29/6ded8e5dd045e27dbebb93f79a195af56969f371f83b7f3da35e2e23c4d6/dijkstra3d-1.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8a2ae3da42c5eb60c343b239973a35b98ea5e5d08c04c70856c502f4eb78b64f", size = 295535, upload-time = "2025-11-06T04:41:21.114Z" },
- { url = "https://files.pythonhosted.org/packages/de/92/b47cf53d485d70b4ce01364417a2614f9641d8f399cac4dd1dc7f8486987/dijkstra3d-1.15.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74a971ec3287b1fc5bb87dd4ca5ae06aade505af3637219ae2e9059076faf084", size = 3539360, upload-time = "2025-11-06T04:41:22.74Z" },
- { url = "https://files.pythonhosted.org/packages/9d/d1/349115c68d5ba611417e70a25d472d0d9e5d8757e658e22fc780666eb289/dijkstra3d-1.15.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:474f8f5d50c5c035f6fdcb3b08f59ffef63c21c6b35262e2b8057deecdf2b1f8", size = 3445457, upload-time = "2025-11-06T04:41:24.01Z" },
- { url = "https://files.pythonhosted.org/packages/4a/95/c6b375acdfc035b21bcf3ca3db7359a83d440fcc47557b00ebd6a8b71099/dijkstra3d-1.15.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bed27483b0a7fbde41864aa65127a8296f671143a7500648bcb2aaacebfa6aa5", size = 4367522, upload-time = "2025-11-06T04:41:25.341Z" },
- { url = "https://files.pythonhosted.org/packages/46/cd/df68f73b583040776f7554f2fa40bcefd9238ad6254c4a3ed0f7507f35a0/dijkstra3d-1.15.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:573d3d9edc9ff7b9432ab0c62a67ee75b8e523664d20a45b261c545ed9183682", size = 4421327, upload-time = "2025-11-06T04:41:26.99Z" },
- { url = "https://files.pythonhosted.org/packages/d3/cd/d4cb0ab04ffc03e1b05a9f0b78f56f139eaf3e49ce7e01159048fa420c39/dijkstra3d-1.15.2-cp311-cp311-win32.whl", hash = "sha256:91a7b05fba8da6f7515c4bdbd3cab53f12ad0801af023ac0fbf0cafa60e84505", size = 261372, upload-time = "2025-11-06T04:41:28.979Z" },
- { url = "https://files.pythonhosted.org/packages/d3/d8/97e2d20583f11180b4df853b094702ead55d4a7772de9e1d980674bf4be5/dijkstra3d-1.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:8c1755f37dfed167b9ee7c7ac3ceb05d5ef045a89e10db550168e83a661c634a", size = 266096, upload-time = "2025-11-06T04:41:28.073Z" },
+ { url = "https://files.pythonhosted.org/packages/71/be/8bd693a0b9d53d48c8978fa5d889e06f3b5b03e45fd1ea1e78267b4887cb/debugpy-1.8.20-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:157e96ffb7f80b3ad36d808646198c90acb46fdcfd8bb1999838f0b6f2b59c64", size = 2099192, upload-time = "2026-01-29T23:03:29.707Z" },
+ { url = "https://files.pythonhosted.org/packages/77/1b/85326d07432086a06361d493d2743edd0c4fc2ef62162be7f8618441ac37/debugpy-1.8.20-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:c1178ae571aff42e61801a38b007af504ec8e05fde1c5c12e5a7efef21009642", size = 3088568, upload-time = "2026-01-29T23:03:31.467Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/60/3e08462ee3eccd10998853eb35947c416e446bfe2bc37dbb886b9044586c/debugpy-1.8.20-cp310-cp310-win32.whl", hash = "sha256:c29dd9d656c0fbd77906a6e6a82ae4881514aa3294b94c903ff99303e789b4a2", size = 5284399, upload-time = "2026-01-29T23:03:33.678Z" },
+ { url = "https://files.pythonhosted.org/packages/72/43/09d49106e770fe558ced5e80df2e3c2ebee10e576eda155dcc5670473663/debugpy-1.8.20-cp310-cp310-win_amd64.whl", hash = "sha256:3ca85463f63b5dd0aa7aaa933d97cbc47c174896dcae8431695872969f981893", size = 5316388, upload-time = "2026-01-29T23:03:35.095Z" },
+ { url = "https://files.pythonhosted.org/packages/51/56/c3baf5cbe4dd77427fd9aef99fcdade259ad128feeb8a786c246adb838e5/debugpy-1.8.20-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:eada6042ad88fa1571b74bd5402ee8b86eded7a8f7b827849761700aff171f1b", size = 2208318, upload-time = "2026-01-29T23:03:36.481Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/7d/4fa79a57a8e69fe0d9763e98d1110320f9ecd7f1f362572e3aafd7417c9d/debugpy-1.8.20-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:7de0b7dfeedc504421032afba845ae2a7bcc32ddfb07dae2c3ca5442f821c344", size = 3171493, upload-time = "2026-01-29T23:03:37.775Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/f2/1e8f8affe51e12a26f3a8a8a4277d6e60aa89d0a66512f63b1e799d424a4/debugpy-1.8.20-cp311-cp311-win32.whl", hash = "sha256:773e839380cf459caf73cc533ea45ec2737a5cc184cf1b3b796cd4fd98504fec", size = 5209240, upload-time = "2026-01-29T23:03:39.109Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/92/1cb532e88560cbee973396254b21bece8c5d7c2ece958a67afa08c9f10dc/debugpy-1.8.20-cp311-cp311-win_amd64.whl", hash = "sha256:1f7650546e0eded1902d0f6af28f787fa1f1dbdbc97ddabaf1cd963a405930cb", size = 5233481, upload-time = "2026-01-29T23:03:40.659Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" },
]
[[package]]
-name = "dill"
-version = "0.4.1"
+name = "decorator"
+version = "5.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" },
+]
+
+[[package]]
+name = "defusedxml"
+version = "0.7.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" },
+ { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
]
[[package]]
@@ -660,29 +700,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/a3/460c57f094a4a165c84a1341c373b0a4f5ec6ac244b998d5021aade89b77/ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3", size = 150607, upload-time = "2025-03-13T11:52:41.757Z" },
]
-[[package]]
-name = "edt"
-version = "3.1.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "numpy" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/a5/ca/432c11df2a261fcae71dea11eeabfc5affd8ada22aaafa378212c1e9c9e2/edt-3.1.1.tar.gz", hash = "sha256:4b0ab5d898077eda76d1ebdec6e3a4085fd33d4c85195d71abf464ff225915c5", size = 3234159, upload-time = "2026-03-01T05:20:30.695Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/54/c6/94335dd01d33fff33b02bc7c79c4b2ec4664ccbbe9b55ccb73868555d6c0/edt-3.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e249ab445adc21ec9c458a3a0215e9bcf3e487154947a4b9172c5506bf2ed39e", size = 274429, upload-time = "2026-03-01T05:19:18.225Z" },
- { url = "https://files.pythonhosted.org/packages/e0/3a/e99a0b59cf953a2c2b3e5e9451389e4b8f40579614229aef44f13f2f8b6a/edt-3.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:11ee8df9bab9e1649190d2a286a9b35a2dea90bf6f60fb9a299c8a8371e258c7", size = 254093, upload-time = "2026-03-01T05:19:19.869Z" },
- { url = "https://files.pythonhosted.org/packages/39/89/358cf81ac3c94cf2c8e243788e6282cb1ec7980c64d12859cbdc016865d8/edt-3.1.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09b6fd94a7146305a9c7d964551d6148249c8692e08d6065a56aa79ccd625585", size = 2836166, upload-time = "2026-03-01T05:19:21.408Z" },
- { url = "https://files.pythonhosted.org/packages/2e/d6/48f59656a8cf4d9672700d42585d83a9dc065f34dd1786ac07f320cc8da0/edt-3.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c8108ce1e4ee328c5bf32948d8da22e78df955147dc3eac4c80ba3de9512f449", size = 2902642, upload-time = "2026-03-01T05:19:23.195Z" },
- { url = "https://files.pythonhosted.org/packages/ad/be/81c0cad69f306fe1ef34a1b86e066e13021c0e813e858eada4fa2810deaa/edt-3.1.1-cp310-cp310-win32.whl", hash = "sha256:15dee70100c553ddf71979c5d7c297a0386a02970b3b18d8ee5a5b778bd51996", size = 175131, upload-time = "2026-03-01T05:19:28.133Z" },
- { url = "https://files.pythonhosted.org/packages/fe/4d/999a42a45a75cbd63811aca365b538c1ed72456d1243fc00edaa2e2e6e11/edt-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:3d6692f010a3ab6fc2b802f22173d8d6ed5e4154cf524a9821f273870f6af45b", size = 210140, upload-time = "2026-03-01T05:19:26.709Z" },
- { url = "https://files.pythonhosted.org/packages/83/50/fa05c178abd6fcfe4369c7b651650fe3395558320766d0334f2606642de9/edt-3.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ce45f983747ba5a096e35dbfc13beafcbe2c6d2746d87c06915ddeac16af7e6", size = 273024, upload-time = "2026-03-01T05:19:29.312Z" },
- { url = "https://files.pythonhosted.org/packages/80/0b/53ba9d3727b1b87a77449e36415a20885718e21886bdff528c1e2a5beb27/edt-3.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9fe86dafa1e4703c670ab5e36cd9334b9a15eb0c97ff42700a544f438bafbfc8", size = 253067, upload-time = "2026-03-01T05:19:30.804Z" },
- { url = "https://files.pythonhosted.org/packages/c6/98/c56d761056c6712edd57f384486508431889f66da850fb32e17f78fee1df/edt-3.1.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9f238097b3f6a93bdbf1860d0efa42bc88a868e5416ff0a18316e001c9c355a", size = 2886678, upload-time = "2026-03-01T05:19:32.081Z" },
- { url = "https://files.pythonhosted.org/packages/55/9d/4e857ba351d6d8cffb7a375b0e557c0feb8a2af8da9cfe331b54fa98c7e4/edt-3.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3630630459b8ad32b12bb9840f204cf405b2c64fa827140e55762528364d7a28", size = 2947202, upload-time = "2026-03-01T05:19:34.019Z" },
- { url = "https://files.pythonhosted.org/packages/0e/c6/44fa2239478d01c4d02b29dcd36a220f2415dcad8bb609776658e580add5/edt-3.1.1-cp311-cp311-win32.whl", hash = "sha256:25134ae70b393682110f081ecfdc773fbd3661b551b6225db96950a69bbedb35", size = 174525, upload-time = "2026-03-01T05:19:36.539Z" },
- { url = "https://files.pythonhosted.org/packages/3e/44/2664e9419358cced35395446c2cddfba9a5b0d538543d779b72a53b40b71/edt-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:9c2694cdc1651def1cd78331f90bf45efaa4cbd4d9e753acc60a3033b422c529", size = 210170, upload-time = "2026-03-01T05:19:35.515Z" },
-]
-
[[package]]
name = "einops"
version = "0.8.2"
@@ -704,6 +721,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
]
+[[package]]
+name = "executing"
+version = "2.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" },
+]
+
[[package]]
name = "faiss-cpu"
version = "1.12.0"
@@ -755,26 +781,12 @@ wheels = [
]
[[package]]
-name = "fastremap"
-version = "1.17.7"
+name = "fastjsonschema"
+version = "2.21.2"
source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "numpy" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/97/c8/d581816df8ee7ab70cd2dd8ee4e60169ceab8062224cc090863e6715f33d/fastremap-1.17.7.tar.gz", hash = "sha256:42776172867d8f2b3348754cf29405ba878af4b06917f12a969514d3097910dc", size = 50034, upload-time = "2025-09-29T23:28:13.031Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/28/fe/774b263411d4ccc1f46789f36a24e85706f55e0b53f67cf909b702271b2c/fastremap-1.17.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9a759677a897df0eb7ad1b466de18d00cfbdafb0ec4851b433b7891eea7cd8e0", size = 811269, upload-time = "2025-09-29T23:27:15.632Z" },
- { url = "https://files.pythonhosted.org/packages/eb/fa/b600288be8990be1e4c668def2f5575a2f57c1c3554d377b8ef157b5d762/fastremap-1.17.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a82b3b67967047c31d9bab8452b3a3fc4e17b143eb9fd98b9cb9e0bfe990840d", size = 652567, upload-time = "2025-09-29T23:27:17.63Z" },
- { url = "https://files.pythonhosted.org/packages/a5/45/8509e51619a3d140051217ee30ce5bcb25d7ba8a2ebd07325b00034a5089/fastremap-1.17.7-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7455ce285a8e431e41138f7b5c6b7c9cd817f419bc4245f1de1d095f0e91feb", size = 7067506, upload-time = "2025-09-29T23:27:18.933Z" },
- { url = "https://files.pythonhosted.org/packages/78/15/6890a8c4c1c90f7a0c0811207ece87082f9fa994ca6ca002e92f65a2c877/fastremap-1.17.7-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b05fb72432faefeebbcb9e0f49a175730f0cb95fd350b584855df263f33c8edc", size = 7223455, upload-time = "2025-09-29T23:27:21.146Z" },
- { url = "https://files.pythonhosted.org/packages/9a/89/bdbb39df082e467338d139c47bb8c8dbe5d8347837afd81ea4b0028a4e2f/fastremap-1.17.7-cp310-cp310-win32.whl", hash = "sha256:f49f79c28d84632de4eb254b7342ae37fa9fa6e79ecd4cc569a15014ee99eb4a", size = 498013, upload-time = "2025-09-29T23:27:23.549Z" },
- { url = "https://files.pythonhosted.org/packages/9e/aa/8f88be7fd7521a79d522fcd13783dd702795c37d977713305895191c5ee9/fastremap-1.17.7-cp310-cp310-win_amd64.whl", hash = "sha256:aabb9ec3d93b75f8f97651cf3067c7415286461296dea5e7e9c0c6ddc9d9858b", size = 684136, upload-time = "2025-09-29T23:27:22.571Z" },
- { url = "https://files.pythonhosted.org/packages/be/7f/98bc1ab6ab9b389a72ed1a97dc34eb57a8e6beb473117c8942481f74e6ca/fastremap-1.17.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4610492ea19f1cc916a05e9195b67de11dc98a18e905de1abf821b2ca2ca1fac", size = 811862, upload-time = "2025-09-29T23:27:24.546Z" },
- { url = "https://files.pythonhosted.org/packages/f7/b6/b88d2a30f50708249bb0414f0581d0f7ccb3785b1a3ca6588565920988f2/fastremap-1.17.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d6130394fc92777d08ca992e70ff6307fe1ef928d2831140ff63ab27f36b6600", size = 655315, upload-time = "2025-09-29T23:27:26.006Z" },
- { url = "https://files.pythonhosted.org/packages/f9/fd/70d7e5ee9b77c3ddbe6d9c479202cf04a0f178c399d94af5993520dab51a/fastremap-1.17.7-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f77a4e48fc9614027d318d23399d91a89b62c56d97880055c538fd42c43fd6", size = 7496837, upload-time = "2025-09-29T23:27:27.344Z" },
- { url = "https://files.pythonhosted.org/packages/ff/5a/3ae0f6425c816ac74e130244c152cc5b7d7c13d5c5ff299af074f0456208/fastremap-1.17.7-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74635d268aa40ef7063319c997c1cbb70d52deeb08a3a61146a6151306c394ea", size = 7621372, upload-time = "2025-09-29T23:27:29.088Z" },
- { url = "https://files.pythonhosted.org/packages/65/9a/193ca90273394cc93d98c9b7a587d134655910e14e12d7813d97d48ed13d/fastremap-1.17.7-cp311-cp311-win32.whl", hash = "sha256:f72d6db9550d9f1308cf78e71ca1bbbedea66048439b0fe688addaedf05c37ff", size = 490649, upload-time = "2025-09-29T23:27:31.719Z" },
- { url = "https://files.pythonhosted.org/packages/71/4b/7a03f72620945f08b40285ff3640e2b0a86f80218c519c8e4c4a557ca645/fastremap-1.17.7-cp311-cp311-win_amd64.whl", hash = "sha256:67cf58fada99981ec1a5b4f3368e1b4c1c4d0f22efaa036748f97475c37ce1f3", size = 685345, upload-time = "2025-09-29T23:27:30.662Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" },
]
[[package]]
@@ -786,32 +798,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" },
]
-[[package]]
-name = "fill-voids"
-version = "2.1.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "fastremap" },
- { name = "numpy" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/f0/11/6dff4280502b81e92a69442d6d82a343610192ccbc2638ab921ffc273505/fill_voids-2.1.1.tar.gz", hash = "sha256:469f543e4ab236cf11aacef106af8e73c730f2a90f1bfae760dc8de29d4d6634", size = 3229026, upload-time = "2025-09-03T05:28:32.579Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/5b/db/e89af150e5e599a7a85c23e4ba54c305cc287aaff16104af59c002267541/fill_voids-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82c05c8a86bbd1e6e2047358ce6ad6722c2fb42a10fa0288749a939104ede712", size = 227581, upload-time = "2025-09-03T05:27:55.2Z" },
- { url = "https://files.pythonhosted.org/packages/50/e9/c9a60269b58527845672cf97d668d441ba8250020d047ab60c3989101313/fill_voids-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:98c70d3c372e7d54a3ce462d8828c42d00232e88f01b5c62ce78551dba99436f", size = 207316, upload-time = "2025-09-03T05:27:56.826Z" },
- { url = "https://files.pythonhosted.org/packages/ab/eb/c325d980f98643f500f872b9fcd35e428384757fd9f46cb19b3fac30cce5/fill_voids-2.1.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5209dc9112d144763b0054460570f3f1dd2d19ed485401def8cbd0d7dc25fbf8", size = 1527166, upload-time = "2025-09-03T12:19:19.287Z" },
- { url = "https://files.pythonhosted.org/packages/20/62/139f190b61784ebdf1d7b9f8084603d164fa0a7fc5eb85ea5ddd04398c9c/fill_voids-2.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f40c9f7dad4eff48d915f9e0e1d56828a99d981f02a44dba0723b054f7a70d2", size = 1538708, upload-time = "2025-09-03T05:27:58.01Z" },
- { url = "https://files.pythonhosted.org/packages/d2/ff/773b31f59d83bc91be9ee846d8901013523ec822c4caf76b144254172dd0/fill_voids-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e9d4e76c565458568242ca97d0719199bb61781152b5fec8eed033c89bed1bc5", size = 2354805, upload-time = "2025-09-03T12:19:21.118Z" },
- { url = "https://files.pythonhosted.org/packages/87/e3/08abadba96c7b2a7e679d04852f049e4e0db459eb41d6bc8c7af9e280a45/fill_voids-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eb3a7d9c25c790ae17f3a4b7853d060c79bf73428ce44deecc1cc4e66ce81fab", size = 2405792, upload-time = "2025-09-03T05:27:59.99Z" },
- { url = "https://files.pythonhosted.org/packages/5a/5c/6cc051d4c7a4cb3ead14fcb95da599cf19b300434d82445e7bd72b535862/fill_voids-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:3dc17ccef0f7fdfee457cb0b33b16e70eedc80f0fa2a29c447991bdb64e2ae87", size = 197497, upload-time = "2025-09-03T05:28:01.427Z" },
- { url = "https://files.pythonhosted.org/packages/03/8b/70d8d8c54b42657ddc01c81fda9c326600e606161c89599809e453827861/fill_voids-2.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d866876f0d692b6b6d0fa5a6d42cfb62696b048286a28866d2e0fe728fb83070", size = 232090, upload-time = "2025-09-03T05:28:02.634Z" },
- { url = "https://files.pythonhosted.org/packages/9e/1e/0e23cbcf1ad4980a8a9834037c268fb95ac63db0d4f7fa9f4472e4dd8d82/fill_voids-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ec9ab0e107d9e876a580f9b954b1029fc20322158c80b2f5d925ee67236e88c9", size = 206937, upload-time = "2025-09-03T05:28:03.556Z" },
- { url = "https://files.pythonhosted.org/packages/2f/b1/8e6457707b2a7806d5c50be08950fcfb54d1e929effdbc8c5b7563015359/fill_voids-2.1.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4dc3e9cb3acceb6834b2149b8ae2e8d76cca3cf7cfcc255200881fbbef54177a", size = 1595690, upload-time = "2025-09-03T12:19:22.61Z" },
- { url = "https://files.pythonhosted.org/packages/6e/38/1c53fe0bf6a667511c93d2e634f7a5ebf8144e3d274fbcca921bd8c0e216/fill_voids-2.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac94f1a70886edbde7ea1d44b60d36d7a86bb149072c069c3e53b43f3d89d5", size = 1602603, upload-time = "2025-09-03T05:28:04.55Z" },
- { url = "https://files.pythonhosted.org/packages/f9/cc/98110bf6f916e3c82e8923b8cb9e55272ede6b2028c01117b4c2a2ddeb33/fill_voids-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:128d29c0e49bceffb748b3b35f174377b4b05da4be65e6ccfe436047924ee858", size = 2421284, upload-time = "2025-09-03T12:19:24.279Z" },
- { url = "https://files.pythonhosted.org/packages/33/c1/8cfe49f4d97e5f870a5605f84c93449a6ff8b71f9fee022badf6f28d3c27/fill_voids-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c31d529bc46d0b17328044b51a98d9773c2453c0f7daac37743ada70944269b2", size = 2462171, upload-time = "2025-09-03T05:28:06.23Z" },
- { url = "https://files.pythonhosted.org/packages/a6/b8/d33df66e9c0e637439421d7c2331f913b3374803971df5b0af067d5a6bed/fill_voids-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:cd3e21ce337bd2eabb08ea423da5ebd215abba1f7e0174f41fbc4f4b82feec85", size = 197397, upload-time = "2025-09-03T05:28:07.646Z" },
-]
-
[[package]]
name = "fonttools"
version = "4.61.1"
@@ -837,6 +823,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" },
]
+[[package]]
+name = "fqdn"
+version = "1.5.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/30/3e/a80a8c077fd798951169626cde3e239adeba7dab75deb3555716415bd9b0/fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f", size = 6015, upload-time = "2021-03-11T07:16:29.08Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" },
+]
+
[[package]]
name = "frozenlist"
version = "1.8.0"
@@ -887,11 +882,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" },
]
-[package.optional-dependencies]
-http = [
- { name = "aiohttp" },
-]
-
[[package]]
name = "google-apitools"
version = "0.5.35"
@@ -941,6 +931,12 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9c/97/7d75fe37a7a6ed171a2cf17117177e7aab7e6e0d115858741b41e9dd4254/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c", size = 28800, upload-time = "2025-12-16T00:40:30.322Z" },
]
+[[package]]
+name = "gputil"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ed/0e/5c61eedde9f6c87713e89d794f01e378cfd9565847d4576fa627d758c554/GPUtil-1.4.0.tar.gz", hash = "sha256:099e52c65e512cdfa8c8763fca67f5a5c2afb63469602d5dcb4d296b3661efb9", size = 5545, upload-time = "2018-12-18T09:12:13.63Z" }
+
[[package]]
name = "greenlet"
version = "3.3.1"
@@ -950,7 +946,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fe/65/5b235b40581ad75ab97dcd8b4218022ae8e3ab77c13c919f1a1dfe9171fd/greenlet-3.3.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:04bee4775f40ecefcdaa9d115ab44736cd4b9c5fba733575bfe9379419582e13", size = 273723, upload-time = "2026-01-23T15:30:37.521Z" },
{ url = "https://files.pythonhosted.org/packages/ce/ad/eb4729b85cba2d29499e0a04ca6fbdd8f540afd7be142fd571eea43d712f/greenlet-3.3.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e1457f4fed12a50e427988a07f0f9df53cf0ee8da23fab16e6732c2ec909d4", size = 574874, upload-time = "2026-01-23T16:00:54.551Z" },
{ url = "https://files.pythonhosted.org/packages/87/32/57cad7fe4c8b82fdaa098c89498ef85ad92dfbb09d5eb713adedfc2ae1f5/greenlet-3.3.1-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:070472cd156f0656f86f92e954591644e158fd65aa415ffbe2d44ca77656a8f5", size = 586309, upload-time = "2026-01-23T16:05:25.18Z" },
- { url = "https://files.pythonhosted.org/packages/66/66/f041005cb87055e62b0d68680e88ec1a57f4688523d5e2fb305841bc8307/greenlet-3.3.1-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1108b61b06b5224656121c3c8ee8876161c491cbe74e5c519e0634c837cf93d5", size = 597461, upload-time = "2026-01-23T16:15:51.943Z" },
{ url = "https://files.pythonhosted.org/packages/87/eb/8a1ec2da4d55824f160594a75a9d8354a5fe0a300fb1c48e7944265217e1/greenlet-3.3.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a300354f27dd86bae5fbf7002e6dd2b3255cd372e9242c933faf5e859b703fe", size = 586985, upload-time = "2026-01-23T15:32:47.968Z" },
{ url = "https://files.pythonhosted.org/packages/15/1c/0621dd4321dd8c351372ee8f9308136acb628600658a49be1b7504208738/greenlet-3.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e84b51cbebf9ae573b5fbd15df88887815e3253fc000a7d0ff95170e8f7e9729", size = 1547271, upload-time = "2026-01-23T16:04:18.977Z" },
{ url = "https://files.pythonhosted.org/packages/9d/53/24047f8924c83bea7a59c8678d9571209c6bfe5f4c17c94a78c06024e9f2/greenlet-3.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0093bd1a06d899892427217f0ff2a3c8f306182b8c754336d32e2d587c131b4", size = 1613427, upload-time = "2026-01-23T15:33:44.428Z" },
@@ -958,7 +953,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/e8/2e1462c8fdbe0f210feb5ac7ad2d9029af8be3bf45bd9fa39765f821642f/greenlet-3.3.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5fd23b9bc6d37b563211c6abbb1b3cab27db385a4449af5c32e932f93017080c", size = 274974, upload-time = "2026-01-23T15:31:02.891Z" },
{ url = "https://files.pythonhosted.org/packages/7e/a8/530a401419a6b302af59f67aaf0b9ba1015855ea7e56c036b5928793c5bd/greenlet-3.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f51496a0bfbaa9d74d36a52d2580d1ef5ed4fdfcff0a73730abfbbbe1403dd", size = 577175, upload-time = "2026-01-23T16:00:56.213Z" },
{ url = "https://files.pythonhosted.org/packages/8e/89/7e812bb9c05e1aaef9b597ac1d0962b9021d2c6269354966451e885c4e6b/greenlet-3.3.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb0feb07fe6e6a74615ee62a880007d976cf739b6669cce95daa7373d4fc69c5", size = 590401, upload-time = "2026-01-23T16:05:26.365Z" },
- { url = "https://files.pythonhosted.org/packages/70/ae/e2d5f0e59b94a2269b68a629173263fa40b63da32f5c231307c349315871/greenlet-3.3.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67ea3fc73c8cd92f42467a72b75e8f05ed51a0e9b1d15398c913416f2dafd49f", size = 601161, upload-time = "2026-01-23T16:15:53.456Z" },
{ url = "https://files.pythonhosted.org/packages/5c/ae/8d472e1f5ac5efe55c563f3eabb38c98a44b832602e12910750a7c025802/greenlet-3.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39eda9ba259cc9801da05351eaa8576e9aa83eb9411e8f0c299e05d712a210f2", size = 590272, upload-time = "2026-01-23T15:32:49.411Z" },
{ url = "https://files.pythonhosted.org/packages/a8/51/0fde34bebfcadc833550717eade64e35ec8738e6b097d5d248274a01258b/greenlet-3.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e2e7e882f83149f0a71ac822ebf156d902e7a5d22c9045e3e0d1daf59cee2cc9", size = 1550729, upload-time = "2026-01-23T16:04:20.867Z" },
{ url = "https://files.pythonhosted.org/packages/16/c9/2fb47bee83b25b119d5a35d580807bb8b92480a54b68fef009a02945629f/greenlet-3.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80aa4d79eb5564f2e0a6144fcc744b5a37c56c4a92d60920720e99210d88db0f", size = 1615552, upload-time = "2026-01-23T15:33:45.743Z" },
@@ -1104,21 +1098,135 @@ wheels = [
]
[[package]]
-name = "importlib-resources"
-version = "6.5.2"
+name = "ipykernel"
+version = "7.2.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" }
+dependencies = [
+ { name = "appnope", marker = "sys_platform == 'darwin'" },
+ { name = "comm" },
+ { name = "debugpy" },
+ { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "jupyter-client" },
+ { name = "jupyter-core" },
+ { name = "matplotlib-inline" },
+ { name = "nest-asyncio" },
+ { name = "packaging" },
+ { name = "psutil" },
+ { name = "pyzmq" },
+ { name = "tornado" },
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ca/8d/b68b728e2d06b9e0051019640a40a9eb7a88fcd82c2e1b5ce70bef5ff044/ipykernel-7.2.0.tar.gz", hash = "sha256:18ed160b6dee2cbb16e5f3575858bc19d8f1fe6046a9a680c708494ce31d909e", size = 176046, upload-time = "2026-02-06T16:43:27.403Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" },
+ { url = "https://files.pythonhosted.org/packages/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl", hash = "sha256:3bbd4420d2b3cc105cbdf3756bfc04500b1e52f090a90716851f3916c62e1661", size = 118788, upload-time = "2026-02-06T16:43:25.149Z" },
]
[[package]]
-name = "iniconfig"
-version = "2.3.0"
+name = "ipython"
+version = "8.38.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.11' and sys_platform == 'darwin'",
+ "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')",
+]
+dependencies = [
+ { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" },
+ { name = "decorator", marker = "python_full_version < '3.11'" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "jedi", marker = "python_full_version < '3.11'" },
+ { name = "matplotlib-inline", marker = "python_full_version < '3.11'" },
+ { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
+ { name = "prompt-toolkit", marker = "python_full_version < '3.11'" },
+ { name = "pygments", marker = "python_full_version < '3.11'" },
+ { name = "stack-data", marker = "python_full_version < '3.11'" },
+ { name = "traitlets", marker = "python_full_version < '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e5/61/1810830e8b93c72dcd3c0f150c80a00c3deb229562d9423807ec92c3a539/ipython-8.38.0.tar.gz", hash = "sha256:9cfea8c903ce0867cc2f23199ed8545eb741f3a69420bfcf3743ad1cec856d39", size = 5513996, upload-time = "2026-01-05T10:59:06.901Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9f/df/db59624f4c71b39717c423409950ac3f2c8b2ce4b0aac843112c7fb3f721/ipython-8.38.0-py3-none-any.whl", hash = "sha256:750162629d800ac65bb3b543a14e7a74b0e88063eac9b92124d4b2aa3f6d8e86", size = 831813, upload-time = "2026-01-05T10:59:04.239Z" },
+]
+
+[[package]]
+name = "ipython"
+version = "9.10.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.11' and sys_platform == 'darwin'",
+ "python_full_version >= '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "(python_full_version >= '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')",
+]
+dependencies = [
+ { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" },
+ { name = "decorator", marker = "python_full_version >= '3.11'" },
+ { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" },
+ { name = "jedi", marker = "python_full_version >= '3.11'" },
+ { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" },
+ { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
+ { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" },
+ { name = "pygments", marker = "python_full_version >= '3.11'" },
+ { name = "stack-data", marker = "python_full_version >= '3.11'" },
+ { name = "traitlets", marker = "python_full_version >= '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a6/60/2111715ea11f39b1535bed6024b7dec7918b71e5e5d30855a5b503056b50/ipython-9.10.0.tar.gz", hash = "sha256:cd9e656be97618a0676d058134cd44e6dc7012c0e5cb36a9ce96a8c904adaf77", size = 4426526, upload-time = "2026-02-02T10:00:33.594Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3d/aa/898dec789a05731cd5a9f50605b7b44a72bd198fd0d4528e11fc610177cc/ipython-9.10.0-py3-none-any.whl", hash = "sha256:c6ab68cc23bba8c7e18e9b932797014cc61ea7fd6f19de180ab9ba73e65ee58d", size = 622774, upload-time = "2026-02-02T10:00:31.503Z" },
+]
+
+[[package]]
+name = "ipython-pygments-lexers"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pygments", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" },
+]
+
+[[package]]
+name = "ipywidgets"
+version = "8.1.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "comm" },
+ { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "jupyterlab-widgets" },
+ { name = "traitlets" },
+ { name = "widgetsnbextension" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4c/ae/c5ce1edc1afe042eadb445e95b0671b03cee61895264357956e61c0d2ac0/ipywidgets-8.1.8.tar.gz", hash = "sha256:61f969306b95f85fba6b6986b7fe45d73124d1d9e3023a8068710d47a22ea668", size = 116739, upload-time = "2025-11-01T21:18:12.393Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl", hash = "sha256:ecaca67aed704a338f88f67b1181b58f821ab5dc89c1f0f5ef99db43c1c2921e", size = 139808, upload-time = "2025-11-01T21:18:10.956Z" },
+]
+
+[[package]]
+name = "isoduration"
+version = "20.11.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+dependencies = [
+ { name = "arrow" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649, upload-time = "2020-11-01T11:00:00.312Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" },
+]
+
+[[package]]
+name = "jedi"
+version = "0.19.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "parso" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" },
]
[[package]]
@@ -1142,6 +1250,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" },
]
+[[package]]
+name = "json5"
+version = "0.13.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/77/e8/a3f261a66e4663f22700bc8a17c08cb83e91fbf086726e7a228398968981/json5-0.13.0.tar.gz", hash = "sha256:b1edf8d487721c0bf64d83c28e91280781f6e21f4a797d3261c7c828d4c165bf", size = 52441, upload-time = "2026-01-01T19:42:14.99Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d7/9e/038522f50ceb7e74f1f991bf1b699f24b0c2bbe7c390dd36ad69f4582258/json5-0.13.0-py3-none-any.whl", hash = "sha256:9a08e1dd65f6a4d4c6fa82d216cf2477349ec2346a38fd70cc11d2557499fbcc", size = 36163, upload-time = "2026-01-01T19:42:13.962Z" },
+]
+
[[package]]
name = "jsonpatch"
version = "1.33"
@@ -1164,41 +1281,244 @@ wheels = [
]
[[package]]
-name = "kimimaro"
-version = "5.8.1"
+name = "jsonschema"
+version = "4.26.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "click" },
- { name = "connected-components-3d" },
- { name = "dijkstra3d" },
- { name = "edt" },
- { name = "fastremap" },
- { name = "fill-voids" },
- { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
- { name = "numpy" },
- { name = "osteoid" },
- { name = "pathos" },
- { name = "pytest" },
- { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
- { name = "tqdm" },
- { name = "xs3d" },
+ { name = "attrs" },
+ { name = "jsonschema-specifications" },
+ { name = "referencing" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
+]
+
+[package.optional-dependencies]
+format-nongpl = [
+ { name = "fqdn" },
+ { name = "idna" },
+ { name = "isoduration" },
+ { name = "jsonpointer" },
+ { name = "rfc3339-validator" },
+ { name = "rfc3986-validator" },
+ { name = "rfc3987-syntax" },
+ { name = "uri-template" },
+ { name = "webcolors" },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2025.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "referencing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
+]
+
+[[package]]
+name = "jupyter"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "ipykernel" },
+ { name = "ipywidgets" },
+ { name = "jupyter-console" },
+ { name = "jupyterlab" },
+ { name = "nbconvert" },
+ { name = "notebook" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/58/f3/af28ea964ab8bc1e472dba2e82627d36d470c51f5cd38c37502eeffaa25e/jupyter-1.1.1.tar.gz", hash = "sha256:d55467bceabdea49d7e3624af7e33d59c37fff53ed3a350e1ac957bed731de7a", size = 5714959, upload-time = "2024-08-30T07:15:48.299Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/64/285f20a31679bf547b75602702f7800e74dbabae36ef324f716c02804753/jupyter-1.1.1-py2.py3-none-any.whl", hash = "sha256:7a59533c22af65439b24bbe60373a4e95af8f16ac65a6c00820ad378e3f7cc83", size = 2657, upload-time = "2024-08-30T07:15:47.045Z" },
+]
+
+[[package]]
+name = "jupyter-client"
+version = "8.8.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jupyter-core" },
+ { name = "python-dateutil" },
+ { name = "pyzmq" },
+ { name = "tornado" },
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" },
+]
+
+[[package]]
+name = "jupyter-console"
+version = "6.6.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "ipykernel" },
+ { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
+ { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "jupyter-client" },
+ { name = "jupyter-core" },
+ { name = "prompt-toolkit" },
+ { name = "pygments" },
+ { name = "pyzmq" },
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bd/2d/e2fd31e2fc41c14e2bcb6c976ab732597e907523f6b2420305f9fc7fdbdb/jupyter_console-6.6.3.tar.gz", hash = "sha256:566a4bf31c87adbfadf22cdf846e3069b59a71ed5da71d6ba4d8aaad14a53539", size = 34363, upload-time = "2023-03-06T14:13:31.02Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ca/77/71d78d58f15c22db16328a476426f7ac4a60d3a5a7ba3b9627ee2f7903d4/jupyter_console-6.6.3-py3-none-any.whl", hash = "sha256:309d33409fcc92ffdad25f0bcdf9a4a9daa61b6f341177570fdac03de5352485", size = 24510, upload-time = "2023-03-06T14:13:28.229Z" },
+]
+
+[[package]]
+name = "jupyter-core"
+version = "5.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "platformdirs" },
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" },
+]
+
+[[package]]
+name = "jupyter-events"
+version = "0.12.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jsonschema", extra = ["format-nongpl"] },
+ { name = "packaging" },
+ { name = "python-json-logger" },
+ { name = "pyyaml" },
+ { name = "referencing" },
+ { name = "rfc3339-validator" },
+ { name = "rfc3986-validator" },
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9d/c3/306d090461e4cf3cd91eceaff84bede12a8e52cd821c2d20c9a4fd728385/jupyter_events-0.12.0.tar.gz", hash = "sha256:fc3fce98865f6784c9cd0a56a20644fc6098f21c8c33834a8d9fe383c17e554b", size = 62196, upload-time = "2025-02-03T17:23:41.485Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl", hash = "sha256:6464b2fa5ad10451c3d35fabc75eab39556ae1e2853ad0c0cc31b656731a97fb", size = 19430, upload-time = "2025-02-03T17:23:38.643Z" },
+]
+
+[[package]]
+name = "jupyter-lsp"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jupyter-server" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/eb/5a/9066c9f8e94ee517133cd98dba393459a16cd48bba71a82f16a65415206c/jupyter_lsp-2.3.0.tar.gz", hash = "sha256:458aa59339dc868fb784d73364f17dbce8836e906cd75fd471a325cba02e0245", size = 54823, upload-time = "2025-08-27T17:47:34.671Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl", hash = "sha256:e914a3cb2addf48b1c7710914771aaf1819d46b2e5a79b0f917b5478ec93f34f", size = 76687, upload-time = "2025-08-27T17:47:33.15Z" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/01/20/26134b124b3f06d899ebef7ae3859e8a0a2f002f5366371d93213ed06614/kimimaro-5.8.1.tar.gz", hash = "sha256:d8bb3e7c5eaa2ba4ded5362cea864cd7f1c3411d6eaf745bd89872f6faf2a581", size = 524327, upload-time = "2026-01-16T17:10:51.064Z" }
+
+[[package]]
+name = "jupyter-server"
+version = "2.17.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "argon2-cffi" },
+ { name = "jinja2" },
+ { name = "jupyter-client" },
+ { name = "jupyter-core" },
+ { name = "jupyter-events" },
+ { name = "jupyter-server-terminals" },
+ { name = "nbconvert" },
+ { name = "nbformat" },
+ { name = "overrides" },
+ { name = "packaging" },
+ { name = "prometheus-client" },
+ { name = "pywinpty", marker = "(os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" },
+ { name = "pyzmq" },
+ { name = "send2trash" },
+ { name = "terminado" },
+ { name = "tornado" },
+ { name = "traitlets" },
+ { name = "websocket-client" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5b/ac/e040ec363d7b6b1f11304cc9f209dac4517ece5d5e01821366b924a64a50/jupyter_server-2.17.0.tar.gz", hash = "sha256:c38ea898566964c888b4772ae1ed58eca84592e88251d2cfc4d171f81f7e99d5", size = 731949, upload-time = "2025-08-21T14:42:54.042Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl", hash = "sha256:e8cb9c7db4251f51ed307e329b81b72ccf2056ff82d50524debde1ee1870e13f", size = 388221, upload-time = "2025-08-21T14:42:52.034Z" },
+]
+
+[[package]]
+name = "jupyter-server-terminals"
+version = "0.5.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pywinpty", marker = "(os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" },
+ { name = "terminado" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f4/a7/bcd0a9b0cbba88986fe944aaaf91bfda603e5a50bda8ed15123f381a3b2f/jupyter_server_terminals-0.5.4.tar.gz", hash = "sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5", size = 31770, upload-time = "2026-01-14T16:53:20.213Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl", hash = "sha256:55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14", size = 13704, upload-time = "2026-01-14T16:53:18.738Z" },
+]
+
+[[package]]
+name = "jupyterlab"
+version = "4.5.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "async-lru" },
+ { name = "httpx" },
+ { name = "ipykernel" },
+ { name = "jinja2" },
+ { name = "jupyter-core" },
+ { name = "jupyter-lsp" },
+ { name = "jupyter-server" },
+ { name = "jupyterlab-server" },
+ { name = "notebook-shim" },
+ { name = "packaging" },
+ { name = "setuptools" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+ { name = "tornado" },
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ac/d5/730628e03fff2e8a8e8ccdaedde1489ab1309f9a4fa2536248884e30b7c7/jupyterlab-4.5.6.tar.gz", hash = "sha256:642fe2cfe7f0f5922a8a558ba7a0d246c7bc133b708dfe43f7b3a826d163cf42", size = 23970670, upload-time = "2026-03-11T14:17:04.531Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl", hash = "sha256:d6b3dac883aa4d9993348e0f8e95b24624f75099aed64eab6a4351a9cdd1e580", size = 12447124, upload-time = "2026-03-11T14:17:00.229Z" },
+]
+
+[[package]]
+name = "jupyterlab-pygments"
+version = "0.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" },
+]
+
+[[package]]
+name = "jupyterlab-server"
+version = "2.28.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "babel" },
+ { name = "jinja2" },
+ { name = "json5" },
+ { name = "jsonschema" },
+ { name = "jupyter-server" },
+ { name = "packaging" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d6/2c/90153f189e421e93c4bb4f9e3f59802a1f01abd2ac5cf40b152d7f735232/jupyterlab_server-2.28.0.tar.gz", hash = "sha256:35baa81898b15f93573e2deca50d11ac0ae407ebb688299d3a5213265033712c", size = 76996, upload-time = "2025-10-22T13:59:18.37Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl", hash = "sha256:e4355b148fdcf34d312bbbc80f22467d6d20460e8b8736bf235577dd18506968", size = 59830, upload-time = "2025-10-22T13:59:16.767Z" },
+]
+
+[[package]]
+name = "jupyterlab-widgets"
+version = "3.0.16"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/26/2d/ef58fed122b268c69c0aa099da20bc67657cdfb2e222688d5731bd5b971d/jupyterlab_widgets-3.0.16.tar.gz", hash = "sha256:423da05071d55cf27a9e602216d35a3a65a3e41cdf9c5d3b643b814ce38c19e0", size = 897423, upload-time = "2025-11-01T21:11:29.724Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d6/1f/de526141168cf8a1e787c70d53eb72822c5f93f673c5228ec8c22dfa0c9e/kimimaro-5.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e91957f959f2e64290ce73e7ad082ebcfd5cbd4b22c3c4312309c33aec38f073", size = 346394, upload-time = "2026-01-16T17:09:51.393Z" },
- { url = "https://files.pythonhosted.org/packages/82/47/ae6d489f455173b65263e8e3f8472790426de0d917cf0b0210396be706b9/kimimaro-5.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91f5f35aa362edfafb4025c0eab521c4df2cc635a5a31fd1819f4e78b1426173", size = 313140, upload-time = "2026-01-16T17:09:52.9Z" },
- { url = "https://files.pythonhosted.org/packages/4c/64/f661318954ba5fa21d89029ce5809e8282dbb70afbe1a10b9f53d4fe3e0a/kimimaro-5.8.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ceb28c72bffeeb2ec6de6665e86dc46c303547bbab1427bd811b0fd53acada38", size = 2620655, upload-time = "2026-01-16T17:09:54.566Z" },
- { url = "https://files.pythonhosted.org/packages/0d/3d/5583e41b8c8c060f57d0114d8e5e9263ed4100a259e299a9f9e5e0efb270/kimimaro-5.8.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f864ef8e8b7436f9d06496cf9498b06b106b620b0a0d4956efed39c62958600f", size = 2692015, upload-time = "2026-01-16T17:09:56.253Z" },
- { url = "https://files.pythonhosted.org/packages/7f/d2/140d26044241585cddf9ea6293aad34c6a55843c7ed935c64041161b1c11/kimimaro-5.8.1-cp310-cp310-win32.whl", hash = "sha256:a9df22efc1ea0f32a5db8af8dc0f67e5e83cd1ff2b79c8bc5cacefd166ca8b15", size = 269770, upload-time = "2026-01-16T17:09:59.203Z" },
- { url = "https://files.pythonhosted.org/packages/96/b7/121136f33875858a205cf81a572de382f89b73a21dc3d5a416ff6676b10a/kimimaro-5.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:ff9d4ca1150fa15fabbb7c4783e6983bc336bd398e2b9e189791accff5c62fd9", size = 318581, upload-time = "2026-01-16T17:09:57.61Z" },
- { url = "https://files.pythonhosted.org/packages/ec/d5/d0cfc3fe47fe04cd0dba8aea35a06a9528524b8411a665d11c09a5b16fce/kimimaro-5.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c9614d2451b4ea411f9878f1542b506a54f4acbb2fe4fbe9038ce2e1fad6d91a", size = 347976, upload-time = "2026-01-16T17:10:00.261Z" },
- { url = "https://files.pythonhosted.org/packages/b2/92/cec2e2ec2c1b9f94d275e61a0c56af04192ad17bed79daf9eab7fcc68fa6/kimimaro-5.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:20958c1d7f6ac9d0d3aadbaff8042c90b59948c87da77f4f8da82a0ec1ed904c", size = 312108, upload-time = "2026-01-16T17:10:01.323Z" },
- { url = "https://files.pythonhosted.org/packages/76/85/2fc1b41b90facd150e07e161963d210e9a1fd687cc4ad01765322a14536a/kimimaro-5.8.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22ee509971a8d01261270c08a4de922fef5e33b9af7c99ad92b0010236fab87f", size = 2749730, upload-time = "2026-01-16T17:10:02.622Z" },
- { url = "https://files.pythonhosted.org/packages/7c/75/39620a67bc49b8e9686ecc65b9d68a9c4ccd3de52d5a55db403840a1876c/kimimaro-5.8.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c1e82b38dbe9c52e44c8f9bbbe0685add4f8b1b683767461fe062bf855fccc8", size = 2818393, upload-time = "2026-01-16T17:10:04.593Z" },
- { url = "https://files.pythonhosted.org/packages/fb/a8/6855fae5a2197aa4c107b83df24dffbf84b4fcee8c3245466ad0a24fef07/kimimaro-5.8.1-cp311-cp311-win32.whl", hash = "sha256:bc155d673d365f68157b9e247680effb1c334b04fbf000ff55944ff350667a0d", size = 270402, upload-time = "2026-01-16T17:10:07.63Z" },
- { url = "https://files.pythonhosted.org/packages/94/52/7329a3444cab562d3b2cd5f91df15f58bdc056b8cf941b8f8f58e6190bd8/kimimaro-5.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:09864ce03edef7ef124eb3846011c1c6abe90929578933d97628ef24289a1d72", size = 319922, upload-time = "2026-01-16T17:10:06.24Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl", hash = "sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8", size = 914926, upload-time = "2025-11-01T21:11:28.008Z" },
]
[[package]]
@@ -1422,49 +1742,24 @@ wheels = [
]
[[package]]
-name = "lazy-loader"
-version = "0.4"
+name = "lark"
+version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "packaging" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/6f/6b/c875b30a1ba490860c93da4cabf479e03f584eba06fe5963f6f6644653d8/lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1", size = 15431, upload-time = "2024-04-05T13:03:12.261Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload-time = "2025-10-27T18:25:56.653Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc", size = 12097, upload-time = "2024-04-05T13:03:10.514Z" },
+ { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" },
]
[[package]]
-name = "lightning-utilities"
-version = "0.15.3"
+name = "lazy-loader"
+version = "0.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/f1/45/7fa8f56b17dc0f0a41ec70dd307ecd6787254483549843bef4c30ab5adce/lightning_utilities-0.15.3.tar.gz", hash = "sha256:792ae0204c79f6859721ac7f386c237a33b0ed06ba775009cb894e010a842033", size = 33553, upload-time = "2026-02-22T14:48:53.348Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/25/f4/ead6e0e37209b07c9baa3e984ccdb0348ca370b77cea3aaea8ddbb097e00/lightning_utilities-0.15.3-py3-none-any.whl", hash = "sha256:6c55f1bee70084a1cbeaa41ada96e4b3a0fea5909e844dd335bd80f5a73c5f91", size = 31906, upload-time = "2026-02-22T14:48:52.488Z" },
-]
-
-[[package]]
-name = "mahotas"
-version = "1.4.18"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "numpy" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/f6/71/bf99df8458c0ca05cb9a16f400e66c09b37b15ea124aaa3becb577555cc5/mahotas-1.4.18.tar.gz", hash = "sha256:e6bd2eea4143a24f381b30c64078503cd8ffa20ca493e39ffa29f9d024d9cf8b", size = 1533222, upload-time = "2024-07-17T21:11:56.204Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/6b/c875b30a1ba490860c93da4cabf479e03f584eba06fe5963f6f6644653d8/lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1", size = 15431, upload-time = "2024-04-05T13:03:12.261Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e1/83/f291cb8d7897509e967a9bb0313b585e4ab81a45d0e4cab5e31cc599f6e3/mahotas-1.4.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0238a4665d55f936c6dfb26293e7348482cf9c71bd1caec3a896bfb988b6623b", size = 1877317, upload-time = "2024-07-17T21:10:14.071Z" },
- { url = "https://files.pythonhosted.org/packages/b9/7b/3c09b1bb0c0c045a33cef7763d094a7857c7475564e223391845f176cdb2/mahotas-1.4.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a33ef7e8bd0ff08990d08274a7d7aaaf1143540983de3e036295d6668ce12cb6", size = 1798019, upload-time = "2024-07-17T21:10:17.99Z" },
- { url = "https://files.pythonhosted.org/packages/54/1a/2f6dbb52599c9da9aac6da0f60950f6b7eed54a3d38779bf0f80d41b3eb9/mahotas-1.4.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f890b79891184a4a6dcc274da847fbf54fe4b8fa8839705af677cdb63536f22", size = 5788791, upload-time = "2024-07-17T21:10:23.018Z" },
- { url = "https://files.pythonhosted.org/packages/ed/88/075d55003e28ff9964cd72b5d3c48f37b4320ab2d209010970005aac63a0/mahotas-1.4.18-cp310-cp310-win32.whl", hash = "sha256:e1804359325ebd5a08998c7d3837ea10883a66678007662e4c849013f7d084ea", size = 1712133, upload-time = "2024-07-17T21:10:25.79Z" },
- { url = "https://files.pythonhosted.org/packages/2b/3c/2ca2b24f311586f341ba254670cc320944e7d3cf1cc741ce5a622611c668/mahotas-1.4.18-cp310-cp310-win_amd64.whl", hash = "sha256:43a605408b2e9fd77f4adb0ff301bc5c096979cab06da32788fc18c3b06378db", size = 1731332, upload-time = "2024-07-17T21:10:28.215Z" },
- { url = "https://files.pythonhosted.org/packages/6c/15/fab81001a735766f8bbe7080e714b9582817bd479b915977e748199f00d9/mahotas-1.4.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:974050ee67913ac2396b4889247577f7202038dc328b50a07f83887c56ca9774", size = 1877309, upload-time = "2024-07-17T21:10:30.758Z" },
- { url = "https://files.pythonhosted.org/packages/4f/f2/3125072f76b7809bd66748b2f6872dbeb0e72f43fdd94e73a9d3df95aa4c/mahotas-1.4.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28c93bdfefd4cf271bf2b30b69c130ab3cac5d840dcd3b5ae6e7f6d3648533aa", size = 1798029, upload-time = "2024-07-17T21:10:33.063Z" },
- { url = "https://files.pythonhosted.org/packages/81/fc/691ed6d7aedaf8caa30786e88462edd84e78d2b50d320a07937e2ce41fb1/mahotas-1.4.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f4a41dd3b49bc2e5240b265b9ab35a6793c20ddcb3b392f3ba27a0940086de6", size = 5804451, upload-time = "2024-07-17T21:10:36.365Z" },
- { url = "https://files.pythonhosted.org/packages/85/45/eded44b0d6d1e4642c87eb79b6f568b8a2cbe7183dbb6aec185ea6a54786/mahotas-1.4.18-cp311-cp311-win32.whl", hash = "sha256:a4e70ead2a2bf6e8ad9a70f9c33fe0e752edeeea1fc5e8e934efcf29d90d69f2", size = 1712194, upload-time = "2024-07-17T21:10:39.565Z" },
- { url = "https://files.pythonhosted.org/packages/e0/0c/3710525e4d3a2cb28852cb77878d8268e3e56c52cdb4018972685a11e6cd/mahotas-1.4.18-cp311-cp311-win_amd64.whl", hash = "sha256:7a9a7b2a9e3e9d9818a901232fc68a2f7bef31483150ac39acb7d56f86e0754c", size = 1731319, upload-time = "2024-07-17T21:10:41.922Z" },
+ { url = "https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc", size = 12097, upload-time = "2024-04-05T13:03:10.514Z" },
]
[[package]]
@@ -1557,6 +1852,30 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" },
]
+[[package]]
+name = "matplotlib-inline"
+version = "0.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" },
+]
+
+[[package]]
+name = "mistune"
+version = "3.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9d/55/d01f0c4b45ade6536c51170b9043db8b2ec6ddf4a35c7ea3f5f559ac935b/mistune-3.2.0.tar.gz", hash = "sha256:708487c8a8cdd99c9d90eb3ed4c3ed961246ff78ac82f03418f5183ab70e398a", size = 95467, upload-time = "2025-12-23T11:36:34.994Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" },
+]
+
[[package]]
name = "monai"
version = "1.5.2"
@@ -1628,32 +1947,76 @@ wheels = [
]
[[package]]
-name = "multiprocess"
-version = "0.70.19"
+name = "mypy-extensions"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
+]
+
+[[package]]
+name = "nbclient"
+version = "0.10.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "dill" },
+ { name = "jupyter-client" },
+ { name = "jupyter-core" },
+ { name = "nbformat" },
+ { name = "traitlets" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a2/f2/e783ac7f2aeeed14e9e12801f22529cc7e6b7ab80928d6dcce4e9f00922d/multiprocess-0.70.19.tar.gz", hash = "sha256:952021e0e6c55a4a9fe4cd787895b86e239a40e76802a789d6305398d3975897", size = 2079989, upload-time = "2026-01-19T06:47:39.744Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/56/91/1c1d5a4b9a9ebba2b4e32b8c852c2975c872aec1fe42ab5e516b2cecd193/nbclient-0.10.4.tar.gz", hash = "sha256:1e54091b16e6da39e297b0ece3e10f6f29f4ac4e8ee515d29f8a7099bd6553c9", size = 62554, upload-time = "2025-12-23T07:45:46.369Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/8b/b6/10832f96b499690854e574360be342a282f5f7dba58eff791299ff6c0637/multiprocess-0.70.19-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:02e5c35d7d6cd2bdc89c1858867f7bde4012837411023a4696c148c1bdd7c80e", size = 135131, upload-time = "2026-01-19T06:47:20.479Z" },
- { url = "https://files.pythonhosted.org/packages/99/50/faef2d8106534b0dc4a0b772668a1a99682696ebf17d3c0f13f2ed6a656a/multiprocess-0.70.19-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:79576c02d1207ec405b00cabf2c643c36070800cca433860e14539df7818b2aa", size = 135131, upload-time = "2026-01-19T06:47:21.879Z" },
- { url = "https://files.pythonhosted.org/packages/94/b1/0b71d18b76bf423c2e8ee00b31db37d17297ab3b4db44e188692afdca628/multiprocess-0.70.19-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c6b6d78d43a03b68014ca1f0b7937d965393a670c5de7c29026beb2258f2f896", size = 135134, upload-time = "2026-01-19T06:47:23.262Z" },
- { url = "https://files.pythonhosted.org/packages/7e/aa/714635c727dbfc251139226fa4eaf1b07f00dc12d9cd2eb25f931adaf873/multiprocess-0.70.19-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1bbf1b69af1cf64cd05f65337d9215b88079ec819cd0ea7bac4dab84e162efe7", size = 144743, upload-time = "2026-01-19T06:47:24.562Z" },
- { url = "https://files.pythonhosted.org/packages/0f/e1/155f6abf5e6b5d9cef29b6d0167c180846157a4aca9b9bee1a217f67c959/multiprocess-0.70.19-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5be9ec7f0c1c49a4f4a6fd20d5dda4aeabc2d39a50f4ad53720f1cd02b3a7c2e", size = 144738, upload-time = "2026-01-19T06:47:26.636Z" },
- { url = "https://files.pythonhosted.org/packages/af/cb/f421c2869d75750a4f32301cc20c4b63fab6376e9a75c8e5e655bdeb3d9b/multiprocess-0.70.19-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1c3dce098845a0db43b32a0b76a228ca059a668071cfeaa0f40c36c0b1585d45", size = 144741, upload-time = "2026-01-19T06:47:27.985Z" },
- { url = "https://files.pythonhosted.org/packages/e3/45/8004d1e6b9185c1a444d6b55ac5682acf9d98035e54386d967366035a03a/multiprocess-0.70.19-py310-none-any.whl", hash = "sha256:97404393419dcb2a8385910864eedf47a3cadf82c66345b44f036420eb0b5d87", size = 134948, upload-time = "2026-01-19T06:47:32.325Z" },
- { url = "https://files.pythonhosted.org/packages/86/c2/dec9722dc3474c164a0b6bcd9a7ed7da542c98af8cabce05374abab35edd/multiprocess-0.70.19-py311-none-any.whl", hash = "sha256:928851ae7973aea4ce0eaf330bbdafb2e01398a91518d5c8818802845564f45c", size = 144457, upload-time = "2026-01-19T06:47:33.711Z" },
- { url = "https://files.pythonhosted.org/packages/7e/82/69e539c4c2027f1e1697e09aaa2449243085a0edf81ae2c6341e84d769b6/multiprocess-0.70.19-py39-none-any.whl", hash = "sha256:0d4b4397ed669d371c81dcd1ef33fd384a44d6c3de1bd0ca7ac06d837720d3c5", size = 133477, upload-time = "2026-01-19T06:47:38.619Z" },
+ { url = "https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl", hash = "sha256:9162df5a7373d70d606527300a95a975a47c137776cd942e52d9c7e29ff83440", size = 25465, upload-time = "2025-12-23T07:45:44.51Z" },
]
[[package]]
-name = "mypy-extensions"
-version = "1.1.0"
+name = "nbconvert"
+version = "7.17.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
+dependencies = [
+ { name = "beautifulsoup4" },
+ { name = "bleach", extra = ["css"] },
+ { name = "defusedxml" },
+ { name = "jinja2" },
+ { name = "jupyter-core" },
+ { name = "jupyterlab-pygments" },
+ { name = "markupsafe" },
+ { name = "mistune" },
+ { name = "nbclient" },
+ { name = "nbformat" },
+ { name = "packaging" },
+ { name = "pandocfilters" },
+ { name = "pygments" },
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/38/47/81f886b699450d0569f7bc551df2b1673d18df7ff25cc0c21ca36ed8a5ff/nbconvert-7.17.0.tar.gz", hash = "sha256:1b2696f1b5be12309f6c7d707c24af604b87dfaf6d950794c7b07acab96dda78", size = 862855, upload-time = "2026-01-29T16:37:48.478Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/4b/8d5f796a792f8a25f6925a96032f098789f448571eb92011df1ae59e8ea8/nbconvert-7.17.0-py3-none-any.whl", hash = "sha256:4f99a63b337b9a23504347afdab24a11faa7d86b405e5c8f9881cd313336d518", size = 261510, upload-time = "2026-01-29T16:37:46.322Z" },
+]
+
+[[package]]
+name = "nbformat"
+version = "5.10.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "fastjsonschema" },
+ { name = "jsonschema" },
+ { name = "jupyter-core" },
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" },
+]
+
+[[package]]
+name = "nest-asyncio"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" },
]
[[package]]
@@ -1710,18 +2073,31 @@ wheels = [
]
[[package]]
-name = "nibabel"
-version = "5.4.2"
+name = "notebook"
+version = "7.5.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "importlib-resources" },
- { name = "numpy" },
- { name = "packaging" },
- { name = "typing-extensions" },
+ { name = "jupyter-server" },
+ { name = "jupyterlab" },
+ { name = "jupyterlab-server" },
+ { name = "notebook-shim" },
+ { name = "tornado" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/1f/6d/41052c48d6f6349ca0a7c4d1f6a78464de135e6d18f5829ba2510e62184c/notebook-7.5.5.tar.gz", hash = "sha256:dc0bfab0f2372c8278c457423d3256c34154ac2cc76bf20e9925260c461013c3", size = 14169167, upload-time = "2026-03-11T16:32:51.922Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f8/aa/cbd1deb9f07446241e88f8d5fecccd95b249bca0b4e5482214a4d1714c49/notebook-7.5.5-py3-none-any.whl", hash = "sha256:a7c14dbeefa6592e87f72290ca982e0c10f5bbf3786be2a600fda9da2764a2b8", size = 14578929, upload-time = "2026-03-11T16:32:48.021Z" },
+]
+
+[[package]]
+name = "notebook-shim"
+version = "0.2.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jupyter-server" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a3/01/3d2cc510c616bc8e27be17a063070d9126f69407961594a9ae734ea51121/nibabel-5.4.2.tar.gz", hash = "sha256:d5f4b9076a13178ae7f7acf18c8dbd503ee1c4d5c0c23b85df7be87efcbb49da", size = 4663132, upload-time = "2026-03-11T13:31:52.42Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/d2/92fa3243712b9a3e8bafaf60aac366da1cada3639ca767ff4b5b3654ec28/notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb", size = 13167, upload-time = "2024-02-14T23:35:18.353Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/3b/d7/601b6396b33536811668935faa790112266c70661be94555999be431f86f/nibabel-5.4.2-py3-none-any.whl", hash = "sha256:553482c5f1e1034fc312edf6fb7f32236c0056439845d1c29293b7e8c98d4854", size = 3300985, upload-time = "2026-03-11T13:31:50.028Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307, upload-time = "2024-02-14T23:35:16.286Z" },
]
[[package]]
@@ -1957,19 +2333,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/47/4f/4a617ee93d8208d2bcf26b2d8b9402ceaed03e3853c754940e2290fed063/ollama-0.6.1-py3-none-any.whl", hash = "sha256:fc4c984b345735c5486faeee67d8a265214a31cbb828167782dc642ce0a2bf8c", size = 14354, upload-time = "2025-11-13T23:02:16.292Z" },
]
-[[package]]
-name = "omegaconf"
-version = "2.3.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "antlr4-python3-runtime" },
- { name = "pyyaml" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/09/48/6388f1bb9da707110532cb70ec4d2822858ddfb44f1cdf1233c20a80ea4b/omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7", size = 3298120, upload-time = "2022-12-08T20:59:22.753Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b", size = 79500, upload-time = "2022-12-08T20:59:19.686Z" },
-]
-
[[package]]
name = "opencv-python"
version = "4.11.0.86"
@@ -2049,30 +2412,12 @@ wheels = [
]
[[package]]
-name = "osteoid"
-version = "0.6.0"
+name = "overrides"
+version = "7.7.0"
source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "fastremap" },
- { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
- { name = "numpy" },
- { name = "pybind11" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/e9/e0/abbf7fe327cf560da5d222b5a9a27efab541666dcf61c3daf04fad9dd17d/osteoid-0.6.0.tar.gz", hash = "sha256:9f1db97939ee746c64dfac0b6cc980512cc193cd27cab39917852e09fd5799eb", size = 416141, upload-time = "2025-12-06T05:08:25.808Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d3/dc/6d8189b280fbfdc206aa05736a2a802aade10366c935b76d19d4cf9cb3e2/osteoid-0.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:57305dcf3ae84f998b7c35f02e04e817f8e6f7e0770bfea152e4eba35c94710c", size = 140051, upload-time = "2025-12-06T05:07:42.274Z" },
- { url = "https://files.pythonhosted.org/packages/a7/99/e208482af0331c3f0eaccbbf25975589c394b1c59243c2dac08bf5701896/osteoid-0.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:124310a61f0aeb53d78cd5e52c78deb8d40f9f4129fd9d7462d7e6297a260b8e", size = 124156, upload-time = "2025-12-06T05:07:44.071Z" },
- { url = "https://files.pythonhosted.org/packages/4c/98/656e999f473bfca07b298b7898094d1826d02b5c1179ece6c46cb07e5b14/osteoid-0.6.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f2ce6900d84d937a46cbfbbfdee6fd17b429f0ff48d25575293f0b77eab6dcd", size = 145131, upload-time = "2025-12-06T05:07:45.346Z" },
- { url = "https://files.pythonhosted.org/packages/b0/58/2e3ec6630fb54ddb7b80624de69246137e9b4c524d7e02e451dc41e32240/osteoid-0.6.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99e9cda5d8aca2e076e931c28bde2513a0ff6b39849eda0a40c8e6a9282e5f36", size = 152548, upload-time = "2025-12-06T05:07:46.558Z" },
- { url = "https://files.pythonhosted.org/packages/1e/6d/08191d9f8dc9d068eac31f088a3eecb07466336d724a8ad553458de0a4c5/osteoid-0.6.0-cp310-cp310-win32.whl", hash = "sha256:a53149889da92ce18e52214a7dd6b29a8f20c14e9d955fa96c2e55b692deeeeb", size = 115941, upload-time = "2025-12-06T05:07:49.114Z" },
- { url = "https://files.pythonhosted.org/packages/28/97/896e666dd3bc38a071de21f51e6f77dcc0cbb63a818f45c71011a358fb7f/osteoid-0.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f96fc55039c6df54f682c90d4a2b799661cb01429e3c5463e5922ceb1f2f845a", size = 123581, upload-time = "2025-12-06T05:07:47.783Z" },
- { url = "https://files.pythonhosted.org/packages/3c/3d/c61cb66bc44a42cd08f5eb0e98f206ba3bd1476f3c436bb71ea3a1aebcb7/osteoid-0.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0574492d99612ee672e1b3f0eaa978ae76cf74c4ef5930785af442609122193", size = 142497, upload-time = "2025-12-06T05:07:49.949Z" },
- { url = "https://files.pythonhosted.org/packages/18/ac/8956eb1d638d18189cd2286bed4f20b73fcbe24e3885ef60ed4ad55269c5/osteoid-0.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:29728be4990ba55ca14700c0ffe1627d1704005149664c9d347e6a857c2b0358", size = 125398, upload-time = "2025-12-06T05:07:51.166Z" },
- { url = "https://files.pythonhosted.org/packages/8f/8e/7a005c3405310a6f535bc1dea366dacb6dca53515c9a05522aba69194a0a/osteoid-0.6.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b800d5eefb53cecbac1d1b8b461c1c33240b3e8f1d433b75e186abfe0e34c14e", size = 146040, upload-time = "2025-12-06T05:07:52.251Z" },
- { url = "https://files.pythonhosted.org/packages/6e/80/795710958f373259a521ebc78e59bad0f43de6fdbe2bd87520e13c0aea21/osteoid-0.6.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cdc76f6d4390400294cccfbf9f4f5d288c317b7681f70488c730ffc577e6cee4", size = 153290, upload-time = "2025-12-06T05:07:53.147Z" },
- { url = "https://files.pythonhosted.org/packages/8f/0e/267b984cdf824e6495800c635268ae08f79d92658a79bd4672925106ecfa/osteoid-0.6.0-cp311-cp311-win32.whl", hash = "sha256:74f336782032aad57facee460731e175ff253b55e11861e3df7bf4c3fde0fb0d", size = 117069, upload-time = "2025-12-06T05:07:55.158Z" },
- { url = "https://files.pythonhosted.org/packages/09/7a/79b57a34a39bf4f460a4161e64a9f8efeaca050d666967b31816a3b02971/osteoid-0.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:accfa55cb22f12e4abeba70bc5a252ee7f9c3de0500d4a2c3bda4ef49f329cc8", size = 124511, upload-time = "2025-12-06T05:07:54.341Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" },
]
[[package]]
@@ -2085,27 +2430,30 @@ wheels = [
]
[[package]]
-name = "passlib"
-version = "1.7.4"
+name = "pandocfilters"
+version = "1.5.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844, upload-time = "2020-10-08T19:00:52.121Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" },
]
[[package]]
-name = "pathos"
-version = "0.3.5"
+name = "parso"
+version = "0.8.6"
source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "dill" },
- { name = "multiprocess" },
- { name = "pox" },
- { name = "ppft" },
+sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/32/37/0c730979d3890f8096a86af2683fac74edd4d15cb037391098dca70dcb1d/pathos-0.3.5.tar.gz", hash = "sha256:8fe041b8545c5d3880a038f866022bdebf935e5cf68f56ed3407cb7e65193a61", size = 166975, upload-time = "2026-01-20T00:06:57.848Z" }
+
+[[package]]
+name = "passlib"
+version = "1.7.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844, upload-time = "2020-10-08T19:00:52.121Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/37/44/be2146c650ee9ca4d9a770c995f5c92c1ea52292dcf618ce1a336d3146dd/pathos-0.3.5-py3-none-any.whl", hash = "sha256:c95b04103c40a16c08db69cd4b5c52624d55208beadf1348681edece809ec4f8", size = 82248, upload-time = "2026-01-20T00:06:56.291Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" },
]
[[package]]
@@ -2117,6 +2465,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" },
]
+[[package]]
+name = "pexpect"
+version = "4.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "ptyprocess" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" },
+]
+
[[package]]
name = "pillow"
version = "12.1.0"
@@ -2164,30 +2524,24 @@ wheels = [
]
[[package]]
-name = "pluggy"
-version = "1.6.0"
+name = "prometheus-client"
+version = "0.24.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+ { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" },
]
[[package]]
-name = "pox"
-version = "0.3.7"
+name = "prompt-toolkit"
+version = "3.0.52"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/44/58/4385741dea1d74fe9dfed7ff42975266634ef8000f2c8e96717079c916b1/pox-0.3.7.tar.gz", hash = "sha256:0652f6f2103fe6d4ba638beb6fa8d3e8a68fd44bcb63315c614118515bcc3afb", size = 119442, upload-time = "2026-01-19T02:09:12.573Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2a/ac/4d5f104edf2aae2fec85567ec1d1969010de8124c5c45514f25e14900b65/pox-0.3.7-py3-none-any.whl", hash = "sha256:82a495249d13371314c1a5b5626a115e067ef5215d49530bf5efa37fbc25b56a", size = 29402, upload-time = "2026-01-19T02:09:11.024Z" },
+dependencies = [
+ { name = "wcwidth" },
]
-
-[[package]]
-name = "ppft"
-version = "1.7.8"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/8b/d2/281aa3466e948283d51b83238fb456f65e14f8ade5f8627822578cd2708f/ppft-1.7.8.tar.gz", hash = "sha256:5f696d4f397ae9b0af39b1faffb31957c51dfbc5a3815856472d4f4e872937ee", size = 136349, upload-time = "2026-01-19T03:03:13.439Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/8e/e1/d1b380af6443e7c33aeb40617ebdc17c39dc30095235643cc518e3908203/ppft-1.7.8-py3-none-any.whl", hash = "sha256:d3e0e395215b14afc3dd5adfc032ccecfda2d4ed50dc7ded076cd1d215442843", size = 56759, upload-time = "2026-01-19T03:03:11.896Z" },
+ { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" },
]
[[package]]
@@ -2260,6 +2614,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" },
]
+[[package]]
+name = "ptyprocess"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" },
+]
+
+[[package]]
+name = "pure-eval"
+version = "0.2.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" },
+]
+
[[package]]
name = "pyasn1"
version = "0.6.2"
@@ -2281,15 +2653,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" },
]
-[[package]]
-name = "pybind11"
-version = "3.0.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a5/98/9118a0659646f1628c592ef9bb48e0056efa6bf27c951fd12a178e0136fb/pybind11-3.0.2.tar.gz", hash = "sha256:432f01aeb68e361a3a7fc7575c2c7f497595bf640f747acd909ff238dd766e06", size = 577131, upload-time = "2026-02-17T04:46:52.556Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/88/c5/e98d9c51f3d5300d5e40ad9037dd6b3b60736fd02ab68dcc98c96be7592d/pybind11-3.0.2-py3-none-any.whl", hash = "sha256:f8a6500548919cc33bcd220d5f984688326f574fa97f1107f2f4fdb4c6fb019f", size = 310158, upload-time = "2026-02-17T04:46:49.91Z" },
-]
-
[[package]]
name = "pycparser"
version = "3.0"
@@ -2482,24 +2845,6 @@ requires-dist = [
[package.metadata.requires-dev]
dev = [{ name = "black", specifier = ">=26.1.0" }]
-[[package]]
-name = "pytest"
-version = "9.0.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
- { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
- { name = "iniconfig" },
- { name = "packaging" },
- { name = "pluggy" },
- { name = "pygments" },
- { name = "tomli", marker = "python_full_version < '3.11'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
-]
-
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
@@ -2535,6 +2880,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d9/c3/0bd11992072e6a1c513b16500a5d07f91a24017c5909b02c72c62d7ad024/python_jose-3.5.0-py2.py3-none-any.whl", hash = "sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771", size = 34624, upload-time = "2025-05-28T17:31:52.802Z" },
]
+[[package]]
+name = "python-json-logger"
+version = "4.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" },
+]
+
[[package]]
name = "python-multipart"
version = "0.0.20"
@@ -2565,26 +2919,18 @@ wheels = [
[[package]]
name = "pytorch-connectomics"
-version = "2.0.0"
+version = "0.1"
source = { editable = "pytorch_connectomics" }
dependencies = [
- { name = "connected-components-3d" },
- { name = "crackle-codec" },
{ name = "cython" },
{ name = "einops" },
- { name = "fastremap" },
+ { name = "gputil" },
{ name = "h5py" },
{ name = "imageio" },
- { name = "kimimaro" },
- { name = "mahotas" },
+ { name = "jupyter" },
{ name = "matplotlib" },
{ name = "monai" },
- { name = "nibabel" },
- { name = "numpy" },
- { name = "omegaconf" },
{ name = "opencv-python" },
- { name = "psutil" },
- { name = "pytorch-lightning" },
{ name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "scikit-image", version = "0.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
@@ -2592,77 +2938,39 @@ dependencies = [
{ name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "tensorboard" },
- { name = "torch" },
- { name = "torchmetrics" },
- { name = "torchvision" },
{ name = "tqdm" },
+ { name = "yacs" },
]
[package.metadata]
requires-dist = [
- { name = "black", marker = "extra == 'dev'", specifier = ">=22.0.0" },
- { name = "connected-components-3d", specifier = ">=3.0.0" },
- { name = "crackle-codec", specifier = ">=0.1.0" },
{ name = "cython", specifier = ">=0.29.22" },
{ name = "einops", specifier = ">=0.3.0" },
- { name = "fastremap", specifier = ">=1.10.0" },
- { name = "flake8", marker = "extra == 'dev'", specifier = ">=4.0.0" },
- { name = "gputil", marker = "extra == 'full'", specifier = ">=1.4.0" },
+ { name = "gputil", specifier = ">=1.4.0" },
{ name = "h5py", specifier = ">=2.10.0" },
{ name = "imageio", specifier = ">=2.9.0" },
- { name = "isort", marker = "extra == 'dev'", specifier = ">=5.10.0" },
- { name = "jinja2", marker = "extra == 'docs'", specifier = ">=3.0" },
- { name = "kimimaro", specifier = ">=1.0.0" },
- { name = "mahotas", specifier = ">=1.4.0" },
+ { name = "jupyter", specifier = ">=1.0" },
{ name = "matplotlib", specifier = ">=3.3.0" },
{ name = "monai", specifier = ">=0.9.1" },
- { name = "mypy", marker = "extra == 'dev'", specifier = ">=0.950" },
- { name = "nbsphinx", marker = "extra == 'docs'", specifier = ">=0.8.0" },
- { name = "neuroglancer", marker = "extra == 'full'", specifier = ">=1.0.0" },
- { name = "nibabel", specifier = ">=3.0.0" },
- { name = "numpy", specifier = ">=1.23.0" },
- { name = "omegaconf", specifier = ">=2.1.0" },
{ name = "opencv-python", specifier = ">=4.3.0" },
- { name = "optuna", marker = "extra == 'full'", specifier = ">=2.10.0" },
- { name = "psutil", specifier = ">=5.8.0" },
- { name = "pytest", marker = "extra == 'dev'", specifier = ">=6.0.0" },
- { name = "pytest-benchmark", marker = "extra == 'dev'", specifier = ">=3.4.0" },
- { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=3.0.0" },
- { name = "pytorch-lightning", specifier = ">=2.0.0" },
- { name = "pytorch-sphinx-theme", marker = "extra == 'docs'", specifier = ">=0.0.19" },
{ name = "scikit-image", specifier = ">=0.17.2" },
{ name = "scikit-learn", specifier = ">=0.23.1" },
{ name = "scipy", specifier = ">=1.5" },
- { name = "sphinx", marker = "extra == 'docs'", specifier = ">=4.0.0" },
- { name = "sphinx-rtd-theme", marker = "extra == 'docs'", specifier = ">=1.0.0" },
- { name = "sphinxcontrib-katex", marker = "extra == 'docs'" },
{ name = "tensorboard", specifier = ">=2.2.2" },
- { name = "tifffile", marker = "extra == 'full'", specifier = ">=2021.11.2" },
- { name = "torch", specifier = ">=1.8.0" },
- { name = "torchmetrics", specifier = ">=0.11.0" },
- { name = "torchvision", specifier = ">=0.9.0" },
{ name = "tqdm", specifier = ">=4.58.0" },
- { name = "wandb", marker = "extra == 'full'", specifier = ">=0.13.0" },
+ { name = "yacs", specifier = ">=0.1.8" },
]
-provides-extras = ["full", "dev", "docs"]
[[package]]
-name = "pytorch-lightning"
-version = "2.6.1"
+name = "pywinpty"
+version = "3.0.3"
source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "fsspec", extra = ["http"] },
- { name = "lightning-utilities" },
- { name = "packaging" },
- { name = "pyyaml" },
- { name = "torch" },
- { name = "torchmetrics" },
- { name = "tqdm" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/8b/ac/ebd5f6f58691cbd4f73836e43e1727f3814311b960c41f88e259606ca2b2/pytorch_lightning-2.6.1.tar.gz", hash = "sha256:ba08f8901cf226fcca473046ad9346f414e99117762dc869c76e650d5b3d7bdc", size = 665563, upload-time = "2026-01-30T14:59:11.636Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/f7/54/37c7370ba91f579235049dc26cd2c5e657d2a943e01820844ffc81f32176/pywinpty-3.0.3.tar.gz", hash = "sha256:523441dc34d231fb361b4b00f8c99d3f16de02f5005fd544a0183112bcc22412", size = 31309, upload-time = "2026-02-04T21:51:09.524Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/0e/93/c8c361bf0a2fe50f828f32def460e8b8a14b93955d3fd302b1a9b63b19e4/pytorch_lightning-2.6.1-py3-none-any.whl", hash = "sha256:1f8118567ec829e3055f16cf1aa320883a86a47c836951bfd9dcfa34ec7ffd59", size = 857273, upload-time = "2026-01-30T14:59:10.141Z" },
+ { url = "https://files.pythonhosted.org/packages/62/28/a652709bd76ca7533cd1c443b03add9f5051fdf71bc6bdb8801dddd4e7a3/pywinpty-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:ff05f12d775b142b11c6fe085129bdd759b61cf7d41da6c745e78e3a1ef5bf40", size = 2114320, upload-time = "2026-02-04T21:53:50.972Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/13/a0181cc5c2d5635d3dbc3802b97bc8e3ad4fa7502ccef576651a5e08e54c/pywinpty-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:340ccacb4d74278a631923794ccd758471cfc8eeeeee4610b280420a17ad1e82", size = 235670, upload-time = "2026-02-04T21:50:20.324Z" },
+ { url = "https://files.pythonhosted.org/packages/79/c3/3e75075c7f71735f22b66fab0481f2c98e3a4d58cba55cb50ba29114bcf6/pywinpty-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:dff25a9a6435f527d7c65608a7e62783fc12076e7d44487a4911ee91be5a8ac8", size = 2114430, upload-time = "2026-02-04T21:54:19.485Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/1e/8a54166a8c5e4f5cb516514bdf4090be4d51a71e8d9f6d98c0aa00fe45d4/pywinpty-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:fbc1e230e5b193eef4431cba3f39996a288f9958f9c9f092c8a961d930ee8f68", size = 236191, upload-time = "2026-02-04T21:50:36.239Z" },
]
[[package]]
@@ -2691,6 +2999,71 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
]
+[[package]]
+name = "pyzmq"
+version = "27.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "implementation_name == 'pypy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/67/b9/52aa9ec2867528b54f1e60846728d8b4d84726630874fee3a91e66c7df81/pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4", size = 1329850, upload-time = "2025-09-08T23:07:26.274Z" },
+ { url = "https://files.pythonhosted.org/packages/99/64/5653e7b7425b169f994835a2b2abf9486264401fdef18df91ddae47ce2cc/pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556", size = 906380, upload-time = "2025-09-08T23:07:29.78Z" },
+ { url = "https://files.pythonhosted.org/packages/73/78/7d713284dbe022f6440e391bd1f3c48d9185673878034cfb3939cdf333b2/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b", size = 666421, upload-time = "2025-09-08T23:07:31.263Z" },
+ { url = "https://files.pythonhosted.org/packages/30/76/8f099f9d6482450428b17c4d6b241281af7ce6a9de8149ca8c1c649f6792/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e", size = 854149, upload-time = "2025-09-08T23:07:33.17Z" },
+ { url = "https://files.pythonhosted.org/packages/59/f0/37fbfff06c68016019043897e4c969ceab18bde46cd2aca89821fcf4fb2e/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526", size = 1655070, upload-time = "2025-09-08T23:07:35.205Z" },
+ { url = "https://files.pythonhosted.org/packages/47/14/7254be73f7a8edc3587609554fcaa7bfd30649bf89cd260e4487ca70fdaa/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1", size = 2033441, upload-time = "2025-09-08T23:07:37.432Z" },
+ { url = "https://files.pythonhosted.org/packages/22/dc/49f2be26c6f86f347e796a4d99b19167fc94503f0af3fd010ad262158822/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386", size = 1891529, upload-time = "2025-09-08T23:07:39.047Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/3e/154fb963ae25be70c0064ce97776c937ecc7d8b0259f22858154a9999769/pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda", size = 567276, upload-time = "2025-09-08T23:07:40.695Z" },
+ { url = "https://files.pythonhosted.org/packages/62/b2/f4ab56c8c595abcb26b2be5fd9fa9e6899c1e5ad54964e93ae8bb35482be/pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f", size = 632208, upload-time = "2025-09-08T23:07:42.298Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/e3/be2cc7ab8332bdac0522fdb64c17b1b6241a795bee02e0196636ec5beb79/pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32", size = 559766, upload-time = "2025-09-08T23:07:43.869Z" },
+ { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" },
+ { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" },
+ { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" },
+ { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" },
+ { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" },
+ { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/81/a65e71c1552f74dec9dff91d95bafb6e0d33338a8dfefbc88aa562a20c92/pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6", size = 836266, upload-time = "2025-09-08T23:09:40.048Z" },
+ { url = "https://files.pythonhosted.org/packages/58/ed/0202ca350f4f2b69faa95c6d931e3c05c3a397c184cacb84cb4f8f42f287/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90", size = 800206, upload-time = "2025-09-08T23:09:41.902Z" },
+ { url = "https://files.pythonhosted.org/packages/47/42/1ff831fa87fe8f0a840ddb399054ca0009605d820e2b44ea43114f5459f4/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62", size = 567747, upload-time = "2025-09-08T23:09:43.741Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/db/5c4d6807434751e3f21231bee98109aa57b9b9b55e058e450d0aef59b70f/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74", size = 747371, upload-time = "2025-09-08T23:09:45.575Z" },
+ { url = "https://files.pythonhosted.org/packages/26/af/78ce193dbf03567eb8c0dc30e3df2b9e56f12a670bf7eb20f9fb532c7e8a/pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba", size = 544862, upload-time = "2025-09-08T23:09:47.448Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" },
+ { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" },
+ { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" },
+]
+
+[[package]]
+name = "referencing"
+version = "0.37.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "rpds-py" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
+]
+
[[package]]
name = "requests"
version = "2.32.5"
@@ -2718,6 +3091,88 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" },
]
+[[package]]
+name = "rfc3339-validator"
+version = "0.1.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" },
+]
+
+[[package]]
+name = "rfc3986-validator"
+version = "0.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/da/88/f270de456dd7d11dcc808abfa291ecdd3f45ff44e3b549ffa01b126464d0/rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055", size = 6760, upload-time = "2019-10-28T16:00:19.144Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242, upload-time = "2019-10-28T16:00:13.976Z" },
+]
+
+[[package]]
+name = "rfc3987-syntax"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "lark" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2c/06/37c1a5557acf449e8e406a830a05bf885ac47d33270aec454ef78675008d/rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d", size = 14239, upload-time = "2025-07-18T01:05:05.015Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" },
+]
+
+[[package]]
+name = "rpds-py"
+version = "0.30.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" },
+ { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" },
+ { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" },
+ { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" },
+ { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" },
+ { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" },
+ { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" },
+ { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" },
+ { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" },
+ { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" },
+ { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" },
+ { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" },
+ { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" },
+ { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" },
+ { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" },
+ { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" },
+ { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" },
+ { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" },
+ { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" },
+ { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" },
+ { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" },
+ { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" },
+]
+
[[package]]
name = "rsa"
version = "4.9.1"
@@ -2908,6 +3363,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6c/4a/465f96d42c6f33ad324a40049dfd63269891db9324aa66c4a1c108c6f994/scipy-1.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:85b0ac3ad17fa3be50abd7e69d583d98792d7edc08367e01445a1e2076005379", size = 24370427, upload-time = "2026-01-10T21:25:20.514Z" },
]
+[[package]]
+name = "send2trash"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c5/f0/184b4b5f8d00f2a92cf96eec8967a3d550b52cf94362dad1100df9e48d57/send2trash-2.1.0.tar.gz", hash = "sha256:1c72b39f09457db3c05ce1d19158c2cbef4c32b8bedd02c155e49282b7ea7459", size = 17255, upload-time = "2026-01-14T06:27:36.056Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl", hash = "sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c", size = 17610, upload-time = "2026-01-14T06:27:35.218Z" },
+]
+
[[package]]
name = "setuptools"
version = "80.10.2"
@@ -2926,6 +3390,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
+[[package]]
+name = "soupsieve"
+version = "2.8.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" },
+]
+
[[package]]
name = "sqlalchemy"
version = "2.0.46"
@@ -2953,6 +3426,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/a1/9c4efa03300926601c19c18582531b45aededfb961ab3c3585f1e24f120b/sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e", size = 1937882, upload-time = "2026-01-21T18:22:10.456Z" },
]
+[[package]]
+name = "stack-data"
+version = "0.6.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "asttokens" },
+ { name = "executing" },
+ { name = "pure-eval" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" },
+]
+
[[package]]
name = "starlette"
version = "0.48.0"
@@ -3017,6 +3504,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/73/c6/825dab04195756cf8ff2e12698f22513b3db2f64925bdd41671bfb33aaa5/tensorboard_data_server-0.7.2-py3-none-manylinux_2_31_x86_64.whl", hash = "sha256:ef687163c24185ae9754ed5650eb5bc4d84ff257aabdc33f0cc6f74d8ba54530", size = 6590363, upload-time = "2023-10-23T21:23:35.583Z" },
]
+[[package]]
+name = "terminado"
+version = "0.18.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "ptyprocess", marker = "os_name != 'nt'" },
+ { name = "pywinpty", marker = "(os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" },
+ { name = "tornado" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154, upload-time = "2024-03-12T14:34:36.569Z" },
+]
+
[[package]]
name = "threadpoolctl"
version = "3.6.0"
@@ -3060,6 +3561,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/09/19/529b28ca338c5a88315e71e672badc85eef89460c248c4164f6ce058f8c7/tifffile-2026.1.28-py3-none-any.whl", hash = "sha256:45b08a19cf603dd99952eff54a61519626a1912e4e2a4d355f05938fe4a6e9fd", size = 233011, upload-time = "2026-01-29T05:17:23.078Z" },
]
+[[package]]
+name = "tinycss2"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "webencodings" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" },
+]
+
[[package]]
name = "tomli"
version = "2.4.0"
@@ -3123,21 +3636,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/61/d8/15b9d9d3a6b0c01b883787bd056acbe5cc321090d4b216d3ea89a8fcfdf3/torch-2.10.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:b7bd80f3477b830dd166c707c5b0b82a898e7b16f59a7d9d42778dd058272e8b", size = 79423461, upload-time = "2026-01-21T16:24:50.266Z" },
]
-[[package]]
-name = "torchmetrics"
-version = "1.9.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "lightning-utilities" },
- { name = "numpy" },
- { name = "packaging" },
- { name = "torch" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/81/34/39b8b749333db56c0585d7a11fa62a283c087bb1dfc897d69fb8cedbefb1/torchmetrics-1.9.0.tar.gz", hash = "sha256:a488609948600df52d3db4fcdab02e62aab2a85ef34da67037dc3e65b8512faa", size = 581765, upload-time = "2026-03-09T17:41:22.443Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c3/a2/c7f6ebf546f8f644edf0f999aa98ece106986a77a7b922316bf6414ff825/torchmetrics-1.9.0-py3-none-any.whl", hash = "sha256:bfdcbff3dd1d96b3374bb2496eb39f23c4b28b8a845b6a18c313688e0d2d9ca1", size = 983384, upload-time = "2026-03-09T17:41:19.756Z" },
-]
-
[[package]]
name = "torchvision"
version = "0.25.0"
@@ -3189,6 +3687,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
]
+[[package]]
+name = "traitlets"
+version = "5.14.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" },
+]
+
[[package]]
name = "triton"
version = "3.6.0"
@@ -3232,6 +3739,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
]
+[[package]]
+name = "tzdata"
+version = "2025.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
+]
+
+[[package]]
+name = "uri-template"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/31/c7/0336f2bd0bcbada6ccef7aaa25e443c118a704f828a0620c6fa0207c1b64/uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7", size = 21678, upload-time = "2023-06-21T01:49:05.374Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363", size = 11140, upload-time = "2023-06-21T01:49:03.467Z" },
+]
+
[[package]]
name = "urllib3"
version = "2.6.3"
@@ -3284,6 +3809,42 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" },
]
+[[package]]
+name = "wcwidth"
+version = "0.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" },
+]
+
+[[package]]
+name = "webcolors"
+version = "25.10.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1d/7a/eb316761ec35664ea5174709a68bbd3389de60d4a1ebab8808bfc264ed67/webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf", size = 53491, upload-time = "2025-10-31T07:51:03.977Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d", size = 14905, upload-time = "2025-10-31T07:51:01.778Z" },
+]
+
+[[package]]
+name = "webencodings"
+version = "0.5.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" },
+]
+
+[[package]]
+name = "websocket-client"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" },
+]
+
[[package]]
name = "werkzeug"
version = "3.1.5"
@@ -3297,23 +3858,12 @@ wheels = [
]
[[package]]
-name = "xs3d"
-version = "1.13.0"
+name = "widgetsnbextension"
+version = "4.0.15"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/8f/5e/2f195c8d5e52f0dfebce62ae840ab73eae53b8edc5fe3b677b8f4d595a04/xs3d-1.13.0.tar.gz", hash = "sha256:2beb1d83ba277a1c29ff70906dffb2e171c163de5411cc8dfae3147492698ba9", size = 41784, upload-time = "2025-10-17T02:28:23.614Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/bd/f4/c67440c7fb409a71b7404b7aefcd7569a9c0d6bd071299bf4198ae7a5d95/widgetsnbextension-4.0.15.tar.gz", hash = "sha256:de8610639996f1567952d763a5a41af8af37f2575a41f9852a38f947eb82a3b9", size = 1097402, upload-time = "2025-11-01T21:15:55.178Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/fe/fa/54e32584cfa6b421c90f595f9d575699bb59dd6e037c752da4a1114945ca/xs3d-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:714152a8741beac991d5c43b2473655f83f5301467d212e01fa7c0a696e5bf37", size = 191574, upload-time = "2025-10-17T02:27:32.074Z" },
- { url = "https://files.pythonhosted.org/packages/e7/7b/cdd2a9ff99079ce54f20c7720b7dea6d909977de13f3814e236d36997225/xs3d-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:898cb65eef5b9d0954996d8b7a7cfc657b15bda4e3128aa1abb45bc1fbf762d3", size = 162167, upload-time = "2025-10-17T02:27:33.56Z" },
- { url = "https://files.pythonhosted.org/packages/16/53/9177cd7ad2d5cd4e8c6c9b02f23e31fa176daa68d1177001646825a979f9/xs3d-1.13.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c2ffd9bed693f8373d78b796b8897c31775d3ffb73ba016dcb3f6880a8f9a3b", size = 184594, upload-time = "2025-10-17T02:27:35.08Z" },
- { url = "https://files.pythonhosted.org/packages/a4/4b/42af8af2a5d9d052e357248c4381115ff64bdf77f8f24cfedf19bd904183/xs3d-1.13.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:613ea62c65413997c1274b15c72fddfe71154f63284c4a80d9c1e640cda40245", size = 222641, upload-time = "2025-10-17T02:27:36.433Z" },
- { url = "https://files.pythonhosted.org/packages/20/7a/4223abee4e3a249d935c3dcee0ee7abd829253afaa64587873faa20c9cfc/xs3d-1.13.0-cp310-cp310-win32.whl", hash = "sha256:4c1853077902dd2940d1b298fe16ddd1e61476d028007351d071aed1ec4d9fec", size = 132360, upload-time = "2025-10-17T02:27:38.692Z" },
- { url = "https://files.pythonhosted.org/packages/7c/67/a42c33cffc5dc9e1708c9c140e5b6a51bb394c787b49e3210678e048795e/xs3d-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:de9254fd7d2340bd2e7f61c8f797d6fff5d869a3dea491ada2a980cba9f6abd9", size = 141546, upload-time = "2025-10-17T02:27:37.736Z" },
- { url = "https://files.pythonhosted.org/packages/82/41/bade76ea52e1696856943da04ea28906cd59e2104ab27cc0f7a20ff64870/xs3d-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73c110c19bbac2d32f9e0ad2d08557743e7865fb473d5ec78702677115dadfe2", size = 193100, upload-time = "2025-10-17T02:27:39.912Z" },
- { url = "https://files.pythonhosted.org/packages/54/a0/cde6421af76af30897cf160c1966d3e7fec714784d9387519b3cda7ee2d8/xs3d-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ca5a9519c2b78ca24632e5bbb53387f177ef2d62b2b377f0c86efff32c8f513", size = 163519, upload-time = "2025-10-17T02:27:41.226Z" },
- { url = "https://files.pythonhosted.org/packages/2d/95/1b17a69245df623c6078bff74b20a6b2c9297070467477068300b85f9168/xs3d-1.13.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a75e38a31aed8b347b1b6c3272558bfb67716f2a98dd3795f9d4bba1b86fc1b7", size = 185417, upload-time = "2025-10-17T02:27:42.152Z" },
- { url = "https://files.pythonhosted.org/packages/ed/75/da5699ebb74ea8916fdd5195336ae08c78eee237382c465a9b9d330971b7/xs3d-1.13.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c77147a4086594be98fc1aa65d471c6c484b484719cea9e5505f5cc5a9a48ce8", size = 224191, upload-time = "2025-10-17T02:27:43.399Z" },
- { url = "https://files.pythonhosted.org/packages/72/c8/97b43f69c1b8028bd28ade1649281a0a3bd5d72d6c24175510167b2b2d03/xs3d-1.13.0-cp311-cp311-win32.whl", hash = "sha256:d8a8b1cffeeb5bf78993d247a24dbb3f9f6662479979bb265dbda120c21462f8", size = 133351, upload-time = "2025-10-17T02:27:45.398Z" },
- { url = "https://files.pythonhosted.org/packages/26/94/3416e98d81d2bdd1b3f5c8ad6399c98f1a34eb349b61c523bce8a28569ac/xs3d-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb053f6ee6159f9bfb9d20ba5f828feb7c73558fb0c4dd542f5089fe42f0b74c", size = 142319, upload-time = "2025-10-17T02:27:44.511Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl", hash = "sha256:8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366", size = 2196503, upload-time = "2025-11-01T21:15:53.565Z" },
]
[[package]]
@@ -3359,6 +3909,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565, upload-time = "2025-10-02T14:37:06.966Z" },
]
+[[package]]
+name = "yacs"
+version = "0.1.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyyaml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/44/3e/4a45cb0738da6565f134c01d82ba291c746551b5bc82e781ec876eb20909/yacs-0.1.8.tar.gz", hash = "sha256:efc4c732942b3103bea904ee89af98bcd27d01f0ac12d8d4d369f1e7a2914384", size = 11100, upload-time = "2020-08-10T16:37:47.755Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/4f/fe9a4d472aa867878ce3bb7efb16654c5d63672b86dc0e6e953a67018433/yacs-0.1.8-py3-none-any.whl", hash = "sha256:99f893e30497a4b66842821bac316386f7bd5c4f47ad35c9073ef089aa33af32", size = 14747, upload-time = "2020-08-10T16:37:46.4Z" },
+]
+
[[package]]
name = "yarl"
version = "1.22.0"