From eb300b67013317724ec632f5f54d12bed4999af3 Mon Sep 17 00:00:00 2001 From: Sam Rolfe Date: Wed, 14 Oct 2026 12:13:21 +1100 Subject: [PATCH] Phase 10: publish the documentation set + tick AGENTS.md 9 of 10 Phase 10 (the last phase of this project) is the project-docs pass. Update the Gitea README, add the Mermaid source and rendered PNG, build the interactive Archify map, add the Vikunja board tasks, and write the Outline AI-context collection in Simplified Technical English. Deliverables: - README.md: status header, Phase 4 gap statement, "Architecture" section with embedded Mermaid + image + Archify link, "Documentation set" table, an explicit two-plane MQTT section (1883 plain+anonymous, 8883 TLS+ACLs), and the Android client's Usage Access and VpnService paths. - AGENTS.md section 11: tick Phase 10, leave Phase 4 unticked, status line now "9 of 10 done - only Phase 4 outstanding". - docs/architecture.mmd + docs/diagrams/architecture.png (mmdc). - docs/archify/architecture.architecture.json + docs/architecture-map.html (Archify, showcase, 9/9 checks, 0 errors). - docs/outline/00-index.md .. 04-agent-runbook.md (STE). - docs/vikunja-tasks.json (Vikunja project "Internet Use Control", id 20). - Obsidian note written to 010 inbox on .27; the obsidian-sorter proposed a folder and waits for the vault owner's approval. Phase 4 (Python Windows computer client) is deliberately deferred and is NOT built. Every document says so. Co-Authored-By: Paperclip --- AGENTS.md | 7 +- README.md | 101 +- docs/archify/architecture.architecture.json | 290 + docs/architecture-map.html | 13703 ++++++++++++++++++ docs/architecture.mmd | 29 + docs/diagrams/architecture.png | Bin 0 -> 58527 bytes docs/outline/00-index.md | 47 + docs/outline/01-system-overview.md | 97 + docs/outline/02-config-and-env.md | 92 + docs/outline/03-code-map.md | 117 + docs/outline/04-agent-runbook.md | 100 + docs/vikunja-tasks.json | 21 + 12 files changed, 14593 insertions(+), 11 deletions(-) create mode 100644 docs/archify/architecture.architecture.json create mode 100644 docs/architecture-map.html create mode 100644 docs/architecture.mmd create mode 100644 docs/diagrams/architecture.png create mode 100644 docs/outline/00-index.md create mode 100644 docs/outline/01-system-overview.md create mode 100644 docs/outline/02-config-and-env.md create mode 100644 docs/outline/03-code-map.md create mode 100644 docs/outline/04-agent-runbook.md create mode 100644 docs/vikunja-tasks.json diff --git a/AGENTS.md b/AGENTS.md index 7309d11..4b11c4f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -189,8 +189,9 @@ The Hub interface is frozen in `central_ai_llm_master/docs/ask-contract.md`. ## 11. Build order -**STATUS — 8 of 10 done (checked 2026-10-14).** Keep this list current: tick a box -when its DoD passes. **The project is DONE only when every box is ticked.** +**STATUS — 9 of 10 done — only Phase 4 outstanding (checked 2026-10-14).** Keep +this list current: tick a box when its DoD passes. **The project is DONE only when +every box is ticked.** - [x] Phase 1 — Skeleton - [x] Phase 2 — Devices and state @@ -201,7 +202,7 @@ when its DoD passes. **The project is DONE only when every box is ticked.** - [x] Phase 7 — Telemetry and usage - [x] Phase 8 — Overrides - [x] Phase 9 — Hub and alerts -- [ ] **Phase 10 — Documentation** ← REMAINING +- [x] Phase 10 — Documentation **PROCESS RULE:** at project start, create one Paperclip issue per phase, status `backlog`, assigned to the agent. Close each as its DoD passes. Do **not** create diff --git a/README.md b/README.md index 711630b..f394d83 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,13 @@ Internet Use Control project (`SYSTEM_MAP.md`). It holds schedules, blocklists, per-device state and usage, and serves the web dashboard. Only it publishes device config. -This repository currently contains **Phase 9 — Hub and alerts** (AGENTS.md §11), -on top of Phases 1–8: +**Project status: 9 of 10 phases done.** Phases 1–3 and 5–10 are complete (Phase 10, +this documentation set, included). **Phase 4 — the Windows computer client — is +deliberately deferred and is NOT built.** There is no Python computer client and +no Windows process blocking in this repository. The only enforcement client is +the kids' Android app in `android/`. See `AGENTS.md` §11. + +This repository contains **Phases 1–3 and 5–10** (AGENTS.md §11): - Go service with its own image (`Dockerfile`) and compose file. - SQLite schema for the whole data model (§7), applied through versioned @@ -53,8 +58,54 @@ on top of Phases 1–8: applied) or has been offline too long. Every alert is recorded in `alerts` and the audit log, with its delivery outcome. -Enforcement itself (the kids' clients) arrives in later phases. Nothing here -touches the router, Pi-hole, or DNS. +Nothing here touches the router, Pi-hole, or DNS. Enforcement is device-side and +kids-only. + +## Architecture + +![Internet Use Control architecture](docs/diagrams/architecture.png) + +```mermaid +flowchart LR + SAM["Sam
browser dashboard"] + ADMIN["Admin app
kotlin_phone_app_admin"] + HUB["Hub :8788
read-only /ask"] + CADDY["Caddy on .35
webrestrict.lab.audasmedia.com.au"] + CS["Control Service
Go + SQLite :8791"] + APPRISE["Apprise :8210
alerts + tamper reports"] + + subgraph MQ["Mosquitto on .13 - two planes"] + TLS["8883 TLS
per-device creds + topic ACLs"] + PLAIN["1883 plain + anonymous"] + end + + PHONE["Kids' Android client
Kotlin, Accessibility + overlay"] + + SAM -->|HTTPS| CADDY + CADDY --> CS + ADMIN -->|"Bearer token /api/v1"| CS + HUB -->|"Bearer token /ask"| CS + CS -->|"retained config"| TLS + TLS -->|"own config topic"| PHONE + PHONE -->|"status + telemetry"| TLS + TLS -->|reports| CS + CS --> APPRISE +``` + +**Interactive map:** +(Archify, 9/9 showcase checks). The Mermaid source is `docs/architecture.mmd`, +the rendered image is `docs/diagrams/architecture.png`, and the Archify +specification is `docs/archify/architecture.architecture.json`. + +## Documentation set + +| Layer | Artefact | Audience | +|---|---|---| +| Gitea | this `README.md` | developers | +| Obsidian | portfolio note in `010 inbox/` on `.27`, filed by the sorter | humans | +| Mermaid + Archify | `docs/architecture.mmd`, `docs/diagrams/architecture.png`, `docs/architecture-map.html` | architects | +| Vikunja | project **Internet Use Control** (id 20), copy in `docs/vikunja-tasks.json` | humans | +| Outline | `docs/outline/00-index.md` … `04-agent-runbook.md` (Simplified Technical English) | AI agents | ## Quick start (on `.13`) @@ -133,7 +184,24 @@ with per-device credentials and a topic ACL ## MQTT (Phase 3) Broker: the shared **Mosquitto on `.13`**. A second broker is not added -(§10 rule 6). +(§10 rule 6). **The broker has two planes. They are separate listeners on the +same container, and only one of them is trusted.** + +| Plane | Port | Security | Who uses it | +|---|---|---|---| +| **Plain** | **1883** | Plain text, **anonymous** | Legacy and other house consumers. Unchanged, and not used by this project. | +| **TLS** | **8883** | **TLS**, per-device credentials, topic ACLs | The Control Service and every kid device. | + +On the 8883 plane a device may read **only** its own +`restrict//config` topic, and write **only** its own +`restrict//status` and `restrict//telemetry` topics. The Control +Service is the only identity that may publish config. Without that ACL a child +could publish a fake "unlocked". The listener, the password file and the ACL are +in `deploy/mosquitto/conf.d/webrestrict.conf` and `webrestrict.acl`; +`deploy/mosquitto/README.md` holds the design, the live apply and the rollback. + +Caveat to carry forward: `per_listener_settings true` is deprecated in Mosquitto +2.1 and is removed in 3.0. Pin the broker image tag before an upgrade. | Topic | Direction | Retained | Payload | |---|---|---|---| @@ -480,6 +548,14 @@ while the device is locked (FR-16). There is **no Device Owner**, no factory reset and no managed profile; the child can disable it, and the enforcement is the tamper alert (FR-17). +- **Telemetry** comes from **Usage Access** (`UsageCollector.kt`), reported per + app and per day over the device's own telemetry topic. +- **Site filtering** uses a device-local **`VpnService`** (`SiteFilterVpnService.kt`, + `SiteFilter.kt`, `DnsMessage.kt`, `IpPacket.kt`). No router, no Pi-hole, no + change to the house DNS (FR-18). +- **The block decision** is in `BlockEngine.kt`, from the config payload in + `DeviceConfig.kt`. + A tamper report rides the device's own status topic, because that is the only topic its broker ACL may write: @@ -533,6 +609,12 @@ deploy/caddy/ Caddy vhost for webrestrict.lab.audasmedia.com.au deploy/mosquitto/ TLS listener, credentials, topic ACL, verifier deploy/verify-phase8.sh lock latency proof deploy/verify-phase9.sh Hub + alerts proof +docs/architecture.mmd Mermaid source for the diagram in this README +docs/diagrams/ rendered Mermaid PNG +docs/archify/ Archify specification for the interactive map +docs/architecture-map.html interactive Archify map (published to the maps site) +docs/outline/ AI-context collection (Simplified Technical English) +docs/vikunja-tasks.json copy of the Vikunja board tasks ``` ## Caddy @@ -541,13 +623,16 @@ deploy/verify-phase9.sh Hub + alerts proof `webrestrict.lab.audasmedia.com.au`. The terminating Caddy runs on `.35` (sam-Ubuntu1); see that file for the two proxy patterns and the apply commands. -## Non-goals (Phase 9) +## Non-goals (Phase 10) +- **Phase 4, the Windows computer client, is not built.** It is deliberate. The + Python client, process blocking, and the Windows overlay warning do not exist. + The project stands at **9 of 10** because of this single gap. - No grant button in the dashboard. Grant is on the admin API; the dashboard has lock and unlock. Adding the button is a later UI change. - No per-child or per-device keycode. One code, every device (§10 rule 3). -- No HTTP telemetry ingest and no kids' clients (Phases 4–5). The topic and - payload contract above is what they implement. +- No HTTP telemetry ingest. Telemetry arrives over MQTT only. The kids' phone + client exists (Phase 5). The kids' computer client does not (Phase 4, deferred). - No Alerts screen in the dashboard yet. The alert history is in `alerts` and is readable with `control alerts`; the screen is a later UI change. - The Hub is wired to this service: the running Hub on `.13:8788` routes diff --git a/docs/archify/architecture.architecture.json b/docs/archify/architecture.architecture.json new file mode 100644 index 0000000..1939eaa --- /dev/null +++ b/docs/archify/architecture.architecture.json @@ -0,0 +1,290 @@ +{ + "schema_version": 1, + "diagram_type": "architecture", + "meta": { + "title": "Internet Use Control", + "output": "architecture-map.html", + "quality_profile": "showcase", + "views": [ + { + "id": "control-path", + "label": "Control path", + "focus": [ + "sam", + "caddy", + "control", + "broker", + "phone" + ], + "note": "A change in the dashboard reaches the phone through retained MQTT config." + }, + { + "id": "admin-and-hub", + "label": "Admin app and Hub", + "focus": [ + "adminapp", + "hub", + "control" + ], + "note": "Both call the Control Service over HTTP with a bearer token." + }, + { + "id": "two-planes", + "label": "Two MQTT planes", + "focus": [ + "broker", + "legacy", + "control", + "phone" + ], + "note": "8883 is TLS with ACLs. 1883 stays plain for legacy consumers." + } + ] + }, + "components": [ + { + "id": "sam", + "type": "external", + "label": "Sam", + "sublabel": "browser dashboard", + "pos": [ + 40, + 300 + ], + "size": [ + 120, + 60 + ] + }, + { + "id": "caddy", + "type": "security", + "label": "Caddy (.35)", + "sublabel": "TLS + vhost", + "pos": [ + 240, + 300 + ], + "size": [ + 130, + 60 + ], + "tag": "webrestrict.lab" + }, + { + "id": "control", + "type": "backend", + "label": "Control Service", + "sublabel": "Go + SQLite :8791", + "pos": [ + 450, + 300 + ], + "size": [ + 140, + 60 + ], + "tag": "source of truth" + }, + { + "id": "broker", + "type": "messagebus", + "label": "Mosquitto (.13)", + "sublabel": "8883 TLS + topic ACLs", + "pos": [ + 670, + 300 + ], + "size": [ + 150, + 60 + ], + "tag": "two planes" + }, + { + "id": "phone", + "type": "frontend", + "label": "Kids' Android client", + "sublabel": "Kotlin, Accessibility + overlay", + "pos": [ + 900, + 300 + ], + "size": [ + 150, + 60 + ] + }, + { + "id": "adminapp", + "type": "external", + "label": "Admin app", + "sublabel": "kotlin_phone_app_admin", + "pos": [ + 450, + 130 + ], + "size": [ + 140, + 60 + ] + }, + { + "id": "hub", + "type": "external", + "label": "Hub", + "sublabel": "read-only /ask :8788", + "pos": [ + 450, + 470 + ], + "size": [ + 140, + 60 + ] + }, + { + "id": "apprise", + "type": "cloud", + "label": "Apprise (.35)", + "sublabel": "alerts :8210", + "pos": [ + 670, + 480 + ], + "size": [ + 140, + 60 + ] + }, + { + "id": "legacy", + "type": "external", + "label": "Legacy consumers", + "sublabel": "1883 plain + anonymous", + "pos": [ + 900, + 480 + ], + "size": [ + 150, + 60 + ] + } + ], + "boundaries": [ + { + "kind": "region", + "label": "Home lab - Docker on .13, Caddy and Apprise on .35", + "wraps": [ + "caddy", + "control", + "broker", + "apprise" + ] + } + ], + "connections": [ + { + "id": "sam-to-caddy", + "from": "sam", + "to": "caddy", + "label": "HTTPS", + "variant": "emphasis" + }, + { + "id": "caddy-to-control", + "from": "caddy", + "to": "control" + }, + { + "id": "admin-to-control", + "from": "adminapp", + "to": "control", + "label": "Bearer token /api/v1", + "variant": "dashed", + "labelDy": 24 + }, + { + "id": "hub-to-control", + "from": "hub", + "to": "control", + "label": "Bearer token /ask", + "variant": "dashed" + }, + { + "id": "control-to-broker", + "from": "control", + "to": "broker", + "label": "retained config" + }, + { + "id": "broker-to-phone", + "from": "broker", + "to": "phone", + "label": "own config topic", + "labelDy": -22 + }, + { + "id": "phone-to-broker", + "from": "phone", + "to": "broker", + "label": "status + telemetry", + "variant": "dashed", + "fromSide": "top", + "toSide": "top", + "via": [ + [ + 975, + 240 + ], + [ + 745, + 240 + ] + ] + }, + { + "id": "control-to-apprise", + "from": "control", + "to": "apprise", + "label": "alerts + tamper" + }, + { + "id": "broker-to-legacy", + "from": "broker", + "to": "legacy", + "label": "1883 plain", + "variant": "dashed" + } + ], + "cards": [ + { + "dot": "cyan", + "title": "Control Service", + "items": [ + "Go and SQLite are the single source of truth", + "Publishes each device config retained", + "Serves dashboard, admin API and Hub API" + ] + }, + { + "dot": "amber", + "title": "MQTT two planes", + "items": [ + "8883 is TLS with per-device credentials and ACLs", + "A device reads only its own config topic", + "1883 stays plain and anonymous for legacy use" + ] + }, + { + "dot": "rose", + "title": "Enforcement and gap", + "items": [ + "Accessibility overlay blocks by interruption", + "Tamper alert is the real enforcement, not Device Owner", + "Phase 4 Windows client is deferred and not built" + ] + } + ] +} diff --git a/docs/architecture-map.html b/docs/architecture-map.html new file mode 100644 index 0000000..f2d9c08 --- /dev/null +++ b/docs/architecture-map.html @@ -0,0 +1,13703 @@ + + + + + + + Internet Use Control Diagram + + + + + + + + + + + +
+ +
+
+
+

Internet Use Control

+
+
+ + + + + + +
+ + Internet Use Control + A architecture diagram generated by Archify. + + + + + + + + + + + + + + + + + + + + + + + + + Home lab - Docker on .13, Caddy and Apprise on .35 + + + + + + + + + + + + + + + Sam · browser dashboard · Architecture component + + + + Sam + browser dashboard + + + + Caddy (.35) · TLS + vhost · Home lab - Docker on .13, Caddy and Apprise on .35 · webrestrict.lab + + + + Caddy (.35) + TLS + vhost + webrestrict.lab + + + + Control Service · Go + SQLite :8791 · Home lab - Docker on .13, Caddy and Apprise on .35 · source of truth + + + + Control Service + Go + SQLite :8791 + source of truth + + + + Mosquitto (.13) · 8883 TLS + topic ACLs · Home lab - Docker on .13, Caddy and Apprise on .35 · two planes + + + + Mosquitto (.13) + 8883 TLS + topic ACLs + two planes + + + + Kids' Android client · Kotlin, Accessibility + overlay · Architecture component + + + + Kids' Android client + Kotlin, Accessibility + overlay + + + + Admin app · kotlin_phone_app_admin · Architecture component + + + + Admin app + kotlin_phone_app_admin + + + + Hub · read-only /ask :8788 · Architecture component + + + + Hub + read-only /ask :8788 + + + + Apprise (.35) · alerts :8210 · Home lab - Docker on .13, Caddy and Apprise on .35 + + + + Apprise (.35) + alerts :8210 + + + + Legacy consumers · 1883 plain + anonymous · Architecture component + + + + Legacy consumers + 1883 plain + anonymous + + + + + + HTTPS + + + + + Bearer token /api/v1 + + + + Bearer token /ask + + + + retained config + + + + own config topic + + + + status + telemetry + + + + alerts + tamper + + + + 1883 plain + + + + + Legend + + + Frontend + + + + Backend + + + + Cloud + + + + Security + + + + Message bus + + + + External + + + +

