workout poke moves to tcp/8774: 8773 is book-guard's desktop wrapper, whose window the poke server had been answering since 2026-10-09 (phone app rebuilt, selftest default, contract doc)
Some checks failed
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Waiting to run
Tests / flutter-coverage (push) Blocked by required conditions
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
release-apk / release (APK) (push) Has been cancelled

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrBEzjPRdx5oyp8NtTLW5F
This commit is contained in:
Krzysztof Rudnicki 2026-10-10 09:38:21 +02:00
parent e1f1c668a8
commit b37b7559d8
9 changed files with 19 additions and 16 deletions

View File

@ -8,9 +8,10 @@ session that arrives both ways is credited once.
## Transport
- `POST http://<pc-host>:8773/v1/workout`, `Content-Type: application/json`.
- `POST http://<pc-host>:8774/v1/workout`, `Content-Type: application/json`.
(8773 until 2026-10-10: that is book-guard's desktop wrapper's port.)
- `<pc-host>` is a phone setting, default `192.168.1.43` (static on the PC).
LAN only: the PC firewall accepts tcp/8773 from `192.168.1.0/24` and nothing
LAN only: the PC firewall accepts tcp/8774 from `192.168.1.0/24` and nothing
else. No WireGuard. Off the home LAN the poke fails fast and the regular
sync is the only path.
- Header `X-Poke-Signature: <hex HMAC-SHA256(key, raw body bytes)>`.

View File

@ -1,7 +1,7 @@
"""The resident LAN listener that credits a phone workout within a second.
``workout-poke.service`` runs ``python3 -m screen_locker.poke_server``: a
stdlib ``ThreadingHTTPServer`` on ``0.0.0.0:8773`` (the firewall admits only
stdlib ``ThreadingHTTPServer`` on ``0.0.0.0:8774`` (the firewall admits only
192.168.1.0/24) answering ``POST /v1/workout`` per
``docs/DOCS-workout-poke-contract.md``. Every import -- the locker, the
earned-time registry, gatelock -- is paid once at startup, so a request costs

View File

@ -3,7 +3,7 @@
What ``workout-poke.service`` runs: load the shared key, warm the imports and
the budget sum, start the Firebase session stream (off-LAN credit,
:mod:`screen_locker._session_stream`) on a daemon thread, then serve ``POST
/v1/workout`` on port 8773 for good. The request handling lives in
/v1/workout`` on port 8774 for good. The request handling lives in
:mod:`screen_locker._poke_server`; the contract is
``docs/DOCS-workout-poke-contract.md``.
@ -29,7 +29,8 @@ __all__ = ["DEFAULT_KEY_FILE", "DEFAULT_PORT", "load_key", "main"]
_logger: Final = logging.getLogger(__name__)
DEFAULT_PORT: Final = 8773
DEFAULT_PORT: Final = 8774
"""Not 8773: that is book-guard's desktop wrapper (loopback, older)."""
DEFAULT_KEY_FILE: Final = Path.home() / ".config" / "workout_poke" / "key"
_KEYGEN_HINT: Final = "run scripts/workout_poke_keygen.sh"
@ -50,7 +51,7 @@ def load_key(path: Path) -> bytes:
def _parse_args(argv: list[str] | None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
# "" binds every interface; the firewall admits tcp/8773 from the LAN only.
# "" binds every interface; the firewall admits tcp/8774 from the LAN only.
parser.add_argument("--bind", default="", help="address (default: all)")
parser.add_argument("--port", type=int, default=DEFAULT_PORT)
parser.add_argument("--key-file", type=Path, default=DEFAULT_KEY_FILE)

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""Drive the running workout poke listener from this PC, signed with the real key.
Each case POSTs to ``http://127.0.0.1:8773/v1/workout`` exactly as the phone
Each case POSTs to ``http://127.0.0.1:8774/v1/workout`` exactly as the phone
would (``docs/DOCS-workout-poke-contract.md``) and prints the status and reply:
* ``sandbox`` -- a signed sandbox request: 200, ok, credited=false;
@ -109,7 +109,7 @@ def main() -> int:
"case", choices=["sandbox", "badsig", "replay", "stale", "duplicate", "latency"]
)
parser.add_argument("--count", type=int, default=20)
parser.add_argument("--port", type=int, default=8773)
parser.add_argument("--port", type=int, default=8774)
args = parser.parse_args()
key = bytes.fromhex(KEY_FILE.read_text(encoding="ascii").strip())
if args.case == "sandbox":

View File

@ -15,7 +15,7 @@ import 'package:workout_app/services/pc_poke_wire.dart';
/// guard per service. A refused connection surfaces as a normal sync failure
/// in the UI, which is the honest answer: the sandbox is offline by design.
///
/// The single exception is `http://<pokeHost>:8773/v1/workout`, so a sandbox
/// The single exception is `http://<pokeHost>:8774/v1/workout`, so a sandbox
/// workout can time the PC round trip. The PC verifies the signed
/// `"sandbox": true` and never writes or credits it. Firebase and GitHub are
/// https on 443 and can never match.

View File

@ -19,7 +19,7 @@ import 'package:workout_app/services/workout_sync_service.dart';
export 'package:workout_app/services/pc_poke_result.dart';
/// Signs and POSTs one session to `http://<pc-host>:8773/v1/workout`.
/// Signs and POSTs one session to `http://<pc-host>:8774/v1/workout`.
///
/// Runs beside [WorkoutSyncService.push], never instead of it: the PC dedups
/// by record id, so whichever path lands first credits and the other is a

View File

@ -11,8 +11,9 @@ import 'dart:typed_data';
import 'package:crypto/crypto.dart';
/// TCP port the PC's poke listener binds (8771 belongs to another service).
const int kPcPokePort = 8773;
/// TCP port the PC's poke listener binds (8771 and 8773 -- book-guard's
/// desktop wrapper -- belong to other services).
const int kPcPokePort = 8774;
/// Request path on the PC.
const String kPcPokePath = '/v1/workout';

View File

@ -36,7 +36,7 @@ void main() {
expect(SandboxHttpOverrides.allows(poke), isFalse);
});
test('allows exactly http + configured host + 8773 + /v1/workout', () {
test('allows exactly http + configured host + 8774 + /v1/workout', () {
SandboxHttpOverrides.pokeHost = host;
expect(SandboxHttpOverrides.allows(poke), isTrue);
for (final other in [
@ -87,12 +87,12 @@ void main() {
} on SocketException catch (error) {
outcome = error;
}
// No listener on this host's 8773 is fine -- what matters is that a
// No listener on this host's poke port is fine -- what matters is that a
// refusal, if any, did not come from the sandbox.
if (outcome is SocketException) {
expect(outcome.message, isNot(contains('sandbox')));
}
expect(printed.first, contains('allowing the PC poke to $host:8773'));
expect(printed.first, contains('allowing the PC poke to $host:$kPcPokePort'));
client.close(force: true);
});
});

View File

@ -1,5 +1,5 @@
[Unit]
Description=Credit a phone workout the second it is poked over the LAN (tcp/8773)
Description=Credit a phone workout the second it is poked over the LAN (tcp/8774)
[Service]
# Resident stdlib HTTP server (screen_locker/_poke_server.py): the phone POSTs