Skip to content

Releases: vakaobr/atalaia-project

v0.7.0 (pre-alpha)

v0.7.0 (pre-alpha) Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 03 Nov 22:00
bac4864

Pre-alpha. Not field-tested. This is a pre-release for builders and testers. The software is
tested in simulation and on a bench with real Heltec V4.3 boards, not in a real game. Do not rely on it
for a game that matters, and expect things to change.

Which file? Field Box (also the Satellite), USB radio modem and I/O pod are separate images (atalaia-field-box-,
atalaia-modem-, atalaia-io-pod-). Each comes as -usb.zip (flash it with esptool over USB, a README inside says
how) and -merged.bin (one file for a browser flasher, written at address 0x0). The Field Box also has the plain
.bin with its signed .release.json, what a controller or a box checks for an update over the air (the modem and
the pod are not updated that way). Check what you downloaded against SHA256SUMS.

USB flash required once when you come from v0.4.0 or earlier: the Field Box, modem and pod images of this release have a new partition table (4 MB OTA slots instead of 2 MB), and an update over the air cannot change the partition table. Flash the -usb.zip over USB once (write, no erase: the settings and the adoption stay; the cached game modes and the field's logo are fetched again). A box still on the old table refuses to run the new firmware and shows "Flash over USB" on its screen; one that gets it over the air from an older firmware goes back to the old one by itself. Later updates over the air work as before.

Signed with a CI release key. The firmware trusts the project's release keys: root 424d053af5cdbc35
(held offline by the maintainer), CI 32b3c815ac30ad43 and the next CI key a4f6002b0122b131, which
takes over from it. The .release.json names the key that signed this release (key_id). A controller
whose [firmware] release_keys lists keys by hand needs the next CI key,
76eY5BoMQ4RHohhX/8iSUlIUA5o5Entfk7EFBUxpbSY=, before the releases it signs (Release signing).

Guides: Firmware guide (download, flash, build it yourself) ·
Release signing ·
Controller setup ·
Set up the radio modem

What is in this release

Features

  • controller: box modules, the capability start check and the GM's outputs on both controllers and the console (#47) (a652cf2)
  • core: scenario triggers switch the boxes' outputs; Triggers blocks in the console (#49) (6010dbe)
  • firmware: an optional passive piezo buzzer; the speaker plays the game's sounds again (#40) (c234ca6)
  • firmware: the simple output and the box's output layer (#45) (39e08d6)
  • modes: Bomb's smoke and siren, the master's siren, and a role's outputs (#50) (a14a5e8)
  • protocol: the expansion port's protocol, vectors and capability type ids (#41) (3f30ef5)
  • release: trust the next CI release key a4f6002b0122b131, sign with it behind a switch (#38) (ee56036)

Bug Fixes

  • ci: link box_host's mbedtls and mbedx509 statically for the app job (#48) (bba5f69)
  • console: forget a saved access code the controller now refuses (#52) (1741a21)
  • preview: the live demo starts again and shows the players' positions (#43) (9030fe6)

Documentation

  • phase 5 notes the HTTPS host-box fetch and the registry keys as done (#34) (dee8722)
  • point the guides to v0.6.1 and describe the release gate as configured (#31) (8e3ded9)
  • RFC draft for the expansion port and the single-GPIO simple output (target addon, relays, sirens) (#36) (c0962a9)
  • the expansion port's decisions EXP-1 to EXP-38 in the decisions log (#42) (c8a3637)
  • the maintainer's review of the open P5F/P5J rows, httpx2 and the release key rotation (#35) (16abd9f)

Details

  • Scenario triggers switch the boxes' outputs (increment 5 of the draft RFC "The expansion port") (decisions EXPI5-1 to EXPI5-15, following EXP-25, EXP-29 and EXP-30; core/scenario/manifest.lua, validator.lua, luagen.lua, runtime.lua, controller/atalaia_controller/engine.py, app/lib/controller/controller_engine.dart, controller/console/src/components/scenario-blocks.js, the console's four languages, protocol/vectors/scenario.json, protocol/vectors/scenario_compile.json, tools/scenario_compile_vectors.py): a scenario manifest's new optional triggers: list switches an output of a box when every objective of a list is completed (completed), the first of them is (any_completed), one fails (failed), or the game starts or ends (game: start, game: end): output: { node, capability, act, seconds } with the act on, pulse, pattern (with on_ms and off_ms, 500-10000) or off, the box one of the scenario's nodes, the function siren, strobe, smoke, light, horn (siren.2), out.1-out.9 or * with off, and 1-600 s (10 s at most for game: end, the box's end grace). The validator says what is wrong in the style of the other rules (a bare on or off, which YAML 1.1 reads as a boolean, asks for quotes); the compiler writes SCENARIO.triggers into field.lua only when there are triggers; the runtime fires each trigger once a game as CMD 12 with src: field (the same path as field.send(node, "output", ...) and the controller's one start a box every 2 s), tries again every second for 10 s what that limit holds back, drops what waits on a pause and does not sound the start again after a restart mid-game. Both controllers now hold back the outputs a field script switches after its own game.end (an objective that wins the game) until the end's CMD has gone, so they come within the boxes' 10 s grace instead of being switched off by the end. The GM console's scenario blocks gain a Triggers category ("when" blocks holding an output block) in pt, en, de and zh-TW, with the YAML round trip (what the blocks cannot hold as it is stays in the YAML view). Tests: the shared vectors (C core, Python and the phone's Dart), a scenario game through the Pi's engine (controller/tests/test_scenario.py), the phone's end-of-game hold (app/test/controller/controller_engine_test.dart) and the blocks in Chromium. Not tried on hardware.
  • The GM console forgets a saved code the controller now refuses (controller/console/src/app.js): reopened with a code kept in the browser that has since been revoked or has expired, the console said "Wrong access code" and kept the code, sending it again at every start. It now forgets it and says the code is no longer accepted, as when a live stream is closed for it; a code just typed and refused still says it is wrong, and a controller that does not answer keeps the code. Service worker cache v19.
  • CI's app job runs the host box again (.github/workflows/ci.yml, tools/docker_app.py): since the host box's HTTPS mode fetch (#32) box_host also links libmbedtls and libmbedx509, but the app job linked only mbedcrypto statically, so in the Flutter image (which has no mbedtls) the host box did not start and every phone-controller test that uses it timed out; main's CI app job had failed on every commit since. All three are now linked statically.
  • The live demo starts again (server/preview/demo/Dockerfile): since box_host fetches modes over HTTPS (#32) it links libmbedtls and libmbedx509, which the demo image's runtime stage did not install, so its simulated boxes did not start and the demo restarted i...
Read more

v0.6.1 (pre-alpha)

v0.6.1 (pre-alpha) Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 02 Nov 21:12
e848e1a

Pre-alpha. Not field-tested. This is a pre-release for builders and testers. The software is
tested in simulation and on a bench with real Heltec V4.3 boards, not in a real game. Do not rely on it
for a game that matters, and expect things to change.

Which file? Field Box (also the Satellite), USB radio modem and I/O pod are separate images (atalaia-field-box-,
atalaia-modem-, atalaia-io-pod-). Each comes as -usb.zip (flash it with esptool over USB, a README inside says
how) and -merged.bin (one file for a browser flasher, written at address 0x0). The Field Box also has the plain
.bin with its signed .release.json, what a controller or a box checks for an update over the air (the modem and
the pod are not updated that way). Check what you downloaded against SHA256SUMS.

USB flash required once when you come from v0.4.0 or earlier: the Field Box, modem and pod images of this release have a new partition table (4 MB OTA slots instead of 2 MB), and an update over the air cannot change the partition table. Flash the -usb.zip over USB once (write, no erase: the settings and the adoption stay; the cached game modes and the field's logo are fetched again). A box still on the old table refuses to run the new firmware and shows "Flash over USB" on its screen; one that gets it over the air from an older firmware goes back to the old one by itself. Later updates over the air work as before.

Signed with two release keys: root 424d053af5cdbc35 (held offline by the maintainer) and CI
32b3c815ac30ad43 (this release is signed with the CI key; both are built into the firmware).

Guides: Firmware guide (download, flash, build it yourself) ·
Release signing ·
Controller setup ·
Set up the radio modem

What is in this release

Bug Fixes

  • controller: a scan ending as the region changes or the controller stops no longer writes after it (#29) (4813977)
  • controller: close a dropped stream's socket and wake every live-feed client; test the pinned lock in CI (#28) (6ecf830)
  • release: the I/O pod's package passes the boot-splash check (#26) (d82d481)

Details

  • The field controller's tests run on the hash-pinned lock too, in a new CI job (decisions PLK-1 to PLK-6; .github/workflows/ci.yml job controller-pinned, controller/tests/test_scenario_api.py, controller/atalaia_controller/ws_client.py, controller/tests/test_remote_gm.py, controller/atalaia_controller/ws.py, controller/tests/test_api.py, controller/README.md, controller/requirements.txt header): the controller installs from Ubuntu's packages (FastAPI 0.101, pydantic 1.10) or with pip from controller/requirements.txt (FastAPI 0.143, Starlette 1.8, pydantic 2.14, httpx 0.28; decision D15), but its tests had only ever run on the first. On the lock, 1 of 1132 tests failed: test_check_reads_surrogate_pairs_and_refuses_half_of_one let the test client encode a lone surrogate, and httpx 0.28 writes JSON as UTF-8 itself and refuses one before the request leaves, so the controller was never reached. The test now makes the request bodies itself and checks the controller's answer (bad_manifest) to half of a surrogate pair sent three ways a client can: as an escape inside the manifest, as JSON's own \udf09 escape, and as the raw bytes ED BC 89 (not UTF-8; Python's JSON reader takes them as the surrogate on both stacks). No bug specific to the newer stack was found, and it shows no deprecation warning from FastAPI or pydantic; Starlette 1.x asks for httpx2 in its test client (left: the lock keeps httpx). A ResourceWarning seen on both stacks was a real leak, fixed: the remote GM's WebSocket client (controller/atalaia_controller/ws_client.py) marked a link that was already down (the site server closed it, a send or a receive failed) as closed, and close() then returned without closing the socket, so every reconnect after such a drop left one socket to the garbage collector; it now always closes the socket (still sending the close frame only on a live link), with a test, and the tests' fake site server closes its accepted sockets too. One run on the lock hung in test_map_features' stream test, a race in the live feed's hub (controller/atalaia_controller/ws.py) on both stacks: it filled every client's queue on the event loop of the latest connection, but FastAPI's TestClient runs each WebSocket on a loop of its own, and an asyncio.Queue filled from another loop's thread can leave its reader asleep for good; the hub now fills each queue on its own loop (one loop on a real server, so nothing changes there), with a test that failed before (HubLoopTests), and the new job stops after 30 minutes. The new job (ubuntu-24.04, actions/setup-python pinned by SHA, Python 3.12, pip install --require-hashes --no-deps --only-binary :all:, the core and the host modem, box and pod built as in the controller job, read-only permissions) runs the same tests with every DeprecationWarning an error.
  • The I/O pod's release package no longer fails the boot-splash check (tools/release_package.py): the check (LOGO-7) wanted exactly one splash_logo.c in every build, but the I/O pod has no screen and its build embeds none, so the v0.6.0 release run stopped on the io-pod package and published nothing. A build with CONFIG_ATALAIA_POD_MODE=y now passes when it embeds no splash, and is refused if it does.
  • A noise scan that ends as the field's region changes or the controller stops no longer writes after it (controller/atalaia_controller/engine.py): a scan reads "done" a moment before it reports, and in that moment the region could change (the scan no longer counted as running) and drop it without waiting for its thread. Its late report then saved the old region's result as the last scan, sent an EU868 modem the new region's channel, and after stop() wrote to the closed database (scan listener failed ... Cannot operate on a closed database). Only the current scan's reports count now (a new scan likewise ignores the one before), and stop() waits for a dropped scan's thread too.

v0.5.0 (pre-alpha)

v0.5.0 (pre-alpha) Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 29 Oct 09:27

Pre-alpha. Not field-tested. This is a pre-release for builders and testers. The software is
tested in simulation and on a bench with real Heltec V4.3 boards, not in a real game. Do not rely on it
for a game that matters, and expect things to change.

Which file? Field Box (also the Satellite), USB radio modem and I/O pod are separate images (atalaia-field-box-,
atalaia-modem-, atalaia-io-pod-). Each comes as -usb.zip (flash it with esptool over USB, a README inside says
how) and -merged.bin (one file for a browser flasher, written at address 0x0). The Field Box also has the plain
.bin with its signed .release.json, what a controller or a box checks for an update over the air (the modem and
the pod are not updated that way). Check what you downloaded against SHA256SUMS.

USB flash required once when you come from v0.4.0 or earlier: the Field Box, modem and pod images of this release have a new partition table (4 MB OTA slots instead of 2 MB), and an update over the air cannot change the partition table. Flash the -usb.zip over USB once (write, no erase: the settings and the adoption stay; the cached game modes and the field's logo are fetched again). A box still on the old table refuses to run the new firmware and shows "Flash over USB" on its screen; one that gets it over the air from an older firmware goes back to the old one by itself. Later updates over the air work as before.

Signed with two release keys: root 424d053af5cdbc35 (held offline by the maintainer) and CI
32b3c815ac30ad43 (this release is signed with the CI key; both are built into the firmware).

Guides: Firmware guide (download, flash, build it yourself) ·
Release signing ·
Controller setup

What is in this release

A pre-alpha, not field-tested pre-release. USB flash required once: the Field Box (and the Satellite), the modem and the I/O pod get a new partition table with 4 MB app slots, which an update over the air cannot change; flash this release over USB once, without erasing, and the box keeps its adoption, keys and settings (firmware guide, section 3.3). Also new: the Sabotage game mode, box penalties and a projected score, I/O pod settings over the radio, data retention per field, the published privacy notice with consent asked again, chrono sharing across a hosted hub's fields, a database role per field in hosted mode, and a read-only live demo.

Changed

  • The app: a marshal's phone no longer keeps the field join key, CA-pinned scoreboard links are remembered, and buzzes reach a closed app on Android (decisions APX-1 to APX-3; app/). (1) The join key stays only where it is used, by a prop (its join card) and by the phone controller; AppSettings.joinKeyUse records which, a settings migration (version 2) clears it on an upgrade for a phone that is a marshal and never ran the controller, and confirming a marshal clears it unless the controller's. (2) A scoreboard link with &ca= is kept per field by the CA's fingerprint (scoreboard_links, read code sealed) and offered under Campos guardados; it is forgotten when the CA changes. (3) While the player view is open on a live game, an Android foreground service with an ongoing notification (four languages) keeps the app's engine running, so the same event buzzes play with the app closed or the screen off; it stops when the game ends or the player leaves; no Play services; iOS is not covered. Manuals (player, marshal, en and pt) and app/README.md updated.

Added

  • Site server, hosted mode: a database role per field, so a SQL injection can no longer switch fields by setting atalaia.tenant (decision S21-13, decisions S2113-1 to S2113-8; tenant_roles.py, tenancy.py, hosted.py, migration 0023_tenant_roles, hosted-roles.sh, restore.sh, server/README.md, docs/DEPLOY.md, ADR-P4-4's amendment of 2026-10-28): the row-level security keyed on a setting the server's own role could set. Now every field has a database role atalaia_t_<field id> (no login, no superuser, no BYPASSRLS, owns nothing) that alone holds the grants on the field's schema, tables and sequences and on its rows of the hub's sharing tables; the server's login role holds none (it reads the registry, adds to the quota inbox and SET ROLEs to a field's role, WITH INHERIT FALSE), and every request's transaction starts with SET LOCAL ROLE to its field's. A statement of field A that sets atalaia.tenant to B is refused B's schema outright, for reading and writing. pg_catalog.set_config is revoked from PUBLIC (hosted-roles.sh, as the superuser) so a field's role cannot switch role or setting from inside a statement; the server refuses to start while any role can call it (and asyncpg's type introspection is told not to). The hub's chrono sharing keeps working across fields: its policy and functions read the caller's field from the role in use (atalaia_caller_tenant()), not from the setting. hosted create/migrate make and grant the roles (the owner role now needs CREATEROLE; hosted-roles.sh gives it), migration 0023_tenant_roles makes the role of every field a hub already has (and does nothing in a single-field server), hosted delete drops it, hosted check verifies the roles, the grants and the set_config restriction, and a hub's restore uses --no-acl and hosted migrate for the grants (roles belong to the cluster, not to the backup). Upgrading a hub: run hosted-roles.sh, then the hub's hosted migrate, in one maintenance window (DEPLOY.md). Not closed, and said so: a second statement that is a SET ROLE command would still switch fields (PostgreSQL authorises it by the session user); the server's driver sends one statement per call, and a login role and pool per field would close it for good. Single-field mode is unchanged. New tests in test_tenants.py, test_hub_share.py and test_migrations.py.
  • The privacy notice is published, and accounts are asked to consent again when it changes (decisions CONS-1 to CONS-6, S17-16; server/atalaia_server/privacy_notice.py, api/privacy.py, web/privacy_pages.py, static/staff/consent.js): the operator puts one Markdown file per language and version in a folder (ATALAIA_PRIVACY_NOTICE_DIR, server/privacy-notice/ in the Compose deployment, files privacy-notice-<lang>-<version>.md, filled in from docs/templates/privacy-notice-*.md) and the server serves the files of the current ATALAIA_PRIVACY_NOTICE_VERSION as a public page (/privacy, pt-PT, English, German, Traditional Chinese, the text rendered by a small escaping Markdown renderer) and as JSON (GET /api/v1/privacy-notice?lang=: version, lang, available, markdown, html, url; another language of the version when the one asked for has no file, 404 privacy_notice_not_published when there is none). GET /api/v1/me gains consent (status current, outdated or missing, the version given, the current version, whether a notice is published); POST /api/v1/me/consents/account with {notice_version} consents again (the live consent for the older version ends, a new one is recorded, users.gdpr_consent_at moves, consent_renewed is audited) and answers 409 notice_version_changed with current_version when the version shown is not the current one; POST /api/v1/auth/register takes an optional notice_version and answers the same 409. The staff pages show a panel at the top after login (read the notice, tick, confirm) in the four languages; nothing is blocked, so the account's own export and erasure stay available. The text of the notice stays the operator's: this is only the mechanism. Tests: tests.test_privacy_notice.

Removed

  • users.data_retention_days (decision CONS-3; migration 0020_drop_user_retention): an unused per-account override that was only exported and set to null on erasure. GDPR asks for a defined period per kind of data, which the daily purge applies; a field per account promised what the code did not do. The access answer and the export's profile.json no longer have it.

Added

  • A box reports the points its teams lost to penalties, so the totals subtract them (decision BSC-1, closing E6; RFC 0001 Amendment 18; core msg_codec, firmware/esp32/main/box_game.c, both controllers, app/lib/radio/rf_codec.dart, protocol/schema/messages.yaml, protocol/vectors/state.json). STATE gets an optional key 9, lost: a per-team map of the points lost to penalties on that node, a grow-only counter merged by maximum like the gains and sent only when a team lost something (a game with no penalty sends the same STATE as before). The controller's total per team is the gains minus the boxes' penalties plus the GM's adjustments, so Kill Confirmed's penalty now lowers the team's total on the controller, the console, the scoreboards, MQTT and the app (the station's own screen already showed it net). The node's lost is in GET /api/v1/nodes and kept across a controller restart. An older box sends no key (nothing changes for it); an older Pi controller skips it; an older Android controller refuses a STATE that has it: update the app first. The Android prop link does not carry it yet.
  • A projected score between STATEs, for modes that opt in (decisions SL-6 and BSC-2; mode.yaml scoring, simulator/atalaia_sim/mode.py, both controllers; modes/README.md "Projected score"). A mode that scores by holding says how (scoring: {hold: {every: tick_s, points: points_per_tick}}, in Domination) and the controller adds the ticks a held box has made since its last STATE to the total it shows: projected on GET /api/v1/games/{id}/score and the stream's `sc...
Read more

v0.4.0 (pre-alpha)

v0.4.0 (pre-alpha) Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 28 Oct 15:11

Pre-alpha. Not field-tested. This is a pre-release for builders and testers. The software is
tested in simulation and on a bench with real Heltec V4.3 boards, not in a real game. Do not rely on it
for a game that matters, and expect things to change.

Which file? Field Box (also the Satellite), USB radio modem and I/O pod are separate images (atalaia-field-box-,
atalaia-modem-, atalaia-io-pod-). Each comes as -usb.zip (flash it with esptool over USB, a README inside says
how) and -merged.bin (one file for a browser flasher, written at address 0x0). The Field Box also has the plain
.bin with its signed .release.json, what a controller or a box checks for an update over the air (the modem and
the pod are not updated that way). Check what you downloaded against SHA256SUMS.

Signed with two release keys: root 424d053af5cdbc35 (held offline by the maintainer) and CI
32b3c815ac30ad43 (this release is signed with the CI key; both are built into the firmware).

Guides: Firmware guide (download, flash, build it yourself) ·
Release signing ·
Controller setup

What is in this release

The first release published on GitHub, a pre-alpha, not field-tested pre-release: firmware images for the Field Box (and the Satellite, the same image), the controller's USB radio modem and the I/O pod, as a USB flash bundle, a single merged image for browser flashers, and the app image with its signed .release.json, built and signed by the release workflow (.github/workflows/release.yml) with the CI release key. Building the firmware yourself still works and is still documented (docs/manuals/en/firmware.md).

Added

  • Firmware releases on GitHub, signed with two release keys (the maintainer's decision of 2026-10-28, decisions REL-1 to REL-8): a pushed tag v* builds the Field Box, modem and pod images with the project's two mode keys and two release keys as defaults and no bench options, and creates a GitHub pre-release with, for each image, atalaia-<variant>-<version>-usb.zip (bootloader, partition table, app, flash_args, README), atalaia-<variant>-<version>-merged.bin (for browser flashers, at offset 0), atalaia-<variant>-<version>.bin with atalaia-<variant>-<version>.bin.release.json (what a controller or a box checks, tools/firmware_release.py), and SHA256SUMS. The root release key (424d053af5cdbc35) is held offline by the maintainer; the CI release key (32b3c815ac30ad43) is a repository secret and signs the releases. Both are the defaults of CONFIG_ATALAIA_OTA_RELEASE_KEYS (and tools/docker_fw.py --release-keys), the Pi controller's and the phone controller's [firmware] release_keys. firmware/esp32/docs/RELEASE_SIGNING.md has what to do if the CI key leaks; docs/community/keys.md lists the keys.
  • The hardware build manuals are brought up to date, with a firmware guide for builders and a controller setup guide (documentation only; English, Portuguese follows): docs/manuals/en/firmware.md (the four images, how to get one today, flashing with esptool on Windows, macOS and Linux with write_flash "@flash_args" at 921600 baud and the --after hard_reset chip_id restart, the build and file options, first boot and the setup page, adoption by code, updates over the air, recovery, the modem board and the I/O pod) and docs/manuals/en/controller-setup.md (parts, the modem board, install, first start, the console address, backups for a Raspberry Pi controller). hardware/field-box/BUILD.md gains a pin reference (signal, GPIO, firmware option, wire colour, destination, voltage; section 15), checked against Kconfig by tools/check_wiring.py; the Satellite's and the pod's pin tables gain the same columns; the target addon page gains its port's pin reference. Fixed as stale against the firmware: the settings file of a Field Box build is firmware/esp32/build/sdkconfig, not firmware/esp32/sdkconfig; the flash baud is 921600; a PCF8574A is found by the firmware (the diagram said "not PCF8574A", re-rendered); the LED ring starts at 40 % brightness; the first screens of a new box. Things not yet tried on a real board are marked as such.
  • The project's mode signing keys (the maintainer's choice of 2026-10-28): an offline root key (d9521a79030ce544, held by the maintainer) and a CI key (7275108d2e3fe583, the secrets of the registry repository, used by its new Verified badge workflow, which a maintainer starts by hand and which opens a pull request). Both are in the registry's keys/project/ and are now the defaults of the Field Box firmware's CONFIG_ATALAIA_MODE_KEYS (and tools/docker_fw.py --mode-keys), the Android app's built-in mode keys (props) and the controllers' [modes] box_keys (Pi and phone); a field still trusts them for its own installs only by listing them in [modes] keys. docs/community/keys.md lists them, why the CI key is a secret, and what to do if it is exposed.
  • A staff page for a field's settings (decisions FS-1 to FS-6; server/). /staff/fields/<field>/settings (the field's admin, linked from the field's sections) sets what only PATCH /api/v1/fields/{field_id} reached: the public page, the check-in rules, the membership rule (accepted list or any, register, check-in, expiry, the country's shipped list and the field's own entries), chrono sharing and the country. The membership words (APD or association) follow the country and change on the page, with the offered list, when a new country is saved; every refusal has words in pt-PT, English, German and Traditional Chinese; the page tells that the controllers get the change at once. No API change; GM manual (en, pt) and the server README say where to set them.
  • APD membership becomes association membership, with the field's country (the maintainer's decision of 2026-10-28; package H, decisions MEM-1 to MEM-6; migration 0019_field_country; protocol/apd.md, protocol/vectors/apd.json and sync.json, registry/apds/): players in other countries may need a membership too (a club, a federation, an insurance body), just not an "APD". Each entry of the shipped list (registry/apds/apds.json, schema and file keep their historical names) has a country (ISO 3166-1 alpha-2; the three shipped are PT). A field has a country: on the site server (fields.country, PATCH /api/v1/fields/{id} country, in the field's JSON, fields-by-slug, the registration list and settings.country of GET /api/v1/sync/field?settings=1, additive; GET /api/v1/apds?country=) and on both controllers ([checkin] country, the console's settings, the country check-in setting of PUT /api/v1/checkin/settings, the phone controller's; 409 when the site's applies with [sync] branding = "site"); the default is PT where the time zone is Europe/Lisbon, Atlantic/Azores or Atlantic/Madeira (the existing fields too, by the migration), otherwise unset. Wording by country: a PT field keeps the "APD" texts; any other country or none says "association membership" / "membership number" in pt-PT (gender-neutral), English, German and Traditional Chinese, in the app (registration, check-in, marshal), the GM console, the phone controller, the site server's staff pages and the public game page. Which list a field offers: the entries of its country plus its own custom ones (none from the shipped list without a country); the accepted-list rule is unchanged. Wire names, verdict codes and error codes stay (apd_id, apd, apd_status, ...: historical, documented in protocol/apd.md); a client treats an answer with no country key as PT. Shared vectors gained country_valid, country_invalid, default_country, wording and choices (a non-PT field) and settings.country cases, run by the server, the Pi controller, the phone controller and the app. Tests: server/tests/test_apd_membership.py (CountryTests), test_roles.py, test_migrations.py, the staff browser test, controller/tests/test_apd.py, the console's browser tests, app/test/protocol/apd_test.dart, app/test/controller/controller_apd_test.dart, the check-in, site and marshal widget tests, registry/tests/test_apds.py. Manuals (game master, marshal, player; en, pt) say what changes outside Portugal.
  • Android props fetch signed mode packages from their controller, as Field Boxes do (the item P5B-17 and P5D-1 left for "a later PE24 slice"; decisions PKF-1 to PKF-8; app/lib/modes/prop_mode_fetch.dart, app/lib/prop/prop_setup.dart, app/lib/ui/settings_screen.dart): when a join names a mode or modifier the app does not carry, asks for a fetch (join key 16) and names the controller (keys 13 and 14), the prop downloads each missing part over HTTPS pinned to the field CA, checks it with the box's own checks (boxCheckRelease and boxCheckArchive, the shared box_checks vectors: the release's exact form, the Ed25519 signature against the prop's mode keys, kind, tier, never a built-in id, size, SHA-256, the canonical archive, then its mode.yaml agreeing with the signed manifest and the script loading in the core's sandbox), keeps it (at most 8 packages and 512 KiB, the least recently used dropped, each checked again with the current keys every time it is read) and reports to the controller with the game key's HMAC, so its start guard stops waiting. An unsigned or badly signed package is refused with a message naming the reason and never runs. The trust roots are the keys built into the build (--dart-define=ATALAIA_MODE_KEYS) and a new setting, Mode keys for fetched modes (at most 4), never a key a controller sends. Texts in pt, en, de and zh-tw.
  • **The field's logo on the Field Box's big screen...
Read more