mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-12 12:52:13 +08:00
FTMv2
This commit is contained in:
@@ -19,7 +19,7 @@ import { ModelManager } from "/assets/components/tools/model_manager.js?v=202603
|
||||
import { LivePlots } from "/assets/components/tools/plots.js"
|
||||
import { ThemeMaker } from "/assets/components/tools/theme_maker.js"
|
||||
import { TestingGround } from "/assets/components/tools/testing_ground.js"
|
||||
import { Tuning } from "/assets/components/tools/tuning.js?v=ftm-workspace-4"
|
||||
import { Tuning } from "/assets/components/tools/tuning.js?v=ftm-workspace-5"
|
||||
import { Troubleshoot } from "/assets/components/tools/troubleshoot.js"
|
||||
import { TmuxLog } from "/assets/components/tools/tmux.js"
|
||||
import { ToggleControl } from "/assets/components/tools/toggles.js"
|
||||
|
||||
@@ -378,6 +378,30 @@ async function applyProfile(profileId) {
|
||||
}
|
||||
}
|
||||
|
||||
async function selectPath(pathKey) {
|
||||
if (!state.report?.reportId || !pathKey || state.runningAction) return
|
||||
if (pathKey === (state.report.selectedPathKey || state.report.primaryPathKey)) return
|
||||
|
||||
state.runningAction = true
|
||||
try {
|
||||
const response = await fetch(`/api/ftm/report/${encodeURIComponent(state.report.reportId)}/path`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ pathKey }),
|
||||
})
|
||||
const payload = await response.json()
|
||||
if (!response.ok) throw new Error(payload.error || "Failed to select tuning path.")
|
||||
state.report = payload.report
|
||||
syncFeedbackState(state.report)
|
||||
showSnackbar(payload.message || "Tuning path selected.")
|
||||
} catch (error) {
|
||||
state.error = error?.message || "Failed to select tuning path."
|
||||
showSnackbar(state.error, "error")
|
||||
} finally {
|
||||
state.runningAction = false
|
||||
}
|
||||
}
|
||||
|
||||
async function revertProfile() {
|
||||
if (state.runningAction) return
|
||||
state.runningAction = true
|
||||
@@ -422,7 +446,7 @@ async function saveFeedback() {
|
||||
})
|
||||
const payload = await response.json()
|
||||
if (!response.ok) throw new Error(payload.error || "Failed to save tuning feedback.")
|
||||
state.report = {
|
||||
state.report = payload.report || {
|
||||
...state.report,
|
||||
feedback: payload.feedback,
|
||||
profiles: payload.profiles || state.report.profiles,
|
||||
@@ -480,7 +504,8 @@ function reportPaths() {
|
||||
|
||||
function primaryPath() {
|
||||
const paths = reportPaths()
|
||||
return paths.find((path) => path.isPrimary) || paths[0] || null
|
||||
const selectedPathKey = state.report?.selectedPathKey || state.report?.primaryPathKey
|
||||
return paths.find((path) => path.key === selectedPathKey) || paths.find((path) => path.isPrimary) || paths[0] || null
|
||||
}
|
||||
|
||||
function renderProfile(profile) {
|
||||
@@ -586,13 +611,22 @@ function renderSuggestion(suggestion) {
|
||||
}
|
||||
|
||||
function renderPathSummary(path) {
|
||||
const selected = path.key === (state.report?.selectedPathKey || state.report?.primaryPathKey)
|
||||
return html`
|
||||
<div class="ftmCard">
|
||||
<div class="ftmCardHeader">
|
||||
<div>
|
||||
<h4>${path.title}</h4>
|
||||
<p class="longManeuverMuted">${path.isPrimary ? "Recommended path" : "Alternate path"}</p>
|
||||
<p class="longManeuverMuted">
|
||||
${path.isPrimary ? "Analyzer recommended" : "Alternate path"}${selected ? " / Active" : ""}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="longManeuverButton"
|
||||
disabled="${() => state.runningAction || selected}"
|
||||
@click="${() => selectPath(path.key)}">
|
||||
${selected ? "Active Path" : `Use ${path.title}`}
|
||||
</button>
|
||||
</div>
|
||||
<p>${path.description || ""}</p>
|
||||
<p><strong>Why this path:</strong> ${path.whySelected || "No path note available."}</p>
|
||||
@@ -758,7 +792,11 @@ export function Tuning() {
|
||||
<p><strong>Car:</strong> ${state.report.car?.carFingerprint || "Unknown"}</p>
|
||||
<p><strong>Control Path:</strong> ${state.report.car?.controlPath || "unknown"}</p>
|
||||
<p><strong>Friction Family:</strong> ${state.report.capabilities?.frictionFamily || "standard"}</p>
|
||||
<p><strong>Recommended Path:</strong> ${primaryPath()?.title || "Recommendations"}</p>
|
||||
<p><strong>Analyzer Recommended:</strong> ${reportPaths().find((path) => path.isPrimary)?.title || "Recommendations"}</p>
|
||||
<p><strong>Active Path:</strong> ${primaryPath()?.title || "Recommendations"}</p>
|
||||
<p><strong>Path Choice:</strong> ${state.report.pathSelectionSource === "manual" ? "Manual override" : "Automatic"}</p>
|
||||
<p><strong>Nonlinear Torque Map:</strong> ${state.report.capabilities?.nonlinearTorqueMap?.asymmetric ? "Asymmetric left/right siglin" : (state.report.capabilities?.nonlinearTorqueMap ? "Symmetric siglin" : "Not detected")}</p>
|
||||
<p><strong>Live Learner Refits Map:</strong> ${state.report.capabilities?.nonlinearTorqueMap ? "No" : "Not applicable"}</p>
|
||||
<p><strong>Processed Segments:</strong> ${safeCount(state.report.summary?.processedSegments)}</p>
|
||||
<p><strong>qlog Fallback:</strong> ${state.report.summary?.usedQlogFallback ? "Yes" : "No"}</p>
|
||||
<p><strong>Samples:</strong> ${safeCount(state.report.summary?.sampleCount)}</p>
|
||||
@@ -790,7 +828,7 @@ export function Tuning() {
|
||||
<section class="ftmCard">
|
||||
<div class="ftmCardHeader">
|
||||
<div>
|
||||
<h3>Recommended Findings: ${primaryPath()?.title || "Recommendations"}</h3>
|
||||
<h3>Active Findings: ${primaryPath()?.title || "Recommendations"}</h3>
|
||||
<p class="longManeuverMuted">
|
||||
${primaryPath()?.whySelected || "Mark the dimensions that match what the driver felt."}
|
||||
</p>
|
||||
|
||||
@@ -581,6 +581,38 @@ def _current_param_state(CP, params: Params) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _nonlinear_torque_map(CP) -> dict[str, Any]:
|
||||
if str(getattr(CP, "brand", "") or "") != "gm":
|
||||
return {}
|
||||
|
||||
try:
|
||||
from opendbc.car.gm.interface import NON_LINEAR_TORQUE_PARAMS
|
||||
except (ImportError, AttributeError):
|
||||
return {}
|
||||
|
||||
raw_params = NON_LINEAR_TORQUE_PARAMS.get(CP.carFingerprint)
|
||||
if raw_params is None:
|
||||
return {}
|
||||
|
||||
if isinstance(raw_params, dict):
|
||||
left = [float(value) for value in raw_params.get("left", [])]
|
||||
right = [float(value) for value in raw_params.get("right", [])]
|
||||
else:
|
||||
left = [float(value) for value in raw_params]
|
||||
right = list(left)
|
||||
|
||||
if len(left) != 4 or len(right) != 4:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"type": "siglin",
|
||||
"left": left,
|
||||
"right": right,
|
||||
"asymmetric": any(not math.isclose(left[idx], right[idx], abs_tol=1e-9) for idx in range(4)),
|
||||
"learnedByLiveTorque": False,
|
||||
}
|
||||
|
||||
|
||||
def _baseline_family_curve(family: str) -> list[float]:
|
||||
getter = {
|
||||
"gm": get_gm_base_friction_threshold,
|
||||
@@ -855,6 +887,9 @@ def _primary_delta_from_summary(summary: dict[str, Any], capabilities: dict[str,
|
||||
supports_curvy_speed_max = _rich_profile_supports_knob(capabilities, "curvy_speed_max")
|
||||
supports_curvy_unwind_extra = _rich_profile_supports_knob(capabilities, f"curvy_unwind_extra_reduction_{side}")
|
||||
supports_curvy_unwind_floor = _rich_profile_supports_knob(capabilities, f"curvy_unwind_floor_relief_{side}")
|
||||
supports_ff_gain = _rich_profile_supports_knob(capabilities, f"ff_gain_{side}")
|
||||
nonlinear_map = capabilities.get("nonlinearTorqueMap", {})
|
||||
asymmetric_nonlinear_map = bool(isinstance(nonlinear_map, dict) and nonlinear_map.get("asymmetric"))
|
||||
|
||||
if bucket == "model_limited":
|
||||
return None
|
||||
@@ -889,12 +924,20 @@ def _primary_delta_from_summary(summary: dict[str, Any], capabilities: dict[str,
|
||||
}
|
||||
|
||||
if bucket in ("understeer", "late_turn_in", "saturation_limited"):
|
||||
if asymmetric_nonlinear_map and direction in ("left", "right") and supports_ff_gain:
|
||||
adjustment = _vehicle_knob_adjustment(f"{rich_profile}.ff_gain_{side}", 0.025 * severity, current)
|
||||
if adjustment is not None:
|
||||
return adjustment
|
||||
current_value = float(current["SteerLatAccel"])
|
||||
scale = 0.04 if bucket == "saturation_limited" else 0.03
|
||||
suggested_value = round(_clamp(current_value + max(scale, current_value * scale * severity), 0.5, 5.0), 4)
|
||||
return {"type": "generic_param", "paramKey": "SteerLatAccel", "current": current_value, "suggested": suggested_value, "delta": round(suggested_value - current_value, 4)}
|
||||
|
||||
if bucket in ("oversteer", "early_turn_in"):
|
||||
if asymmetric_nonlinear_map and direction in ("left", "right") and supports_ff_gain:
|
||||
adjustment = _vehicle_knob_adjustment(f"{rich_profile}.ff_gain_{side}", -0.025 * severity, current)
|
||||
if adjustment is not None:
|
||||
return adjustment
|
||||
current_value = float(current["SteerLatAccel"])
|
||||
suggested_value = round(_clamp(current_value - max(0.03, current_value * 0.03 * severity), 0.5, 5.0), 4)
|
||||
return {"type": "generic_param", "paramKey": "SteerLatAccel", "current": current_value, "suggested": suggested_value, "delta": round(suggested_value - current_value, 4)}
|
||||
@@ -1039,6 +1082,8 @@ def _likely_interpretation(summary: dict[str, Any], adjustment: dict[str, Any])
|
||||
return "This looks more like a friction-threshold problem than a whole-tune problem; the controller is busy around center and needs a calmer deadzone slope."
|
||||
if adjustment["type"] == "vehicle_knob":
|
||||
symbol = adjustment["symbol"]
|
||||
if "ff_gain_" in symbol:
|
||||
return "This car has a directional nonlinear torque map, and the mismatch is concentrated on one side. Correct that side's feedforward layer before moving global authority."
|
||||
if "low_speed_angle_assist_max_torque" in symbol:
|
||||
return "The main torque path is waking up too late below about 8 mph, so the low-speed assist layer needs a little more authority."
|
||||
if "crawl_turn_in_ff_boost" in symbol:
|
||||
@@ -1067,6 +1112,8 @@ def _why_this_knob(adjustment: dict[str, Any]) -> str:
|
||||
return "This changes the threshold that maps small lateral-accel error into friction compensation without pretending the whole torque slope is wrong."
|
||||
if adjustment["type"] == "vehicle_knob":
|
||||
symbol = adjustment["symbol"]
|
||||
if "ff_gain_" in symbol:
|
||||
return "This compensates the affected side without flattening the car's separate left/right nonlinear torque response into one global value."
|
||||
if "low_speed_angle_assist_max_torque" in symbol:
|
||||
return "This directly raises the crawl-speed assist ceiling that fills the gap before the normal torque path wakes up."
|
||||
if "crawl_turn_in_ff_boost" in symbol:
|
||||
@@ -1474,7 +1521,10 @@ def build_recommendation_paths(report_id: str, summaries: list[dict[str, Any]],
|
||||
|
||||
def _render_report_html(report: dict[str, Any]) -> str:
|
||||
report_paths = [path for path in report.get("paths", []) if isinstance(path, dict)]
|
||||
primary_path = next((path for path in report_paths if path.get("isPrimary")), report_paths[0] if report_paths else {})
|
||||
selected_path_key = str(report.get("selectedPathKey") or report.get("primaryPathKey") or "")
|
||||
primary_path = next((path for path in report_paths if path.get("key") == selected_path_key), None)
|
||||
if primary_path is None:
|
||||
primary_path = next((path for path in report_paths if path.get("isPrimary")), report_paths[0] if report_paths else {})
|
||||
findings_html = []
|
||||
for suggestion in report.get("suggestions", []):
|
||||
evidence = suggestion.get("evidence", {})
|
||||
@@ -1508,7 +1558,12 @@ def _render_report_html(report: dict[str, Any]) -> str:
|
||||
|
||||
path_html = []
|
||||
for path in report_paths:
|
||||
badge = "Recommended" if path.get("isPrimary") else "Alternate"
|
||||
badges = []
|
||||
if path.get("isPrimary"):
|
||||
badges.append("Analyzer recommended")
|
||||
if path.get("key") == selected_path_key:
|
||||
badges.append("Active")
|
||||
badge = " / ".join(badges) or "Alternate"
|
||||
path_html.append(
|
||||
"<section class='ftm-card'>"
|
||||
f"<h3>{path.get('title', 'Path')}</h3>"
|
||||
@@ -1566,10 +1621,11 @@ def _render_report_html(report: dict[str, Any]) -> str:
|
||||
f"<p>{report['car']['carFingerprint']} | {report['car'].get('gitBranch', '')} {report['car'].get('gitCommit', '')}</p>"
|
||||
f"<div class='ftm-grid'><section class='ftm-card'><h3>Routes</h3><p>{', '.join(report.get('routeNames', []))}</p></section>"
|
||||
f"<section class='ftm-card'><h3>Control Path</h3><p>{report['car'].get('controlPath', 'unknown')}</p></section>"
|
||||
f"<section class='ftm-card'><h3>Friction Family</h3><p>{report['capabilities'].get('frictionFamily', 'standard')}</p></section></div>"
|
||||
f"<section class='ftm-card'><h3>Friction Family</h3><p>{report['capabilities'].get('frictionFamily', 'standard')}</p></section>"
|
||||
f"<section class='ftm-card'><h3>Nonlinear Torque Map</h3><p>{'Asymmetric left/right siglin' if report['capabilities'].get('nonlinearTorqueMap', {}).get('asymmetric') else ('Symmetric siglin' if report['capabilities'].get('nonlinearTorqueMap') else 'Not detected')}</p></section></div>"
|
||||
f"{''.join(path_html)}"
|
||||
f"{start_here_html}"
|
||||
f"<h2>Recommended Findings: {primary_path.get('title', 'Recommendations')}</h2>"
|
||||
f"<h2>Active Findings: {primary_path.get('title', 'Recommendations')}</h2>"
|
||||
f"{findings_block}"
|
||||
"<h2>Trial Profiles</h2>"
|
||||
f"{profiles_block}"
|
||||
@@ -1624,6 +1680,8 @@ def analyze_routes(route_names: list[str], footage_paths: list[str], feedback: d
|
||||
hyundai_canfd=hyundai_canfd,
|
||||
torque_control=torque_control,
|
||||
)
|
||||
capabilities = dict(capabilities)
|
||||
capabilities["nonlinearTorqueMap"] = _nonlinear_torque_map(car_params)
|
||||
current_params = _current_param_state(car_params, params)
|
||||
|
||||
if torque_control:
|
||||
@@ -1698,6 +1756,8 @@ def analyze_routes(route_names: list[str], footage_paths: list[str], feedback: d
|
||||
"usedQlogFallback": used_qlog,
|
||||
},
|
||||
"primaryPathKey": path_decision["primaryPathKey"],
|
||||
"selectedPathKey": path_decision["primaryPathKey"],
|
||||
"pathSelectionSource": "auto",
|
||||
"pathDecision": path_decision,
|
||||
"paths": paths_payload,
|
||||
"findings": summaries,
|
||||
@@ -1737,6 +1797,31 @@ def load_report(report_id: str) -> dict[str, Any]:
|
||||
return report
|
||||
|
||||
|
||||
def select_report_path(report_id: str, path_key: str) -> dict[str, Any]:
|
||||
paths = ensure_ftm_workspace()
|
||||
report = load_report(report_id)
|
||||
report_paths = [path for path in report.get("paths", []) if isinstance(path, dict)]
|
||||
selected_path = next((path for path in report_paths if path.get("key") == path_key), None)
|
||||
if selected_path is None:
|
||||
raise ValueError(f"Unknown FTM path: {path_key}")
|
||||
|
||||
report["selectedPathKey"] = path_key
|
||||
report["pathSelectionSource"] = "manual"
|
||||
report["suggestions"] = list(selected_path.get("suggestions", []))
|
||||
report["addTheseParametersAndStartHere"] = _add_parameters_start_here(
|
||||
report.get("capabilities", {}),
|
||||
report["suggestions"],
|
||||
path_key,
|
||||
)
|
||||
report.pop("html", None)
|
||||
(paths["reports"] / f"{report_id}.html").write_text(_render_report_html(report), encoding="utf-8")
|
||||
_write_json(paths["reports"] / f"{report_id}.json", report)
|
||||
return {
|
||||
"message": f"Using {selected_path.get('title', path_key)} for this report.",
|
||||
"report": load_report(report_id),
|
||||
}
|
||||
|
||||
|
||||
def list_workspace() -> dict[str, Any]:
|
||||
paths = ensure_ftm_workspace()
|
||||
reports = []
|
||||
@@ -1910,6 +1995,7 @@ def record_feedback(report_id: str, feedback: dict[str, Any]) -> dict[str, Any]:
|
||||
report = load_report(report_id)
|
||||
report["feedback"] = normalized
|
||||
if isinstance(report.get("paths"), list) and report.get("paths"):
|
||||
selected_path_key = str(report.get("selectedPathKey") or report.get("primaryPathKey") or "")
|
||||
flattened_profiles = []
|
||||
for path in report["paths"]:
|
||||
if not isinstance(path, dict):
|
||||
@@ -1924,7 +2010,7 @@ def record_feedback(report_id: str, feedback: dict[str, Any]) -> dict[str, Any]:
|
||||
)
|
||||
path["profiles"] = profiles
|
||||
flattened_profiles.extend(profiles)
|
||||
if path.get("isPrimary"):
|
||||
if path.get("key") == selected_path_key:
|
||||
report["suggestions"] = list(path.get("suggestions", []))
|
||||
report["profiles"] = flattened_profiles
|
||||
else:
|
||||
@@ -1937,6 +2023,7 @@ def record_feedback(report_id: str, feedback: dict[str, Any]) -> dict[str, Any]:
|
||||
"message": "Saved FTM feedback.",
|
||||
"feedback": normalized,
|
||||
"profiles": report["profiles"],
|
||||
"report": load_report(report_id),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -105,6 +105,8 @@ def _install_ftm_import_stubs(tmp_path):
|
||||
get_ftm_capabilities=lambda *args, **kwargs: {"richProfileKey": "hyundai_ioniq_6", "frictionFamily": "hkg_canfd"},
|
||||
get_ftm_rich_profile_key=lambda *args, **kwargs: "hyundai_ioniq_6",
|
||||
get_ftm_supported_vehicle_knobs=lambda: {
|
||||
"hyundai_ioniq_6.ff_gain_left": {"min": 0.0, "max": 0.6, "precision": 0.001, "defaultValue": 0.1, "profile": "hyundai_ioniq_6"},
|
||||
"hyundai_ioniq_6.ff_gain_right": {"min": 0.0, "max": 0.6, "precision": 0.001, "defaultValue": 0.12, "profile": "hyundai_ioniq_6"},
|
||||
"hyundai_ioniq_6.turn_in_boost_left": {"min": 0.4, "max": 2.8, "precision": 0.001, "defaultValue": 1.64, "profile": "hyundai_ioniq_6"},
|
||||
"hyundai_ioniq_6.unwind_taper_left": {"min": 0.0, "max": 1.2, "precision": 0.001, "defaultValue": 0.4, "profile": "hyundai_ioniq_6"},
|
||||
"hyundai_ioniq_6.low_speed_angle_assist_max_torque": {"min": 0.0, "max": 0.8, "precision": 0.001, "defaultValue": 0.46, "profile": "hyundai_ioniq_6"},
|
||||
@@ -231,6 +233,36 @@ def test_build_suggestions_baseline_prefers_generic_lat_accel_for_understeer(tmp
|
||||
assert adjustment["suggested"] > adjustment["current"]
|
||||
|
||||
|
||||
def test_build_suggestions_baseline_respects_asymmetric_nonlinear_map(tmp_path):
|
||||
module, _ = _load_ftm_workspace_module(tmp_path)
|
||||
summary = {
|
||||
"bucket": "understeer",
|
||||
"dimensionId": "understeer:right:mid",
|
||||
"direction": "right",
|
||||
"speedBand": "mid",
|
||||
"severity": 1.0,
|
||||
"evidence": {"speedBand": "mid", "directionBias": "right", "eventCount": 3, "segments": [{"label": "route/2"}]},
|
||||
"plotSvg": "",
|
||||
}
|
||||
capabilities = {
|
||||
"richProfileKey": "hyundai_ioniq_6",
|
||||
"frictionFamily": "gm",
|
||||
"nonlinearTorqueMap": {
|
||||
"type": "siglin",
|
||||
"left": [2.6, 1.1, 0.19, 0.0],
|
||||
"right": [2.7, 1.0, 0.15, 0.0],
|
||||
"asymmetric": True,
|
||||
},
|
||||
}
|
||||
current = {"SteerLatAccel": 1.8, "SteerFriction": 0.2}
|
||||
|
||||
suggestions = module.build_suggestions([summary], capabilities, current, strategy="baseline")
|
||||
adjustment = suggestions[0]["primaryAdjustmentRaw"]
|
||||
assert adjustment["type"] == "vehicle_knob"
|
||||
assert adjustment["symbol"] == "hyundai_ioniq_6.ff_gain_right"
|
||||
assert adjustment["suggested"] > adjustment["current"]
|
||||
|
||||
|
||||
def test_build_suggestions_rebases_rich_knob_against_active_override(tmp_path):
|
||||
module, _ = _load_ftm_workspace_module(tmp_path)
|
||||
summary = {
|
||||
@@ -481,3 +513,45 @@ def test_delete_report_removes_saved_artifacts(tmp_path):
|
||||
assert not (workspace["profiles"] / f"{report_id}.json").exists()
|
||||
assert not (workspace["feedback"] / f"{report_id}.json").exists()
|
||||
assert not (workspace["snapshots"] / f"{report_id}-recommended.json").exists()
|
||||
|
||||
|
||||
def test_select_report_path_persists_manual_override(tmp_path):
|
||||
module, _ = _load_ftm_workspace_module(tmp_path)
|
||||
workspace = module.ensure_ftm_workspace()
|
||||
report_id = "report-path"
|
||||
suggestion_base = {
|
||||
"evidence": {"speedBand": "mixed", "directionBias": "center", "eventCount": 0, "segments": []},
|
||||
"currentVsSuggested": None,
|
||||
"observedBehavior": "test",
|
||||
"likelyInterpretation": "test",
|
||||
"primaryAdjustment": "test",
|
||||
"whatNotToTouchYet": "test",
|
||||
"ifThatWasWrong": "test",
|
||||
"plotSvg": "",
|
||||
}
|
||||
cleanup_suggestion = {**suggestion_base, "dimensionId": "cleanup", "bucket": "model_limited"}
|
||||
baseline_suggestion = {**suggestion_base, "dimensionId": "baseline", "bucket": "understeer"}
|
||||
report = {
|
||||
"reportId": report_id,
|
||||
"routeNames": ["route"],
|
||||
"car": {"carFingerprint": "TEST", "controlPath": "torque", "gitBranch": "", "gitCommit": ""},
|
||||
"capabilities": {"frictionFamily": "standard", "richProfileKey": "hyundai_ioniq_6", "nonlinearTorqueMap": {}},
|
||||
"primaryPathKey": "cleanup_pass",
|
||||
"selectedPathKey": "cleanup_pass",
|
||||
"pathSelectionSource": "auto",
|
||||
"paths": [
|
||||
{"key": "cleanup_pass", "title": "Cleanup Pass", "isPrimary": True, "suggestions": [cleanup_suggestion], "profiles": []},
|
||||
{"key": "baseline_fix", "title": "Baseline Fix", "isPrimary": False, "suggestions": [baseline_suggestion], "profiles": []},
|
||||
],
|
||||
"suggestions": [cleanup_suggestion],
|
||||
"profiles": [],
|
||||
"addTheseParametersAndStartHere": [],
|
||||
}
|
||||
(workspace["reports"] / f"{report_id}.json").write_text(json.dumps(report), encoding="utf-8")
|
||||
|
||||
result = module.select_report_path(report_id, "baseline_fix")
|
||||
selected = result["report"]
|
||||
assert selected["selectedPathKey"] == "baseline_fix"
|
||||
assert selected["pathSelectionSource"] == "manual"
|
||||
assert selected["primaryPathKey"] == "cleanup_pass"
|
||||
assert selected["suggestions"] == [baseline_suggestion]
|
||||
|
||||
@@ -5702,6 +5702,20 @@ def setup(app):
|
||||
except RuntimeError as error:
|
||||
return jsonify({"error": str(error)}), 409
|
||||
|
||||
@app.route("/api/ftm/report/<report_id>/path", methods=["POST"])
|
||||
def select_ftm_report_path(report_id):
|
||||
data = request.get_json(silent=True) or {}
|
||||
path_key = str(data.get("pathKey") or "").strip()
|
||||
if not path_key:
|
||||
return jsonify({"error": "pathKey is required."}), 400
|
||||
|
||||
try:
|
||||
return jsonify(ftm_workspace.select_report_path(report_id, path_key)), 200
|
||||
except FileNotFoundError:
|
||||
return jsonify({"error": "FTM report not found."}), 404
|
||||
except ValueError as error:
|
||||
return jsonify({"error": str(error)}), 400
|
||||
|
||||
@app.route("/api/ftm/workspace", methods=["GET"])
|
||||
def get_ftm_workspace():
|
||||
return jsonify(ftm_workspace.list_workspace()), 200
|
||||
|
||||
Reference in New Issue
Block a user