Skip to content

Commit bf2eab9

Browse files
authored
fix: the wizard's expected end state was a dashboard reading zero (#223)
Step 3 says "Already have an account? Enter your credentials below." A reasonable user concludes that entering them is what makes earnings appear. It is not: those values only configure the CONTAINER. The dashboard shows no balance until the same credentials are entered again under Settings → Collectors. The service-detail view says exactly that. The wizard — the one screen a new user actually sees — said nothing, so completing onboarding correctly still left the dashboard at zero with no explanation of why. /api/services/available, which is what the wizard reads, did not even report has_collector, so the wizard could not have known. It does now. The notice is one function used by both screens rather than two copies. A notice that exists twice drifts, and the wizard's is the one that matters most. The wording keeps "the service earns either way": without it the notice reads as "your deployment is broken", which it is not — the container is running and earning, only the in-dashboard balance is missing. Negative control: removing it from the wizard fails 3 tests; dropping has_collector from the endpoint fails 1; giving the detail view its own copy again fails 2. Closes CashPilot-p6s
1 parent 3c02c45 commit bf2eab9

3 files changed

Lines changed: 135 additions & 7 deletions

File tree

app/main.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1211,6 +1211,9 @@ async def api_services_available(request: Request) -> list[dict[str, Any]]:
12111211
services = catalog.get_services()
12121212
deployments = await database.get_deployments()
12131213
deployed_slugs = {d["slug"] for d in deployments}
1214+
# Imported here rather than at module scope: app.collectors pulls in every
1215+
# collector module.
1216+
from app.collectors import COLLECTOR_MAP as collector_map
12141217

12151218
# Also check worker containers for deployed status (catches externally-deployed services)
12161219
worker_containers = await _get_all_worker_containers()
@@ -1235,6 +1238,11 @@ async def api_services_available(request: Request) -> list[dict[str, Any]]:
12351238
svc["deployed"] = slug in deployed_slugs or slug in worker_slugs
12361239
svc["manual_only"] = not has_image
12371240
svc["node_count"] = len(worker_node_counts.get(slug, set()))
1241+
# The setup wizard reads this endpoint, and it needs to know whether
1242+
# earnings tracking takes a SECOND set of credentials — the service
1243+
# detail view already says so, and the wizard is the screen a new user
1244+
# actually sees (CashPilot-p6s).
1245+
svc["has_collector"] = slug in collector_map
12381246
available.append(svc)
12391247
return available
12401248

app/static/js/app.js

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1886,6 +1886,26 @@ const CP = (() => {
18861886
}
18871887
}
18881888

