Commit Graph

64 Commits (09e160e955a18a59ea91c001ebdf22f5cab3fb9a)

Author SHA1 Message Date
VinPropane 9c054fbcf2 NEO-27: add Slice 3 telemetry hook-site documentation
Map SelectionEvent to target_changed, reserve ability_cast_requested and ability_cast_denied hook locations, and align plan/manual QA/decomposition docs with TODO(E9.M1) telemetry follow-up.
2026-04-25 21:56:28 -04:00
VinPropane 5127fe59a7 NEO-26: emit selection_event from TargetSelectionClient
Add selection_event signal when lockedTargetId changes; attribute tab, clear,
and server_correction from POST/GET paths. Optional log_selection_events print.
GdUnit coverage; README, E1.M3 snapshot, manual QA, and plan checklist updated.
2026-04-25 19:33:53 -04:00
VinPropane ba667f1581 NEO-25: queue last-wins interact POST when HTTP client is busy 2026-04-24 20:48:50 -04:00
VinPropane 06d9104157 NEO-25: route E/R interact via main _unhandled_key_input for reliable input 2026-04-24 20:44:43 -04:00
VinPropane 0e72d98177 NEO-25: move resource_node anchor clear of Obstacle at (6,1,5) 2026-04-24 20:42:12 -04:00
VinPropane ce54fcc44f NEO-25: world interactables GET, registry rows, fetch-driven Godot props + glow 2026-04-24 20:36:15 -04:00
VinPropane d6ef4fd811 NEO-24: race-free target/select via optional positionHint
Followup #5: after locking alpha, walking into beta's ring, stopping and
pressing Tab could still return `out_of_range` even when client and server
agreed on position (Δ=0). The freshness-kick move-stream POST and the
target/select POST race on separate HTTP connections so the select can be
validated against a stale stored snap.

Extend TargetSelectRequest v1 with an optional positionHint {x,y,z}.
Server uses the hint for the radius check and for the echoed
targetState.validity; the hint is advisory and does not write to the
position store (move-stream stays the sole write path). Client attaches
the hint automatically whenever set_freshness_kick(...) is wired, so
headless unit tests that skip the wiring fall through to the no-hint
behavior. Freshness kick is kept in place to refresh the stored snap for
later validity GETs but is no longer load-bearing for denial correctness.

Server: 3 new tests (hint accepts when stored is stale, hint-far denies,
hint never writes to position store). Client: 1 new test asserting the
hint is in the POST body when wired. All 62 server + 96 client tests
pass.

Plan Decision 8, review follow-up #5, manual QA section 6, and NEO-23
contract notes updated. New bruno request exercises the hint path.
2026-04-21 23:52:20 -04:00
VinPropane a7453eb82f NEO-24: unify prototype target radius at 6 m, overlap rings at origin
All prototype targets now share `PrototypeTargetRegistry.SharedLockRadius`
(= 6 m). Anchors moved to alpha `(-3, 0.5, -3)` and beta `(3, 0, 3)` so
the two 6 m rings overlap ~3.5 m wide centered at origin, letting Tab
flip between targets without locomotion. Spawn `(-5, -5)` keeps alpha
in range (~2.83 m) and beta out (~11.31 m) so the existing soft-lock
denial QA path at spawn still holds.

