Zo doen wijdat in oost
DevPlace CI / test (push) Failing after 3m13s

This commit is contained in:
2026-10-16 07:20:44 +02:00
parent 25d9cb1c7d
commit 6f80a820da
229 changed files with 23894 additions and 74 deletions
+260
View File
@@ -154,5 +154,265 @@ page. Every endpoint follows the shared [Conventions & Errors](/docs/conventions
"`403` with a `reason` when the device is suspended or its owner no longer passes the consent gate.",
],
),
endpoint(
id="nodes-me",
method="GET",
path="/nodes/me",
title="Your donation status",
summary="Whether you may donate right now (and the reason when not), your settings, your devices and what you have donated.",
auth="user",
interactive=True,
sample_response={
"consent": True,
"can_donate": True,
"reason": "",
"message": "",
"settings": {"enabled": True, "bandwidth_mb_day": 500, "max_concurrent": 2},
"platforms": ["reddit"],
"devices": [DEVICE_SAMPLE],
"max_devices": 5,
"donated": {"lifetime_requests": 12, "lifetime_bytes": 482113, "today_requests": 3},
"limits": {"max_donor_mb_per_day": 20480, "max_concurrent_per_node": 2, "default_donor_mb_per_day": 500},
},
),
endpoint(
id="nodes-settings",
method="POST",
path="/nodes/settings",
title="Change your donation settings",
summary="Update bandwidth, concurrency, active hours, platforms and display. Unsent fields keep their value.",
auth="user",
ajax=True,
encoding="form",
params=[
field("enabled", "form", "boolean", False, "true", "Switch donation on or off. Switching on needs the `bandwidth_sharing` consent and is refused when sent by the Devii assistant."),
field("bandwidth_mb_day", "form", "integer", False, "500", "Daily bandwidth cap in megabytes."),
field("max_concurrent", "form", "integer", False, "2", "Simultaneous requests."),
field("active_from", "form", "string", False, "08:00", "Start of the active window, HH:MM."),
field("active_to", "form", "string", False, "22:00", "End of the active window, HH:MM."),
field("timezone", "form", "string", False, "Europe/Amsterdam", "IANA timezone of the window."),
field("allowed_platforms", "form", "string", False, "reddit", "Comma separated platforms you are willing to serve."),
field("pause_on_battery", "form", "boolean", False, "true", "Stop donating on battery power."),
field("public_display", "form", "boolean", False, "false", "Show your name on the donor leaderboard."),
],
sample_response={"consent": True, "can_donate": True, "settings": {"enabled": True}},
notes=["`403` with `reason: no_consent` when switching on without the consent."],
),
endpoint(
id="nodes-stats",
method="GET",
path="/nodes/stats",
title="Community donation totals",
summary="Aggregate numbers only: requests relayed, bytes relayed and number of donors. No per-member data.",
auth="public",
interactive=True,
sample_response={"requests": 1200, "bytes": 90311234, "donors": 7},
),
endpoint(
id="nodes-leaderboard",
method="GET",
path="/nodes/leaderboard",
title="Donor leaderboard",
summary="The top donors who chose to be shown. Members are not listed unless they switched on public display.",
auth="public",
interactive=True,
sample_response={"donors": [{"username": "alice", "requests": 400, "bytes": 31200000}]},
),
endpoint(
id="nodes-social-search",
method="GET",
path="/nodes/social/search",
title="Social search",
summary="Search public developer discussion on Hacker News, Stack Overflow, GitHub, dev.to, Mastodon and, when donors are online, Reddit.",
auth="user",
interactive=True,
params=[
field("q", "query", "string", True, "sqlite wal", "What to search for, up to 200 characters."),
field("platforms", "query", "string", False, "hackernews,github", "Comma separated subset. Empty means all."),
field("limit", "query", "integer", False, "8", "Hits per platform, 1 to 20."),
],
sample_response={
"query": "sqlite wal",
"platforms": ["hackernews", "reddit"],
"hits": [
{
"platform": "hackernews",
"title": "SQLite WAL mode explained",
"url": "https://news.ycombinator.com/item?id=1",
"snippet": "",
"author": "someone",
"score": 120,
"comments": 33,
"created_at": "2026-01-01T00:00:00Z",
"via": "direct",
}
],
"unavailable": {"reddit": "no_nodes"},
},
notes=[
"Platforms that could not answer are listed under `unavailable` with a reason code; results are never invented for them.",
"Counted against a daily per-member quota (`429` when reached). Bluesky and Lobsters are not covered because they block unauthenticated server access.",
],
),
endpoint(
id="nodes-internal-fetch",
method="POST",
path="/nodes/internal/fetch",
title="Fetch a page through the donor network",
summary="Fetch one page from an allowlisted site through a donor node. Meant for agents and services that hold an API key.",
auth="user",
encoding="form",
params=[
field("url", "form", "string", True, "https://www.reddit.com/r/programming/.json", "Absolute URL on an allowlisted host."),
field("cache", "form", "boolean", False, "true", "Allow a cached copy."),
field("timeout", "form", "integer", False, "15", "Seconds to wait, 1 to 60."),
],
sample_response={
"url": "https://www.reddit.com/r/programming/.json",
"status": 200,
"via": "fetch",
"truncated": False,
"elapsed_ms": 812,
"content_type": "application/json",
"text": "{...}",
},
notes=[
"`403` with `reason: host_not_allowed` for any host that is not on the allowlist, `503` with `reason: no_nodes` when no donor is online, `429` when the daily node quota is reached.",
"`via` says how the body arrived: `fetch` (relayed by a donor browser, treat as unverified) or `cache`.",
],
),
endpoint(
id="nodes-client-version",
method="GET",
path="/nodes/client-version",
title="Client versions and checksums",
summary="The protocol versions this server speaks and every downloadable client with its SHA-256.",
auth="public",
interactive=True,
sample_response={
"version": "0.1.0",
"protocol": 1,
"min_protocol": 1,
"artifacts": [{"name": "devplace-node-linux-amd64", "size": 6291456, "sha256": "ab12..."}],
},
),
endpoint(
id="nodes-download",
method="GET",
path="/nodes/download/{artifact}",
title="Download a client",
summary="Download one client artifact listed by the client version endpoint. The SHA-256 is also sent as `X-Content-SHA256`.",
auth="public",
params=[
field("artifact", "path", "string", True, "devplace-node-linux-amd64", "Artifact name."),
],
sample_response="(binary file)",
),
endpoint(
id="admin-nodes",
method="GET",
path="/admin/nodes",
title="Node network overview",
summary="Pool, queue, device breakdowns, today's totals, open flags, recent devices, settings and the kill switch.",
auth="admin",
interactive=True,
sample_response={
"enabled": True,
"pool": {"online": True, "connected": 12, "capacity": 2000, "accepted": 40, "kicked": 3, "jobs_done": 310, "jobs_failed": 4, "open_circuits": 0, "heartbeat": "2026-01-01T00:00:00+00:00"},
"queue": {"depth": 2, "max_depth": 500, "queued": 1, "assigned": 1, "done": 30, "failed": 1, "expired": 0},
"devices_by_status": {"active": 14, "suspended": 1},
"devices_by_kind": {"native": 10, "extension": 5},
"devices_by_asn_class": {"unknown": 15},
"devices_by_country": {"unknown": 15},
"usage_today": {"requests_ok": 120, "requests_failed": 3, "bytes_up": 40000, "bytes_down": 9000000, "cache_hits_served": 0, "integrity_failures": 0},
"flags": [{"uid": "FLAG_UID", "kind": "refusal_mismatch", "detail": "", "status": "open", "device_uid": "DEVICE_UID", "device_label": "Home laptop", "user_uid": "USER_UID", "owner_username": "alice", "created_at": "2026-01-01T00:00:00+00:00"}],
"devices": [dict(DEVICE_SAMPLE, owner_uid="USER_UID", owner_username="alice", suspend_reason="")],
"settings": [{"key": "nodes_max_total", "label": "Maximum connected nodes", "kind": "int", "value": "2000", "default": "2000", "ceiling": 2000}],
"limits": {"max_devices_per_user": 5, "max_connected_nodes": 2000},
},
notes=[
"Per-destination circuit state lives in the memory of the worker that holds the sockets and is not readable from other workers; only the number of open circuits is reported.",
"`usage_today` is the current UTC day. Lists are capped at 100 rows.",
],
),
endpoint(
id="admin-nodes-killswitch",
method="POST",
path="/admin/nodes/killswitch",
title="Switch community donation on or off",
summary="The global kill switch. Switching it off disconnects every node within one reconcile tick.",
auth="admin",
encoding="form",
destructive=True,
params=[
field("enabled", "form", "boolean", False, "false", "True to switch on, false to switch off. Omit to flip the current state."),
],
sample_response={"ok": True, "redirect": "/admin/nodes", "data": {"enabled": False}},
notes=["Audited as `node.killswitch`."],
),
endpoint(
id="admin-nodes-settings",
method="POST",
path="/admin/nodes/settings",
title="Update node network settings",
summary="Change the caps and quotas. Only the fields you send are changed; values above the ceiling are clamped to it.",
auth="admin",
encoding="form",
params=[
field("nodes_max_total", "form", "integer", False, "2000", "Maximum connected nodes, 0 to 2000."),
field("nodes_daily_quota_user", "form", "integer", False, "200", "Daily node requests per member, 0 to 10000."),
field("nodes_daily_quota_guest", "form", "integer", False, "0", "Daily node requests per guest, 0 to 1000."),
field("nodes_cache_ttl_seconds", "form", "integer", False, "600", "Result cache lifetime in seconds, 0 to 600."),
field("nodes_daily_social_user", "form", "integer", False, "300", "Daily social searches per member, 0 to 10000."),
field("nodes_social_via_nodes", "form", "string", False, "0", "`1` routes social search through nodes, `0` keeps it direct."),
],
sample_response={"ok": True, "redirect": "/admin/nodes", "data": {"changed": ["nodes_max_total"], "clamped": []}},
notes=["Audited as `node.admin.settings` with the old and new values."],
),
endpoint(
id="admin-nodes-suspend",
method="POST",
path="/admin/nodes/devices/{device_uid}/suspend",
title="Suspend a donor device",
summary="Suspend one device for 1 to 10080 minutes. The device is disconnected on the next tick and cannot reconnect until the period ends.",
auth="admin",
encoding="form",
destructive=True,
params=[
field("device_uid", "path", "string", True, "DEVICE_UID", "Device uid."),
field("minutes", "form", "integer", True, "60", "Suspension length in minutes, 1 to 10080."),
field("reason", "form", "string", False, "Repeated refusals", "Reason, up to 500 characters."),
],
sample_response={"ok": True, "redirect": "/admin/nodes", "data": {"uid": "DEVICE_UID", "minutes": 60}},
notes=["Refused (audited as `denied`) when the device owner is a more senior administrator. Audited as `node.suspend`."],
),
endpoint(
id="admin-nodes-unsuspend",
method="POST",
path="/admin/nodes/devices/{device_uid}/unsuspend",
title="Lift a device suspension",
summary="Return a suspended device to active.",
auth="admin",
encoding="form",
params=[
field("device_uid", "path", "string", True, "DEVICE_UID", "Device uid."),
],
sample_response={"ok": True, "redirect": "/admin/nodes", "data": {"uid": "DEVICE_UID"}},
notes=["`409` when the device is not suspended. Audited as `node.unsuspend`."],
),
endpoint(
id="admin-nodes-flag-resolve",
method="POST",
path="/admin/nodes/flags/{flag_uid}/resolve",
title="Resolve a node flag",
summary="Mark an open integrity or refusal flag as handled.",
auth="admin",
encoding="form",
params=[
field("flag_uid", "path", "string", True, "FLAG_UID", "Flag uid."),
],
sample_response={"ok": True, "redirect": "/admin/nodes", "data": {"uid": "FLAG_UID"}},
notes=["`409` when the flag is already resolved. Audited as `node.flag.resolve`."],
),
],
}