mirror of
https://github.com/saymrwulf/BraiinsRatchet.git
synced 2026-05-14 20:37:52 +00:00
Show live experiment progress in app
This commit is contained in:
parent
7dea61f9ce
commit
69bd3ea420
8 changed files with 306 additions and 8 deletions
|
|
@ -59,6 +59,8 @@ Print the latest mirror snapshot:
|
|||
./scripts/ratchet mirror
|
||||
```
|
||||
|
||||
When an experiment watch is running, Flight Deck now shows live progress: run id, countdown, approximate cycle count, progress bar, and local finish time. The countdown updates every second; backend state refreshes roughly every 30 seconds.
|
||||
|
||||
Advanced fallback for a 6-hour CLI monitoring session:
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -91,6 +91,8 @@ If you ask for help, this file is the fastest way to show the exact app state in
|
|||
./scripts/ratchet mirror
|
||||
```
|
||||
|
||||
When a watch is running, `Flight Deck` shows a running-experiment progress panel with the run id, countdown, approximate cycle count, progress bar, and local finish time. The countdown updates every second; backend state refreshes roughly every 30 seconds.
|
||||
|
||||
## Research Pathway
|
||||
|
||||
The app has three time horizons:
|
||||
|
|
|
|||
|
|
@ -76,6 +76,8 @@ Layer 7 is the visual self-reflection layer.
|
|||
|
||||
The SwiftUI app renders a `Reality Mirror` HUD and tab. It writes the semantic state it believes it is showing to `data/app_visual_state.md` and `data/app_visual_state.json`. This is not screenshot OCR; it is the app's own rendered-state ledger.
|
||||
|
||||
During active watches, the backend exposes `active_watch_details` through `app-state`: run id, PID, start time, planned cycles, interval, elapsed seconds, remaining seconds, progress percent, next-cycle ETA, and estimated finish time. The SwiftUI Flight Deck renders this as a live progress panel and refreshes backend state roughly every 30 seconds.
|
||||
|
||||
### Data Flow
|
||||
|
||||
Normal data flow:
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ What you do: do nothing. Leave the app open or closed. The background engine kee
|
|||
|
||||
`WAIT` means a watch is already running.
|
||||
|
||||
What you do: do not start anything else.
|
||||
What you do: do not start anything else. The Flight Deck should show a running-experiment progress panel with progress percent, approximate cycle count, and local finish time.
|
||||
|
||||
`COOLDOWN` means the last watch already produced evidence and the system is intentionally waiting.
|
||||
|
||||
|
|
@ -189,6 +189,8 @@ It collects repeated samples:
|
|||
|
||||
The default watch stage used by the forever engine is 2 hours. It samples every 5 minutes, so a full stage normally has 24 cycles.
|
||||
|
||||
During a watch, the app shows the active run id, countdown, progress bar, approximate cycle count, and local finish time. The visual countdown updates every second. Backend state refreshes about every 30 seconds.
|
||||
|
||||
## What The Forever Engine Does
|
||||
|
||||
`Start Forever Engine` starts a repo-local background monitor loop.
|
||||
|
|
|
|||
|
|
@ -147,6 +147,11 @@ struct FlightDeckApp: View {
|
|||
.task {
|
||||
await store.refresh()
|
||||
store.writeRealitySnapshot(section: selection)
|
||||
while !Task.isCancelled {
|
||||
try? await Task.sleep(nanoseconds: 30_000_000_000)
|
||||
await store.refresh()
|
||||
store.writeRealitySnapshot(section: selection)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
withAnimation(.easeInOut(duration: 4.8).repeatForever(autoreverses: true)) {
|
||||
|
|
@ -395,6 +400,10 @@ struct FlightDeckView: View {
|
|||
|
||||
InstrumentRibbon(appState: store.appState)
|
||||
|
||||
if let activeWatch = store.appState?.operatorState.activeWatchDetails {
|
||||
ActiveWatchProgressPanel(activeWatch: activeWatch)
|
||||
}
|
||||
|
||||
EngineConsole(store: store)
|
||||
}
|
||||
.frame(width: max(430, proxy.size.width * 0.38))
|
||||
|
|
@ -737,6 +746,86 @@ struct InstrumentChip: View {
|
|||
}
|
||||
}
|
||||
|
||||
struct ActiveWatchProgressPanel: View {
|
||||
let activeWatch: ActiveWatchPayload
|
||||
|
||||
var body: some View {
|
||||
TimelineView(.periodic(from: .now, by: 1)) { context in
|
||||
let live = liveMetrics(now: context.date)
|
||||
LiquidGlassSurface(tint: .orange.opacity(0.20), cornerRadius: 32) {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
HStack(alignment: .top) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Label("Running experiment", systemImage: "timer")
|
||||
.font(.title3.weight(.black))
|
||||
Text(activeWatch.runId)
|
||||
.font(.caption.monospaced())
|
||||
.foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
Spacer()
|
||||
Text(live.remainingText)
|
||||
.font(.title3.monospacedDigit().weight(.black))
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
|
||||
ProgressView(value: live.progress)
|
||||
.tint(.orange)
|
||||
|
||||
HStack(spacing: 12) {
|
||||
WatchMetric("progress", "\(Int((live.progress * 100).rounded()))%", "done")
|
||||
WatchMetric("cycles", "\(live.cycleEstimate)/\(activeWatch.plannedCycles)", "about")
|
||||
WatchMetric("finish", activeWatch.estimatedFinishLocal.shortClockText, "local")
|
||||
}
|
||||
|
||||
Text("The app refreshes backend state every 30 seconds; this bar counts down locally every second.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(18)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func liveMetrics(now: Date) -> (progress: Double, cycleEstimate: Int, remainingText: String) {
|
||||
guard let started = activeWatch.startedDate, let finish = activeWatch.finishDate else {
|
||||
return (Double(activeWatch.progressPercent) / 100.0, activeWatch.completedCyclesEstimate, "\(activeWatch.remainingSeconds / 60)m left")
|
||||
}
|
||||
let total = max(1, finish.timeIntervalSince(started))
|
||||
let elapsed = min(total, max(0, now.timeIntervalSince(started)))
|
||||
let remaining = max(0, Int(finish.timeIntervalSince(now)))
|
||||
let cycle = min(activeWatch.plannedCycles, max(1, Int(elapsed / Double(activeWatch.intervalSeconds)) + 1))
|
||||
return (elapsed / total, cycle, formatDuration(remaining))
|
||||
}
|
||||
}
|
||||
|
||||
struct WatchMetric: View {
|
||||
let title: String
|
||||
let value: String
|
||||
let unit: String
|
||||
|
||||
init(_ title: String, _ value: String, _ unit: String) {
|
||||
self.title = title
|
||||
self.value = value
|
||||
self.unit = unit
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(title)
|
||||
.font(.caption.weight(.heavy))
|
||||
.textCase(.uppercase)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(value)
|
||||
.font(.headline.monospacedDigit().weight(.black))
|
||||
Text(unit)
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
struct HashflowView: View {
|
||||
@ObservedObject var store: RatchetStore
|
||||
|
||||
|
|
@ -1536,6 +1625,9 @@ struct RenderedReality: Codable {
|
|||
let latestStrategyAction: String
|
||||
let latestReport: String
|
||||
let activeWatch: String
|
||||
let activeWatchProgress: String
|
||||
let activeWatchEta: String
|
||||
let activeWatchRemaining: String
|
||||
let completedWatch: String
|
||||
let activeManualExposure: String
|
||||
let braiinsFreshness: String
|
||||
|
|
@ -1554,6 +1646,8 @@ struct RenderedReality: Codable {
|
|||
RealityRow(label: "engine", value: engineRunning ? "running" : "stopped", unit: "state"),
|
||||
RealityRow(label: "strategy", value: latestStrategyAction, unit: "proposal"),
|
||||
RealityRow(label: "braiins", value: braiinsFreshness, unit: "freshness"),
|
||||
RealityRow(label: "watch progress", value: activeWatchProgress, unit: "live"),
|
||||
RealityRow(label: "watch ETA", value: activeWatchEta, unit: "local"),
|
||||
RealityRow(label: "manual exposure", value: activeManualExposure, unit: "blocker"),
|
||||
] + instrumentRows
|
||||
}
|
||||
|
|
@ -1587,6 +1681,7 @@ struct RenderedReality: Codable {
|
|||
let operatorState = appState?.operatorState
|
||||
let engineStatus = appState?.engineStatus
|
||||
let activePositions = operatorState?.activeManualPositions ?? []
|
||||
let activeWatchDetails = operatorState?.activeWatchDetails
|
||||
let completedWatch = operatorState?.completedWatch
|
||||
let buttons = visibleButtons(section: section, appState: appState, isWorking: isWorking)
|
||||
let truths = operatorTruths(
|
||||
|
|
@ -1615,6 +1710,9 @@ struct RenderedReality: Codable {
|
|||
latestStrategyAction: operatorState?.action ?? "none",
|
||||
latestReport: operatorState?.latestReport ?? "none",
|
||||
activeWatch: operatorState?.activeWatch ?? "none",
|
||||
activeWatchProgress: activeWatchDetails.map { "\($0.progressPercent)% (\($0.completedCyclesEstimate)/\($0.plannedCycles) cycles)" } ?? "none",
|
||||
activeWatchEta: activeWatchDetails?.estimatedFinishLocal ?? "none",
|
||||
activeWatchRemaining: activeWatchDetails.map { formatDuration($0.remainingSeconds) } ?? "none",
|
||||
completedWatch: completedWatch.map { "\($0.reportPath), remaining \($0.remainingMinutes)m, earliest \($0.earliestActionLocal)" } ?? "none",
|
||||
activeManualExposure: activePositions.isEmpty ? "none" : activePositions.joined(separator: "; "),
|
||||
braiinsFreshness: freshnessText(operatorState),
|
||||
|
|
@ -1794,6 +1892,9 @@ enum RealitySnapshotWriter {
|
|||
"- latest_braiins_sample: \(reality.latestBraiinsSample)",
|
||||
"- latest_report: \(reality.latestReport)",
|
||||
"- active_watch: \(reality.activeWatch)",
|
||||
"- active_watch_progress: \(reality.activeWatchProgress)",
|
||||
"- active_watch_eta: \(reality.activeWatchEta)",
|
||||
"- active_watch_remaining: \(reality.activeWatchRemaining)",
|
||||
"- completed_watch: \(reality.completedWatch)",
|
||||
"- active_manual_exposure: \(reality.activeManualExposure)",
|
||||
"",
|
||||
|
|
@ -1871,6 +1972,7 @@ struct OperatorStatePayload: Codable {
|
|||
let latestOceanTimestamp: String?
|
||||
let latestMarketTimestamp: String?
|
||||
let activeManualPositions: [String]
|
||||
let activeWatchDetails: ActiveWatchPayload?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case hasOcean = "has_ocean"
|
||||
|
|
@ -1885,9 +1987,49 @@ struct OperatorStatePayload: Codable {
|
|||
case latestOceanTimestamp = "latest_ocean_timestamp"
|
||||
case latestMarketTimestamp = "latest_market_timestamp"
|
||||
case activeManualPositions = "active_manual_positions"
|
||||
case activeWatchDetails = "active_watch_details"
|
||||
}
|
||||
}
|
||||
|
||||
struct ActiveWatchPayload: Codable {
|
||||
let label: String
|
||||
let runId: String
|
||||
let pid: Int?
|
||||
let startedUtc: String
|
||||
let plannedCycles: Int
|
||||
let intervalSeconds: Int
|
||||
let totalSeconds: Int
|
||||
let elapsedSeconds: Int
|
||||
let remainingSeconds: Int
|
||||
let progressPercent: Int
|
||||
let completedCyclesEstimate: Int
|
||||
let nextCycleEtaUtc: String?
|
||||
let nextCycleEtaLocal: String?
|
||||
let estimatedFinishUtc: String
|
||||
let estimatedFinishLocal: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case label
|
||||
case runId = "run_id"
|
||||
case pid
|
||||
case startedUtc = "started_utc"
|
||||
case plannedCycles = "planned_cycles"
|
||||
case intervalSeconds = "interval_seconds"
|
||||
case totalSeconds = "total_seconds"
|
||||
case elapsedSeconds = "elapsed_seconds"
|
||||
case remainingSeconds = "remaining_seconds"
|
||||
case progressPercent = "progress_percent"
|
||||
case completedCyclesEstimate = "completed_cycles_estimate"
|
||||
case nextCycleEtaUtc = "next_cycle_eta_utc"
|
||||
case nextCycleEtaLocal = "next_cycle_eta_local"
|
||||
case estimatedFinishUtc = "estimated_finish_utc"
|
||||
case estimatedFinishLocal = "estimated_finish_local"
|
||||
}
|
||||
|
||||
var startedDate: Date? { parseISODate(startedUtc) }
|
||||
var finishDate: Date? { parseISODate(estimatedFinishUtc) }
|
||||
}
|
||||
|
||||
struct CompletedWatchPayload: Codable {
|
||||
let reportPath: String
|
||||
let ageMinutes: Int
|
||||
|
|
@ -1992,6 +2134,13 @@ extension String {
|
|||
var lastPathComponent: String {
|
||||
URL(fileURLWithPath: self).lastPathComponent
|
||||
}
|
||||
|
||||
var shortClockText: String {
|
||||
guard let date = parseISODate(self) else { return self }
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "HH:mm"
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
}
|
||||
|
||||
func sats(_ btcText: String) -> String {
|
||||
|
|
@ -2000,6 +2149,30 @@ func sats(_ btcText: String) -> String {
|
|||
return "\(sats) sats"
|
||||
}
|
||||
|
||||
func parseISODate(_ text: String) -> Date? {
|
||||
if let date = ISO8601DateFormatter().date(from: text) {
|
||||
return date
|
||||
}
|
||||
let formatter = DateFormatter()
|
||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssXXXXX"
|
||||
return formatter.date(from: text)
|
||||
}
|
||||
|
||||
func formatDuration(_ seconds: Int) -> String {
|
||||
let safe = max(0, seconds)
|
||||
let hours = safe / 3600
|
||||
let minutes = (safe % 3600) / 60
|
||||
let secs = safe % 60
|
||||
if hours > 0 {
|
||||
return "\(hours)h \(minutes)m left"
|
||||
}
|
||||
if minutes > 0 {
|
||||
return "\(minutes)m \(secs)s left"
|
||||
}
|
||||
return "\(secs)s left"
|
||||
}
|
||||
|
||||
func phText(_ ehText: String) -> String {
|
||||
guard let eh = Double(ehText) else { return "n/a" }
|
||||
return String(format: "%.3f", eh * 1000)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,25 @@ class CompletedWatch:
|
|||
earliest_action_local: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ActiveWatchDetails:
|
||||
label: str
|
||||
run_id: str
|
||||
pid: int | None
|
||||
started_utc: str
|
||||
planned_cycles: int
|
||||
interval_seconds: int
|
||||
total_seconds: int
|
||||
elapsed_seconds: int
|
||||
remaining_seconds: int
|
||||
progress_percent: int
|
||||
completed_cycles_estimate: int
|
||||
next_cycle_eta_utc: str | None
|
||||
next_cycle_eta_local: str | None
|
||||
estimated_finish_utc: str
|
||||
estimated_finish_local: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OperatorState:
|
||||
has_ocean: bool
|
||||
|
|
@ -37,6 +56,7 @@ class OperatorState:
|
|||
latest_ocean_timestamp: str | None
|
||||
latest_market_timestamp: str | None
|
||||
active_manual_positions: list[str]
|
||||
active_watch_details: ActiveWatchDetails | None = None
|
||||
|
||||
|
||||
def get_operator_state(conn) -> OperatorState:
|
||||
|
|
@ -45,11 +65,13 @@ def get_operator_state(conn) -> OperatorState:
|
|||
proposal = latest_proposal(conn)
|
||||
latest_report = _latest_report()
|
||||
freshness = _freshness_minutes(market.timestamp_utc if market else None)
|
||||
active_watch_details = _active_watch_details()
|
||||
active_watch = active_watch_details.label if active_watch_details else _active_watch_from_process_table()
|
||||
return OperatorState(
|
||||
has_ocean=ocean is not None,
|
||||
has_market=market is not None,
|
||||
action=proposal.action if proposal else None,
|
||||
active_watch=_active_watch(),
|
||||
active_watch=active_watch,
|
||||
completed_watch=_recent_completed_watch(latest_report, market.timestamp_utc if market else None),
|
||||
is_fresh=freshness is not None and freshness <= 30,
|
||||
freshness_minutes=freshness,
|
||||
|
|
@ -58,6 +80,7 @@ def get_operator_state(conn) -> OperatorState:
|
|||
latest_ocean_timestamp=ocean.timestamp_utc if ocean else None,
|
||||
latest_market_timestamp=market.timestamp_utc if market else None,
|
||||
active_manual_positions=_active_manual_positions(conn),
|
||||
active_watch_details=active_watch_details,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -80,6 +103,9 @@ def build_operator_cockpit(conn) -> str:
|
|||
f" Research stage: {_research_stage(state.active_watch, state.completed_watch)}",
|
||||
]
|
||||
|
||||
if state.active_watch_details:
|
||||
lines.extend(_active_watch_status_lines(state.active_watch_details))
|
||||
|
||||
if state.completed_watch:
|
||||
lines.extend(_cooldown_status_lines(state.completed_watch))
|
||||
|
||||
|
|
@ -406,6 +432,17 @@ def _progress_bar(elapsed: int, total: int, width: int = 20) -> str:
|
|||
return "[" + ("#" * filled) + ("-" * (width - filled)) + "]"
|
||||
|
||||
|
||||
def _active_watch_status_lines(active_watch: ActiveWatchDetails) -> list[str]:
|
||||
remaining_minutes = max(0, active_watch.remaining_seconds // 60)
|
||||
return [
|
||||
f" Active watch progress: {_progress_bar(active_watch.elapsed_seconds, active_watch.total_seconds)} {active_watch.progress_percent}%",
|
||||
f" Active watch cycles: about {active_watch.completed_cycles_estimate}/{active_watch.planned_cycles}",
|
||||
f" Active watch started: {active_watch.started_utc}",
|
||||
f" Active watch ETA: {active_watch.estimated_finish_local}",
|
||||
f" Active watch remaining: about {remaining_minutes} minutes",
|
||||
]
|
||||
|
||||
|
||||
def _running_runs() -> list[str]:
|
||||
if not EXPERIMENT_LOG.exists():
|
||||
return []
|
||||
|
|
@ -423,26 +460,73 @@ def _running_runs() -> list[str]:
|
|||
|
||||
|
||||
def _active_watch() -> str | None:
|
||||
from_state_file = _active_watch_from_state_file()
|
||||
if from_state_file:
|
||||
return from_state_file
|
||||
details = _active_watch_details()
|
||||
if details:
|
||||
return details.label
|
||||
return _active_watch_from_process_table()
|
||||
|
||||
|
||||
def _active_watch_from_state_file() -> str | None:
|
||||
def _active_watch_details() -> ActiveWatchDetails | None:
|
||||
if not ACTIVE_WATCH.exists():
|
||||
return None
|
||||
try:
|
||||
payload = json.loads(ACTIVE_WATCH.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
return "state file exists but is unreadable"
|
||||
return None
|
||||
pid = payload.get("pid")
|
||||
run_id = payload.get("run_id", "unknown-run")
|
||||
if isinstance(pid, int) and _pid_exists(pid):
|
||||
return f"{run_id} pid={pid}"
|
||||
return _active_watch_details_from_payload(payload, run_id, pid)
|
||||
if not isinstance(pid, int):
|
||||
return _active_watch_details_from_payload(payload, run_id, None)
|
||||
return None
|
||||
|
||||
|
||||
def _active_watch_details_from_payload(
|
||||
payload: dict[str, object],
|
||||
run_id: str,
|
||||
pid: int | None,
|
||||
) -> ActiveWatchDetails | None:
|
||||
started = _parse_utc(str(payload.get("started_utc", "")))
|
||||
if started is None:
|
||||
return None
|
||||
try:
|
||||
planned_cycles = int(payload.get("planned_cycles", 0))
|
||||
interval_seconds = int(payload.get("interval_seconds", 0))
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if planned_cycles <= 0 or interval_seconds <= 0:
|
||||
return None
|
||||
|
||||
total_seconds = planned_cycles * interval_seconds
|
||||
now = datetime.now(UTC)
|
||||
elapsed_seconds = max(0, int((now - started).total_seconds()))
|
||||
remaining_seconds = max(0, total_seconds - elapsed_seconds)
|
||||
progress_percent = min(100, max(0, int((elapsed_seconds / total_seconds) * 100)))
|
||||
completed_cycles_estimate = min(planned_cycles, max(1, (elapsed_seconds // interval_seconds) + 1))
|
||||
next_cycle_index = completed_cycles_estimate if completed_cycles_estimate < planned_cycles else None
|
||||
next_cycle_eta = None if next_cycle_index is None else started.timestamp() + (next_cycle_index * interval_seconds)
|
||||
finish = datetime.fromtimestamp(started.timestamp() + total_seconds, UTC)
|
||||
next_cycle_utc = datetime.fromtimestamp(next_cycle_eta, UTC) if next_cycle_eta else None
|
||||
return ActiveWatchDetails(
|
||||
label=f"{run_id} pid={pid}" if pid is not None else run_id,
|
||||
run_id=run_id,
|
||||
pid=pid,
|
||||
started_utc=started.isoformat(timespec="seconds"),
|
||||
planned_cycles=planned_cycles,
|
||||
interval_seconds=interval_seconds,
|
||||
total_seconds=total_seconds,
|
||||
elapsed_seconds=min(elapsed_seconds, total_seconds),
|
||||
remaining_seconds=remaining_seconds,
|
||||
progress_percent=progress_percent,
|
||||
completed_cycles_estimate=completed_cycles_estimate,
|
||||
next_cycle_eta_utc=next_cycle_utc.isoformat(timespec="seconds") if next_cycle_utc else None,
|
||||
next_cycle_eta_local=next_cycle_utc.astimezone().isoformat(timespec="seconds") if next_cycle_utc else None,
|
||||
estimated_finish_utc=finish.isoformat(timespec="seconds"),
|
||||
estimated_finish_local=finish.astimezone().isoformat(timespec="seconds"),
|
||||
)
|
||||
|
||||
|
||||
def _active_watch_from_process_table() -> str | None:
|
||||
if os.environ.get("BRAIINS_RATCHET_IGNORE_PROCESS_WATCH") == "1":
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@ import unittest
|
|||
from unittest.mock import patch
|
||||
|
||||
from braiins_ratchet.guidance import (
|
||||
ActiveWatchDetails,
|
||||
CompletedWatch,
|
||||
_cooldown_status_lines,
|
||||
_active_watch_status_lines,
|
||||
_do_this_now,
|
||||
_pathway_forecast,
|
||||
build_operator_cockpit,
|
||||
|
|
@ -149,6 +151,32 @@ class GuidanceTests(unittest.TestCase):
|
|||
self.assertIn("Earliest next action: 2026-04-28T00:00:00+02:00", text)
|
||||
self.assertIn("Cooldown remaining: 270 minutes", text)
|
||||
|
||||
def test_active_watch_status_includes_progress_and_eta(self) -> None:
|
||||
active_watch = ActiveWatchDetails(
|
||||
label="run-example pid=123",
|
||||
run_id="run-example",
|
||||
pid=123,
|
||||
started_utc="2026-04-29T08:48:06+00:00",
|
||||
planned_cycles=24,
|
||||
interval_seconds=300,
|
||||
total_seconds=7200,
|
||||
elapsed_seconds=1800,
|
||||
remaining_seconds=5400,
|
||||
progress_percent=25,
|
||||
completed_cycles_estimate=7,
|
||||
next_cycle_eta_utc="2026-04-29T09:18:06+00:00",
|
||||
next_cycle_eta_local="2026-04-29T11:18:06+02:00",
|
||||
estimated_finish_utc="2026-04-29T10:48:06+00:00",
|
||||
estimated_finish_local="2026-04-29T12:48:06+02:00",
|
||||
)
|
||||
|
||||
text = "\n".join(_active_watch_status_lines(active_watch))
|
||||
|
||||
self.assertIn("Active watch progress: [#####---------------] 25%", text)
|
||||
self.assertIn("Active watch cycles: about 7/24", text)
|
||||
self.assertIn("Active watch ETA: 2026-04-29T12:48:06+02:00", text)
|
||||
self.assertIn("Active watch remaining: about 90 minutes", text)
|
||||
|
||||
|
||||
def _completed_watch(age_minutes: int) -> CompletedWatch:
|
||||
return CompletedWatch(
|
||||
|
|
@ -165,6 +193,7 @@ def _isolated_operator_files():
|
|||
return patch.multiple(
|
||||
"braiins_ratchet.guidance",
|
||||
_active_watch=lambda: None,
|
||||
_active_watch_details=lambda: None,
|
||||
_latest_report=lambda: None,
|
||||
_running_runs=lambda: [],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -93,6 +93,10 @@ class MacAppPackagingTest(unittest.TestCase):
|
|||
self.assertIn("RealityHUD", text)
|
||||
self.assertIn("RenderedReality", text)
|
||||
self.assertIn("RealitySnapshotWriter", text)
|
||||
self.assertIn("ActiveWatchProgressPanel", text)
|
||||
self.assertIn("activeWatchDetails", text)
|
||||
self.assertIn("TimelineView(.periodic", text)
|
||||
self.assertIn("The app refreshes backend state every 30 seconds", text)
|
||||
self.assertIn("AppStatePayload", text)
|
||||
self.assertIn("EngineStatusPayload", text)
|
||||
self.assertIn("loadAppState", text)
|
||||
|
|
|
|||
Loading…
Reference in a new issue