`prototype_target_constants.gd` mirrors the shared radius + new anchors.
The range-aware Tab picker now **skips the currently-locked id** so Tab
always expresses a swap intent — if the only in-range target is already
locked, the fallback picks the next cycle id and the server denies
(preserves NEO-23's "Tab = visible denial when no swap possible" rule).

Tests:
- Server: new `PlayerTargetStateReaderTests.ComputeValidity_AtOrigin_…`
  asserts both targets report `Ok` at origin. Existing boundary test
  rewritten for the new alpha anchor.
- Client: new `test_tab_from_locked_alpha_at_origin_swaps_to_beta_when_both_in_range`
  and `test_tab_when_only_current_lock_is_in_range_falls_back_to_cycle_for_denial`.
  Existing in-range/out-of-range positions updated for the new layout.

Server 59/59 and client 95/95 suites pass.

Plan Decision 7, review follow-up #4, manual QA section 5 (new "Tab-flip
at origin" overlap sanity), and README updated. Per-target radii will
return with real combat design (E5.M1) — this is a prototype stub.
2026-04-21 23:37:42 -04:00
VinPropane 63fecb1099 NEO-24: range-aware Tab cycle skips out-of-range anchors before POST
Previously Tab from no-lock always picked `ORDERED_IDS[0]` (alpha), so
standing next to beta and pressing Tab was denied with `out_of_range`
even though the client HUD clearly showed beta in range.

Added `PrototypeTargetConstants.next_in_range_id_after(current, world)`
which walks the cycle and returns the first anchor whose client-side
horizontal distance is within the anchor radius.
`TargetSelectionClient.request_tab_next()` prefers that pick when the
player reference is wired (same path `set_freshness_kick` uses) and
falls back to the plain cycle order when nothing is in range so the
server still owns the denial reason.

Server contract (NEO-23) untouched. Tests cover the beta pick, the
nothing-in-range fallback, and the no-player-ref regression. Plan
Decision 6 + review follow-up #3 + README + manual QA updated.
2026-04-21 23:26:56 -04:00
VinPropane e31cd2df68 NEO-24: freshen server position before target-select + surface divergence on HUD
Fix "Tab says denied even though I'm in range of beta": root cause is the
race between the 20 Hz move-stream sampler and the target-select POST. A
Tab press right after the capsule stops can be validated against the
server's last sampled position, which trails the visible capsule by
several meters.

Client-side fixes (server contract unchanged):

- TargetSelectionClient.set_freshness_kick(authority, player): before each
  POST /target/select, submit a move-stream with the current player
  position so the server's stored snapshot is as fresh as possible when
  the range check runs. Wired in main.gd; optional so tests without the
  wiring still pass (regression-covered).
- PlayerPositionLabel now shows a "srv: (x,y,z) Δ=<d>m age=<ms>" line
  populated from PositionAuthorityClient.authoritative_ack, so any
  residual client/server divergence is visible in the HUD and future
  reports can point at it directly.
- Two new tests: select POST kicks exactly one freshness stream with the
  current player position before POSTing, and the kick is a no-op when
  the wiring is absent.

Docs: plan Decision 5, review follow-up #2, manual QA section 5 and
section 1, and client README updated to describe the new behaviors.
2026-04-21 23:21:16 -04:00
VinPropane 71ae829482 NEO-24: add visible target-anchor markers + HUD distance lines
Players reported targeting "feels inconsistent" because the server
PrototypeTargetRegistry anchors (alpha (-5,-5) r=8, beta (8,8) r=4) have
no visible meshes — the only console visible in the scene is the NEO-9
PrototypeTerminal at origin, which is unrelated to NEO-23/24 targeting.

Adds a display-only mirror of the server registry:

- client/scripts/prototype_target_markers.gd spawns a colored mast +
  translucent flat radius ring per anchor so the lock radius is
  visible in-world.
- client/scripts/prototype_target_constants.gd gains ANCHORS with
  position/radius/color, matching PrototypeTargetRegistry.cs. Marked
  display-only; server remains authoritative for validity.
- TargetLockLabel now renders a per-anchor distance line each physics
  tick ("<id>: <d> m / <radius> (in|out)") so players can see the
  relationship between the visible capsule and the lock radius and
  immediately spot client/server position drift.

Also updates README, plan (Decision 4), manual QA (Section 1 boot +
Section 4 locomotion), and the review file's follow-up section to
describe the UX fix.
2026-04-21 23:04:03 -04:00
VinPropane 435a32b3b3 NEO-24: fix movement-driven target refresh via authoritative_ack signal
Review 2026-04-21 caught that the hybrid soft-lock refresh path is
unreachable during normal WASD locomotion. `main.gd` was wiring
`TargetSelectionClient` to `PositionAuthorityClient.authoritative_position_received`,
which only fires on boot sync and move-rejection resync — successful
`move-stream` 200s are intentionally silent to avoid RTT rubber-banding.
So walking out of an alpha/beta radius never triggered the throttled
`GET /target` the HUD depends on, and validity stayed stuck at `ok`.

Add a separate `authoritative_ack(world)` signal on `PositionAuthorityClient`
emitted from both `BOOT_GET` 200 and successful `move-stream` 200, and
switch `TargetSelectionClient` (+ `main.gd`) to it. The snap signal keeps
its current semantics so rubber-band suppression is untouched; the ack
signal is a pure heartbeat that cooldown-throttled consumers can hook.

Also adds an integration test that wires both real clients together (per
the review's suggestion 1) so renames or mis-wires between the two
scripts fail loudly rather than silently skipping the end-to-end path.

- `client/scripts/position_authority_client.gd`: new `authoritative_ack`
  signal; extracted `_parse_world_from_response` helper to share JSON
  parse across `BOOT_GET` and `STREAM_POST`; emit ack from both.
- `client/scripts/target_selection_client.gd`: rename
  `on_authoritative_position_snap` to `on_authoritative_ack`; drop
  `apply_as_snap` gate (no longer meaningful).
- `client/scripts/main.gd`: connect the new signal.
- `client/test/position_authority_client_test.gd`: assert ack on boot
  200 and stream 200.
- `client/test/target_selection_client_test.gd`: rename tests for new
  handler.
- `client/test/target_refresh_on_locomotion_test.gd`: new integration
  suite wiring real authority + target clients.
- Plan Decision 3 + Tests row + Files-to-modify row updated.
- `docs/reviews/2026-04-21-NEO-24.md`: committed with blocking issue +
  suggestions struck through with `Done.` notes per
  planning-implementation-docs rule.
- `docs/decomposition/modules/documentation_and_implementation_alignment.md`:
  E1.M3 row now reflects NEO-24 landed.
- `client/README.md` + `docs/manual-qa/NEO-24.md`: refreshed signal names
  so the soft-lock refresh description matches the wiring.

Tests: 88/88 GdUnit headless passed locally.
2026-04-21 22:35:41 -04:00
VinPropane ee572e3661 NEO-24: client tab-target + lock HUD synced to server
Adds `TargetSelectionClient` (Godot, HTTP to NEO-23 targeting endpoints):
- Tab cycles ids in ascending order matching `PrototypeTargetRegistry`
  (`prototype_target_alpha`, `prototype_target_beta`) and wraps.
- Esc clears by POSTing with `targetId` omitted.
- UI always paints from the server's authoritative `targetState`
  (success and denials alike); `UICanvas/TargetLockLabel` shows
  `lockedTargetId`, `validity`, `sequence`, and (on deny) `reasonCode`.
- Movement-triggered refresh: while a lock is held, subsequent
  `authoritative_position_received` snaps fire a throttled GET
  (~250 ms cooldown) so `validity` flips to `out_of_range` after
  locomotion without duplicating server radius math on the client.

Registers `target_tab` (Tab) / `target_clear` (Esc) in `project.godot`;
documents bindings + manual QA in `client/README.md`.

Tests: `target_selection_client_test.gd` covers GET parse, tab ordering
from empty/alpha/wrap, POST body shape, denial preserves server state,
clear omits targetId, movement refresh fires only while locked, cooldown
collapses bursts to one GET, and `apply_as_snap=false` is ignored.
All 85 client GdUnit cases pass; gdlint and gdformat clean.

See `docs/plans/NEO-24-implementation-plan.md`.
2026-04-21 22:14:16 -04:00
VinPropane c79c58c298 NEO-22: Unit-test WASD locomotion pure helpers
Extract static helpers (floor-ray continuation, median feet Y, micro-slip
XZ projection, wish-aligned horizontal velocity) from the tick path so
GdUnit can cover seam math without physics doubles. Add
player_locomotion_wasd_test.gd, document in README, and update NEO-22
plan/review.
2026-04-17 20:16:25 -04:00
VinPropane a4095cb9bf NEO-22: Remove click-to-move client and legacy move POST/verify
Delete ground_pick and GroundPick wiring; PositionAuthorityClient now only
boot GET and move-stream POST. main.gd snaps on authoritative position only.
Update client/server READMEs, cursor rule example, NEO-22 plan note, and
GdUnit authority tests (stream 400 + boot resync).
2026-04-17 20:06:46 -04:00
VinPropane 6cc2ac60d4 NEO-22: stop applying move-stream HTTP body as client teleport
Successful move-stream responses echoed server position roughly one RTT behind
integrated move_and_slide, so apply_as_snap still fired whenever dh exceeded
the locomotion skip threshold. Treat stream 200 as ack-only: do not emit
authoritative_position_received. Boot GET and click-move verify unchanged.
Update README + GdUnit expectation.
2026-04-17 19:34:45 -04:00
VinPropane 878b1f4aee NEO-22: fix stream rubber-banding (coalesce, idle gate, soft snap)
Queue only latest move-stream target per POST; clear pending while busy.
Skip stream submits when idle; skip tiny authoritative snaps after boot.
2026-04-17 19:27:10 -04:00
VinPropane a2c6c9f93e NEO-22: add move-stream API and WASD client locomotion
Introduce POST /game/players/{id}/move-stream with ordered targets, full-chain
validation before apply, and PositionStateResponse. Godot client uses camera-
relative WASD, queues samples, and snaps to stream responses; debug builds keep
click-to-move via ground_pick. Update READMEs, E1.M1 snapshot, and tests.
2026-04-17 19:21:33 -04:00
VinPropane a7ad695143 NEON-29: Apply stashed WIP (main.tscn, movement validation, docs) 2026-04-13 13:24:05 -04:00
VinPropane 4a51e64f25 Merge branch 'main' into NEON-29-expand-prototype-district 2026-04-13 13:22:51 -04:00
VinPropane 7e17ff563b chore: point docs and rules at Linear NEO issue ids
Migrate Jira NEON-* references to Linear NEO-* and linear.app links in
docs, Cursor rules, READMEs, and traceability comments. Renamed
implementation plans and dated reviews to NEO filenames.

Branch is based on origin/main with this commit only (no NEON-29
gameplay chain). client/README follows main’s district/map copy;
player.gd matches main aside from NEO-14 debug trace prefixes.
2026-04-13 13:15:32 -04:00
VinPropane 1f6625c659 NEON-29: Fix nav column steer ping-pong — path lookahead + air bee-line 2026-04-12 18:03:47 -04:00
VinPropane 7666cce7e5 NEON-29: Ledge fall — multi foot probes + zero snap when void
Single center ray + flat floor-normal gate missed tilted lip contacts
and still hit deck under capsule center while leading edge hung over void.
Moving FLOOR_SNAP_MOVING then cancelled gravity each tick.

Sample down rays at center and offsets along walk direction; depth 0.32;
set floor_snap_length 0 when is_on_floor but no probe hit.
2026-04-12 17:56:56 -04:00
VinPropane 6cb66d7567 NEON-29: Walk gravity when is_on_floor but feet probe finds void
Jolt + floor snap could keep is_on_floor true over open space toward a
lower click target, so vy stayed 0 until arrival. Add a short downward
ray under the capsule feet (WALK_SUPPORT_PROBE_DEPTH) and apply gravity
when no upward-facing hit is close, only on fairly level floor normals.
2026-04-12 17:50:09 -04:00
VinPropane 2821c5973b NEON-29: Fix nav path steer ping-pong (get_next_path_position)
Vertical+column branch scanned path for first point with XZ offset >5 cm;
Jolt moved the body across that edge each tick and flipped the chosen
waypoint → 180° velocity oscillation (vlat && ncol).

Use NavigationAgent3D.get_next_path_position() when navigation not
finished; fall back to auth goal when finished or degenerate.
2026-04-12 17:37:48 -04:00
VinPropane 103f8bbe19 NEON-29: Add approach treads for green, gold step, violet terrace.
TerracePlatformC_Approach (PCS_*): four cardinals, 7.2 m arms, ~0.104 m rise.
TerraceStepB_Approach (TSB_*): S/E/W, 6.2×1.0 m NS treads.
TerracePlatformB_Approach (TPB_*): N/E/W with six risers for 0.6 m to floor;
south uses existing gold step. New ramp-tint materials. README + plan.
2026-04-12 16:43:02 -04:00
VinPropane dbd9d67f45 NEON-29: Cap step assist to navigation agent_max_climb (0.35 m).
TerracePlatformB top is ~0.6 m above the floor; agent_max_climb only
limited nav links, while chained step assist could still climb in one
click. Refuse assist when goal surface is more than 0.35 m above feet
so the gold TerraceStepB path (or a second click) is required.
2026-04-12 01:12:10 -04:00
VinPropane e448075797 NEON-29: Bee-line walk to goal; nav path only for vertical routing.
Default get_next_path_position steering caused crawl/stuck behavior with
Jolt at 120 Hz. Steer XZ toward the authoritative target always except
when the goal surface is meaningfully above/below the feet and the
player is within DIRECT_APPROACH_RADIUS — then use the baked path for
ramps/treads. Update README and NEON-29 plan.
2026-04-12 01:07:35 -04:00
VinPropane 44984780d4 NEON-29: Widen TerracePlatformA approach treads past capsule width.
Tread run was 0.45 m vs 0.8 m capsule diameter, causing straddle stuck
on stairs and blocked motion toward edges when exiting. Use 1.0 m
depth (NS/WE boxes), recenter PAS_* transforms flush to the lip.
Tighten NavigationAgent path/target desired distance 0.35 to 0.22.
2026-04-12 00:59:40 -04:00
VinPropane bfa932c834 NEON-29: TerracePlatformA cardinal approach treads (geometry fix)
Twelve walkable StaticBody treads (three per side) bridge floor to teal
lip; darker teal material. Fixes Jolt/box-lip stuck when script-only
mitigations had no user-visible effect. README + plan note.
2026-04-12 00:49:37 -04:00
VinPropane d81e915a0a NEON-29: Teal pad lips — wall normal assist, rim descend, +12mm lift
Step assist: use is_on_wall + get_wall_normal opposing want; keep slide loop
with dot < -0.02.

Descend: relax floor_block on shallow floor normal at internal edges; widen
vertical-ish slide threshold slightly.

Raise TerracePlatformA Y by 0.012 m vs main floor for coplanarity.
2026-04-12 00:42:25 -04:00
VinPropane b4c0defc29 NEON-29: Distinct colors for terraces and floor slab
TerracePlatformA teal, TerraceStepB gold, TerracePlatformB violet,
TerracePlatformC green; explicit neutral floor material. README props
table adds tint column and material ids for communication.
2026-04-12 00:40:32 -04:00
VinPropane 81ceeaab27 NEON-29: Doc fix — lip QA on TerracePlatformA, not Terrace B
Clarify README, plan, and player.gd comments: screenshots were the large
SE pad (10×10, 0.3 m step), not the NW TerraceStepB/TerracePlatformB pair.
2026-04-12 00:39:04 -04:00
VinPropane 7d03b17136 NEON-29: Remove descend bypass; fix walk gravity on terraces
Applying gravity whenever the nav goal was below feet stayed true for the
entire walk across TerracePlatformB (feet ~0.6 m vs floor goal Y=0),
corrupting horizontal velocity and slide behavior.

Drop the descend bee-line branch; follow the nav mesh for vertical routing.
Restore full horizontal velocity set with vy=0 and air-only gravity.
Update README and NEON-29 plan to match.
2026-04-12 00:32:02 -04:00
VinPropane 6b471b2102 NEON-29: Fix walk steering when goal shares XZ; air gravity; player mask
- _set_horizontal_velocity_toward: optional fallback xz when delta.xz ~ 0 (avoids +X hack).
- Direct-approach branch: if horiz_dist to goal is ~0 but not arrived vertically, steer
  using the first navigation path point with horizontal separation.
- Apply get_gravity()*delta while walking and not on_floor so capsules land and Jolt
  registers floor contacts.
- Set Player collision_mask=3 explicitly; README collision bullet updated.
2026-04-11 23:46:09 -04:00
VinPropane ae66b6b313 NEON-29: Fix player–world collision pairing (env mask includes layer 2)
Player uses collision_layer 2 so camera pick rays (mask 1) skip the capsule.
Static bodies defaulted to collision_mask 1, so they never scanned layer 2 and
Godot produced no floor/wall contacts—walk appeared stuck with is_on_floor
false. Set collision_mask=3 on all NavigationRegion3D StaticBody3D walkables
and occluders; bumps inherit from Floor at spawn. Document the pairing in README.
2026-04-11 23:42:14 -04:00
VinPropane afb810db9d NEON-29: correct README — FarPad no longer rejects, only pedestal top does
Horizontal distance was never a valid rejection reason; remove references
to FarPad as a reject target and clarify that MaxVerticalStep (pillar top)
is the only remaining server rejection criterion.
2026-04-11 18:20:43 -04:00
VinPropane ac75af9c73 NEON-29: remove horizontal step limit; add HorizontalStepEnabled flag
Expanded 45×45 district made the 18 m MaxHorizontalStep too restrictive
(floor diagonal ~63 m). Add HorizontalStepEnabled to MovementValidationOptions
(default false, matching DistrictBoundsEnabled pattern) so the check is
skipped unless explicitly opted in. Add test for disabled case (9 pass).
Update README and plan with the decision.
2026-04-11 18:17:32 -04:00
VinPropane 6879d283a9 NEON-29: expand prototype district to 45×45 with terraces and obstacles
Replace 20×20 floor with 45×45 (~5× area). Add three occluder
obstacles (ObstacleB/C/D) distributed across the expanded space for
camera/occlusion stress QA. Add three terrace structures (TerracePlatformA,
TerraceStepB+TerracePlatformB, TerracePlatformC) for vertical variation;
each height transition respects agent_max_climb=0.35 m so the nav mesh
routes onto all surfaces. Update README with district layout table and
designer/QA limits.
2026-04-11 18:12:33 -04:00
VinPropane d1bd20582a chore: Fix gdlint/gdformat and harden pre-push on Windows
- main.gd: class member order (prvvars before @onready); wrap doc lines to 100 cols.

- isometric_follow_camera.gd: wrap doc lines to 100 cols.

- position_authority_client.gd: gdformat.

- pre-push: resolve .venv-gd/Scripts/gdlint.exe and gdformat.exe (Windows venv).

- Add scripts/install-git-hooks.ps1; README notes hook install and why CI can still fail.
2026-04-11 00:01:25 -04:00
VinPropane 398317d64f NEON-28: Dev obstacle smoke, nav reparent on hide, and docs
- Input action dev_toggle_occluder_obstacle (Ctrl+Shift+K): toggle obstacle off nav source via reparent to World, rebake NavigationRegion3D, sync NavigationAgent3D; physics/process/collision hardening unchanged intent.

- Occluder dictionary keys by instance id; related client fixes and README/project input note.

- Plan and review: PARSED_GEOMETRY_BOTH / runtime geometry removal lesson for future mechanics.
2026-04-10 23:55:50 -04:00
VinPropane 6340693b46 NEON-28: run follow camera in physics after Player (fix jitter) 2026-04-10 22:23:50 -04:00
VinPropane 1f5c1e7619 NEON-16: harden idle jitter follow-up
Harden idle settling so flat ground, obstacle contacts, and rare bump-edge cases stop drifting instead of looping corrective nudges. Also resolves the saved review follow-up items and syncs the runtime docs to the final feet-height and idle-anchor behavior.
2026-04-10 00:46:53 -04:00
VinPropane 1f8c0d04d1 NEON-30: address review — check AC boxes, add manual-check section to README 2026-04-09 23:03:17 -04:00
VinPropane 0fa2c0f93b NEON-30: skip occluder bodies in ground-pick raycast loop
ground_pick.gd now checks _collider_is_occluder before the walkable
break: when the ray hits a body in the "occluder" group it advances
OCCLUDER_PICK_THROUGH past the hit point and continues, unconditionally
and regardless of OcclusionPolicy fade state.  Non-occluder click
targeting is unchanged.

Three unit tests added to ground_pick_test.gd covering occluder ancestry
detection.  Plan open questions and module doc updated.
2026-04-09 22:51:22 -04:00
VinPropane cdf9d59bd9 NEON-26: add pre-push GDScript lint hook
Document the repo's enforced GDScript lint rules and add a local pre-push hook so gdlint/gdformat failures are caught before CI.
2026-04-08 23:10:40 -04:00
VinPropane ff830813c7 NEON-26: address code review follow-ups (zoom validation, docs) 2026-04-08 22:58:28 -04:00
VinPropane 4930fd89a6 NEON-25: isometric follow camera, CameraState, tests, and docs 2026-04-07 23:46:04 -04:00
VinPropane fa19ab7337 chore: migrate Jira references from NS-* to NEON-* in docs and rules
Rename implementation plans and dated reviews to NEON issue numbers per
Atlassian project re-key. Update browse URLs, cross-links, module docs,
client/server READMEs, and Cursor rules (project key NEON).
2026-04-07 00:15:06 -04:00
VinPropane da72ea8264 NS-21: fail CI if Godot logs SCRIPT ERROR during GdUnit discovery
GdUnit exits 0 when broken suites are skipped; grep log for parse/load errors.
2026-04-06 22:34:43 -04:00