Skip to content

Commit 971cd15

Browse files
authored
fix: an unreachable machine is not a machine earning nothing (beads batch 8) (#190)
* fix: a machine CashPilot cannot see is not a machine earning nothing Batch 8. Two findings, one mistake: both endpoints are built only from ONLINE workers, and both then presented the resulting absence as a measured fact. CashPilot-daq — an offline worker was told to switch itself off. per_worker_gross is built from online workers only, and the endpoint defaulted a missing entry to 0.0, so a host that had merely stopped heartbeating produced a confident financial recommendation: "geiserback earns about 0.00 a month and costs about 9.49 in electricity — roughly 9.49 out of pocket. Since this machine runs only these services, turning it off would save that." Its earnings were also being silently reattributed to whatever workers were still reporting, so the rest of the fleet looked better than it was at the same moment. assess_machine now takes monthly_gross | None and returns UNKNOWN with no cost, no net and no gross when it is None. A machine that genuinely earned 0.00 is still judged — asserted, because a fix that stopped judging anything would pass the other tests. CashPilot-1qy — an unreachable worker read as "you have no services". Three minutes after a host stops heartbeating — a reboot, a network blip, a worker container restart — the table emptied and the dashboard stated as fact that the user had nothing and should start over, with a Setup Wizard button. The containers were still running and still earning. The empty state now asks /api/workers before deciding which sentence to show, says the containers keep running and earning while a worker is offline, and — when it cannot even reach that endpoint — asserts neither case rather than guessing. A genuinely empty install still gets the wizard. Negative controls: removing the unknown-gross branch fails three tests, collapsing the empty-state condition fails one. * fix: the fleet total says what it could not see Found in my own fresh review of this PR, not by an audit agent. Making an unreachable machine report monthly_gross: None fixed the per-machine verdict but pushed the same mistake up one level. The fleet total sums that None as 0.0, so the headline gross silently shrinks by whatever the unreachable machine earns, with nothing saying so — a number that quietly gets smaller and looks like a real decline. fleet_summary already guards exactly this for cost, and its docstring says so: 'so the total never quietly understates what the fleet costs'. The guard had simply never been extended to gross, because until this PR gross was never unknown. It now counts unreadable machines and says so in the summary. Also asserted that fleet.html can render a null gross. Its money() helper already returns an em dash for null, but monthly_gross only became nullable in this PR, so a later simplification of that helper could turn it into '0.00' or NaN without any test objecting.
1 parent 17fbe57 commit 971cd15

4 files changed

Lines changed: 251 additions & 5 deletions

File tree

app/machine_economics.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ def break_even_price(monthly_gross: float, watts: float) -> float | None:
8181
def assess_machine(
8282
*,
8383
name: str,
84-
monthly_gross: float,
84+
monthly_gross: float | None,
8585
watts: float | None,
8686
price_per_kwh: float | None,
8787
metered: bool = True,
@@ -95,6 +95,28 @@ def assess_machine(
9595
measure, so the honest output is the cost of the box rather than a verdict
9696
pretending the services caused it.
9797
"""
98+
# None means CashPilot could not READ this machine's earnings — an offline
99+
# worker, not a machine that earned nothing. The two were indistinguishable,
100+
# so a host that had merely stopped heartbeating was told:
101+
# "earns about 0.00 a month and costs about 9.49 in electricity ... turning
102+
# it off would save that." A confident financial recommendation about a
103+
# machine we cannot see, and its earnings were being silently reattributed
104+
# to whatever workers were still reporting.
105+
if monthly_gross is None:
106+
return {
107+
"machine": name,
108+
"verdict": UNKNOWN,
109+
"monthly_gross": None,
110+
"monthly_cost": None,
111+
"monthly_net": None,
112+
"break_even_watts": None,
113+
"summary": (
114+
f"{name} is not reporting, so CashPilot cannot see what it earns. Nothing here "
115+
"says whether it is worth running — the last figures it sent are not evidence "
116+
"about now."
117+
),
118+
}
119+
98120
gross = float(monthly_gross or 0.0)
99121

100122
if not metered:
@@ -189,6 +211,12 @@ def fleet_summary(machines: list[dict[str, Any]]) -> dict[str, Any]:
189211
"""
190212
known = [m for m in machines if m.get("monthly_cost") is not None]
191213
unknown = [m for m in machines if m.get("monthly_cost") is None]
214+
# A machine that is not reporting has monthly_gross None, and summing that
215+
# as 0.0 makes the fleet total silently exclude whatever it earns. That is
216+
# the same "absent read as measured" mistake this function already guards
217+
# against for cost, so it is counted and reported the same way rather than
218+
# left for the reader to notice a number quietly getting smaller.
219+
gross_unknown = [m for m in machines if m.get("monthly_gross") is None]
192220
gross = sum(float(m.get("monthly_gross") or 0.0) for m in machines)
193221
# Net must compare LIKE WITH LIKE. Subtracting the cost of the machines
194222
# whose cost is known from the gross of ALL machines flatters the result by
@@ -205,11 +233,19 @@ def fleet_summary(machines: list[dict[str, Any]]) -> dict[str, Any]:
205233
"monthly_net": round(known_gross - cost, 4) if known else None,
206234
"cost_known_for": len(known),
207235
"cost_unknown_for": len(unknown),
236+
"gross_unknown_for": len(gross_unknown),
208237
"losing_money": [m["machine"] for m in machines if m.get("verdict") == LOSING_MONEY],
209238
"quality": "estimated",
210239
"summary": (
211-
f"Costs are known for {len(known)} of {len(machines)} machine(s)."
212-
if unknown
240+
(
241+
f"Costs are known for {len(known)} of {len(machines)} machine(s)."
242+
+ (
243+
f" {len(gross_unknown)} machine(s) are not reporting, so what they earn is not in this total."
244+
if gross_unknown
245+
else ""
246+
)
247+
)
248+
if (unknown or gross_unknown)
213249
else f"Across {len(machines)} machine(s): about {gross:.2f} earned against {cost:.2f} of electricity."
214250
),
215251
}

app/main.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2516,7 +2516,14 @@ async def api_fleet_economics(request: Request) -> dict[str, Any]:
25162516
assessed.append(
25172517
machine_economics.assess_machine(
25182518
name=worker.get("name") or f"worker {worker.get('id')}",
2519-
monthly_gross=per_worker_gross.get(worker.get("id"), 0.0),
2519+
# None, not 0.0, when this worker's containers were never
2520+
# counted. per_worker_gross is built only from ONLINE workers
2521+
# (_get_all_worker_containers skips the rest), so defaulting an
2522+
# offline machine to zero earnings is what produced the
2523+
# "turn it off" advice about a host we cannot see.
2524+
monthly_gross=(
2525+
per_worker_gross.get(worker.get("id"), 0.0) if str(worker.get("status") or "") == "online" else None
2526+
),
25202527
watts=watts,
25212528
price_per_kwh=price or None,
25222529
metered=power.is_metered(info),

app/static/js/app.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,11 +585,34 @@ const CP = (() => {
585585
]);
586586

587587
if (!services || services.length === 0) {
588-
container.innerHTML = `
588+
// An empty list means one of two very different things, and saying the
589+
// wrong one is worse than saying nothing.
590+
//
591+
// /api/services/deployed is built only from ONLINE workers, so three
592+
// minutes after a host stops heartbeating — a reboot, a network blip, a
593+
// worker container restart — the table emptied and the dashboard stated
594+
// as fact that the user had no services and should start over. The
595+
// containers were still running and still earning.
596+
let unreachable = 0;
597+
try {
598+
const workers = await api('/api/workers');
599+
unreachable = (workers || []).filter(w => w.status !== 'online').length;
600+
} catch (err) {
601+
// Cannot tell which case this is; say so rather than guess.
602+
unreachable = -1;
603+
}
604+
container.innerHTML = unreachable === 0
605+
? `
589606
<div class="empty-state" style="padding:32px 0; text-align:center;">
590607
<div class="empty-state-title">No services deployed yet</div>
591608
<div class="empty-state-text">Get started by deploying your first passive income service.</div>
592609
<a href="/setup" class="btn btn-primary" style="margin-top:12px;">Setup Wizard</a>
610+
</div>`
611+
: `
612+
<div class="empty-state" style="padding:32px 0; text-align:center;">
613+
<div class="empty-state-title">Can't reach ${unreachable > 0 ? escapeHtml(String(unreachable)) + ' worker' + (unreachable === 1 ? '' : 's') : 'the workers'}</div>
614+
<div class="empty-state-text">Services running on ${unreachable === 1 ? 'it' : 'them'} are not shown here — this does not mean they stopped. Containers keep running and earning while a worker is offline.</div>
615+
<a href="/fleet" class="btn btn-ghost" style="margin-top:12px;">Check the fleet</a>
593616
</div>`;
594617
return;
595618
}

tests/test_beads_batch_8.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
"""Batch 8: a machine we cannot see is not a machine earning nothing.
2+
3+
Two findings, one mistake. Both endpoints are built only from ONLINE workers,
4+
and both then presented the resulting absence as a measured fact — one as a
5+
financial recommendation, the other as "you have no services".
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import re
11+
from pathlib import Path
12+
13+
ROOT = Path(__file__).resolve().parents[1]
14+
APP_JS = ROOT / "app" / "static" / "js" / "app.js"
15+
16+
17+
def without_comments(text: str) -> str:
18+
"""JS source with comments stripped, for guards that scan raw text."""
19+
text = re.sub(r"/\*.*?\*/", "", text, flags=re.S)
20+
return "\n".join(re.sub(r"(^|\s)//.*$", "", line) for line in text.splitlines())
21+
22+
23+
class TestAnUnreadableMachineGetsNoVerdict:
24+
"""CashPilot-daq: an offline worker was told to switch itself off.
25+
26+
per_worker_gross is built only from online workers, and the endpoint
27+
defaulted a missing entry to 0.0. So a host that had merely stopped
28+
heartbeating produced: "geiserback earns about 0.00 a month and costs about
29+
9.49 in electricity — roughly 9.49 out of pocket. Since this machine runs
30+
only these services, turning it off would save that."
31+
32+
A confident financial recommendation about a machine CashPilot cannot see —
33+
and its earnings were silently reattributed to the workers still reporting.
34+
"""
35+
36+
def _assess(self, gross):
37+
from app import machine_economics
38+
39+
return machine_economics.assess_machine(
40+
name="geiserback", monthly_gross=gross, watts=65.0, price_per_kwh=0.20, dedicated=True
41+
)
42+
43+
def test_unknown_earnings_produce_an_unknown_verdict(self):
44+
from app import machine_economics
45+
46+
assert self._assess(None)["verdict"] == machine_economics.UNKNOWN
47+
48+
def test_no_cost_or_net_is_asserted_for_it(self):
49+
out = self._assess(None)
50+
assert out["monthly_cost"] is None
51+
assert out["monthly_net"] is None
52+
assert out["monthly_gross"] is None, "0.0 here is the fabrication being removed"
53+
54+
def test_the_summary_says_it_is_not_reporting(self):
55+
assert "not reporting" in self._assess(None)["summary"]
56+
57+
def test_it_does_not_recommend_switching_anything_off(self):
58+
summary = self._assess(None)["summary"].lower()
59+
assert "turning it off would save" not in summary
60+
61+
def test_a_genuine_zero_is_still_judged(self):
62+
"""The control. Without it this fix could pass by never judging anything."""
63+
from app import machine_economics
64+
65+
out = self._assess(0.0)
66+
assert out["verdict"] == machine_economics.LOSING_MONEY
67+
assert out["monthly_cost"] is not None
68+
69+
def test_a_profitable_machine_is_unaffected(self):
70+
from app import machine_economics
71+
72+
assert self._assess(50.0)["verdict"] == machine_economics.PROFITABLE
73+
74+
def test_the_endpoint_passes_none_for_an_offline_worker(self):
75+
source = (ROOT / "app" / "main.py").read_text(encoding="utf-8")
76+
assert 'monthly_gross=per_worker_gross.get(worker.get("id"), 0.0),' not in source
77+
assert 'if str(worker.get("status") or "") == "online"' in source
78+
79+
80+
class TestTheEmptyDashboardSaysWhichEmptyItIs:
81+
"""CashPilot-1qy: an unreachable worker read as "you have no services".
82+
83+
/api/services/deployed is built only from online workers, so three minutes
84+
after a host stopped heartbeating — a reboot, a network blip, a worker
85+
container restart — the table emptied and the dashboard stated as fact that
86+
the user had nothing and should start over. The containers were still
87+
running and still earning.
88+
"""
89+
90+
def _empty_state(self) -> str:
91+
source = without_comments(APP_JS.read_text(encoding="utf-8"))
92+
start = source.index("if (!services || services.length === 0)")
93+
return source[start : start + 2000]
94+
95+
def test_it_checks_for_unreachable_workers_first(self):
96+
assert "/api/workers" in self._empty_state()
97+
98+
def test_it_does_not_claim_nothing_is_deployed_when_a_worker_is_offline(self):
99+
block = self._empty_state()
100+
assert "unreachable === 0" in block, "the two cases are not distinguished"
101+
102+
def test_it_says_the_containers_are_still_running(self):
103+
"""The user's real question is whether their earnings stopped."""
104+
block = self._empty_state()
105+
assert "keep running and earning" in block
106+
107+
def test_a_genuinely_empty_install_still_gets_the_wizard(self):
108+
"""The control: the onboarding path must survive this change."""
109+
block = self._empty_state()
110+
assert "No services deployed yet" in block
111+
assert "/setup" in block
112+
113+
def test_a_failed_worker_lookup_does_not_assert_either_case(self):
114+
"""If we cannot tell which it is, saying nothing beats guessing."""
115+
block = self._empty_state()
116+
assert "unreachable = -1" in block
117+
118+
def test_the_worker_count_is_escaped(self):
119+
"""It reaches the DOM through innerHTML."""
120+
block = self._empty_state()
121+
assert "escapeHtml(String(unreachable))" in block
122+
123+
124+
class TestTheFleetTotalSaysWhatItCouldNotSee:
125+
"""Found in my own fresh review of this PR, not by an audit agent.
126+
127+
Making an unreachable machine report ``monthly_gross: None`` fixed the
128+
per-machine verdict but pushed the same mistake up one level: the fleet
129+
total summed that None as 0.0, so the headline gross silently shrank by
130+
whatever the unreachable machine earns, with nothing saying so.
131+
132+
The function already guards exactly this for cost — "so the total never
133+
quietly understates what the fleet costs" — and the guard simply had not
134+
been extended to gross, because until this PR gross was never unknown.
135+
"""
136+
137+
def _summary(self, machines):
138+
from app import machine_economics
139+
140+
return machine_economics.fleet_summary(machines)
141+
142+
def test_an_unreadable_machine_is_counted(self):
143+
out = self._summary(
144+
[
145+
{"machine": "watchtower", "monthly_gross": 40.0, "monthly_cost": 9.0},
146+
{"machine": "geiserback", "monthly_gross": None, "monthly_cost": None},
147+
]
148+
)
149+
assert out["gross_unknown_for"] == 1
150+
151+
def test_the_summary_says_the_total_is_incomplete(self):
152+
out = self._summary(
153+
[
154+
{"machine": "watchtower", "monthly_gross": 40.0, "monthly_cost": 9.0},
155+
{"machine": "geiserback", "monthly_gross": None, "monthly_cost": None},
156+
]
157+
)
158+
assert "not reporting" in out["summary"]
159+
assert "not in this total" in out["summary"]
160+
161+
def test_a_fully_readable_fleet_says_nothing_extra(self):
162+
"""The control: this must not nag when everything is known."""
163+
out = self._summary(
164+
[
165+
{"machine": "watchtower", "monthly_gross": 40.0, "monthly_cost": 9.0},
166+
{"machine": "nuc", "monthly_gross": 10.0, "monthly_cost": 3.0},
167+
]
168+
)
169+
assert out["gross_unknown_for"] == 0
170+
assert "not reporting" not in out["summary"]
171+
172+
def test_the_template_can_render_a_null_gross(self):
173+
"""monthly_gross became nullable; something has to draw it.
174+
175+
fleet.html's money() already returns an em dash for null — asserted so a
176+
later simplification of that helper cannot turn a null into "0.00" or a
177+
NaN.
178+
"""
179+
text = (ROOT / "app" / "templates" / "fleet.html").read_text(encoding="utf-8")
180+
assert "v == null ? '—'" in text

0 commit comments

Comments
 (0)