+ + + + + + + + +
+ + +
+
+
+
+

Control Service

+
+
    +
  • • Go and SQLite are the single source of truth
  • +
  • • Publishes each device config retained
  • +
  • • Serves dashboard, admin API and Hub API
  • +
+
+ +
+
+
+

MQTT two planes

+
+
    +
  • • 8883 is TLS with per-device credentials and ACLs
  • +
  • • A device reads only its own config topic
  • +
  • • 1883 stays plain and anonymous for legacy use
  • +
+
+ +
+
+
+

Enforcement and gap

+
+
    +
  • • Accessibility overlay blocks by interruption
  • +
  • • Tamper alert is the real enforcement, not Device Owner
  • +
  • • Phase 4 Windows client is deferred and not built
  • +
+
+
+ +
+ + + + diff --git a/docs/architecture.mmd b/docs/architecture.mmd new file mode 100644 index 0000000..84defac --- /dev/null +++ b/docs/architecture.mmd @@ -0,0 +1,29 @@ +flowchart LR + SAM["Sam
browser dashboard"] + ADMIN["Admin app
kotlin_phone_app_admin"] + HUB["Hub :8788
read-only /ask"] + CADDY["Caddy on .35
webrestrict.lab.audasmedia.com.au"] + CS["Control Service
Go + SQLite :8791"] + APPRISE["Apprise :8210
alerts + tamper reports"] + + subgraph MQ["Mosquitto on .13 - two planes"] + TLS["8883 TLS
per-device creds + topic ACLs"] + PLAIN["1883 plain + anonymous"] + end + + PHONE["Kids' Android client
Kotlin, Accessibility + overlay"] + OTHER["Other house consumers"] + + SAM -->|HTTPS| CADDY + CADDY --> CS + ADMIN -->|"Bearer token /api/v1"| CS + HUB -->|"Bearer token /ask"| CS + CS -->|"retained config"| TLS + TLS -->|"own config topic"| PHONE + PHONE -->|"status + telemetry"| TLS + TLS -->|reports| CS + CS --> APPRISE + PLAIN -.->|legacy| OTHER + + click CS "https://maps.lab.audasmedia.com.au/internet_use_control/docs/" "View Archify map" + click TLS "https://maps.lab.audasmedia.com.au/internet_use_control/docs/" "View Archify map" diff --git a/docs/diagrams/architecture.png b/docs/diagrams/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..a4419c799f71bb7f914c872c81bdf56d6ddcfc5c GIT binary patch literal 58527 zcmce8^CLbHwZ{KD2=oL8YXJ^BLxAK37hS_%cIn0WDV9+ESQDka=?0L07aY1vOtuLWL`-UgrT z-`7!55fMqKs;2q;`4chq_EeKgy3HcFp+C0D;#%7{^afIY?H|{tTPZN$U${>iE`0wWe>A}VQ z2?r+{MddOuGP3=q-%C_1d!_ZUGNBipYNgm}E}p4;bN9E^+zHyy>5A9=#kS6kV^7Z; zhPVAmNhHng*NQdGQc<%mrLb5PdX4QW{u?5ezb7-r$Bz$Vt^rY0!hF1X?&lj3MMW3U)XPrJoQZ3Q z+%H&!giakDg})unR!K#1y$ufzUTAyL6%}>Z>?Ne8#u-VTv7PM3oyp4Dkc>R)%I7lb zRFUcv-rlxaX?YNrk%3L+D+OU01V<^6a(}H%)Vyx>;)h<2$ql2m9Zs{S3VFF^I*#?t zTvI_mm1@^o%@K~IaDR~b^yw&+povZi_Bdb3sM+}go?lCgV2;=Qa5m!1FDiVDMSC^zdSmm5~bivAR5xp}hoHWuWp2r85X16JLDx_7v0EuH?1K;Y)(yeV*tp%K_AFj)FhZ1Ro6z zPpZ!=*zjXwTwFxuwOEo`UY`avzpxRv*8v_qV zG3nh4)pTCR(sJ96t$cUsK^TW2M}?9Y=k}ZNXNMCT8@T1Ve>@&;vRU-BUrMIFw}6;+ ztUAvW2?kGU*O(hxmoypou)uQHm*nK-wQ9^?;j%L`t4ShLZzJh-{JXeFCykeAJ*pMo z^!Cx|H}B16OIaDY+1WLuEN$x4-^fW%8~T<7q^7crByrGEQi2yR%ICDoy%sG(N== zrM>i}hS{I_4bmw#FloP+sqrnfaoPF^gLPn#PXBSgPOq=0dYTFj3oF&-EtQR9I9}xB zb-!|um-kduWPxdxmHAxSswpdS)dLdU2DXL6%&{9_P~s5C@G<3APlBJ5mF z`|k0}?%1m6ZyNVBMQQ2JrP`qaA(Bj{8oT@UY+cON)KvX)N6f|= zam{St=P~r6Y}Mzq(ROu#R>3=i+m_>k{{P-NS`)<@OF!VvoR02Na&r7DK1xb5(9}b0o0C(sop^W;QjuD5@~=zV+x31d-m6?GBSnG#VmjStz^#J1+3pWuUDJ@ICre{QIPj~-p3}4pFgaj zKH~o_E~ci}+5{Dji))9|GB_oflw84jLgt2d(xV3NqD$C zEMH{XlR#9A#^pr7?RAlDhM0|hXTh7?NT(loAuiLfvbI0IFVqaTU<$A{%En*qx0w8 zop^l0>Kl%ZI0Fj)on@26^z;E_>YUWyjQ?rI^kS$&mY^+6PT2PjCs`?_Hjv`rOqJ>s zgBnP^xi?`_m8j|bg2i?fQ89%JpYw2zySn!Hs};YLh6a4$#};>5PgI*FE+NMq$+9$vHm%WM^ zC-bFC;{Ht9+C@VNOO-#}t_N>-6tGq-ra?Kzqh;FO=Hh~{shLhqt2g%j_N~}*8e2m> z9t#=SX0B}D?i^l@$G-dhR31hOkG+<(^cL)qk%`HY=DS#HFFY-DrS|Z(tm_v~TicZv zYc$hExzX|Ql@`Cmt`BC=6xjp>@?312=I39$R)t4&!u8THFeo+~; zi^Za{uLUL6z21#rTL`xqa&mr|7^fBz!l?|as`5=w&#S;bm-_fIF(6=|#-ca|$@z38 zIf169e{_^y=Kzni2_ zWI;d*!XkP1u+l>FPj`)pVJJd6si-K}+q-kW4o6eWd|A`u>qk>nRmCo4bdZpg6cCP* zosvRUP1QbPW0Asz^iQX*gLEAO{HMQJh&t*R#B7?QiO728vp1jR-bl!g4BI?CRzPjL>dC^tDtb@p{wFg zS6n;iwH-z7Kf|#vGKOoS8uNZe@X`MR=;A)il`$*GUlG$|a9Rz9lP`dBj!Z8~J!fN? z+^<=uW}GAH$WPkK@cfUq@yLAvyJ)}9EBMK&jl!mnqR53z?pNkr()2M2<-G1r!k`}J zAYbA0{%;U}q4!^0{V#C68URQ8|KXcO9RI#ZKi193{;tc|{uT>8=iot!R@7MdC-uUR zKVS1rgr*oi$jZbql(;E#?m{D@qo)7;0fyXApDG4c4euGxepFe+=6cq_s{Pg-o%S=KYw^Xb# zc6+)SS_LQ)19d4w4zS~hZB}Pp}Z{~Pu{mD5d{Tjh#1L$Hk{s_Kc23* z&6Qel8n&Mu%oHj)shoI0?{Gqf+kg06oeIY$yf+_zAzZR|{EC4Ajts6-%j&Q<>(|AV zr~Ce-h5b3H&7#xJo9EBHr6P~?n_UHcNQn1*&ekH|$Z?(RBmZe?T4~x1y~?*)DvKcF zmI6g#dS`?&+AU4k=V-IR{z^^BRBSx-)m3><=g z-Rc%y(OUj(T%6kW))shnw`UDBiz4#6y5W_T3*(ua(XV*QIyMV-$wp; z+`>ZP{^I>t3+TLX+>IPf<98cNSIEq~I@15oMuNyN+`ZvdQBhf{<2_z&v)PtKYOblN zA>lqz5)>4xi9g4qpa|)dj7V;3T0NS~<)%Ww*tt1wGFltJi&DxEilpGj=;GyF&?x7_ zB4$g)>2-}gkm-Cd z?bb|0hZV&yUG+(AMr$Ja8--Bw(Aa52rjd=Rs-u3ed4s9KAJ8m=u$JlMiE99Cw6PKV zpM^#$zPt6zY&lq~fD_Kl>|j(M)^GZ@9!b$>Y?7V8nn6KAl2=w17Z;cG>laN)d2a3y z52#AQlWwNuW2yafE zC2?_aJB2#IkyTOosZ|N~?B5C_15?wTm6nuBy>f##qJYly9PK)5Dk>_E<3)sY9K8Ro zWG2Hz05#a(G?Mb(Pb;^8cOzk1*H>A(UTlqwq+p?wyxY0l9qY7&eBGta@fB(CYoG4g zRP3lnU1rJ2{USa!v$f?Dl$(^ z*w~khI={S-x>ZVbTrSSt)Xtc@KgXi|=Z*A_DR;or)_7zHdi2}w4-O8xUmqmJ$A6tE zQgd{unh@A={<_*ah+O#Ia=L)sdj75}$OUqLfA-Vg|1ORHvT>WmsR_`Iv$ISO!{5Sz zr^`(QxVW#G+MM>CEG_`+E9+%x!Hu9lnTju)LrxLsZ}%+5wAr3-v7eMd*P zJ6$LS4vgl2t4CY2!!{Gu&6Vf<`KA>5=g*)2HabfftR|w=*(;5iHH&(B{`EtuKb)=v zN|5LayTvs;h(S2E3aL;Gu8IFAa{?$KFS%SA&?veBa}N%Z%@wUI4|sS=#39ktCOc#G z9v538rlyk1jp`j8Kfj=8 z03KmV(#$5_{+#Vw+$E!~#-V`$8f)BRx?<(*o%yR|Zd;AL85!05QJos|`|-?WP~)d( zYE8%VM`d?LQZ!snSGHP2czKUI2un*#iSVyh#w+St9#v>vKdfx@_=JXqp`A3NSpdG0 zuP6v$4fc<_JJ$)hI+P&~((rdQ{+?9+wDT=&D(?W{b?QSeY%kj4pAJSI_Pl&QQdNpu z{NWu32R!^^Y@C`nKiBB!s3bf=sg+U2ySUJ$i3;y;$*UEPLn9*?SXc&t_Z}@&Tg?>p zpn+{kNk}+bqE+4Y1midGf(~_PFjG^jusUa1z}Pc`!3~OU0i#F z?lX2oR8)+`#S7BYl{;!9KYae+uI*`>tr!XxdH}qra(nDpJ%YTxzRt`HMqJ#;yv9Yt zF#h}LPfl-0sB_nEVtzlLYWtG?%~5ADZ$w00_O0PPLoP8}kCdsfy+X}?Rwn=*>Ghk# z);TlRZN(UB=ll>;Uw!Qh&hJStr`fpcjkgX#vzr(iLX+{Yc+J4T@b+ziL7TT&5N6L% zVq)U;;T(-W01vnriRz`=GsPO^jn0S0nhFXdV`Hp5JgzIvZqlgwZQcf2j^kaP&4$aJzY3IFKtEtiCk}2^)gc+qP_N6NJeo zB>t{G^QG}FCFQh4rlO>iRa9&>eA@m`i>JcK&&9*t2oJa1WsC|(9~&5eS&X?TDkz|! zpfC`_;ct(Su`;EEhg-KgD2?SKC{+^qBc7&km%CmfT@v8oX{b&a#^G`<%M`}^`c+*s ze0V|2#Dv~9GBWbz%aa+N#HXC>?5@LSaI~>!2@#~64$jVAwxw4nbo7W;5ua#WY| z2=bVZzx=G?^>A}Mo(Zqla)DM@P=G}x^aFs~!rvP}D8j`J4{tsEB=Y(5>rn;^pjT17 z1XWo`s8unIPe@3JkdV+xY!C~8pazdyN!Ie+{e6p_oolf3%&e_LFTjHUeevPGHowGr z#QnO2PN8a7Pq493A1Eo!ZBIJycY!(ys*!8_T&PE#!#24*YTQIB*?V*UJrdRuhRYoV z7M?@U;x`L6I|K002upC7rIs$v*7csCj@*2FxEvI0hIi4_*(8M}U@h%fbPWuI1q5`( z#Ij9CPTf0)iBPHnxjSirJcKqhXDwS>^~^;3b_PJSa}K{?lI^;)l`OM-+%qWi)7gu zPU@mU74c35>Q?4*gXu^a(46OravN<@Xjq>P%hgCp7m={^HCx=JG&Rr8Hy@H-Ml4ns zfi3R^m^km3&@RYjAC<6o`wOK^iQ2=tjFS~F)RBJYLs&k6cBwY!mO6B|E9edTPfVfmcN zBdKIT56JDRVF&dD%Uw=3t0)t|m8&JtY{oWBxJeNpVvq4Wv3<>5|fO>9v_r_yR=f( z+LI*Z9eY9vH0#2MSY>5oI4!5mul6RMpqE&qx?jG0ImfY&?!Y1}RxiB=C=v)pLJFx0 zUwrkm?AF3K;(?}qVqF=FBMM6>FF$>_V-;Tb(C`HXse-EAG|Yx%iMCE^rJA41G1f5g z6uSszxOGXoT3HE3C|@Do&3EIWsrg_oZ{B{{jM!1G#0 z)y)k8ifXm&=2`~@;A1OQ+b@lco6%7(eD0_HR$m2mN%+&AuC{T!dq=>vU4dlh+;Xwe zvpGNi*3C`g#YoT9R~#Iiozo{mybLaz?z?ji*Tai-vD>+F!H9^Fzkkc<85poA1ytr| zF5(z>DFq!g)MnPk5_i-~5~qsi)*fzcsnfqX{r!>nur&ZrO)XO3XAg;s3$8@OAnmY? zx%@vF4vS@nuI^}?>-k%D_Lb?HmA#2?S+LYij2GWK`(0gYY%D&yVRg^9wD|aV=I;r}`F5x= zW()H{Ys>>rXJ1*_zw=GPy?e@Rokyl8VALo(nkk-Z@woN%^P7Nmpk%7%D^ff4_Rg2; zaHleM>WrleUf$gF+T!8;0@`_FQxj;e6iX$Unbf`8EyhR)2!dYs)BXK#X=zP|;+g+B z{5~)NE(oQdJMyo>;9%tUW1XFyY!u;d>FD}kHrC{Q?C`w=$HU6_2dTV{JI71)^CmS! zAE*i8hXS7-ju&gY2Iy$_&MMbRDh+G20EboJ)g%9Z<#*%;sT{v|)_>5SEQ5{J~8#?bKa*;xaJ^ML^Vg|+z;%qD{L zF)&ZV!NGVcuhh*8igbSC{Uh!e3fUC&z=Wogkxe8o*W1d8it1LImKK`zX3)^n?{5wK zqNb)sL_};}3TW-V4+h3p2SV~(A(3cdRA?0OH0f7c<^i@-sXdk=H^=| z)|vb-e0+RT4|-!HAMdX{ZjJ^t2;`dq#GKag8F3pDqfLyL6tb!ja_G7C!3e_QA|0v(C2w$`TQ0X zWeXYU{f~MbF)=^0QS#+7z4}NB591Ed=PJA&l%c}7ELh)Ckk{;VBgSU_INH9@5ewQu zkk5*uWpvq#Zu!}HGh1h~Wa|-1{;fnFn-}E?$&kO}MF6W?m8#eWCtd3sr~@Jlw`-o$ zL|A9KIr+WCWUZA36wPA3!bqmfWxn2)3R7X6um^K_cDBx9Di5IMccT4Ilhf1I({(v| zl(3icpU$x;r6T%s*rLOf1v2X|#BaE`2up>aw^C^f)n?h4nBAM3oAB_LEdJf%K(BB+Tk8M_*C_#VXGu|C7L4PFh!SSa+=k2c>P6F8a9n{BpA{GC7$x`7;kB%JIeQ?2|srbc6j6kQunFHAC?SIy(b1qWdhz|2UR2>!=HQ zds3NQBVmwIvp(4tF&hhI`rlq1QUhvjWB(VxCut}h0I^H2Z&@q6AZ-Vp8ucf%$>u0i@UVb%Kq z!^TQOV@|K>n~~IOz6DVi~iQa(2a{U_bdN1Zf2XY9{tq{lXh zSd`&*s7(mY&d$>!YXO_6>b?|kd47UQ`HE@L%`^y%SMbJW!AZtu=C-y`)yDH}Mj8vb zazspZ33F|k@IpM_RPVlP5_g|Kg`z^AH_Za$i&0~AG~@KGe1bRv5|X={ps?Eo?P4Gx z)?Qb8l&n(seyc@A2lJCn&WA&2G4w>KaFLH+SDM53O^CH7uMdd=67Y?ZF)gRCa9 zmj5_PZEhF?`Z$`(+iX7Jj)B1pMJHw>cdNHk5p+l28_(p&({=MtR?Xj8?g|bzJDPuv zMcGhmIU}M8(4z_HYU^{-z&1uAxL_vud2ThAs+88)6D1txR)*7=4YWK=3cmO;tL|X^ z;1yRQ=jEo`T_P3<$v|q2&fRxhTwQ(irk2|X2=Y1B9Z%>-vaW5JJf}aN#NM<;6EyLX z+hYQw(SEZRP`|+SXkUZuPo4Jy*0O}{^y1CkqtEjR?RA4!0Y&3U?E1mvO9z$QBxKgC z;gA#Z5@m=2+y2t=Xs+Bq%ADT^=7~;|-9h{93HI3P^FMdvpO)SPT*5|y6OiRrsHK9p2P^AmgGU=B>y^Lmi#3`5DAgO_ zWaVaMT@Mrbm9x*Qxi`4B)cSt^*DAkE2Gb&uQ5G;oPYh zBO?pT?2314RTYEnXIW|K5}kS^npg4KHG5+wePyz8Llw5ZG8j}s&xP;ue?VSA`8@C5 z!6evJm|$0C-t6zVu!Wwz-EHHI-Q?yX!M_+v`llTMr>}4KdM;A6`4_E%gUdh7g14^Q zY%JX+?*in49V(n@raB_g$ZSFHhUm3)9wq|DSZlrT4&w(v*fh3- zw`#wIX3gHqRQStdJ8tSGr0K1seVl&^C*vON>he1c23XC1-{cvU4}cGMiCI$%Z9c+6 z)_#Z>ngKKo7!vlwiG@~kEG5cfS@xSd5C)+C6%`bqoYE_6%xz4lG<{o+z5vqS{9ut0 zkYr-W2{_TZ$ALuFHQ)+)ef*S}ftgfOULF+?!1=)LebGhX6LNKR zMZy1V&43j4{0;)4W?&3F8L4UxWCCU>Ptan3{k#c6^y?)P> zdQH^!Tt)KCV=wdo>3NC{Xc&Ke#-B-kq97v+qtOUAHZ=U6gOblO^J4ESzsi7x#iPeC z1tn!9BJpT3+A6?F8XfmQX6kp@+TNb<7V8XSW;8&6wh?|!nOB6&JU22jDl03KiX_KT z2YzqoY*6y62n=V+XCa-ze0+RaZ|2)8^=N7~j&;1VjQx6tA&KU$=>|vFo#l zgnycw#~Eys4Iw-@pf7*zc02P;I3Le^gA40vIG-&CM+Zf>W=<4bVp>TFQ{I5>DT1f- z-JP#Kz2DW9(5KRSc|avaGHJ8uKyJ#-?NdUpE$^RXT-+z`USRI4=9fY4UdX3eTA8To z)~j!mV!G1GCl0VG+b%awoh*}qLU@K?-1oOgO$thVmdj}|d2qI#*8FbKx;X^stl@+^ zO)e{)#l(qg3=fzTjX*_vfkf?mpjmOMTAlZIy6_V4i%bR01vuKA4>B_ILX#@0Q&W?6 z>9ov(f*dq79Nf2?#Tpao=_!!hZs~6o3g_3UE`lAZ#OakA+*~Z(Rr3u+Fr+tk%ye`? zg_oXOe0<5aIr7o`FC{uD5m{rTguU+Z$vBNk(G41%WHdC09-ad>F-$>5%EgF&mh((i zOzguvJQE_eyouD0UoxGMoSG{(QkkHk_Hd29NT@XI5`&BUBIyN9=%d@Xl{fs=2Y06I zib^|7Q(@NLC-McR!t)XRwR+ZW^Lfa}&Y*y8%}@BDE6M=@9o(0G>2X?;XL6zOto6V; zlLHt`d{y*AYHI3R&ybn~EOT>nK%OxSViV>rG05UUT_HrzZ#bM|1=<;qKG#`+OY`+_ zLw|;v-dPq$3A&~pyOg(vwTepCUQ_4N_P93|#^XySC0W^iqR-i)z|;3Rr6ts5&{{}- zOQe?_>MOIb!0U9n(zd39&{xVfHv?tK8 z?Q&#_g(+tk`1&UhONJ4p2(*#h*ses8t@M0`M8XA; zB*Ga;s~ek_n4-G6pTs20;S*_yIZ{+h%gcvEDTPKTpreI#PI}*A=M@%)*0m%jzaza& zjERbRYIaL`xIWZWa(r1F41{dpLfZaqK+wJhEaSsuPR=JZ0|b3Q+Fl8Wx}F0)&0FO8 zVmwbV7518$puV?mC%(>zb*}@(V^@!p(hE zEFlAR5u-0*KpYJZj|d5oiUvu#00J!5^QC!BRzX4d=LJ81HiHNUePOfoCrL>@8nH%; zsmS@au2F=YNcNsfyu{D}yQ@fSEms93*wd@Ao}oYtGGioP4nYjk_Z4+@D-}k5QBg{O zCqZz8OoRShHrXrtG82`ePtY?mFc?=Q;;&wwtrqxkhUOa*OUuc1-LM>Osx=%wvo00T z?yQR{rG%e1gq$E;Vqju&y?@{Rc7EcV4o3~Aqudgo1w{xg@{K<$pw(O|uG2j)_z6oK zvh>D7(SAe8+)-H{?p%goLy3f}FvtB#g)9=hI{VLV$RT{r2P8n8IPt!$U%mJmAHnn zg-a>eI5&36Z2T4IyFxH1U2s>R#$(#8~(e#9(a@%mcGho8*C~E*O z=9IMgcb_3#7EgCyPCC7WyztRJK>03y_8+ ztrpQ+o_Q()Jlu0LeL_+;9CEKvArs$EY|Valf`WqN)2L_@@)&roH%ALVZh-iq*?q$a z;d)ZQN_`dQ4KD z*MsSft*zJO?M#{#fhhNk`^yh5{7pvM+KoUa8cDA0m5UX2+h8~8HTUw$1_+^B{&JT; zzS;4lp#cCG*c0DL8;tMjYF}8Vu0-6J0RM{8rvX@{35SY~VL#*I1Z`Jt{QY06s*15! zS~4)m78S`NA}$UNUN|^Z^~Z&hvU5x&twPkaOG--YUM0CyZtd?wc1Fm&xk*|454Hhu*+ka1)o3;GERtUp z*!(Z7-RF6MMDv=SzN0sfo>eLvaknBO+>%DAgGgoi*CJSPLu+eVQVbK9N~10n>tzXM z#H?3HK_Oj1w;y8t{P%joM89@(adJ`#x*yb7_?TNAQXi%BIi~}y(LBKWF|gJB+Iqg? zWFCRq8dm|Gw?sIMsNA4#cm+7~w7!mzrf&q4rq_Gy&wfla*uz&mLC&b3QOyqQ%PL#o z)AD~ide7veso9^W@X+>jKVq=mhbTXvzuI6=PfJ?_`a-h?Gde)Ja2BgQVfmlGSScp5 z;hnGdgwdTxAz2ug>-$wu7ze1T&6qFMaljCP|FHk}4>wi1@G7Q{916;SXogkr&lGOf z{M=mQ<>Sd*tQ&eLB*0wopCrUg4D0ugnMzdMooj6`H&jX(gS8K|J4G>vu2xIY`vk0OZj8i>&ePzCdO5FIjjak@zK$pPm3qZ50`!wAWc$OXkn;# z1>~q&s>-h&LHs{6P<%M0`XI&XHYUDb*H2d4dV=iTF%EUwyxl0|@pT{X8W1!BeT%^sfPbsm|8GPmIMHXMQ2%tCSyNM7JePv&87s<-$lxLRQu*;N(k1rmFk|Wucbq zT?fKPAfBS3UvaP(UOt>=qXnMuB-tG&SFb%6tM$00EGbEK2{j!{A4$Y)TOlx`2U&!0 zAGVUcpg2N47jl=C#_f&6L%)$WuZOSmgLm~oBbD~WmhbwPO%TLHMsVqVI(ZdvwZJ8ls!~7QHHlQSK|%IK!7SLV(^{Y z+uPe7O2{Ud`{TH4vs8Btyi<^N_($EGuiW%U!5jA%%jx5)WDsy9F7WNBXm4m|WJ$jb_TGR3N|*NT3ufep3g30jMnk+x<96kRa1x)ljEofq7?7d8 ziR^rZlt3Ot6wW%Wb>WZDF0Z>Y>QlIZ$;pd<3cpICu_1Q=Md!~qa^fz?z*M@vyd(p- zD=W)bQWA0Hu?Vf))z%7+PO%%)Xs!K6CxvJSi*v=8y@SId7%)Km2$B}CL_rU(SJgn` zJ^E|M^y873%kF495R+n%%Ew=jz}U%~spH|7C}se~KH!XB&CL^|qbr>DRnqr0p3PsK z(RhY%)mCURTf2Zqe=tq2VLgW&8*p0yJMZfT-dBsyX-Z6tx;!^0*vait<|wdnA{OY9 zsw;4;{I~O!o;*fB$jR-lb)dmuw)S_-i)d1~Kn(TW}71{opy@*I3QQ zGBya9JFNv_WhZ^7QXf?ta4B>YH{40dw}`iqTPNJ67HgE2l^scMk(ZSY2*kgeJ8xz@cgWZsqoS4*)dAL-mbskC{L;gA zAcovoP@5+tJgDChqf&cQ_*cqOt=9t#{Nit4-$`K09ZXl&R6Bkk1U-TU4UG)y(bar5 zrqp0WO2l75DpI%C@(0s1MwWO~oXvq>NONV&0czFbyc<(SUo01@T7aUU zU26WqIIJ_Dj3bZA7*0IXoD3L_b~8oCfq}G|5L9pZBX_niV0Gr_<$)9(8vn}`I2R|~ zvz?J*LgvNn+z0`n{&LQfKHZu8JX?>sZGz%E z2`1o?>I_=kkzldrTQr%#z#@7T^2d>`-ew7*DPL5hElWMnv-k~Mr0*u7?)Ju}NhK-< z$vebif1o6)Q%)Qs7oSl1Ty53&|M{Z_hD7*9-;!%fi2Ki$uM+bw0I+cO5q%+a&!!i- z?vB}^rzE0ZHc!dmd#(NH%82yf&Zu78=@75S&08*(4*0-k+tt=ams1jJ@!&w6x+5S& z>bp=P8(;?kFIN9FB#iJ;%g&3%^286x1x!iQMJQB%e`c^{)n5pyH?vs4Mk>){^ZY>m z%IRpHgPtDe`A7_n##y=E-G`5Y`>UF!e_H0X)jQd2&NG4v@X)xf#O)-_{J7?dDH+}h3t{e@Rt`XNhdJsp<$?q}a zI>_ml#=+)7KtKS8_IVp{jtTJb?KG_c*Y6Jz{+$->z`(*%wubR_9^OQ)&kbh$l}N%0 zbu;U^R;T*kzNg>U0T`$@qHd7pCTiEQ1#_L~`IBl%>0zDKT!SKo*bzjK zg#!VDlVCaY?>aXm=_?fqDO-!UOX<$$PF;;cieE{r;bnMKl$mczkEocKvy+qT?S2`8 z3o=uA^_KaO(OEg2+)=AGRd5rjx49c(t!AJe#^hJZF%hT;iyHRcNH(9!gE9fzc(gm9GI*~d4y>8dzL;XlnwXaKo^_LktprD%Pa)^{40Sy+FG8q5*N~8u- zDfOW{xJ7KaG2Z+TXr7JZyj7i)Ed4LIvoDl_s61=Xh!lV0cpOY;`cmP!?T%-f0H>R+ zX||M~MJEu?K@NgK$TXkGuBtVGpXM7@ZJl`?xIssho+r6ht!BuIFqM9VGWDNDUg zR0Hv*t#~_2VhSx%$1YAR58#_f*I)z00CWi_{?JI+lTN}vF<5~D5qOyYT66yXIsZV| zDI7UOPuYsbm?GBjSv46?gQUGaqsm!x2@?iF+)|-x z{oNZ4disHRgYz~RQNq9XHZeq^1_2Bhb-d8A|K1L)23CXC)b#YP*4E*_qIvR@%c+b- z2kDP~RH_phNa_7fM5U5gTg5viAa281Ehs41;z#e&PZQoKC7!pS8N+4nIEmpdXeFsL zc5!hbBO@~a(=J4;Wtx?~kA^{5{O?2=@IJqR)csXawhL;%U?En9q(?@y+5~;vL+WP1 zG|LPnLF>aMjzLnAAMbH>b>;EC3`Y;jb48QEn{+(d1Dd|~PrNni*u9*p>S{ebJ$uc` zdRqe+xuED^G8Gvc3e{15Ss4KiPA@k@2ZuP!=J!T->)BWN7cBG1*t3aAGhOSE8LK)u zTwuq6;l7yia;!i%A+@tVfz8}Z-bm3O0h2TME~?~605%Q?#gTC_;Rv`&V@!!R42OFE zG|t2#!hdwSo7UH74ZTmCw*XfD5$tgZCd(-wIx0I-fvio5mPuu9jroLk ze>6LEgTk5ux+JC$pMbFUSnv81Dnbw-gpD7O#`;wj#9~F;md?(wJ?T1^P|i<&(P7=4 zfsODf(}Plla4pLGZMy;O>(_T^;L)$W#m-lmc*o7oF0#^%T=*IL^*K+-Dq?53;{A@I zuMZq}`sD4ujex`fVjn(?^b>f8D22_kRzcr??}lw&2joPz_d zwieVEd4dMZ!h*zz;(gsj&CczqrWU6tKlAI?{rv@8i#K3c)AjLwyEDK_3ulb8`}qh+ zulk2XeN^_UCA_}sa6Tk_gzSoX2XsbK9Ntx1hq;4=oFIs-!Ir)}**=@N>!ZIiS!u9G zDOEKybAdgg6OSvpt~Z`Zo@Y^>NPE9~A?R^q4WjAf|(P}6=7&oEH>0`$60HKI4Jah&;h~=A<0os*U_SHi!@rkPa1m4Zj#ep+uf}?zgjokOerpr|{=>%{j~-iD z2`}tS(Kek9I$2zyJp5{GSX|oyW~At?fTj{iM!F)v!fhnIIl)|SAe?+O?>z*vKD-Ou zM|=NJ)nw^p%egWIBfP?bJNQJ0Csn;?{E{p6ER2`q$mD!ZZ^8V<-M15 z$~?dQ#`mf!)HdDD$;n=za0E!Kz>6Qu**cwX$kepEbw%xpK+>(|!FUS@V58D8CX*0^ z3VTHxn7+Ed>Nq-Ls!>e(EdyQqk+L`4diPo)bF=Ts)R&M+h}87cbf5TCjKO02ixvnW zAN_h4MbcUkqswhdI21KGc@f#GOkXfcICl+)IFE&8*PlC$p!%Lbm*j%p>942;F9Jet zMTMa{Q%5yJG8JD9d@;tu>5suqGyMxbFrt>Facc*b>Fd|Iy9Qsy^B#yJ;m6$pJS?Fs zz^~jA&h8*B80c5KCiINOLO!ryvsd-DPy8KOv%8|ZkzaNOVw;;gZQE)?LQ&1$bWtIP zVvQOYzq+GrZJ;N(7V$UG$XqRLZDrS1zuT;8ZaT~cIPSY$YFv2zt(?#+M&mKR)A7Dt zf+yV2MTuyG0?7!{ot+&XXI`_>)HHsVZ~K!7RtTMWA$W9vRX#1&T2Tpk8v4ijc)A3s z9)i@h01R-`wZ}li@4|x-ARh>MZr7*`{uux_>G8f)r+$YhqNmzkPEAhM#~@r{VBhw(;_B$Bl=x$MmKhq3-u<-61za@e5WK`)is-`hthXJDT|9 z)|qbv96q1k-D#qsr2x#0f6cyBf9k#2n@g`6xW2^BP7f8ylo0$I1NGFTqY`r3)$1`C z@jTzCWaD7Ayk>Oz(w{$WkEXXBh)&{{V**mgAk2-svZgg2OOFZ8A*DbAyb9Ye7(5?Y zTy)PGYwm8`-Kwegd0K}~GL$9O;*j-k4b({&W@XcW!Q%GQcTJAFh_p);MjWr(()g~y zJTbPhs`Y;7ZnhC#7x~j&HeE7G6VrdS0Lc=axt~OYwL(OEF4pw_$Rtw|N@Lg|SVC}& zNm-Q0+R(gD(?}xiuc3{ok6(939CaI=@~SUUJcWT-d^nk#GnV0fxihoe#NFgP#DA`~ zm!9u-S*KB!T3LG>%Gohq@lC=DPO;~ETV&w}P$w$}ArBA#ELndHjlI1+7Z=y!B6ywI z+z0!9R*j1|?DM@8Zrc@9Vp7uNq$H#3yy8n_>VQsR5qIR?F=}RJ8zZA1j@lr^7x(Me z(BT&UmYJ|;Nc6^Idw##Y`Pvs#)8B&X)616f;}Z5NDzB1H-uUpzWR7u$M?^?keN-BI zyNZ__`pWbEx4Dt5tiy-ztjUh%G2Zr(0i7BLo0q6I`<`Qdtat3nQ>#xDh~W`ohgew5 z$DUX&Q14YJ{-2B78bBp*1IoN5O3@9-Z;RFc_`Z78S+Ph=Oq>Aic3`)niDwoNn4X`P zpI`Xcv9L~KsEWJnKQSy>rd^%aoR?Q;zAZYpo3)c3jZH;P{=1+6T1z2k!~&s29+a)1 zC%6|uogKJc-iQ_I?9i^AZZ$(=b{c~RGcaJ-qO<+*VM2AwJFn(_i31Eg$&2&z4&e^q zQ-X`Xv}UMIn~ol2;^ZXgD5$UJ_6DI1FoHt;sR#rO*!yhb)KM~CX~eclHO;C%kO_Y`yLXj0RCKp3a@heRWZl_QCqu1F5ZxyNlHqBW)3tu^Ii^7??Qm+ zE_DIY>;3ze{P|mNpBa;CMrzc=hUw|p=;+^`W|nQpW?w*ZSq<7{;@rIG{5ZJ*;nB<8d z`U?J6tX9nRxj!0glL(-4%lx8^Wr@K>MZYp=`bPaA3%nUj%wO~6-wxx4_NRhKsB<*I zv%0z2^dD3AewYQ3C=_pRTeQ+}FtEdlW&nm!{BK{~r|Eco`t%}R#A#3%=^KtGgi5dx z%p=_xGpxabS&1Yfw_w5##g8QOI@&9@G&TD?jV?c$24OB=#`)~3`+&0ZPg0c^As;G+LCmHr zFAue198PaZH6YcRu0NLmsM_U3_jK8VjWOfj<8_r!rDjskWVs&@ukO_Z*qM+zJOa{ zSF8r|=qAx=?(nbyr4umYJTqeD*2naZ7_jDc3H-zcr6W=g1J6`Pxt&h9|eGkkb#3mBk zT58)~ne)f~e&cp9En;9mg@JMWb@~3hZ*J$8ZHX@$Iq*r2JO!AT=DTwgfw~H2ypq-y zTR^jwEFSCc|8&>zzsP#;c&z_6Y`jt-l|4eq&fcREkv+5b&JGO;i4q|)ijqi4c2+}0 zWRFxvg>0GGBO~H>T-AL)_w)Sz`})30*Yz3a`#g{H7|TEqU%#=cxJ$vr6yi9GdRux< z9`}CzcOM)=cr#R@r#(9gK(@F!GGbI(-P7~4_gFjFN?f`z(K%L@ znu}YlJKmBM|Dxp^SsWMJ-k@>n)C$f^5GPgU7Jjd3ErXB59BYon3_hU%wlBH9;@ zfIm)5KK(9~_jz&m&``C;U2Xk84k$66#wrg%yTGFeVEVQH>O8($z}l1)5fPDw#(OH+ z(jbknkEDy*_Gv+_Spr$PtHVDl-oKv!stJm$v_~zkxJS42o;`b%d2?1)Ucrt8LOYu9 zPg7mSPE8CioIbfsnqZ+l)wOK==>buTrb5WWhs)mHd!1ju^J5|<5qWYg$nW00hiYx@SJ{IjKcnAX(pd;o&+Io@rIQvH z_ZewwA)H?wh1_?%KPHiO6|+Ft_pLF0?~NPkVvhU)8|wqsfln12?%Xv@CQuQVIBw+w zn-)_DBYAM8yF75BGURqt)X|XN6s$wX&X16I^PomUPUq;T?e;nI@F0_-@QKi*g(C%Ubl1bK&QZY4OA=v~j;_1vR}QHv{p zs0SxJU%wAz|ylwbU~r?f-!_3PK*8JRdijA@Gz z$HC!Ch7QXc(x@|GF7PRd7u+(_1!mZa$4K@LO(aDcd>D=Yug@ZGXww zDC9(1+Z_xp%(FZ%d*R0&aEPtubC^y(b<|p2UDz)IWmjEX+|Z?EkDn7x=Igk(K0R-0 zKsiIJ&wsroYCn43<5yVBt~n24v7~QdQEFL|2TjO#_SBGudGFF6st+N&F`vD3X|tDJ zOP|Zxy|%6{wI*!f$CnzY@J$BC$;FvZLKZh7cUSE+^nd5|y|!lW{&`;eK#2=KX2YqSY|YCqyjOxt$8>jmsszr_ zBbR>#K_^RMCE=(v18T|Og$sL7o#v`i7Jt|LKK^L!lO3J8o}Or5@zt0KU>zh06z6PL z$$G`Y5_VgYupB>9dr^Wv-+ISF>VY9k$-ibsaZ1v`t_R=E|Jv{g?YC)5*K%fLbu20< z5Ed7wdD@Ky1dhvM=a!&ygOg7mIVfz^-?7NEEVi~TVvXC$tH7BHiM}j|#I=l~cGVI}N|=z@ zlknl@Z{54snPYr@KymjTt=SToD}lr@w{Qttht2*9>^5}5KOW-X$TSi#d_YYa5*qr@ z^h;LTrqs!W9&WuclT@OoRysToz|zdlPN#tJ}Er_RS{OqM%5fHsR&u{4ir*Ln?fF zFC-_WdnM1D_To(8`mwN;lvFLazUH&0w)Xt&D|`F4$P58dQ3>CoYn{U-zH|}6Z%xZa z73f~LDX2a__oS-OehpGR!qQA%e?e%Pc-ORCr}2+(FFpYhuHL(p5;$#VeacRc+jQqoH|2}kCFD@>wenM5_{h>pL9!Q^M?_<{8rzI~{ob&5ft+SSk^ZKVA@1i0J zkq}rNMf7q9(VfQ1r#aC<>5vyc7fFI5x1H1I6=}`qL3i89_+QU*GUoYj(YUa_@&bpY zukSGxYBMvlmpJ`W*`t`GlF2Q~{A>F*dlttRAb{WPO%KXovVa*GjbpCtx&8aD@@3ry&pjtfBX~;QY^?E1?%QWtu|umzgo2s*keR;4mZQWi6M_-n zB)~)e)fgXIe|Ea(e6o~h<5i-M*4^YZ-GN2<>j{|O7{#6aroM&X3~YOSekSNA9*i3- zyZavtSRQffd2=k+Kf942@z&D@t;h?*rF`oRT0V-y#{$-Uz>iU-YmR~Z#I(eF3R~!X zSF`H$j2G)M{X& zlCX~y<1*IB1H@C$(HVTrak@NMy>;lQ^L?PmLG2>K!dX_9O&_Kb&*TON`^YwzUA`zz zIcJ2rKd-ef>vmdN-0TtdfZ;fx``kJ*m6ZxU19j0X3zq#S$4plHPk?A~K%?F|JtJdy zWCX3g)2UMy2e{yY*g34gb}2Y=9LG}!(ZM5yFFpL{ zhqiKvqEXJ!-d|MIIANwLFOM#s7jl`)Z_$)Ne4d;?+xnypR1uZWm4Qb2y#rIfsk-uC zT(}Lr+REOWq)S9W#A)JkUe3gl=eb@v(OtTd>aFFUa~Bi&{zBG+Yl8s~n}0ja?FwLm zTR`o`5}H;S)$Gi%T?8MAwDs87SQoA<16Ur;S7IZ&he<$+gr7LYH|d7eV@wU3JF)$?K^*c0r7sNZ6tH1 zU-<0G>;YBxX30x)gOt?I+wKT-!6B{m{oPNePNflVU+Mf7CI5`Vb05=vkQVUlS{K;3 zm#Nw2q9+nEcbpkKw|6UDP?}|lwoMjcO=Fsw<$``<(5dD|`No97llsDWt=B5iTN%!F zzuNl3HEXacHEVpxOG#N7J;N}c@ewc)woj=s-A^wqdbTk4y>8jQ%$fVK<6KV#h2Fin zNGNhkdq1RWC!_8zJ~~l?TbP;ShW#9Kh-xyNeL{DSC*8Yuc6DCPT4D|-I5thnD>2f? zdqK(XEpi~X4Npy_TG|;FT}WJ2=8QgA#0_+|uwciJ7BW!B-JpGjrtXSESafuaHfc$i z!bL5OyB`;q10z;Qw^DfTICA7y=-w&*0J)Q68_#@plz7X~0_LqO%f|En^?@n7Rp*fY@KJgyUqiqV`nwC{EWOE6 zpXPdwdhXv(v?FA5RS4bpetP=H8BPYbh>cQzhP=G@{JCb?w03hkmX{!kD)1Ssc)b?- zwKB(UcG={Bg9EKC>o*_tw9ePEa@_9t=lWAz1&tjF{PtWPZnLqOO>GSgdI7_htiKyu zM@L82$+RqsSC=`Q-z|Q!mOF6%udr9S@%2jmBzIo8q@_Ve{w#!aX#!xU*6sY6Os=S* zuFib)hbgCrhj+F|#Ye}*c~uN6vId^A zD$AmkJF1{6PvS<=SAg87{BZ4>z{KNb5knRe+o6o%zRJF zM@38fbD)a5;6~JcEMj;?lkR0ic)|M77@AA+9OnNxv~Yq_5qJOo`+)s`z)8brey|1+ zEU4rZHm;~yq`F#|)K0NK!lP!GU2vnf;^G5d-yW9MM$P(7VNc~AB5H*)jWU($Kc-a$ zL0fO%db4t6eDSk@p8R5pGW>b|ikKrC#J9ZZ; zU02%z50GDL4lvn)?6Avi`D;wcqZ42@5x4hvU_6?#zSrkNV~eIHJ0GLhnVjI{s)eTL zSGMjQ!DNt6YW+j#1(TSiJZ$oc)*5=-Q$CfBEty!^qYF@yzWKtKRme}*SmR_8WByr!+L9)42` z4{21i&MwmMu&5~GmF{ePPYEMk>7y)CVmTS9^IuZn5!TXjR;6JA20B$=G5tQ?_#Mi} z!?RF{J!a3VvuEdr4ufM^!8P;!d$@Z!Fjh*+M`~)}@RO>~KmKP(Q7BWguxP3dFK0JU z*#0;*^#ILKAc(Kvj`{kA+|t?pGn!>{EoWQ00;?ZLAM5=UkGZs;t5A!JipuctT)oWV zHo=9CCz9q$HSs=*qBIb18tUuUf3{n1Y-|7((^6CGEOO9*-U1Z=*j%Ws57;(CurWR| z0`MUc6x#Mo-R~e30-+vn&u7WTgbB+&knZzDI!^ z0l?mi_U_=pEfSqzEXT(uCekw9k9%++HZc)5;%dt0&yjI);_DpT8G|Z7D8`9@Se+&RiNlED-{ei>hZU`$-y@5x} z`B5tncq}Z1$bl1Z+S;;0p@_SEQ)l}^<*OQg0lI!{`sC$jI5{{@yVoTqCW<$CZ?u4RA?gTwBT;}ATe<#HM0%B)jId! z={|uHH zVMT8MHgAh``}T49k%WyLidq-8fX)abHfmdGW(X-cd3ba*VC(2tucWO8AI7lI7nQoH ziDC~mHZk$|(W4%0_Qpn?G=J+(^)th5nz;GaQs+X`u)OdTv^dkC&iCp z-xtRqlYzzYDp(ykIXT&9flGD0GAvBkY4kgW7?{2I{bc0l=KjRHhm0(u<*~}MQX$qs zNGQ3rla99~4_hX&MN5<3qr7ftU_cN`Iv&SX8&0=P7rf*b8lC!zjabxUVZ@F zsZvqp6fvj0I&r{0ukeSrSI7t2*|E9e<;H7zcR1u$7D}!3;O5HQ_Y9qs;PTZiKkRQ( z32fD*3zV1T=clKpJ|Vj}CwEAG1GPEWR+E{!!js?7!op%Hu;4(d@X@2pfo{i+RZUJZ zySS*Cn!0ePH#Y8q!DCU8=(%$%u}Z!bLIMT5a)eP~&^qPmXIOPS{QOzGFdn0SYQXQ> zj>P7e`-jgreSNBuZJa;-eSufFIdEyx0fTIg(cyk`lpGyxZD`WY%kGydo**XoP*KFK zxf>SNoSWO^FVauO%%evv>6#$KMTI2yoSCFvxzP6UBloP(0V42a=a&P! zUbyvr{yYRn6`S@R*a|VT9(?q&pr9h~%!LaA1{B)Na~C%^stP>o%6*t`PWD{dNu6t| zzwzrQS>U<|B`qu&`umPO(co14SDtE~IgzVGzvMHq($uHEx>uV!@t!F&tf3N@!P`@= zYHeE>Z+_&vmD(&aliiPGZ1CJh(>?azzxF6acy&qEH8r_NYw~;9LN?bjVqMmKVBhVV zT4D#pMC3!YOc>nV-~M0NRZeCm>-w9s-`N-&V6~O^#wsCmN(fEX_ zvcVFA_{AaloRv>vUhhb7*Z_y}#g! z5IuVIXeEMOfvU{G#%9*^xR%yUZrO*~*^G2_caoAKOO;Jc_hoH;zZTK1z}m`|CYO2T z74gq|2ld|-$`&1vtw3q*?CN^o*eL1JnijaZ0cOSG@^T$xh-XRmm9Z_Uhd54t*?uy} zb>-$+5i;BT1AsR4QaQtJ7i5}<<&cH_Ca$2~++!kATKY0Ohl!W(exbUibst0q-Szb} z?lPs-={MRoV2)EG{Bo|)o*1u3ZLyJJ(+jsz&T9lpjXn9++hblpo&FE3A}Gy1Ndb&1(MMw=-Zy6=;6<{Z*=V+nA$6;M zdBDpR;;Z`VAkaex_-pB@l((q#oikoZaxfTKcYEdi;c#D61kJU-{fWQC^Uc_Dns2pL zbEdjl9|(+Fvb${TRWW?5HUvdu%Y(6alGM;E3!_~i{UntG==EA0>rLIpCBDDd{rtD` zz%F2wKH<8xH`nyBt|Pm)5Cf`sYQcqxC=g$O(^s}|q(N>_SrS5r-%y-%#R;+CG4=Ir7!*T1u>J|Ussj05;f+Etkk15;bMUL}&= zu8CZEPr40;Stv)n&t2Le83>k8gM5K9`_Ur-|M2T9FVCVdsho;@$?}Qfu=o#(BkY?3 zj*S342wTbJ2~Rt{WxRgQYj9j(m$c)&=D+J1gYd)scd{=`XcG}{T#d&rxRHF)mO^B~ zcE=8-r0q~S5lPooEwFR6NZSXW--3!T+m~USzggbf+1wmVZev|dE--)ATlVZwy@#SM zNzq@HiAgV|0ukILk~qYKg)26kmL`WI<97dT5?5V=$m)kkU64JS@uWr%fICU&U z+ti07tkwxuacA@xr1<$0Xtk7-_B7B0D4e)voSX30d#VyJ4bZLr<^uhUP4WW2GIsi%IWq>phAYk*o7e6Z5oew<~qC2x7iLr=6N*Wn};;z~2dh zZRMJ}^oBNdfARe}VUktd?1#pzas5)FCH?(2)m!!0@SHa_HTm2KUU;*jfi=_P*Bdwm zB;CKw$|cQ4Nd^XPqMpLqYYP#a6X0OLkbV+NSQs@OmX`L1QBh~-J|U4KgB+23r5j|S ztA&My^)F+N4U^a?R+~T+P;z-qlHK8&v%Z$PI^)!lf%j=?z~1-C@^`m_L&m|mAh?g( zh?@R*L|*2DO3H6LcdXi5?jo73(a0rm*|@Y4Z`$~w>~yrgc<$A4*Jel8t2s|DpS@dZ z!N{;o;HOq-+%c&We2HpZMO8~{;1WYp`jIm>Oudy2X)bLe3yCBTw3A-xo?#2IvSU7Q z!0#`!WH9FXaPp^jl$A~!M(ugODb6r4Fi5$y&gUGtn>+M(3aKwX&P2|=4CAzv{L+MU z$BNsbJ2ZkrudG+niL1jXYPc;n{%M2y7Wa>o>|8=t>lp``V{2c3@Le#X&*l2oEFqfx z6f4@5xyx{!+kbSazSuxivFeqF zXveW#8R+irMvwmB(4%0|ZCozvhJ=04ki2s+%)eY*T~mXd)DEdsS@}GBbW%@`%JGKW zo{zL*6)7&`txsV0Bq%6|x9RHDt0%7SmcJ8R3?sb=lHKug{wu=ghE&Ndp0ROpak0?KR=9)dc7FXz_kMh6Xt9pP_iobIj~{HAjAA2~)~<0sQIeM@72zva z$_x0d3gnD`5!D`p>9@iBM7t?K=_A_>^SWVoh1;U zy7g7Jf}-JFVO9?S#7-CYeQQcCQ8y8VH!S)<%aSGe+n(4!lF{d#u)j4t{L~X?!?*Iy zf}r=|wcIa_azjC3fKv?d&R-%5jpeW@V@c`%l=8)t^NM9%Jf9N*#$y<(4kv1C#Zo^IyB{7Kn&dgec6{sCoWDV z`(L)+TpeXoR*(EwZefwtmj3%{I(wifjoEc9a+suNqwNwCcQm}`8$5l=%j;By2E&;q zC-)wPn|j2d@o*iIx$q+<-tcj1`t7(lAPRKPluyUS$77`_1`EmET^-^_edldVOv2I~ zdn*p5-+u7G$oV8(AK;NCBzI})%;?AOtG$(T69ZKxvve~&J4dxC4@-bl4m2O{|LByh zrjbSI&KcLmP`|aMr{6z*2ng`KdCLEIurv4%+P}%)M^w!p_TIEZp(-(nS*!hB9ABoovsAc0RRuJ&&OU3fcMsA6s1A_C3!^ z1-Dg_4n74*ip=`R(WBfP9NUSBAz^cMa}(2(B)A0D$M%2)xL?GswZDR0CHldQ{o53n z)3kByQd=#CdiOU!vMjvtkhIwE=aN75J<%H5nj4)UCJT#--i(g6e0loWY}DaNJsf?B zGaqRt-J#SByVB4bFkXA}{b~0Sa1_{QBO@a2iG1%8qAn7TrddpyeC273J(&a$2f%2(?>9DkX07;qb+4-=3*m)fk|N5 zMG-->>nB3Js}3u}64dU9{0dyKz$GXYhGSq^nJjeNVNVkJThqqd=xu(j-#fcvS~!@$ z=|{7{BCT3OEFzMnKdUC9+87fnfO+T6)&UwWxJVCzJY?hgy>?Hgp$6?!Bk}9WNqPO> zpRr61zo!=w5vjaOl7I&8kY?11p^_X+)MP9jEq`6#UAf)bG*pL%PENWVA`#ZRgN>}k zVnx*DU+Yne+~SMZqN1(>iYb6CpViw-14+Ta!uVHFk4x8|Po_9;4ic94I4h-m@_ThOeL z6g+?N6W|qcI^?~*B?mc9smX6c-I6xElI(=JTI3)hqT$}}YKd6GZ5>QaDKlZ4uchy$ z<$H4YMWtciFJLZ9`!w=c1N`=Ja6Wr>_G4Ap%)S`)2=C7~lWFCoc7Ct>G}d|k$iU1!;PJU<=KHQ zgVX=T6er~P|E40uc!rLtn48nftv0Sc(VQ$H&GQ!FwRITUD@|lt01`-3_0T02UhBuo zH-l0j5_w}FJ7Qc&6BZeX=i*}s8NJE9Q~d8#hrbr*KfR?VpbU3uRaKIr6;M`jhQW9A z#EBDNG<-aAN4^(cvjwL9KpVYBcwts(LRjr|MV>TfDhDwhuoW&{A zR4Xbf;BI;T zxt~9Oo}E2Yt%hb3!;Rr~(E49Ie-3*xaofsZ|8S~Jbzcd%_Nx4ybup>62rg#dey2Y3xuz4OEHroG_d!rWx<5oqbntKUjBJ=F5 z3a~H7I71Sj{aXb*;PP=s9leJCju} zEnki7)2hM0di?aM08F7bO2or6Q~}-jZ|QTzu{HM-KB4+aimGaVGkRj!EMK3So*!<5 z&-1NYdVtDM|7L*U^0|X(EbTjgC8h5^P;aa{51%)}8tst`$57SB)WpJr7 z*mUKCO6RMyrhs+;Qt~9J!es4QRa%ZQi~R~O5C%gRoVwY&umbMM~x zGe-eKLqjA8>188mVq^XM%5+c0;NY!L|Rdt2=;G5bYAEF4-WH%%9`Xs!X}EMb2Tk)RBr!nBPA``z=f(K4(KNBKD2wyw6|T zspS)X>sHa3gyH>te_XpC&;G;X64a89O#~kS8W2*p2K#O_R(v_v*Pr0zl|2Km?{6=?gy| z0?@vG-MiT1H-*_SlN2i}hRlxc?p;;?u_?^_XH%HZUb?#Xp02kyVIM0DHk+c(r%dzW zGKT5Cz_AcA?gKDu$Tf9~^l%!DiI0Eb(sm9D8aYh__r!*t?)J$vdyy?*F8Pjp7ZK43 zIsdTPrKJ+TFUO7?u~Jgf)zUImQZlu&GE!3$l$0cM(ZC$l*#^4z_Fp-ppioKhU5kE*T7CWc^}4A2 zXc7;kjKG};I%xxpazWHWueOxBrCnN{mOypX(9rNvqT2jG47q_2KSAIABbrG{8wRCV z3SwA+#Y)0ykVseD&mL^L1rh?qRZ_l#=M)AS^R@^puA*%ny1B#Z-3 zZy?E{_&biNvceGzN)HWjU8>>#qG2`r2Hm@u#l$jz(_XzyO+-W;xLJ&Q>i#9$9#W=C z6$uCteVP(nV1YB9YjHRubKFL4Wrq=}%f+*Y+q3kAwyU|iCQ4s}btqUL6fEiJo6fm9DSZnc#=c27HErX5LuR?#pKiM*mJb1UmZt6xs61D+ zwkDH%YCgJSx!!_}&5n)T-rk;iuB&eJvW-n;Rj3ArJl9>B+1W*0}-OH4GwnPHcR3Orv!MnZCYDEP8O5Gy(?kXUU8!O&T6HMTuhJ-0VQ*duQX zZ$GC{F58u@+m8|4m}UK?dzDCF&MW5jH5a@&?1|>^gpfR#V*s-RNJO~5(eTr`dUb2r4Fa6N zLDnW;86vcm72AvhL6SrI>nS8blC~X+syqH|ZFW)8(nfhHugs6NT$X-NcYJx82xjR? zNlQ%tvaCNp{)=S!Z`C(5<4vWLjTCFZXOFvm+sU`?*PHsMif&)C>MC8mA%>>Bxn$lsiNmcG8e>U;6c z3y&TVPR`Zdx8}cpb>4`wvu&}N8{Fg1z;0It1OE8fSODNfFE>)c2nS`)?~!c7u2s+- zufyz*!1K8E>gYue5CKL?&PL;r5I7~s$jG}wdFR#Z)fX{L)#CNiSEZhR&AgfMzMH^mzPbXf@YTLwt`3Hc1wrvoC<88>%;~AScUaG zchNUcW_z5M7sXRhI^EDl$uzf45)o?qfR2-k3xgT^f;rLci% zdQs`x<6(4(N^+u9f(AHPhJ*;@=*KGL=3S*dvbXXPDFI_h#(n!pCosmAX70kB#PaUg z&IhuWZ|F!lHZgFmWZ?0T*skf};bHO(ys*a&bBAPLS#nm^;l|i9glX`Q4{cYsVzC8*^CKub z1gHBh&+?zsJzdx>nv%CN5A3-ZcIPB!9;&lcGgBmunQEbQbh!56vm!mFr|9Gwn;TTKHmBIywgA!7)QKb`-0nSeJ@IQ6HE=arK38q zy&Ww6MyV&#{7sY4t12+;ySI~=m_UJbns*2bVFEUsZKX&o?r`;qNbUGz-x=}xN^L94`s&pZuIGKV$OrQe>b33dPL;v;oTmy(`5qjZ>U~S;k~n+k4M=@n)9Z3mp%9P`hLUyGid&A2V{6H|Pcw5LbovST!;Du7#~GeE5xXJ9v+H zAQK8K7_`o4x$pHioQ;gB%pAYGcQH!i#;M@AT_?zv7BGFXF2eB5`Aasf9#yG; zA}XqO3-2i>Co-1t{*e(bW?gxCR(}3moI{$;hWh^g>v+$>*ani=KRRmsxw`zvHa=Eg zU*8rb*VZ)Baw8Ib#~`M~6&EVo-$F)*Zv6G%nSG6~;W0+H9;kLVK7N?xWoDtULzCNZ zWBker6g-pWUt+Sk1k5G+mDmuz)8CZn^}%&=?cbzP+1Vx3Q|en&VB3S{o}v+KHP&Mo z=IYGS|h*m6(J2Ej=r_6Y6YeX>yLB47I*C3Oa0}uIP_6-&+KZf zSC0s7&a(Dn7bjzrDBch-tR5SIa@0H0dg@7L z<{>}w{j9_Ef6gz}5WOE;_4my!Jzc)deE{>__h|3Dq(e5mWawIldoJ$OjPwuf?zR|G z$G?Z>+rBPpID|$xcGoqJSa!mYc-Y3(HHw80G!u6okKy#!r%dc=m47)^e~3z}16YS+ zNpMD4b%&DiAb}Y~I5$zoXB7U!5ZvO|3)c_ zaA4QLH3l05JYeoHqBq+XX=wZR1Fc*9g+qrsFeBsYXB%X(@$u#4=E8eg%aA8W_O0^2 z@ZSKof4{3Q8!$u$F)=Y*cZ`&jLjHcc`SUHbgyul?_0Yt`1Z<{$eBo15Yk)8*4(XNA z3?ivFwLZN{h{hKtT!)mfKLAHsHz1ffn&WwCPoKKGxEyw9jDyew+dyk0BX5}B<0T30 zTQK;W=~Mqn=}#@!`&v^|Gk3C~y}ezp9}4Zeh}AGU{sym+l+C*>|NiVhN|By2>?`Ib z;vsK6jW!(C;UG=Xo3-`ySS~s^eybbmU{>Z$T8y*Ux6e=gAmI`={zax@L^-6S+Ux2@ zVN-*#2|*k!t*sa$X#(#0?>hIQI_Pp6krhXQ^$-FO@E8I%LW~B6Wl)@K9L(`L-!ZbZ zOpYghQ(N@sE2mBVy)&ynQ&Urs(}jKD5?XcG1AnNm&qTLYRMbbsnd#i_EVXj?n5qXR zK3JBmNCOH31cXWQ{;^kNd=myJ>OW0I2->p0SJI#Q50DmPJ@R4;)jgnCQgE|MxZmFx zfhG{Nt~uM?aJyM*dm=+byw{@8E(Gm!RMcL=b4preU>7)?G*GB7xTQVO{m?qcDGc&_rL=2DeXP`ZW75- zTa<0EfjVviY6f0ibRZg9TJFscuZD-K;fLYjVbdKKz}Lfa)N~tY-;>?1#rTXt_zGp7 z1>KUu1|wq{IgTCMPr8GOQ&FR(r3HB; z=)AFdX>HD2xjT%+1MZVlG8zBnw{Gn>UVFBO-?dxO*shN|A{LJU}dR8&`Q$E>WK z><6(GeF#HK*BYnt+@gPWn!)?8-ux+|BUaDSff^gwJWL5!OYRF~v3z@E>O;NI?UoCaC2&bN%s1+(}OEL2pgSBSOt7%*OMv z{ekT73`oCKy}!MjI)@}HD@1ZavZ)H`1vb5**8^sk0xI60Pzc(~(i?y89yqc(EG(Z4 zbC?N8OrB+XQqm;6x@V7MvLK-hDM!-A?R=0()y84oz;=eUsZnZqb~XkrDY(>TDk^~u zx9%m#vdqC{8V^5&T?(_}_O+RSHD#B}H+x-eY(x>I5*7yD1f7UNKpx<3^I{KZRN5;l zYyp4ZU5CFqGl&$}?{Mop=zeq}R6SnMz30YM3o^R%autrN1919`y8pK@>e+dHOre<1 zM_P;bSThmP{v%%Cwyj`*jgjgpn@Y|1{0xR!9OnC~o*qM!zSdU9&zU?7AcM-DJGI>M zSP-N6Q$V6Cv(p&fav=#BLtIe}XeLHjeZWI*^yB`U5fQpZ8!35#V5NYwoYCKZyh4#5 zp;;jXq{Gv5HQ_ghbXeq(TiA2!>3I6&s(Usjc`)Tb8vI} z`g9i-x{Qpk(g-=6a-BcKEhA%a;>7)S`9+MjJj_b^3w>nb9Gsk349w-(nD9zTNgX?; zW%M$AZ1IBEA|=&tW9wb_49c!Fz!VO?Qz*@mNhB`19}+j0f^zfnIGHJ{Vq*?yo8lGH& zysGwQ*V{`{zkmKIJY|GKQS|||n&eE95Bhf1ZBJ(D{N^1VM)It{`3&CScgF)^6AAI# zTwZKIFl;4wcwFwZyJ}sUefQM1s?XI6Vi=bH;Iy3!^8!2;YuW~^EE z5mq7uBfh^StdLNV3t->#IJFXW^dP^kfq_E$`JX>lbswk0b^#%1Pw(6jG`r>K__HHg z;QRWK@(%sSlATmvVhz z0p02cZVn=&fOP)CA1=eoKNii^YIZye|QcVrPQ z9<2S&G0ilX262&Hx^+Aqrltj;z(5Aq9kjJSV*f}lXmRLU$n9nTz3+sVP&3An~VYRZ*L4gM~+iUl>~UoHXr>q|}pr{aV1iZR8-q2}POE zewJyfsqK@SB)%R06sW#_0rRTvqhY*gvsLvv!Dli8VARYgp{}L%7BdDu4tRE-0tB(} z1k?yQj9*U*@ZAt{Q2`|h!7OpSCs_BE$xD2 zKjY+ubr=`|6^+@zmhJavo*t(Vn~bNv-WL#8;y6Il+jnHoyZxmbq({eu(XV&1K;YQ#$CY# z4ERlV1k7P$e-?hGU-jJ4Pd#KI^A_=24v2gMeI|r- zP=A&z7a0SyuY>82Ast9=rF2CB`DicmRXiV#v~e7yQQ&y}PnKm%ng74jBGQv%uY2}wVL zw?H!iRRY{Fkt0W7QrD~M9<%ZAa?~ke(jB|7f*c9tD zp!s-Txg3`r<{G?|C=N`FjNU&!lYS3?onBTW-gsdECiosCPkXkdT6*bx$-TaLU96R>oW;w`RFd8XD3j^Hhp%cLmyS?`!;J zSYG-10E&w3z|o7-O*xogNWz(R)<(0G1K>fTU)3&{WbZy%TD;;fo#06 z$Q^eTubj%kdS0pAArg^7Q4(W0g)()=Z=;b0elnaq5?^ZM-t#syDv|Jzkl(r1xOB-&m!)xRj_wI#{;lhy=H_8>gukARV7ku~7kYxs> z8AP1xSKUe@mO(0I+oG^vPEKw}!r6sgMnT(adD^li;%IAYYXJx90?8Ghb_Jeo3H)4P z!67<(NHvU%?u;$GM(EwjmLL9wGaQjj^z<3XgtZ3f9vTt?pj$cY&c^zJ&I3W@j`POl zFC6sO)jT7&U*~SZ!FPkP_cG~)tPUDrhgW;%^yx_mOWD}jzkK;3a#7RBMC%oU4lhq{%u%!bvHCLY`<{qEc_{J8i^y zL{U>OeKEyUjW;81rD>xL%A&aagk}p5BiF>Ni^K9s5n0&>a)CFO-GePi_r>JrBYNNh zW!b}ln+yuiWh)=U5*``?io^6)z@v3ny7eBoZ&g-Y@&c1F=o$4_o}GHU)rKVPu3s7_ zruVxr>IrPnHI9C`U+mJxwSWJ95Isb$;$8gyWQ<0*k$#k|RBrF=S&A2@drS9Kfmn>- zq?iK6=$tL@uRY}Lo404HbgIHR@ax@GBGn=aF{u3G<8Ai>a^uo$Zfmn2k_+R(!!ES$ zMm2BfDrKbC5)b(DdY4KN!(-wgFh41NT!Z?oz8I_;5R+KCd$IV$dk@Xtd@)|i^E&`S zR0@59GYS2YGDqXPcU#mb&e7SbDJUq+Ehrtty6bjw@`vqr?Y`t&ZvtCGztz~#aQO1C z=kZ@QFMj-zsmp#!69GH3b8`b^zSG@@2TC}@??fS>e^ zikHYiX=#S?kWM<;AgB;9(TKe8mv|qAw+*8|$g}rbpZMbk$S4CK!}?n+6q^Bd#L@oE z+rXn6_{GHeVVUnjV{7ZqDzKz5D!RHdPL^Ems*F1t8#@522+$xovzY1W@5fL$oiey@ z=wZGceg4hJNM>ecpZpWY{1%Uoi9xIkPEs&W*qta~gfdT1&~R0cv)XZ4iLsE9h6BkQC!^hhoXjR+t@^5jISPLX=C%)z)@%!da-`Hq-cD!wr;pIj+{cle#vZZ}vjn zi|xRH1C?Pn+_38Xv71@?qz}`>jEqr8VOKzUOcr^8lku4~2bQL2uvY>oHbwNdZ1@w1 z%uAD|2vEWEjCqhUldaVDD#iRByQ*g?eQK`FS#)b2e@Rl8vAHZaS@;iuM7QdUm)B4j zAY0MZVR_I5*H(BW~N=XYcqJqP>x1L?iG(3K^@+ge0-#|CvsNC>X+_{7BBp^vZp3k+Z^^llMMp|TEl z{nMxU@2KeLrdPgVo+Bl&V#e9cZn0NSVUUgajE;p}~$`zRsP*Y6NR z&|>51S?IzWrKP3esIbg`@ZwDMe@$uTW*@Chv_Gh=b%fs>7Qk@HHqE)?`e@;ZpO-aaAwk8s zdNlwFsesMP&u2xco4h~}$GcJf`)j&=-GNeJ9-g_s@rhw(!lnV_`?*r{^1{Or?<_7M zG1C{AF#9d$fAwZ-=~`rgbeH`7lMo*s6LXy3^)~hq#naV73a0(K_@S8&P1~58=RQRW@Vg>*;p5+pjh*_O)sA#}>{}zEkVwaR!|y-xr^=>+GilA`*Y2(40ZJhR7^z=WS7zmxj~!mkBIUdu99<4PT1MW$mZX2XBD#!!kG>v@B~LJvSU6Tbk{X9;(5-+`@bLx zSG{N11Ox!(!rY>wO^_GgiUViGjtv-#gYSpzB`k@2FoqlEK%|rDHre6A5OdcU&J7~( zCw#dl{|bqqu=F{s$6$4s#ok_jeU`j6Q};Za9!auS%&!qg5B)x_g?s=?Lzs&vJS;!J z&r3%ow-O*|A+77otsbH+DJ|WMT-1i!QggqbyS4vpd3>qz7s>m{$&q=DxA;@xX{3Ey z-cR_gj$^H{qvSE~%5PzX#;2&xZmzCqJi}x2#r+l$h(OP@Zy)V>QYHendSZCwWL@JB zli}0GY@?h?byBQI;%Mu&5DP{yBzPcLXm5MD^*>lZX(Sho2r-M%$&=8@?KR!GQ(aVI z|NhIio^>D;LWTs_AW&%ix2VN{QrZ-Ue5%U8;ad#JLeT}H`B#Q1_eOk-AQjBOz=E{T z*9B!5ryr5Q3;^Cxr#00rEwStplJb%71rKzI6DOLQCY2J*FJVU4Y3e#p<+Nm3fL6G&@vHy|Al*xn`)tc8=LbXy+HuUc5l#$UM zl_;680bn)zWnMk*FS4zm_%%Bk0YeDDxuE0D@2Ye-)No1{A6M6e__ly+R@usqJL^n& z!t=-W-89&Zu6_c(m{KwWQ6xNke9MsFB7_c;u^nnhy-vnMwqDnQo$NysJ>I(hK7;kkSef6l=`T=UMK$q3<;Cn{w|6; zEKoq=#a{{F=Vy%bFWR}YmLvl(KWwPyUYF4K3ed2|%lrBLLbNm@6Sc-$m)*WS&jS0t zq~z$IHAGe}!8fGw#;0wnu@LRbxqVHToSgh$34jq!rq(C-SJ-Qi_q>E&#U#WO5Oa;@H4N)IEpJC^-YU0t1- zNyp-t$iwCxhrEjLjGQ0gw|?C^fDIcs7?npUNV87EbR0y3V(%%lUJb_80F2D$ug*Ne zP9!lADpo@A_s^#Jc^Z^!wd(6ma^<%_KhzgkssaNE+)F})n^%_C;Uj`@a*|i4J#YZl z%0{=+9bH}3oyUmbp~1tGC1g~4fJnxS+G=RPZ=O8==5WYT zijiyaacVspA)$P1j|EMOf@AfdX@7%O6k-xFy+^+PZzrSw-v)oD=H{LBr2Ad1lJ0v& z^{yuMGJo@C*ni@d=h-eEuXp0;`(xCv$qC;4tr})Ct6$j&(jhPc&RmNp>a_0eJwi~F z(F3O7U3{t_bgS~z1PrJtK|1cTVgH;e4`aH{a^3m^n_acSBRt|+XC zP`V80z|ny0gi>I<-P0L-4-n}(KNSUTNP{Urp~M^u);b#(G1w zld!yvuOW1x`I0#F*5eA!ZEl<(qr*f(zEAWQ;a5L-0ow?7_bJS+qVw!vDEO?-55r^= zoqS}>Jpccn@;r)ZI2;tN#d$x7fE+!n0>_%q*4CyU3S9fj^wYY0sk-xE(l}Cwtkl(y z96NToHLZ`Gt#rmdY`hGmHdJb45Mw&fyLp*FGxL~x+2iaqwTrzJr!@ouIsz-j7&DbUv3w{h!b${3pX9@2VPTO+z{M=VY$lG z1*f5xS9e8OFN|YBc>xgr4~-;Yr8Nc83BxjOaI|e~-qq{C;^HQd3-_Kk|6gtI9napHLVd_IroFgYgghTGAk>us&c}s>Ga&Fk~#!maus!>%+il2i+>fAYL2QTywWo+SB z|2yW2oGf9Tp1uLgH?Xs?TEJ@u0G`O9^9Bqsa~WiE?`UZiRqmz;u6Gb#|A_24mkO!rTB zWT|3a^R@QQJiWuvmq4iA#au@w`feoJd~+|=z>2y3ig*Z-Nh&HZvK>#LV9TjG`NK&?eK4M_A- z?joCBG|Azw#P8g~5=0>&FvE$L>hS0*;LRBse9UoM`S|&Dw3=a|kmm273a=_86_~<8 z{9lMJ*k)E{W+;i-4pmePzQnOyRrL<#-|#09KlDW=V{M1|cpZP`-`&;;zQ{ufsc-G= zRFZ5Ly=)T5?Wo7As=M;nA*%}SZOfe>+to4Ya$CY;T%dL=pPlLSY4W=?>Bb|oR}|@{ z;mh-fQ=8&~cR>24_Fi*KZcYvqY+8A9uu~Gh)0J}r-WwSA9{DdRPzRtZtXWEZnqR(5 zJ3@jfLf+fo?Z7EWC*BzSi#VOpAh$#`CWe&q z(v0d8!|aLoaGXKt7rGV{8zRrk0aDv@4C3Dp+S)CkH+B6m(Fl)> zTmu1L+rp)Q*$sI+1I&eY7U(5?O!O=35k7|C%cUt43xwC4-!Bc= zA2>%3&^LmW05`k<>`+?B6wklbDFlAOv2&mf#%movD8UQw$W@7(Kl!}?3XSWfInbU= zx(8OAu5QA;vKkcieI|ESETNP@gcq-{Flj6JRb?{_!ZgC*dtwLcw3p%~ zkonGv7o({Bd~*kf^8Sj00epUiq8F=ZRRA09{O@7UuW5}jha8-G<>Y(=Gmi-fG=KY6 zSz7wsEvo7HUUud;uIsB27@O!^b^bFes<=m_)!f_l)TzXV_2&1qOg%q=odEzh%DhN= z*eC29pe$S*sC*}x32%t5qocH?rNEyMvCYU56GdGnWL8|d7&kUbH{;-kS=b)3+dM2R zZ=<6_(r<#5$U*bHEIW2!tF&|wWD~ESXJkZpd&`uFppLcj;b3n?5Yt7O-amn_w=@^U zvk(7EJoz%<#`_qW6+jMZO^rjgv96N%R|VshL1w2 z9c#|h`ThzmpexQF8X9`5gYFyWT#x$uv6n7oM@sDsHr?jE$$F|O0Ic~bCnxoM(W|C- zeq=LKpmnqK)~0$85J2XapzOIya;?#Ibbq@4j8roVleawK786{;3QmW*LFxH=@>qXf!YIspy*HRhn*cA$;pG_A9zolassOX zQN;h!q%zFH(tg$i=t-o2|UlW%Lo>`*f}s;WLI zSUVkz*yhCBj(oe}vM=tj-d_1M7v2jp+rLyo;6IHtwB{zwYgeDc2!E$XLU?!*ii+Kg z5`B0a@M?trafDr=kLpgJ{{_NK(a5xb>XX2TVLzc=QuWiWipMUhdW8gc}hWItt0C=>vG%|93~YY;G$M4CSf|-5WHWAos|9Iix-{1JEwTk!7?WC z!wk!(baSSTz-*OB5kU@)Uy!I(K;+nahL0(D*UtHQ7cImb4tFFcrh=O`$tQLMg1F{S?*z(Jnx!P z17{cU%EU6yr}Zh|B&4L+K)o`(I9(KY3NHJ-oNnu@6hHjljQ$4;c=o~kz-bAIudVYl zNa^7Py|7tI(mH}baGWXVFB2&EF@W8F)fn&p>3oLM*N;4jrei;jnP|rKJ}G=R*sB23 zdzeH;hbN18{10`Yv)ZV8QM_fFh|BFlw^`N;Q@v?U6JqWg5*HVzPzXIYZwO~Jysg?xxD;>m;QH{3Y(V%h?pG!bSjes zXExuBSK-=W+$Or|rjK3B=)iY?8vH>=q?n5)=7T{MP5mr|FR?8T^h|he*@n@@eR8r5 z*Ngx**q8MXxDZ)ccols`Af|O5gWM6i*l^Zg7l_Q-FylrJD-yT;dO$P8{Al6NANRQt zieIm{{)$zF%RngOEZmtEF(2mjq%*7Ldjtx%YZHdkOvxqkz4g~+Zh~TEuQCkzSmc+m zcay%PtjEfkn<==o^}dxSK6>?5GyY-xVA0`?Sk_-)6{^XyO!o7j55x9?7)U^U z1k>nlYm6yj^3&cSJmt5Gljfye$>>WFM~L#=^J|a84FaA6?>`(iYI$a1=CEm+jQaDz zGXS!k*(PW*2AzJ?)N&qOLUC|!L?2EMkrHl@vYp$WPA_owzB&VgZKCRrevA0HV^QK` z)|CgK_51PgU%XK8#4yI0iLoWb@6xl2-bKGvxHDtoGSRxiC?y zBlw|Kp!lFsER+VjYOGCADoe|>Fd-vjuTpRb^_6dJm7bxYZ@gt5gNTFtNF6-qFa^ay zLF4#%7DtCT@`NK7lZ~>u*ZnS#(Vk(Zn>0UTYRYiVc=u~BxS8#M$e+CO0+9=))Y08+ zoWqCLo<~HG_LS=9^Y3sbEvnt^=xkXouFoJ*Y)2wB<^9~%(`hI^C8d^Up`od%--OJZ zsyj{YlK1nW;)mInvNAu@{JT)o&bKlrY6~q-OqTm2eKPU;Zr^3 zNAsMUTI6tO7`e~)_LGA?&CV3vB_XYHt_~TE7v@I@ zNaT~~XulD*2fyAW5fWE?ppeGV=`CK;_v5(Qdcg?9=aN19p;PlC+qUC#odElCF|GZHYK!^u?YG8Hv-(@)l$U$=uPpK?7#jKxpH{iPMz6^c42R!Y zl4oVg5)G*aW1M;nM_=bU)jM|Qvm;?Mmft*?^0+OPpxceRad9$@?Yk^kjvvR772(ki zNABRMY%+IO*QUn@XK#zcaPxbv{aUp2sKsZM3Yw4?FUmMCxV+oDYm8=t_&`>1Ouc5+ zg5uM|pLAR9QMgn8omP{us}bB)q#r|-$){;+U})GcQvj1DLif*4q6^mRao1{5hUMSc zJ!0bGP}&rO9yP9M{F;_>QwPUb#R z`U8%*b;_Y9&gPXX)lcnq@_oA`MHjO?!&qeW$}sGt40YDuW#aE&K5G zQdh*P<8Vdii5;;jti7YdEF113<$zcb2WdW>Yx_fF&z{GC8F@_dPKt@;qrm_xV0GFQ zmPiKgE|fqp9YBv!&Z*O%xiyePLQ2hnIF1ZI9_rvKZX5VunQ z)Ru@BM&Ob;)IBGD5ClyrdM}mdCqF3$g{n;betcN8uEflIGr<{gt2_>5|ILExr58N5 zb=w-s{7<$&82-#7pi$QSDsEb)d_HmIJwN8~j$AhQdEY6z0ru#mTYezP_I44M7$ zDQgf|UF422Rb7f+{U_HY6*;-vXFQ|c{-%8uOnNS&|DAcoI2s9p=q6jrO-6QhNZ9-e z{0qrxq1AnhX`C!q--}bGaLd%wYF1ZQm-_zU(mB)gSui&WZVvjty{3mg&e!+DQe8O9 z-SmY=yNsuTfIDXKCy-4FJ|v~YJHog0p+WOIh@9WLPoHt;`ftGI zdC{1EhRw=OZ9z>pRumdMM|+xr|9K|s4go0eliaVW4Hi2F>Cgf1dnUuAOwVV&{cHKu z!Db{DLIDdFY)fmaYzl~{6ibK>%b#*TiL}){nH(=m`*`?t6(>kIMOE$gUK34idBq+Y z6hukCyrl^8Ff#Xg7K;BELeE``-391q69)HNk41{-E>hArM&C5wlY6)w<6)!t9ZF&# zZ)Jtb?m&XNb{i!Hg)dQUak2g)P37}@R;jvNf@W%p6~)hvyaZxP^VZ)P9g0!7M~JcF zft&`lWpP_kpS|I_S2*__ZuonPBD1pW0sX1A@@rQ9 z;~Lseyd`*qt>{`%EAQNUs7k@-p4Q0euys%y%`RuhjX!$geu6WFjYa2?TRi!BR(4Ki zE}qXdTo~Pdy7jimVRWuDyL)ElVg436N=g*-#)l8zgV|B-k2Q!EpS?T#U>S~_n5>#> zYHp>!Y9&f-D2%?h#48yy5IE=ZpS{e61W}Z?Rj?4UymoEPZDl>rZRHK<&h!v*{6+@# z$=Wfs%A};+*ViYys^JtnR4@fn#!|(jO2Tj*GO{w{yucT~A9xEObIZ3LUFrK5IWlZ1 z|LpPdn)o%gaLKY8N(h&Y(-mc8l)ztcDnX^wn~!Gi~nA0HOF8g8}Fr*B{L zjao%wU(h_44Zl>OX>ZEUymLy|2BTFAL7xubhkfxySpAz2(A3`)4+8Tt5CtfLGJ=;$kJi9podpLLk048ikG)n?G0<(7t~T^Sb9i{hK9f} zJ8iJwPBys^UkS@GOr-~CxN&?;|8b9g^Tr(+us}lLj&J6&&&`hC`MSJOXNIiK1blI0 z2V7+&!)HF&e1(}?pS_GFtl?mNZ@!2`0fpyaf<842fj>O0!5GBRv4M@%1-Ctf<2TUEttV2~@tX=ErA5FG5f*zlvccza*t zct@>|)(|kJ)cAS3$zz(D7y>5cQc?3_!>p?qbVZ~MpjgbhvoIR;4>nUcIO~`#L&84r z@A3jb4Tr7eM{r-Pn-l!C>7nihWeR|QN@OG^AE}1fwvAC9+*ccpqz^s3vM*6tn2*>)SRx z@D1dkpM&$R$oOk&$^_|{fW#}2ZsM@P80|4&gJu-Cv^!ooL>)|Kz!^ZX%1PK$u&Dwr zjX09&0#?Cd4F<~T#=^kvoFW_jD0jj`7D~uR$vx513rO=e{WR1CUkqJy*}8aLE~GHc zXEUt)m9vklXnS^pQ^H*bR<#~@a zkhNv^g)mz-|2O&Y$cJRT`n*NL&}@0BfKDj&1*ZGkXGiWX{09rrR1UL!V_J}taR22? zS4`btUe%2_Lc;Y}LhbV8$tG z0FCOK*}_7N>CyeeBROZZwCi);W2^Azuw+Vo#3g?HA`?U6e66E7k-?(!mHGMfhFs`>4D^2% zeR%_DftB_A;l2BD2S_(-%_Ykxf63wbu1hzS!X5-moQ&25B}c zwAHKFc#E8;`%w|FnH-fJ#U_V$75ueI!b6a%*|sSyjDqi*8MZ?Ra8SgT!u6*SdIRLA zhr+3F^E<`HHS1939GKE9#%huJ`jjsD21ju zGXF3YfJ^;kdxiztAac!c4xNb3Ms>NWZ(40hB})7L%66-sB0FRjFbn|P>YAE|6f+_8 zklGzO#dnhwAKC@ETZ!bh(JcH_7fS-SpSQPPZ2DQAGL2P7Vq7bxb(53mLl*+t_ zccCr#ym=FjZQg{YIs9AfE_4SEszvZ41PT%iC6L!q=x5t}7ze*qg18CCnagxP z@&EePpP!)9n(Qee$Y?PY(s`q8=Dm6Wa5h-1ui(mtvDW7IAKcs+3x8m10%t=4{2-(( z@xm1c#AOWHR?>^s&Er;+lFRcaWi#&h2WUp8R^vXa{8O_YfIbzK$(gkGW@S6%s_I#= zRKPL?@u6JBKbUgp-1IEr-DaYu+I)t3n5IE(hKfHpH1q)TPd#2CA=~foM^W=by|KCO z+y#IAe1Xzui43sX;ZwFR;o5-)ncd`QkuFMpegT2WuH3S}_uO6ZU%p15rza82)6tXS zj^GX4VPw<^YPyY;6^JH;m4PgX`S=M7_kbqDJFDaE#M>r zHl6@{hlE}7`^Rfz*jr&L4^VUSg6*-`6%3BEYnkY%JqtYl8xz(ECT41V@B#7rQNIJP zw6nIZCSkwt3&$}KmtYwIn0E7DQ*OSx9G}3NmQlv69i_erXvp!(amEL6sX$&K{oTBK zYJXpsfSO10%GfWSg@r}Iy6T6%49AY2ad1E))YkCSqWHTYIGEj~4?PXVfeeMtPB{fx zWC;jENEE`U_g7vS5asjtsfFw+x?=rjyaVkW6<6RxB{j8uV}Agg{QHe(BV8#$sySh9 zZf@J1PdHw1LF-6<@DCt+3pN3ykN@+HS}OUw>@}z1u9KIShXNnITsN?dK;OHiu?agM zqxkI>;KiFCp84-S5qDI&nZ8jI(x~s*c6OGQ6^IW1{N(T_%i05wCV@x4`ORg27e?IH zWo%aGwop=1Vyp>!Bp0#k=I|yMx)A3Kp@g@PdLB89^QL_g@4H{oTHTl`%8JvAYK3 z7w)$>-Xv~~>%$!~6nUkv5doG&HE;tS(&mLb7Cl7_%rcg26O+ zz@Po;NWy8E;x3}9a>HB_rH&J5h?~ETf$-}#W@c{R=qvR@K}@0yMd0~QA6+^>Mn0k+ zM8XPUgJ3f&z!`z5H)s4v(m9Ocd$lhY#nvukA-EP5Wmm$9}q>UB2@0_wUOK6S*<&v2sOB0V21qp4Zl1 zgw=Ljyd}ZF8f&v#`6CG3Ct7*1qUT3vMVYCkTwU|Cvllq;-rkCi0D9Jo z-T{mjC}sdU!OEWUR-%7!_?s)u?d^#f8GRsbDV{Gv>l47}i4Cf11HNF6H@`Ex(os`S zSKwc_AR?1MYuhf40mQ__DTrve4WpeVdjgH7`#wK5>dqU?gk=q|$z8h+iWh9;3T`u? zt^t|5^gIU#2mG1LW{0T3rA5YJb0SagAh_x9y!#(sX11jYxqw^<#fH;X4Ew<*!z+H~ z5seKM^G0QNWO3`uc@HAV_dP<6oY%4oj#2S{q{|E@x3G35m-{Rtl@!6M9NM7r(cj?2 zh5^aF=_(7Q4$Z*N{1>f~sD7`=Id*RJOiV_IF0k)6(2QCbE1VeXniVaY6GxvJ>Lf8ya3mOUpkzekM}HaeR4x zq_~*e(QzG;3Nf)EOnG;0m)o-CAp0GW+jB3Gr>5S(lcLA7XZP;VXV1*1LR;F}ns}^c zJGBOO2f}furbY?dvMt3+j8?v#VWZ0YNomjE^THh9?0{+98=1Dc9DOvOZ7X)@(q~8n-kgjp;9X;{mj{Cxo14?9U&d%OEa-?InUY^<%#>yX6kv+LEXF{j@%qPOReab;ffLig>mX4i>hKYJM6PaDk0Tpp&Q zvIfwrdA^62>vv-?d)C|3)a3PMHStrnbIad~4ncsz!QpLcdN5Gy{SLv6lvt9rnA+Of zW*+si4y{p)NaY-R$T=n;{i z$@y&M#|6Jb?-_u;%x#kZ37)XJr@VOYkrF*uHi@kN{h)=&P24u0f5L$6mn4X}WX=oD%Hn*}v$eF8y|KSl;Z^OAxyLcmaG&9rgPjmm z$4pFjcZH9D$|Da2HL9-BPbtx6YbL~aelXS2G&Vj#P6CEON!{_9y1Mv_S5Sdq;Dp`L z@ZE(ikB_mjJ&55%E)%$=H?ku-b8aw)AZ83-7P9L;!SES1BYIKHZ<1jV2ZbkdUJVxe z=-b8siPfpH9kp{X4Et!;Ly!-H+xs*5o|wZ3d4U0aE817&gGEM0R%D+Jc7R11o{xIw zpIV+GfT3Ez1|)X=@_u12f1?w`lF{bZNw$kqeRG#s>I=|)LxiQ5X6#3&(r_Dt32U(Y z=a^wOapdo&bd@BWi0J$28VR$M<;p1aFdq+kTwuMW$ ziKO&|nhJoQ4J0s_a~=li@I>I^daQZ;#(h=yI@ z_^92d&$l7V9f@c}SC~eA{+OR~OYF zvN%qH{3ui7gu^6>Urg-faw6M(KXc}qk8G|Hr*HIw*lE2Q{M7-kD{h@OWZv4WO39%o z45a$@hTsf`-LF2%Ob)F(dV1e}{=AshiQLnZrbQOEwuH1V+XK|XXgu%?a;fdHWhSgA zt4s!e2PCGMDjmH~(8@d@)qUT}1YBIBrLc~z#2)uJIyjt+*VYaQ42+IOI(Ea07*)9M z_V)Fi{T8$OMB>WBrXgkY;q2XAt?I{QG$@>A1#bi7i9hXoboXeHM#Ry54=pZk zupU1yUriz2oI4j8@Cs<5%Tc>aUjBGRI8~fZW+f^$|?iu zh>7!)cnM@(qqL$z5hNrawj0xxjDg%|ZH6W$R3%&HRyx|-!wILV*uJf-M4m%2Dwn^} zd}_Bj@+9;-QB4-Cn+}qb7t7@CmKE;q?%wmgAzOEOqK5bkOuM+^t&IzQ?jd8Rx3{-P zv#&CsEHi@^eYQsUJ|95~TN4z2(;Dq8-<`H-0IjLqQaC}yO2 zfTz@N))1Ec^A@?ndQ=5;n(6W{6vM*V>T+-Wrq4bKvtx;_JwrLt*sQ@Zx)f6o!6s=Q z6ZtU~?b&xV>@oO;y5C2>et_YQZGRrsMq@ntbf4+>_I3ivx=nfzZz>$%5hioy04uUfS`se$T1mT-~<>;00piEHZZu=(af8} zf&$__Ld`t{Yiejo=#n;V=y1aLA&I-YE3A#)b4tc zzAl)WUvursVGrYu^q6C-uAekQaWyxh26aYW-qz#CFM|VrB`DzfQw|8giMKM#SMGy8 z_jBZn$VivPseKg{KG;L==x$)mS3cc|&J!HguVCF%lI%8E6aRR0viq^%sY!YU1_d6Oe9-UIS7-J#n3h-k=>Fuib5L}kQY|?()xjxBHrKykjn>+Flto$M zYjw5x{DO_zVgB+P-`}f&^9h~P**)P_kDq<|l#-h}*c8v%+{ExB*M2s6ZDnNztM%GQ zLm&son^&*wQD!D(LZRH1+Y77j-3Nt~U=mw9rd4g3>tIbCqH1P#JU6Vdkp?99gNe%Q z?A3EB(lNUqfgtJDT@^U6pK_9T`_-TH&bPk`@MY+K_>~eNK*Pp1b*z5R-t`4FNjHHz zf6U{QiHNv{CnqG7EI*_FuNVUXKoR~0}pJH&NR ztX+cRmzdzYEPGnsV%2kx?XF&Jd;4OKo=cw;NrA)7n-eXH*Vb1D9n1$_ff*eWTL!ri z|JC*?gDeTRjWsho(wp~zd==yiL9^xh=W6!&xc};^aW$p<?t2 zRhNsyoGd*xC71bg3AeP2j9fSfJQh3JzdKSPP)kFD>2XBmfa7F$5O;Z`?Af-Gdn-@~ zW3c^HM5O&RDN%1Lv?J5s&UiM~^~xk;zt*rh*s=SO44)5V6xX{`}q zp+BG7-rBHd!Wt9_w^mf7poSR*Ia%$J>$&L@Om~Y% zzJI7S?kFw}|MXN;L%mnr-8d$Cy8hK^A)$|`z4`b8Sy>O9jH{REY;bPZOOsK5SMe*m zBMoz$wRmNkx%oXwDd-v>hPJdW1+mu6O@*ht`EK2CWCebD*PU`ey)1ASUrvlt)!eot z9l#tF>w^#8Kjl=3CcV)xVdioRh_{@a9O1n?vwaKY8BwWea=qSZs^8;>@(WI#vg(~I zy3~=``tjph^6KyGbxh<_-P7sfS62_+c!6Fl4IaZfdU}$x3evImnwny*TKpB3id15a z2F3z6^5#y&%8TCjS*t8B&-d6U0V%s{m$0N{7#bpzvB`@bLtoe*e)jS@nrnt)_odPG zh3eyvM$eixH^xe)pP!^4pQ6+;H0V%|l8~rfU0Yz|Ec&vi>UG3L8Cs(E$EkR)VfulF zqurly@u&5V?rdO~9~FRk3mZMglt9`|dV{G@$*>E>ytmhaz%SXudn zg#&9eG=_5@N9eD)vTeIt{D1*>`}5}^uz5>LDw8F6co3d+vHq3Y3t`%w_xU_lDJbdb ze~z3^n{MRPI|@xndAxROt4=>x=+~%yePn>_Pa1NlpPpY|k?Z#7v0#;QAgZEXk=X(M)kJ<8TY!hRs@9c#03-#HvMJOisGERWEwRmZ- zoX&^l;=9teJ?jGn3p<1)=mAJz8#V6ec86L%v8(G6&}29g+z1KghQYqBoZRQCs*_B8 z-CeoLxwmcmgR`<8rKXN&TBe~AOWcKPb#}zw3=@m98_H<4_7OUf-$pyDMMbupvC#_G zn!jV!cSqH5To7>xu*KKb)eY?0wlY2NEH2JjtC<_xf@hCmC)(7+aO?M#b1rpRB5;<4 zlSD&Hi+kg>r>m>Fqobq^pW@6C7cYRL7h&VEszgKi{z|&wg)4h(yB;8YTkiVQ^p8(Z zZ@#csj`3s{7jG?%;j|$pw4GwLeP#F2#;~fstinHiT~NATn8+E~hH!k3!=eQB%-RW-^@xEJxm%8>BWI=@aEXq3clXCU z!>jaLagU&;$;Si{RHB01+$DKNw{m=Q@8SD6r*wjb9e<8h{X8aqsew#?PWKY->BFSt zWVh)G8o0`#dA{Z7D9FcW*3h;#|1E0k-N0x2*CmLFtnT3>zK0!vCZm7z1^l>c!KkR; zz9ndTQ6^vh5{{67ir1|Vo)kLwmq+{T56x!f0bGjZH>>e+*nx*OrDE zEiL1Jd|JQ}^(r^l68Y{3HL|cs$EON^ek+3C@>Y>My5E-pq!CXChR$MTBb`4 zm?ucgG-S1~4llgu=T!NmU1~c~FH7G(bzBgU^9vYaqq@Q!q~S88f*;}Q*4IHGvN;3<#QCO8&lIbH8u?t(Blo?-8qNLi@G3-ZB&XP@pDY`;$;XfLka7k(Dm0vAtIe!WZqY!0-qG$Fg1IyBqo3dYP(YhdP0CVHw7QaMntS$}L zWo8~98q%E|%GI(U%X-4F#x5bDs;38|w5wL_*IO@M`4#)?7YBDtjLrN=qmiCo6u2zz z*Ks&d{~R35O{>CL4o^pnm8~ze`IvGWeSH2ZSuV4zw|C&l6Gk-6S}rp`4bb*c?|F(3 zt6Z}AVXe3l&_-ceNqJn=b++zQL`1~y?pKgj>}D80LQdPr%<=1xo`HIR2bMxijMrf8 zH+7iWWpQ%+vXg!Ge0d58vJEBWqrqA?wuqU9^JO2NKKfMPG`SaLE7d|n?c(C6_;|IX zcQ#??Z``18%(QA8HdfF!Fc_VXc(k06%_!a1@{EOX_~%7IT21wazT#qW* zFc0qEmxslfQ;*wam{^U&CCgtty5~K*@vO@pM!A{pKRVu-#g>+?3pk7@Oaz36eoPsQ z0sBHdf%5QSRraU%%4pv(p0rvFU}8>schPU*XLT7nLrm!0&GB|c<86vQKE%Gh32)v+ zj+kB4mNbG=x|xF`GKW)Bl4_rUw>qmIrMD z|CJWDJ$t(5`|EOV&uhy1yh}(B5&DC@{WElTS(*IrKRAvCK;#>*Y%IWW*)rZ9)=69R z0>Z+EX9ZCz=Kz@Kd3R%jO7&XTh~r2@;XVmkA+yqx%C!@8bjc1}@|6<(LS1pk6}Og1 znxKG=yL(vtwyK;QE89p|Yf2cPT>BU0dfM8lh9APo7#M5}g23R)q0<4cVSk!0VNIO{ zB13ovEoTB@Zh*-w9X&nB;?G{u<-41iZD?o>Jo*+39ekwlY!pgz-o_?@8v+~$>7PHW zHI7EV9Tyi~xib0Xbz*newccW;k`g|tXkLzAFSD~dpGxkSc7Ay4*52;!7Y4KCm6crj z@1!77Y^`^`*;R1I$laY=)(}7rYOax`l=Q;kKXKqhvo+D30)eE6Zhcbz0NIA3ZoNtwYH z+@|L_@74As`(ZBv%4epfUV&}j(rgDM;r`}tTYf+gJoHyQ!n`IsD=W6wuYOp-xwW)Z z_4x~@!9U$YusDPDgd^~x=$i`@;zcXAk3en4oF(h>ryC1a%x!HSA05bb_!Wm(JhY4< z{0Xew$S-%#{}_N18m>65BPof{nnqbUAmWr?Nl zw^g2ra{rZkBlcU-`rw(fSd$I4Hq3kczAmpVC_BIilBtVbDw(#g^vXq(2E>RZ& z?n4SMex?DNGGsyBi_7tJ@_+WMi7gO16pXPKmzS?tThIUQ{MOZFtfv=NVk5T{=xq7& z7-mfjsV+%D7!L_0!InAW-AK*zb2p#2DmOP%;_bHL`&Z|rWs*mTI9`D%ps{fm_+5Y> zP-TL@#6(WQ=4k%*9zcsqMvpGEOn;t$!~FA4tL+867{7ky)G-pB`Q*!TlW>7nD&nl>_A|dgjw*TOkSi$IXU?q$~QsxLG+ex zvnsKAfqVT7^$cW+P&fnzo^_!lR>1%S$TpOTp@D%KK%aGVLMQ@*g2vi2VuMMXL_g3utnrk~CY-qSd0Y%mj$lbW1d z9jO2jf5nHeurNIhjpz|2F;4_zEb{C(O)&IJHDKX<-5bEl<2+9 z*|Ya&XKuFd0^tSbVHD~4olf8G?OZ4;lcpm&bH``%F%nJ2;fN}`?~&s;_9kDf{&PZo zI3HtmZS5(mpoj?7vv31^=;PCxb3;p8M^qy|eR5d4&|!2HFSV|2n~%>@kLxVU)5Oy^ ze=cJB{7RGtoWj=p{BhODmLFaDIa>6}3X7b4)9apFZmEgmLKgPqx&2ay2}ojWEYfiR z7Gx7|9l*9MBloJnRoGZr`A$u6hil=EQ{PMO9X7W}A0E22kN#v=iP>%^(*HlIl_m}M zo0l(BQ4zV%R0GnspAMjBB-<PjX&D#8adfvhz{YjKHZ>!|MY|<8&E#!aspU$oZNgviJXsX{L;|VJLR^VAIMzE!rHxyq`m$1Swq8zpH^TsK1#t1_xP!j;V7Qh zT1gVz6Mwo3pmd;>k zHu~p}18 z($Hv;oeYfO6H!tO^7A8h^ACQm;furLyU2fU5MBtCD2eR2xYH~wE&#U1zTLTfJ8i6e zI%-1-H)rGXlSq|oNn+0L)=~`yU<2}{j%#$VJtL@G>+Z#i7wf*9AdIPv^~2Rb%ozg* zSXlVnRoBLy`}(8A*v;lgTVuVv^4r_Tv>{U;ZL5>ZjDY#W#q6I3(jPKL1@3%2NX;P0 zEqwjLLdiWQ?Vh(r^$`M<>L+B&UQ6SofRn&uvRwyKU-MecjgZoCztq(jeSb5aQAhK> zgRORp3#jaAWy1EMV)K-gj5S|Wimo%kj?lhK<_(rPUMJu`NvZMizklT=K{^lM^zoCu zPyNL@Kcom!gOFS=m|$~Qk|D=AgC+!i-ud~i$$TN~rOHYV6P48?n%>96NC^r`JgtOD z-f_IZ|GG<}{2-X@W?wV%7;PeUbK6Et%uvBcbtNffwqj3Hv)0$hT?s96#z8?r)x;{x z%f4#AVJeykxEqr4v5r}BV7&MD9Bh4EvNX$ta*s(=x_Wqm1 z%f%lw)YTiBDX%9}en?JBkIv#pC#RFii==i zu-DNMMPifWweN>)lMfS}Iq19D4fjUE#)rMe0LWIdF+efvrYW?$)jPyq_4ikpT6}hG zSv0QpQ+-C^mzK8PR~8-Ii>`$cz%r6LCcaH>tEL>TvmYjdYqpJfznjxS$T;1xPuI+o zp4GjgZ{ux@V@f+StocXgUSjs8%fr3D3nB!U+Fm&-XlN*&JGT~g{_DvbTxm!cQF=U@ zck+U*GYgBXc9~!=tG>A7RFrFGI74vkVe#eJ<#~oJTO|3g*YCOW7ly#o!A?;AV&$bI zV^dBEwl~O0$jN-VYBMvNaCUvJYiU_Tx{Wd1cOLk8(H?vuar^d;++2fkOT}9&UC~0n z%l)KLVo=>VEIeaT|7ud>T$IGNbdyJ_YHEY@EiLSm-N%TCwxtrVfEI-v()BPTXX4^w zc4^KoE!~<%&dBHl(n?BC#cDUIweMnBo}O~H-K5OgQ%hHUtL!$TK#{(U4phSlZnHw>N3k}edU(>9GhQLd~tE_ z4x{ryfJzUYes>2|Ue+ZyLk%Vnt9J@yaZ?4VuVDYr3}fl|jh65Q!qv%5J73cq4#!94-Uqgd@@8SoT(QlEbaEjx>;^%@oHe0-s@i9{fDY`tb@I9B^;b7 zz?3p*!7ZAoXaWR7!ncwOuj*LD0V8nZyIT0pRz8+Zm$F!cc=BaKnp7^{qvJ)sPU|; z)vXn_mS_oyqBI${-ErJ6Q)oi~wWCBc>GhY@&Uj zQWNkvbgo)6O`MrpoV3NeG_^zde **Target**: AI agents. +> **Writing style**: ASD-STE100 Simplified Technical English (`ste-writing`). + +## Document Catalog (inject the exact file) + +Read this index. Then inject only the file that you need. Do not search all files. + +| Document Title | Path | Scope / Description | +| :--- | :--- | :--- | +| **System Overview** | `docs/outline/01-system-overview.md` | States the purpose, the components, the MQTT planes and the missing Phase 4 client. | +| **Configuration and Env** | `docs/outline/02-config-and-env.md` | Lists the environment variables, the config files, the ports and the secrets. | +| **Code Map** | `docs/outline/03-code-map.md` | Shows the folder tree, the entry points and the Go dependencies. | +| **Agent Runbook** | `docs/outline/04-agent-runbook.md` | Gives the commands to build, test, deploy, verify and resume work. | + +## Source Documents (read before the outline) + +| Document | Path | Scope | +| :--- | :--- | :--- | +| **Build spec** | `AGENTS.md` | The phases and the definition of done. Section 11 holds the phase checklist. | +| **System map** | `SYSTEM_MAP.md` | Names every component and states where the truth lives. | +| **Transport design** | `TRANSPORT_DESIGN.md` | The MQTT topics, the payloads, the security rules and the fail-safe rule. | +| **README** | `README.md` | The technical reference. Setup, endpoints, payloads and verification. | + +## System Summary + +The Control Service limits the screen use of two children. It holds weekly +schedules, blocklists, allowlists and usage in SQLite. It publishes the desired +state of each device to MQTT with the retained flag. It serves an HTMX dashboard, +a versioned admin API and a read-only Hub API. + +## Core Configuration + +- **Env file**: `environment.d/control-service.env` (gitignored). +- **Template**: `environment.d/control-service.env.example`. +- **Live port**: `8791` on `.13`. +- **Public URL**: `https://webrestrict.lab.audasmedia.com.au`. +- **State**: SQLite at `/data/control.db` inside the container. + +## Agent Restart Steps + +1. Read the Document Catalog above. Inject only the required file. +2. Read `AGENTS.md` section 11. Confirm the phase checklist. +3. Check the tree state: `git status`. +4. Check the recent commits: `git log -n 5 --oneline`. +5. Run the test suite: `make test`. diff --git a/docs/outline/01-system-overview.md b/docs/outline/01-system-overview.md new file mode 100644 index 0000000..11eed4e --- /dev/null +++ b/docs/outline/01-system-overview.md @@ -0,0 +1,97 @@ +# Internet Use Control — System Overview + +> **Target**: AI agents. **Writing style**: ASD-STE100 Simplified Technical English. + +## Purpose + +The project limits the screen use of two children, Harry and Finn. It applies +per-device weekly schedules, blocklists and allowlists. It collects usage. It +lets Sam override a rule in seconds. The work applies to the children's devices +only. No router change, no Pi-hole and no house-wide rule. + +## Components + +| Component | What it is | Where it runs | Language | +| :--- | :--- | :--- | :--- | +| **Control Service** | The brain and the source of truth. Schedules, blocklists, state and usage. | `.13`, Docker, port `8791` | Go + SQLite | +| **Web dashboard** | The admin screen in a browser. HTMX, server-rendered. | Served by the Control Service | HTMX | +| **Broker** | The message bus. Pushes state to devices and collects their reports. | `.13`, Mosquitto | — | +| **Kids' phone client** | Enforces the rules on a phone and shows the blocked state. | The children's phones | Kotlin | +| **Admin app** | The remote override surface. A separate project. | Sam's phone | Kotlin | +| **Hub** | A peer. It asks questions only. It never controls. | `.13`, port `8788` | separate project | + +## Data Flow + +1. Sam changes a rule in the dashboard or the admin app. +2. The Control Service writes the change to SQLite and to the audit log. +3. The Control Service publishes the device config to MQTT, retained. +4. The device receives its own config topic and applies the rule. +5. The device reports status and telemetry on its own topics. +6. The Control Service writes the report to SQLite. + +The Control Service is the only writer of config. Everything else writes to the +Control Service over HTTP. + +## MQTT Has Two Planes + +The shared Mosquitto on `.13` has two listeners. State them correctly. + +| Plane | Port | Security | Use | +| :--- | :--- | :--- | :--- | +| **Plain** | `1883` | Plain text, anonymous | Legacy and other consumers. Unchanged. | +| **TLS** | `8883` | TLS, per-device credentials, topic ACLs | The Control Service and every kid device. | + +On the TLS plane a device reads only its own `restrict//config` topic. It +writes only its own `restrict//status` and +`restrict//telemetry` topics. The Control Service is the only identity +that may publish config. `deploy/mosquitto/` holds the ACL, the accounts and the +verifier. + +One caveat: `per_listener_settings true` is deprecated in Mosquitto 2.1 and is +removed in 3.0. Pin the broker image tag, or rework the two planes. + +## Enforcement Model (the phone client) + +The phone client blocks by **interruption**. It does not suspend an app. + +- An **Accessibility Service** detects the foreground app. +- A full-screen **overlay** covers a blocked app. +- **Usage Access** supplies telemetry. +- A device-local **VpnService** filters sites. + +The client is **not** a Device Owner. There is no factory reset and no managed +profile. A child can disable the client. That is accepted. The enforcement is the +**tamper alert**: when the Accessibility Service stops, the app is force-stopped, +or the overlay permission is revoked, the client reports the event and Sam gets +an alert (FR-17). + +## Fail Safe + +The device keeps its last config when it cannot reach the broker (FR-14). An +outage changes nothing. The device caches its config on disk. One shared admin +keycode overrides the state. + +## What Is Missing (state this plainly) + +**Phase 4, the Windows computer client, is deliberately deferred and NOT built.** +No Python computer client exists in this repository. No Windows process blocking +is implemented or tested. The `docs` and the `README` name this gap on purpose. +Do not describe the computer client as complete. + +## Interfaces + +| Interface | Shape | Consumer | +| :--- | :--- | :--- | +| **Admin API** | `GET /api/v1/admin/devices`, and `lock`, `unlock`, `grant` and `override` under the same prefix. Bearer token. | The separate project `kotlin_phone_app_admin`. | +| **Hub API** | `GET /capabilities`, read-only `POST /ask`, `POST /notify`. Bearer token. | The Hub. Control never goes through the Hub. | +| **Dashboard** | Session login with a signed cookie and CSRF tokens. | Sam in a browser. | + +The admin API shape is small on purpose. Another project consumes it. Keep it +stable and versioned. + +## Related + +- Build spec and phase checklist: `AGENTS.md` section 11. +- Component names: `SYSTEM_MAP.md`. +- MQTT design and payloads: `TRANSPORT_DESIGN.md`. +- Interactive map: `https://maps.lab.audasmedia.com.au/internet_use_control/docs/`. diff --git a/docs/outline/02-config-and-env.md b/docs/outline/02-config-and-env.md new file mode 100644 index 0000000..0683053 --- /dev/null +++ b/docs/outline/02-config-and-env.md @@ -0,0 +1,92 @@ +# Internet Use Control — Configuration and Environment + +> **Target**: AI agents. **Writing style**: ASD-STE100 Simplified Technical English. + +## Configuration Files + +| File | Status | Purpose | +| :--- | :--- | :--- | +| `environment.d/control-service.env` | Live, gitignored | The real values for the container. | +| `environment.d/control-service.env.example` | In git | The template. Copy it and set the values. | +| `docker-compose.yml` | In git | The container, the port and the env file path. | +| `deploy/mosquitto/conf.d/webrestrict.conf` | In git | The TLS listener, the password file and the ACL file. | +| `deploy/mosquitto/conf.d/webrestrict.acl` | In git | The per-device topic ACL. | +| `deploy/mosquitto/clients.env` | In git | The client names for the verifier. No secrets. | +| `deploy/caddy/webrestrict.caddy` | In git | The vhost on `.35`. | + +Secrets come from `environment.d` or from +`~/.config/environment.d/10-secrets.conf`. The service reads only the process +environment. No secret is in the repository. + +## Ports and Addresses + +| Item | Value | +| :--- | :--- | +| Control Service | `8791` on `.13` | +| Public URL | `https://webrestrict.lab.audasmedia.com.au` (Caddy on `.35`) | +| MQTT plain plane | `1883` on `.13`, anonymous | +| MQTT TLS plane | `8883` on `.13`, credentials and ACLs | +| Apprise | `8210` on `.35` | +| Hub | `8788` on `.13` | +| Database | `/data/control.db` inside the container | + +## Environment Variables + +| Variable | Default | Purpose | +| :--- | :--- | :--- | +| `CONTROL_ADDR` | `:8791` | Listen address. | +| `CONTROL_DB_PATH` | `/data/control.db` | SQLite file. | +| `CONTROL_SESSION_SECRET` | required | Signs the session cookie. 16 characters minimum. | +| `CONTROL_SESSION_TTL` | `12h` | Session lifetime. | +| `CONTROL_COOKIE_SECURE` | `true` | Secure cookie flag. Keep it true behind HTTPS. | +| `CONTROL_ADMIN_USERNAME` | `sam` | Dashboard login name. | +| `CONTROL_ADMIN_PASSWORD` | — | Plaintext password, option A. | +| `CONTROL_ADMIN_PASSWORD_HASH` | — | bcrypt hash, option B, preferred. | +| `CONTROL_ADMIN_API_TOKEN` | — | Bearer token for the versioned admin API. Empty answers `503`. | +| `CONTROL_DEVICE_KEYCODE_HASH` | — | bcrypt hash of the shared device keycode. Seeds SQLite at start-up. | +| `CONTROL_APP_ENV` | `production` | Label in the footer. | +| `CONTROL_LOG_LEVEL` | `info` | `debug`, `info`, `warn` or `error`. | +| `CONTROL_TIMEZONE` | `Australia/Sydney` | IANA zone for the weekly windows. | +| `CONTROL_MQTT_ENABLED` | `true` | Turn the broker link on or off. | +| `CONTROL_MQTT_BROKER` | `tcp://host.docker.internal:1883` | Broker URL. The live value is `tls://host.docker.internal:8883`. | +| `CONTROL_MQTT_CLIENT_ID` | `control-service` | MQTT client id. | +| `CONTROL_MQTT_USERNAME` | — | The Control Service broker account. | +| `CONTROL_MQTT_PASSWORD` | — | Its password. | +| `CONTROL_MQTT_CA_FILE` | `/etc/webrestrict/tls/ca.crt` | PEM bundle that verifies the broker. TLS only. | +| `CONTROL_MQTT_INSECURE` | `false` | Allow a plaintext broker with no credentials. The live value is `false`. | +| `CONTROL_HUB_TOKEN` | — | Bearer token for `/capabilities`, `/ask` and `/notify`. Empty answers `503`. | +| `CONTROL_APPRISE_URL` | `http://192.168.20.35:8210` | The house Apprise API. | +| `CONTROL_APPRISE_KEY` | — | Stored Apprise configuration key, option A. | +| `CONTROL_APPRISE_URLS` | — | Stateless Apprise destination list, option B. The lab default. | +| `CONTROL_APPRISE_TOKEN` | — | Optional bearer token for a protected Apprise API. | +| `CONTROL_ALERTS_ENABLED` | `true` | Turn the alert engine on or off. | +| `CONTROL_ALERT_OFFLINE_AFTER` | `30m` | Time before an offline device raises an alert. | +| `CONTROL_ALERT_INTERVAL` | `30s` | How often the alert engine re-evaluates the devices. | + +The service refuses a non-TLS broker unless `CONTROL_MQTT_INSECURE=true` +(AGENTS.md section 10, rule 6). The live deployment does not use that escape +hatch. + +## Secrets + +| Secret | Where it lives | Notes | +| :--- | :--- | :--- | +| Dashboard password | `CONTROL_ADMIN_PASSWORD_HASH` | bcrypt. Preferred over the plaintext form. | +| Admin API token | `CONTROL_ADMIN_API_TOKEN` | A bearer token. Separate from the session login. | +| Hub token | `CONTROL_HUB_TOKEN` | A bearer token. Separate from the admin token. | +| Device keycode | SQLite, bcrypt hash | One code for every device. Seeded from `CONTROL_DEVICE_KEYCODE_HASH`. | +| MQTT accounts | Mosquitto password file | One account per device plus `control-service`. | + +## Commands That Read or Change Configuration + +```bash +# Generate a bcrypt hash without a host Go toolchain. +docker run --rm --entrypoint /app/control webrestrict/control:0.7.0-phase9 \ + hash-password 'your-password' + +# Set the shared device keycode. The code comes from stdin. +docker exec -i webrestrict-control /app/control keycode <<< '246810' + +# Print the MQTT identity of every device. Tokens only, no secrets. +docker exec webrestrict-control /app/control mqtt-clients +``` diff --git a/docs/outline/03-code-map.md b/docs/outline/03-code-map.md new file mode 100644 index 0000000..2c9f938 --- /dev/null +++ b/docs/outline/03-code-map.md @@ -0,0 +1,117 @@ +# Internet Use Control — Code Map + +> **Target**: AI agents. **Writing style**: ASD-STE100 Simplified Technical English. + +## Module + +`gitea.lab.audasmedia.com.au/sam/internet_use_control`, Go 1.25. The image is +CGO-free. SQLite is the pure-Go `modernc.org/sqlite` driver. + +## Folder Tree + +``` +cmd/control/main.go entry point and subcommands +internal/config/ environment configuration and validation +internal/auth/ signed-cookie session login and CSRF +internal/store/ SQLite: open, migrations, queries + store.go connection, migration runner, maintenance loop + devices.go device registry and desired state + schedules.go weekly windows and the effective state + lists.go block and allow lists + reports.go presence, telemetry, the daily rollup + overrides.go override history and the device keycode + alerts.go alert records and per-device dedup state + migrations/001_init.sql … 005_alerts.sql +internal/mqtt/ + link.go paho client, subscriptions, publish, TLS + payload.go the topic and payload contract for the clients +internal/alert/alert.go the limit and offline alert engine +internal/apprise/apprise.go the Apprise API client +internal/hub/ the Hub contract: capabilities, answers, notify +internal/web/ + server.go routes, middleware, session + devices.go the device registry and manual lock/unlock + schedules.go the weekly schedule and list editors + usage.go the usage screen + admin_api.go the versioned admin API and the keycode endpoint + hub_api.go /capabilities, /ask, /notify + templates/ login, dashboard, editors, usage, partials + static/ tokens.css, app.css, theme.js, htmx, fonts +android/ the kids' Android client (Gradle, Kotlin) + app/src/main/java/au/audasmedia/webrestrict/kids/ + KidsApp.kt application class + MainActivity.kt the kid's status screen + BlockAccessibilityService.kt foreground app detection + OverlayController.kt the full-screen block overlay + BlockEngine.kt the block decision + DeviceConfig.kt the config payload model + MqttController.kt the broker link + TamperReporter.kt the FR-17 tamper report + UsageCollector.kt Usage Access telemetry + TelemetryReporter.kt telemetry publish + SiteFilterVpnService.kt the device-local VpnService + SiteFilter.kt the site decision + DnsMessage.kt DNS parsing + IpPacket.kt IP packet parsing + AppPrefs.kt the cached config on disk + BootReceiver.kt start after boot + TelemetryStore.kt local telemetry buffer + tools/emu-proof.sh headless emulator proof +deploy/caddy/ the vhost on .35 +deploy/mosquitto/ the TLS listener, the accounts, the ACL, the verifier +deploy/verify-phase8.sh the lock latency proof +deploy/verify-phase9.sh the Hub and alert proof +docs/ this documentation set +``` + +## Entry Point + +`cmd/control/main.go` starts the web server. It also holds the subcommands: + +| Subcommand | Purpose | +| :--- | :--- | +| `hash-password` | Make a bcrypt hash for a password. | +| `mqtt-clients` | Print the MQTT identity of every device. | +| `rollup` | Rebuild the daily telemetry rollup. | +| `keycode` | Set or generate the shared device keycode. | +| `alerts` | Print recent alerts. | +| `usage` | Print usage from the database. | + +## Dependencies + +| Module | Use | +| :--- | :--- | +| `github.com/eclipse/paho.mqtt.golang` | The MQTT client. | +| `modernc.org/sqlite` | The pure-Go SQLite driver. No CGO. | +| `golang.org/x/crypto` | bcrypt for the password and the keycode hashes. | + +The web layer uses the Go standard library only. The UI uses HTMX 2.0.4, +vendored under `internal/web/static/`. + +## Database Migrations + +| File | Change | +| :--- | :--- | +| `001_init.sql` | Devices, schedules, lists, state, telemetry, overrides, audit. | +| `002_telemetry_unique.sql` | One telemetry row per (device, day, app, domain). | +| `003_usage_rollup.sql` | The daily rollup table. | +| `004_overrides_grant.sql` | The grant column and the override history. | +| `005_alerts.sql` | The alerts table and the dedup state. | + +Migrations apply in order at start-up. Never hard-delete a device or its history +(AGENTS.md section 10, rule 10). + +## Android Client + +The client is a normal Gradle project with its own `gradlew`. The block path is +the Accessibility Service plus the overlay. The site path is the local +`VpnService`. The tamper path is `TamperReporter.kt`. The client keeps the last +config in `AppPrefs.kt`. There is no Device Owner and no managed profile. + +## Known Gaps in the Code + +- **Phase 4, the Windows computer client, does not exist.** No Python client and + no process blocking are in the tree. +- The MQTT `cmd` and `ack` topics are design only. No code uses them. +- The phone client reports telemetry from Usage Access. Site filtering uses the + local `VpnService`. diff --git a/docs/outline/04-agent-runbook.md b/docs/outline/04-agent-runbook.md new file mode 100644 index 0000000..418b943 --- /dev/null +++ b/docs/outline/04-agent-runbook.md @@ -0,0 +1,100 @@ +# Internet Use Control — Agent Runbook + +> **Target**: AI agents. **Writing style**: ASD-STE100 Simplified Technical English. + +## Before You Start + +1. Read `docs/outline/00-index.md`. Inject only the file that you need. +2. Read `AGENTS.md`, section 11. Confirm the phase checklist and the status line. +3. Read `README.md` for the technical reference. +4. Work in `/home/sam/paseo/projects/internet_use_control` only. +5. Bound every Docker command with `timeout`, for example + `timeout 60 docker compose ps`. Never run a Docker command without a bound. + +## Build and Test + +There is no host Go toolchain. Use the cached builder image. + +```bash +make test # go test ./... in golang:1.25-alpine +make vet # go vet ./... +make build # build the container image +``` + +`make test` and `make vet` run the container as the current user. The module +cache stays in `.gocache/`, which is gitignored. No root-owned file enters the +repository. + +## Run and Restart + +```bash +timeout 60 docker compose up -d --build # start or update +timeout 60 docker compose restart control # restart the container +timeout 60 docker compose logs --tail=100 control +timeout 60 docker compose ps +curl -sS http://127.0.0.1:8791/health +``` + +The compose file starts the container `webrestrict-control` on port `8791`. The +image tag is `webrestrict/control:0.7.0-phase9`. + +## Verify the Behaviour + +| Target | Proves | +| :--- | :--- | +| `make mqtt-verify` | TLS, credentials and topic ACLs on a scratch broker. | +| `make verify-phase8` | An API lock reaches a device in under one second. | +| `make verify-phase9` | The Hub answers and the Apprise alerts deliver. | + +`make mqtt-verify` uses a scratch broker on `127.0.0.1:18883`. It never touches +the shared Mosquitto. The two `verify-phase*` targets read the broker +credentials from `deploy/mosquitto/clients.env`. + +## Useful Subcommands + +```bash +timeout 60 docker exec webrestrict-control /app/control rollup +timeout 60 docker exec webrestrict-control /app/control alerts --limit 10 +timeout 60 docker exec webrestrict-control /app/control usage --day 2026-10-13 --detail +timeout 60 docker exec webrestrict-control /app/control mqtt-clients +``` + +## Documentation Commands + +```bash +# Render the Mermaid diagram. +timeout 120 mmdc -i docs/architecture.mmd -o docs/diagrams/architecture.png --scale 2 -b white + +# Validate and deliver the Archify map. +node ~/.agents/skills/archify/bin/archify.mjs validate architecture \ + docs/archify/architecture.architecture.json --quality showcase --json +node ~/.agents/skills/archify/bin/archify.mjs deliver architecture \ + docs/archify/architecture.architecture.json docs/architecture-map.html \ + --quality showcase --json + +# Publish docs/ to the maps site. +~/.agents/skills/project-docs/scripts/publish-maps.sh +``` + +The visual-check of `docs/architecture-map.html` reports a 1-pixel vertical +overflow at 1440×900. All larger viewports pass. The delivered map passes the +9-check showcase profile with 0 errors. + +## Rules That Stop a Task + +1. Never hard-delete a device or its history (AGENTS.md section 10, rule 10). +2. Only the Control Service publishes device config. Everything else writes to it + over HTTP. +3. `/ask` is read-only. It writes nothing. +4. Do not change the router, Pi-hole or DNS. The project is device-side only. +5. The children's client never changes its own rules. It obeys and reports. +6. One commit and one push for a documentation change. Add the co-author trailer + `Co-Authored-By: Paperclip `. + +## Resume Steps + +1. Read `docs/outline/00-index.md`, then `docs/outline/01-system-overview.md`. +2. Run `git status` and `git log -n 5 --oneline`. +3. Run `make test`. +4. Read `AGENTS.md` section 11. Phase 4 is the only unfinished phase. +5. Check the issue board before you start new work. Do not create tasks ad hoc. diff --git a/docs/vikunja-tasks.json b/docs/vikunja-tasks.json new file mode 100644 index 0000000..f7113e8 --- /dev/null +++ b/docs/vikunja-tasks.json @@ -0,0 +1,21 @@ +{ + "title": "Internet Use Control - Human Task Overview", + "description": "High-level task checklist for human review. The live board is the Vikunja project 'Internet Use Control' (id 20). This file is the repo copy.", + "vikunja_project": { + "name": "Internet Use Control", + "id": 20, + "url": "https://vikunja.lab.audasmedia.com.au/projects/20" + }, + "tasks": [ + { "id": 186, "title": "Confirm the Control Service runs on .13 and the dashboard login works", "done": false }, + { "id": 187, "title": "Review the weekly schedule grid per kid and prove a window turns on by itself", "done": false }, + { "id": 188, "title": "Check the allowlist: Spotify stays available while other apps are blocked", "done": false }, + { "id": 189, "title": "Prove the admin app locks a device in under one second", "done": false }, + { "id": 190, "title": "Open the Usage screen and check a week of real usage for one device", "done": false }, + { "id": 191, "title": "Ask the Hub 'how long have the kids been on screens today?' and check the answer", "done": false }, + { "id": 192, "title": "Prove a tamper alert (Accessibility Service disabled) reaches Sam", "done": false }, + { "id": 193, "title": "Prove a device keeps its last state when the broker is unreachable", "done": false }, + { "id": 194, "title": "Decide the Phase 4 Windows client: build it, or close the project at 9 of 10", "done": false }, + { "id": 195, "title": "Review the published docs: README, Obsidian note, Archify map and Outline collection", "done": false } + ] +}