maybe_start_location_tracking/2 matched %{track_location: true} in the
track_settings argument. No caller passes that key -- all five call sites
send %{map_id: _, track: true | false} -- so the clause never matched and
the function could not turn location tracking on. maybe_start_ship_tracking/2
had the identical defect.
Match on the state's active_maps instead: a character on at least one map
should be polling their location.
Without this, a character is stranded by an event they never initiated. When
a browser's presence lapses past the grace period the character is untracked
server-side, active_maps empties, and maybe_stop_tracking/2 clears
track_location. The character is still online in EVE throughout, and the only
other writer -- update_online/1 -- is gated on an online-status transition
that a character who stays logged in never produces. So the browser
reconnecting cannot restore it. update_location/1 then falls through to its
catch-all on every tick, and the character stops moving on the map while
every liveness signal keeps reporting healthy.
This is why #620 reports that toggling tracking off and on does not help but
relogging in EVE does: toggling routes through the dead clause, while
relogging produces the online transition that restores the flag as a side
effect.
Fixes#620
Track an empty no-op docker-compose.override.yml (with a .gitignore negation)
so devcontainer.json's compose file list resolves without the initializeCommand
hack, which is removed. Add USER_UID/USER_GID build args and drop the sudo
chown from setup.sh. Correct the Elixir pin comment: the OTP suffix pins only
the major, so the Erlang patch may still differ from .tool-versions.
- correct the Jita/Hek comment to match the systems the tests actually use
- document the sandbox contract on IntegrationCase and make shared mode an
explicit opt-in that is reverted on exit
- drop the blanket rescue around Sandbox.allow/4: it returns
{:already, :owner | :allowed} rather than raising, so the rescue only
masked genuine infrastructure faults
- use Cache.insert/3 and document the :online option
The dev container did not come up cleanly. This fixes the correctness
problems and fills in the missing developer ergonomics.
Correctness fixes:
* Erlang/OTP: base image was elixir:1.17-otp-27, but .tool-versions
pins erlang 26.2.5.5 / elixir 1.17.3-otp-26. Switched to
elixir:1.17-otp-26 so container builds match CI and local asdf.
* Postgres: bumped 13-alpine -> 16-alpine. AshPostgres declares
min_pg_version 15 in lib/wanderer_app/repo.ex, so 13 was below the
supported floor; 16 also matches production. The data volume is
renamed db-new -> db-pg16 because a PG13 data directory is not
readable by a PG16 server, so reusing the name would fail to start
rather than upgrade.
* host.docker.internal: replaced the common-utils feature's
networkArgs (which only applies at feature install time) with a
plain compose extra_hosts entry.
* Dropped `apt --fix-broken install` and the duplicated
jakebecker.elixir-ls extension id.
* setup.sh: the assets block was indented as if inside the ecto
conditional and never ran `yarn build`. Fixed and added an explicit
build step.
* setup.sh: chown deps/ and _build/ to the container user; they come
from the bind mount and carry host ownership.
Non-root user:
Added a `developer` user (uid/gid 1000, passwordless sudo) instead of
running as root, so files created in the bind-mounted workspace are
owned by the host user rather than root.
Build artifacts:
Removed the elixir-artifacts named volume. A named volume mounted
inside the bind mount masks the repo's own deps/ and _build/, and only
for the main working tree — git worktrees came up empty and recompiled
from scratch. Host and container now share _build; after changing the
OTP version, `rm -rf _build` once.
Database seeding:
setup.sh now runs priv/repo/seeds.exs only when the SDE reference data
is missing, checked with `mix run --no-start` so the check does not
boot the supervision tree and its outbound pollers.
Developer experience:
* zsh + oh-my-zsh with autosuggestions/syntax highlighting, persistent
shell history in a named volume, and mix/git aliases.
* gh CLI, ripgrep, tree, htop, lsof, git-lfs, inotify-tools.
* en_US.UTF-8 locale generated.
* post-start.sh prints environment info and checks Postgres.
* More VS Code extensions (Phoenix, Credo, GitLens, dotenv, Docker)
and _build/deps excluded from search.
* docker-compose.override.yml.example for host-specific mounts (SSH
keys, gh auth). The override is gitignored and auto-created empty by
initializeCommand, so it is optional.
* .devcontainer/ added to .dockerignore so it does not invalidate
production image build cache.
The upload/download-artifact steps added in the previous commit were pinned
to @v4, which declares `runs.using: node20` and would emit the same Node 20
deprecation warning this repo is trying to clear.
Verified against upstream action.yml rather than release notes, which are
misleading here: download-artifact v5 and v6 are still node20 despite v6
being labeled a breaking "supports Node v24" release. node24 begins at
download-artifact v7 and upload-artifact v6.
upload@v4 -> v7, download@v4 -> v8 (current majors, both node24). Checked
that every input we pass still exists, and that the intervening breaking
changes do not apply: v5's path change affects downloads by artifact-id
(we download by name), v7 upload's `archive` defaults to true (we upload a
directory), and v8 download's `digest-mismatch: error` default is desirable.
Review of the previous commit found that Ash returns NotFound wrapped
inside Ash.Error.Invalid, not as a bare struct. Verified against a live
query: `Api.Map.by_id(<missing uuid>)` returns
{:error, %Ash.Error.Invalid{errors: [%Ash.Error.Query.NotFound{}]}}
so the bare-NotFound clause never matched. A genuinely missing map would
have propagated as a generic error and logged a spurious error line.
Match the wrapped shape as well. Confirmed `MapRepo.get/2` now returns
{:error, :not_found} for a missing map with no error log. No caller
depends on the :not_found atom specifically (all use wildcard or
with/else), so propagation of real faults is unaffected.
Also make track_character_on_map/2 clear stale per-map location caches,
mirroring TrackingUtils.track_character/4, which the helper claimed to
mirror but did not.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PRs from forks receive a read-only GITHUB_TOKEN for pull_request events
regardless of the workflow's `permissions:` block, so the comment step in
test.yml failed with 403 "Resource not accessible by integration" and took
the whole job down with it. Every fork PR was red despite the test run
itself succeeding.
Split the reporting: test.yml now renders the comment body and uploads it
as an artifact, and a new pr-comment.yml posts it via workflow_run, which
runs in the base repo context with a writable token.
The render step interpolates tool output from env vars in the shell rather
than through Actions expression substitution, so tool output can no longer
inject workflow syntax. pr-comment.yml never checks out or executes PR code,
re-validates the PR number as numeric, and passes the body via body-path.
Also mirrors the report to $GITHUB_STEP_SUMMARY so it stays visible if the
follow-up workflow does not run, and fixes the empty "Updated:" timestamp
(github.event.head_commit.timestamp is unset on pull_request events).
Four independent causes, each confirmed by instrumentation rather than
inference.
1. Sandbox access. MapPool GenServers are spawned dynamically and load
map state from the DB during init (six Task.async loaders in
do_init_state/1). IntegrationCase's one-shot supervision-tree grant
runs before any pool exists, and its polling monitor granted only
mock ownership, never Ecto sandbox access -- and would lose the race
regardless. Every loader died with DBConnection.OwnershipError.
Fixed with an opt-in @moduletag :shared_sandbox. Shared mode is the
only mechanism covering a process that queries immediately upon
spawn. It is opt-in rather than a global flip because Ecto shared
mode is node-global, and it is now reverted to :manual on exit --
not reverting is why an earlier global-flip attempt broke
CommonAPIControllerTest.
2. Characters were never tracked. update_characters/1 iterates
get_tracked_character_ids/1, which needs the character in
map.characters AND a tracking_start_time cache key. The tests set
only presence_character_ids, which that path never reads, so the
tracked list was empty and the whole body was a no-op. Added
track_character_on_map/2.
3. Characters were offline. With online: false the :character_online
handler deletes the solar_system_id cache key that
check_location_update just wrote, so no movement was ever observed.
4. Map scopes. get_effective_scopes/1 prefers the scopes array over
scope, and scopes defaults to [:wormholes]. The tests set only
scope: :all, so hi-sec movement was rejected as an invalid
connection.
Separately, these tests asserted that Jita is added to the map. Jita is
hardcoded in @prohibited_systems and can never be added, so those
assertions could never pass. Rather than weaken them, the start system
is now Hek (not prohibited), which preserves each test's intent, and a
new regression test pins the Jita prohibition itself.
Verified by set-diff against merge-base b7ddbc48 at full-suite scope
(two runs each, counts are unstable so sets were compared): zero new
failures, all 9 target failures fixed. Suite is 15 tests, 0 failures
across seeds 0, 1 and 42, with no OwnershipError and no map-start
timeout remaining.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MapRepo.get/2 flattened every error from Api.Map.by_id/1 into
{:error, :not_found}. That turned infrastructure faults into a
"map does not exist" signal: a DBConnection.OwnershipError in test
surfaced as "Failed to load map state" -> "map not loaded" ->
"Timeout waiting for map ... Check Map.Manager is running", pointing
at Map.Manager (which was running fine) instead of the real cause.
Keep the :not_found translation for a genuine Ash NotFound, and
propagate + log anything else.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>