1889+
// Earnings tracking takes a SECOND set of credentials.
1890+
//
1891+
// The container credentials only configure the container; the dashboard shows
1892+
// no balance until the same values are entered again under Settings →
1893+
// Collectors. The service-detail view said so; the setup wizard — the one
1894+
// screen a new user actually sees — did not, so the expected end state of
1895+
// onboarding was a dashboard reading zero (CashPilot-p6s).
1896+
//
1897+
// One function rather than two copies: a notice that exists twice drifts, and
1898+
// the wizard's version is the one that matters most.
1899+
function collectorCredentialsNotice(slug) {
1900+
return `
1901+
<div style="font-size:0.8rem; color:var(--text-muted); background:var(--bg-subtle, rgba(255,255,255,0.03)); border:1px solid var(--border); border-radius:6px; padding:8px 10px; margin:10px 0;">
1902+
The credentials above run the service. To also see its <strong>balance</strong> on the dashboard,
1903+
add earnings-tracking credentials under
1904+
<a href="#" data-action="openCredentialModal" data-prevent="1" data-a1="${escapeHtml(slug)}" style="color:var(--accent, #3b82f6);">Settings → Collectors</a>
1905+
after deploying. This is optional — the service earns either way.
1906+
</div>`;
1907+
}
1908+
18891909
function renderServiceSetupForm(svc, workers) {
18901910
const isDeployed = svc.deployed || false;
18911911
const dashboardUrl = (svc.cashout && svc.cashout.dashboard_url) || svc.website || '';
@@ -1954,6 +1974,8 @@ const CP = (() => {
19541974
19551975
${envFields}
19561976
1977+
${svc.has_collector ? collectorCredentialsNotice(svc.slug) : ''}
1978+
19571979
${(() => {
19581980
const onlineWorkers = (workers || []).filter(w => w.status === 'online');
19591981
const { rows: workerRows, allDeployed } = workerCheckboxList(svc, onlineWorkers, 'setup-deploy-worker-cb');
@@ -2343,13 +2365,7 @@ const CP = (() => {
23432365
// The fields above only configure the container that earns; they don't
23442366
// let CashPilot read your balance. Make that explicit at deploy time.
23452367
if (svc.has_collector) {
2346-
html += `
2347-
<div style="font-size:0.8rem; color:var(--text-muted); background:var(--bg-subtle, rgba(255,255,255,0.03)); border:1px solid var(--border); border-radius:6px; padding:8px 10px; margin:10px 0;">
2348-
The credentials above run the service. To also see its <strong>balance</strong> on the dashboard,
2349-
add earnings-tracking credentials under
2350-
<a href="#" data-action="openCredentialModal" data-prevent="1" data-a1="${escapeHtml(svc.slug)}" style="color:var(--accent, #3b82f6);">Settings → Collectors</a>
2351-
after deploying. This is optional — the service earns either way.
2352-
</div>`;
2368+
html += collectorCredentialsNotice(svc.slug);
23532369
}
23542370

23552371
if (allDeployed && onlineWorkers.length > 0) {

tests/test_beads_batch_37.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""CashPilot-p6s: the wizard's expected end state was a dashboard reading zero.
2+
3+
Step 3 of the setup wizard says "Already have an account? Enter your credentials
4+
below." A reasonable user concludes that entering them is what makes earnings
5+
appear. It is not: those values only configure the CONTAINER. The dashboard
6+
shows no balance until the same credentials are entered again under
7+
Settings → Collectors.
8+
9+
The service-detail view says exactly that. The wizard — the one screen a new
10+
user actually sees — said nothing, so completing onboarding correctly still left
11+
the dashboard at zero with no explanation.
12+
13+
``/api/services/available``, which is what the wizard reads, did not even report
14+
``has_collector``, so the wizard could not have known.
15+
"""
16+
17+
from __future__ import annotations
18+
19+
import re
20+
from pathlib import Path
21+
22+
import pytest
23+
24+
ROOT = Path(__file__).resolve().parents[1]
25+
APP_JS = ROOT / "app" / "static" / "js" / "app.js"
26+
27+
28+
def js() -> str:
29+
text = APP_JS.read_text(encoding="utf-8")
30+
text = re.sub(r"/\*.*?\*/", "", text, flags=re.S)
31+
return "\n".join(re.sub(r"(^|\s)//.*$", "", line) for line in text.splitlines())
32+
33+
34+
class TestTheWizardSaysIt:
35+
def test_the_setup_form_renders_the_notice(self):
36+
source = js()
37+
i = source.index("function renderServiceSetupForm")
38+
j = source.index("function ", i + 10)
39+
assert "collectorCredentialsNotice" in source[i:j], "the wizard still says nothing about collectors"
40+
41+
def test_it_is_gated_on_the_service_having_one(self):
42+
"""A service with no collector has no second step to warn about."""
43+
source = js()
44+
assert "svc.has_collector ? collectorCredentialsNotice" in source
45+
46+
def test_the_endpoint_the_wizard_reads_reports_it(self):
47+
"""The wizard could not have known: the field was not in the payload."""
48+
source = (ROOT / "app" / "main.py").read_text(encoding="utf-8")
49+
i = source.index("async def api_services_available")
50+
j = source.index("\n@app.", i)
51+
assert 'svc["has_collector"]' in source[i:j]
52+
53+
def test_the_notice_names_where_to_go(self):
54+
assert "Settings → Collectors" in js()
55+
56+
def test_it_says_the_service_still_earns(self):
57+
"""Without this it reads as "your deployment is broken", which it is not."""
58+
assert "the service earns either way" in js()
59+
60+
61+
class TestThereIsOnlyOneWording:
62+
"""Two copies of a notice drift, and the wizard's is the one that matters."""
63+
64+
def test_the_helper_exists(self):
65+
assert "function collectorCredentialsNotice(slug)" in js()
66+
67+
def test_both_screens_use_it(self):
68+
source = js()
69+
assert source.count("collectorCredentialsNotice(") >= 3, (
70+
"expected the definition plus a call from the wizard and the detail view"
71+
)
72+
73+
def test_the_detail_view_no_longer_inlines_its_own(self):
74+
source = js()
75+
i = source.index("function collectorCredentialsNotice")
76+
after = source[source.index("}", source.index("return `", i)) :]
77+
assert "The credentials above run the service" not in after, "a second copy of the notice is back"
78+
79+
def test_the_slug_is_escaped(self):
80+
"""It goes into a data attribute reached through innerHTML."""
81+
source = js()
82+
i = source.index("function collectorCredentialsNotice")
83+
assert "escapeHtml(slug)" in source[i : i + 900]
84+
85+
86+
class TestTheDistinctionIsRealNotCosmetic:
87+
"""The premise: container credentials and collector credentials differ."""
88+
89+
def test_a_service_with_a_collector_is_flagged(self):
90+
from app.collectors import COLLECTOR_MAP
91+
92+
assert "honeygain" in COLLECTOR_MAP
93+
94+
def test_a_service_without_one_is_not(self):
95+
from app.collectors import COLLECTOR_MAP
96+
97+
assert "proxybase-xyz" not in COLLECTOR_MAP
98+
99+
@pytest.mark.parametrize("slug", ["honeygain", "iproyal"])
100+
def test_the_collector_takes_its_own_arguments(self, slug):
101+
"""If they were the same values, re-entering them would be pointless."""
102+
from app.collectors import _COLLECTOR_ARGS
103+
104+
assert _COLLECTOR_ARGS.get(slug), f"{slug} has a collector but declares no arguments"

0 commit comments

Comments
 (0)