The Hetzner on-demand compute implementation is complete:
- ARM64 builds working in CI
- Shardok running on Hetzner CAX41 in Helsinki
- Production Eagle connected via TLS + token auth
- IPv6/NAT64 networking configured
Delete the completed planning doc and add a new doc covering
future latency optimization strategies:
1. Client-side animation masking (recommended first step)
2. Split Shardok architecture (future if needed)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Move Custom Battle to lobby, use OAuth authentication
- Remove _createConnection() from _internalCustomBattle() since connection
already exists when called from lobby
- Hide gameSelectionPanel instead of connectionPanel when entering custom battle
- Add customBattleButton field for lobby UI button
- Add cancelCustomBattleButton and CancelCustomBattle() to return to lobby
- Wire up button click handlers in SetupLobbyUI()
The Custom Battle button should now be placed in the gameSelectionPanel (lobby)
in Unity and assigned to the customBattleButton field. A cancel button in the
customBattlePanel should be assigned to cancelCustomBattleButton.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up Custom Battle and Cancel buttons in Unity scene
- Assign customBattleButton in lobby panel
- Assign cancelCustomBattleButton in custom battle panel
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix CancelCustomBattle to properly reset canvas states
Hide shardokCanvas and ensure connectionCanvas is visible when
returning to lobby from custom battle.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Back button onClick handler in custom battle panel
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Update DEPROTO_PLAN.md: 100% action files now protoless
- ResolveBattleAction migrated to Scala types (PR #5048)
- All 52 action files are now fully protoless
- CommandChoiceHelpers fully migrated to Scala types
- Update progress summary and validation checklist
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove dead execute method from ProtolessRandomSequentialResultsAction
The execute(startingState: GameStateProto, applier: ActionResultProtoApplier) method
was never called - all actions using this base class now call .results() directly
and apply results via RandomStateSequencer or ActionResultApplier.
This removes the dead method and its unused dependencies (SeededRandom,
ActionResultProtoApplier, VigorXPApplier, ActionResultProtoConverter).
Note: The proto GameState export is retained because downstream actions use
ProvinceViewFilter which has overloaded methods taking both proto and Scala
GameState - overload resolution requires both types to be visible.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- ResolveBattleAction migrated to Scala types (PR #5048)
- All 52 action files are now fully protoless
- CommandChoiceHelpers fully migrated to Scala types
- Update progress summary and validation checklist
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The deploy user doesn't have passwordless sudo. Change from trying
to configure Docker (which fails) to just checking and warning.
Docker IPv6 is a one-time server setup done manually.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Change ResolveBattleAction to take Scala GameState and use ActionResultApplier
- Add resolvedBattle field to ActionResultT, ActionResultC, and proto converter
- Update EngineImpl.resolveBattle to use Scala-based applier
- Update test to convert proto GameState to Scala and handle Scala results
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The Hetzner Shardok server is IPv6-only. Docker containers need
IPv6 support to reach it.
Changes:
- docker-compose.prod.yml: Add IPv6-enabled network
- docker_build.yml: Configure Docker daemon for IPv6 on deploy
(with ip6tables for NAT)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The auth service loads users.pb into memory at startup. When authcli
modifies the file on disk (e.g., to grant admin), the service still has
the old in-memory copy. On next login, it would overwrite the file with
stale data, losing the admin grant.
Fix: Check file modification time before each FindOrCreateUser call.
If the file was modified externally, reload it before proceeding.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Replace proto notification detail imports with Scala NotificationDetails
- Use NotificationConverter.fromProto to convert proto -> Scala
- Remove dependency on action_result_notification_details_scala_proto
- Add dependencies on notification_trait and notification_converter
The internal pattern matching now uses Scala sealed trait NotificationDetails
instead of proto case classes, reducing proto coupling in the action layer.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Allows switching between local Shardok (default: shardok:40042) and
remote Hetzner Shardok (shardok.prod.eagle0.net:40042) via GitHub
Actions secrets.
To use Hetzner Shardok in production:
1. Add secret SHARDOK_ADDRESS=shardok.prod.eagle0.net:40042
2. Add secret SHARDOK_AUTH_TOKEN=<your-token>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix admin console OAuth flow and route protection
Changes:
- Gate all admin routes with requireAuth (games, settings, JFR, APIs)
Only login, logout, health, and static files are public now
- Add return_url to OAuth flow so auth service redirects back to admin
console after callback, instead of showing "close this window"
- handleLoginComplete now immediately completes login on redirect
- Update authcli help text with production usage examples
The OAuth flow now works properly for web clients:
1. User clicks Sign in -> admin console redirects to OAuth provider
2. OAuth provider calls auth service callback
3. Auth service redirects back to admin console's /login/complete
4. Admin console sets JWT cookie and redirects to /games
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix oauth_test.go for GetAuthURL signature change
Update test calls to pass empty string for the new returnURL parameter.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Document all media assets in Unity project for licensing review before
potentially opening public access to game downloads. Identifies:
- Asset Store purchases (properly licensed)
- CC-licensed music with attribution
- Items needing verification (Shardok sounds, StrategyGameIcons, etc.)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Replace sealed trait + case class pattern with Scala 3 enum
- Update all import sites to use `ProvinceEvent.{BeastsEvent, ...}` pattern
- More idiomatic Scala 3 with cleaner syntax
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add admin user management to admin console
Adds functionality to view and manage user identity→display name links:
Backend (auth service):
- New admin.proto with AdminService gRPC (ListUsers, SetUserDisplayName, SetUserAdmin)
- AdminHandler validates JWT is_admin claim for authorization
- UserService methods for listing users and admin operations
Frontend (admin console):
- OAuth login flow with Discord/Google (JWT stored in HTTP-only cookie)
- Auth middleware requiring is_admin claim for /users routes
- Users page with search, display name editing, and admin toggle
- HTMX-powered table with infinite scroll pagination
Deployment:
- admin container now connects to auth service for user management
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add authcli tool for user management
CLI tool for managing auth service users directly via the users.pb file.
Useful for bootstrapping the first admin user or emergency access.
Commands:
- list: List all users with their admin status
- find <email>: Find user by email (partial match)
- set-admin <id> true|false: Set admin status by user ID
- grant-admin <email>: Grant admin to user by exact email match
Usage:
authcli --data-dir=/app/data list
authcli --data-dir=/app/data grant-admin your@email.com🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add authcli to auth server Docker image
- Include authcli_linux_amd64 in auth_binary_layer
- Update auth_build.yml paths to trigger on authcli and admin proto changes
Usage on VM:
docker exec auth-server /app/authcli_linux_amd64 --data-dir=/app/data list
docker exec auth-server /app/authcli_linux_amd64 --data-dir=/app/data grant-admin user@email.com🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add Scala overloads to ProvinceEventUtils
- Add Scala overloads for isBlizzardEvent, isBeastsEvent, isEpidemicEvent,
isFestivalEvent, isDroughtEvent, isFloodEvent, isImminentRiotEvent
- Add Scala overloads for beastsCount and beastInfo
- Update BUILD.bazel with required Scala model dependencies
- Add province event dependency to LegacyProvinceUtils for overload resolution
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Replace isInstanceOf with pattern matching in ProvinceEventUtils
- Use match expressions instead of isInstanceOf for type checks
- More idiomatic Scala style
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add protoless overload that takes ScalaGameState and CombatUnitC
- Uses BattalionViewFilter (protoless) and BattalionViewConverter for output
- Exports CombatUnitC and ScalaGameState types for downstream callers
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- EligibleDiplomacyStatuses: Add Scala overload for maybeImprisonStatus
that takes provinces Iterable instead of proto GameState
- AvailableResolveTruceOfferCommandFactory: Add Scala overload that
filters Scala TruceOffer instances and converts to proto output
- AvailableResolveAllianceOfferCommandFactory: Add Scala overload for
AllianceOffer filtering
- AvailableResolveBreakAllianceCommandFactory: Add Scala overload for
BreakAlliance filtering
- AvailableResolveInvitationCommandFactory: Add Scala overload with
protoless invitation validation using FactionUtils
- AvailableResolveRansomOfferCommandFactory: Add Scala overload with
protoless RansomValidity and conflict checking
All Scala overloads take ScalaGameState and output proto commands,
allowing callers with Scala model types to avoid GameStateConverter.toProto().
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
TokenStorage was calling PlayerPrefs.GetString() which can only be
called from Unity's main thread. When PersistentClientConnection
attempts to reconnect from a background thread, JwtAuthInterceptor
tries to get the access token, causing a UnityException.
This caused an infinite loop: DeadlineExceeded → reconnect attempt →
PlayerPrefs exception → reconnect fails → idle timeout → retry...
Changes:
- TokenStorage: Add in-memory cache for thread-safe token access
- OAuthManager: Initialize cache on main thread in Awake()
- PersistentClientConnection: Schedule reconnect when stream ends
normally (was missing, causing silent connection death)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Enable QA Eagle to connect to production auth service over TLS.
- Use TLS for external connections (non-localhost hosts)
- Keep plaintext for local container-to-container connections
(localhost, auth, 127.x.x.x)
This allows running QA Eagle with --auth-service-url prod.eagle0.net:40033
to share user accounts with production.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add io.sentry:sentry dependency
- Initialize Sentry from SENTRY_DSN environment variable
- Report uncaught exceptions via ExceptionInterceptor
- Add SENTRY_DSN to docker-compose and CI workflow
When SENTRY_DSN is configured, uncaught exceptions in gRPC handlers
will be reported to Sentry for email/Slack alerts.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add filteredProvinceView(ProvinceT, ScalaGameState, FactionId) overload
- Use protoless FactionUtils.hasAlliance, Visibility.hasFullVisibility, and ProvinceUtils.incomingOthers
- Add helper methods: fullProvinceInfoScala, maybeIncomingAttackersScala, unaffiliatedHeroInfoScala
- Handle reconned provinces directly from Scala FactionT.reconnedProvinces
GameStateViewFilter improvements:
- Eliminate GameStateConverter.toProto() call in Scala overload
- Use new ProvinceViewFilter Scala overload for faction filtering
- Convert battalionTypes and chronicleEntries to proto only at output boundary
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Phase 2 of moving user management to Go auth service:
- Eagle now forwards setDisplayName, getCurrentUser, logout to Go auth
- Removed InternalUserServiceImpl and internal gRPC server (port 40034)
- UserService is now optional (only created when not using external auth)
- Updated docker-compose to remove port 40034 exposure
This makes Eagle stateless for user data when configured with
--auth-service-url, enabling QA to point to prod auth service.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Migrate CheckForFulfilledQuestsAction to protoless BattalionTypeFinder
- Change battalionTypes parameter from proto Vector[BattalionType] to Scala Vector[BattalionType]
- Update callers (EngineImpl, EndVassalCommandsPhaseAction) to pass Scala types directly
- Eliminate wasteful BattalionTypeConverter.toProto() conversions
- Update test to use Scala BattalionType
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add Scala overload to ExpandedUnaffiliatedHeroUtils, eliminate proto conversions
- Add ExpandedUnaffiliatedHeroUtils.expandedUnaffiliatedHero(ScalaGameState, UnaffiliatedHeroT)
- Add UnaffiliatedHeroConverter.unaffiliatedHeroTypeToProto() helper for efficient enum conversion
- Use reverse map instead of inefficient .find() in UnaffiliatedHeroConverter.toProto()
- Update AvailablePleaseRecruitMeCommandFactory to use Scala overload directly
- Eliminate wasteful GameStateConverter.toProto() and UnaffiliatedHeroConverter.toProto() calls
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add exports to expanded_unaffiliated_hero_utils for transitive type visibility
Callers that import ExpandedUnaffiliatedHeroUtils now see both overloads in
method signatures, which exposes ScalaGameState and UnaffiliatedHeroT types.
Add these as exports so callers can compile without needing explicit deps.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Move user management to Go auth service (Phase 1)
Auth service now manages users locally instead of calling Eagle:
- Add users.go with UserService for protobuf-based user persistence
- Implement SetDisplayName, GetCurrentUser, Logout handlers
- Extract JWT from gRPC metadata (authorization header)
- Add ValidateAccessToken function to jwt.go
- Add user_go_proto target for user.proto
- Add AUTH_DATA_DIR env var and /app/data volume mount
Auth service no longer depends on Eagle's InternalUserService.
Eagle's user management code remains for now (Phase 2 cleanup).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add unit tests and migration path for user service
- Add comprehensive unit tests for UserService (users_test.go)
- Add automatic migration from Eagle's users.pb location
- Add atomic write pattern for crash safety (write .tmp, then rename)
- Add AUTH_LEGACY_DATA_DIR config for migration path
- Mount Eagle's saves volume read-only for migration access
Migration strategy:
1. Auth service checks /app/data/users.pb first
2. If not found, reads from /app/saves/auth/users.pb (Eagle's location)
3. Saves migrated data to new location
4. Subsequent reads/writes use new location only
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add sortKey to FactionUtils to match LegacyFactionUtils API
- Add sortKey method and sortIgnoredChars constant to protoless FactionUtils
- Update LegacyFactionUtils to use direct proto field access (avoid converter
failures on incomplete test data)
- Both implementations now have matching APIs for the deproto migration
- Update DEPROTO_PLAN.md with progress
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Document Legacy* utility migration status in DEPROTO_PLAN.md
All Legacy* utilities now have protoless equivalents with matching APIs:
**Parallel Implementations (proto mirrors protoless):**
- FactionUtils / LegacyFactionUtils - 24+ boundary callers
- HeroUtils / LegacyHeroUtils - 10 boundary callers
- ProvinceUtils / LegacyProvinceUtils - 20 boundary callers
**Awaiting migration of callers:**
- BattalionUtils / LegacyBattalionUtils - 4 callers
- BattalionViewFilter / LegacyBattalionViewFilter - 3 callers
- BattalionTypeFinder / LegacyBattalionTypeFinder - 2 callers
Legacy versions are appropriately used by boundary code (availability
factories, view filters, action appliers) that works with proto GameState.
The AI layer already uses the protoless versions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When saving to paths like "auth/users.pb", the parent directory
may not exist. FileOutputStream doesn't create parent directories,
so the save would silently fail. Now we ensure parent directories
exist before writing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Update Hetzner docs with actual deployment details
- Use shardok.prod.eagle0.net as the domain name
- Step 4: Specify GitHub Actions secrets location
- Step 5: Recommend Hillsboro, OR (hil) + IPv6 floating IP
- Update all code examples and cloud-init scripts
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire SHARDOK_AUTH_TOKEN through Eagle's Shardok connection
Implements Step 7 of Hetzner setup guide:
- Main.scala reads SHARDOK_AUTH_TOKEN env var and creates ShardokSecurityConfig
- TLS is auto-enabled when Shardok address contains ".eagle0.net"
- Security config passed to both newGamesManager and newCustomBattleManager
- docker-compose.prod.yml passes SHARDOK_AUTH_TOKEN to Eagle container
- docker_build.yml passes secret during deployment
This is backward compatible: local Docker Shardok (shardok:40042) continues
to work without TLS/auth since the address doesn't contain ".eagle0.net".
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update ShardokInstanceManager for DigitalOcean registry and TLS
- Update cloud-init script to use DigitalOcean Container Registry instead of ghcr.io
- Add Let's Encrypt/Certbot setup for TLS certificates
- Configure automatic certificate renewal via cron
- Pass TLS cert paths and auth token file to Shardok container
- Default to shardok.prod.eagle0.net domain
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix cloud-init to use eagle0.conf instead of environment variables
The Shardok C++ server reads configuration from /usr/local/share/eagle0/eagle0.conf,
not environment variables. Updated cloud-init script to:
- Create eagle0.conf with TLS and auth paths
- Mount the config directory into the container
- Remove unused -e environment variable flags
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Simplify spin-up strategy: trigger on human turns with 10-min idle timeout
Instead of trying to predict battles, we now:
- Spin up Shardok when any human player takes a turn
- Shut down after 10 minutes of no human turns
This is simpler and more reliable. Battles happen regularly during active
play, so Shardok will be ready when needed. The 10-minute timeout is long
enough to cover thinking time but short enough to minimize idle costs.
Updated:
- SHARDOK_ON_DEMAND_COMPUTE.md with new strategy and state machine
- ShardokInstanceManager: renamed onPlayerActivity -> onHumanTurn,
changed default idle timeout from 60 to 10 minutes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Document Hetzner testing progress and ARM64 blocker
Testing status:
- Hetzner CAX41 ARM64 server created in Helsinki
- Floating IPv6 configured with netplan persistence
- DNS and Let's Encrypt certificates working
- NAT64 configured for IPv6→IPv4 registry access
BLOCKER: ARM64 container crashes with runfiles error:
"cannot find runfiles (argv0="/app/shardok-server")"
Needs BUILD.bazel investigation for ARM64 image packaging.
Also documented known issues:
- IPv6-only servers need NAT64 DNS (nat64.net)
- Floating IP requires manual netplan config
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix ARM64 container crash: add missing environment variables
The ARM64 Shardok container was crashing with "cannot find runfiles"
because SHARDOK_RESOURCES_PATH and SHARDOK_MAPS_PATH weren't set.
Without these, the binary tries to use Bazel runfiles which don't
exist in the container.
Added the required environment variables to the docker run command
in the cloud-init script, matching docker-compose.prod.yml.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When the only ruling faction hero departed from a province, clearRulingFaction
would create duplicate UnaffiliatedHeroC entries - one from the departure logic
and another from clearRulingFaction iterating over all rulingFactionHeroIds.
Fix: Filter out heroIds that already have entries in newUnaffiliatedHeroes
before creating new ones.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix upload section styling in admin panel
- Add explicit positioning to prevent overlap with other elements
- Style the details/summary for clearer expand/collapse indicators
- Add border and background when expanded for visual clarity
- Prevent button from shrinking in flex layout
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix upload section contrast and button sizing
- Use card background color for summary with proper text contrast
- Change Upload button to btn-small class for appropriate sizing
- Add proper border and styling for expanded form area
- Remove inline styles in favor of CSS classes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Upload button to not be full-width
- Add width: auto !important to override Pico CSS defaults
- Add flex-grow: 0 to prevent stretching in flex container
- Add position: static to prevent any fixed/absolute positioning
- Add z-index: auto to upload section to prevent stacking issues
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This was never successfully deployed. OAuth callbacks are handled
directly by the Go auth service via nginx routing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Remove dead code from LegacyHeroUtils
- Remove unused `seniorityOrder` method (protoless version in HeroUtils is used)
- Remove unused `sortOrdering` method (protoless version in HeroUtils is used)
- Remove unused `discordance` method (protoless version in HeroUtils is used)
- Remove unused `faction` method (protoless version in HeroUtils is used)
- Remove unused `Faction` import and proto dependency
- Remove corresponding tests for deleted methods
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove dead code from LegacyFactionUtils
- Remove unused `trust` method (protoless version in FactionUtils is used)
- Remove unused `factionHead` method
- Remove unused `ownedNeighbors` method (protoless version in FactionUtils is used)
- Remove unused `hostileNeighbors` methods (protoless version in FactionUtils is used)
- Remove unused `neutralNeighbors` method (protoless version in FactionUtils is used)
- Remove unused `truceExpirationDate` method
- Remove unused `alliedFactions` method (protoless version in FactionUtils is used)
- Remove unused `provincesWithHostileNeighbors` method
- Remove unused `ProvinceWithHostileNeighbors` case class
- Remove unused `Hero` import and `hero_scala_proto` dependency
- Remove corresponding tests for deleted methods
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Update DEPROTO_PLAN.md with Phase 5 progress
- Add section for thin wrapper refactorings (LegacyRansomValidity, LegacyRecruitmentOdds)
- Note LegacyHeroUtils dependency on LegacyFactionUtils
- Note LegacyBattalionUtils has intentionally different power multipliers
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove dead code from LegacyBattalionUtils
- Remove unused `power` method (superseded by BattalionPower.power)
- Remove unused `estimatedPower` method (superseded by BattalionPower.estimatedPower)
- Remove unused `powerMultiplier` map (only used by removed methods)
- Remove unused `BattalionView` import and proto dependency
All callers already use BattalionPower for power calculations.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The code checked _lobbySubscriber != null before enqueueing a lambda,
but the lambda captured the field reference and executed later on the
main thread when _lobbySubscriber could have become null (e.g., during
Dispose).
Fix by capturing the subscriber in a local variable before the null
check. This ensures the lambda uses the subscriber that was active
when the message arrived, even if _lobbySubscriber changes later.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Runs daily at 3am UTC to delete container images older than 5 days from
DigitalOcean Container Registry. Protected tags (latest, arm64-latest)
are never deleted.
Also runs garbage collection after cleanup to reclaim storage.
Includes manual trigger with dry-run option for testing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Phase 5 of deproto cleanup - remove Legacy* utilities that have no production callers:
- LegacyProvinceDistances: no callers besides its own test
- LegacyBattalionSuitability: no callers besides its own test
- LegacyFoodConsumptionUtils: no callers besides its own test
- LegacyHandleRiotUtils: no callers besides its own test
Updated DEPROTO_PLAN.md to track Phase 5 progress.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Eliminate GameState toProto() conversion in LLM pipeline
Phase 1 of deproto optimization: Make LlmRequestWithGameState use Scala
GameState instead of proto GameState, eliminating wasteful round-trip
conversion in UnrequestedTextHandler (the heaviest proto conversion in profiles).
Changes:
- LlmResolver: Change LlmRequestWithGameState.gameState to Scala GameState
- UnrequestedTextHandler: Remove GameStateConverter.toProto() calls, pass
gameHistory.stateAfter() directly
- Update all prompt generators (~38 files) to use Scala model types:
- FactionT instead of proto Faction
- HeroT instead of proto Hero
- ProvinceT instead of proto Province
- Scala enums (Gender.Male, Profession.Mage) instead of proto enums
- DivineMessagePromptGenerator: Rewrite quest pattern matching to use
Scala QuestC types instead of proto quest types
- ChronicleUpdatePromptGenerator: Inline province lookup, remove
LegacyFactionUtils dependency
- BattalionDescriptions: Add Scala BattalionT and BattalionTypeId overloads
- Update test files to use concrete Scala types (HeroC, FactionC, ProvinceC)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Make AttackCommandChooser fully protoless
- Add `estimatedPower(BattalionViewC)` to `BattalionPower` for handling recon
data with optional training/armament (defaults to 50.0 when unknown)
- Update `AttackCommandChooser.chosenAttackCommand` to use Scala `BattalionViewC`
instead of proto `BattalionView`
- Update `MidGameAIClient` to pass battalions directly without proto conversion
- Remove `LegacyBattalionUtils` and proto `battalion_view_scala_proto` dependencies
- Update DEPROTO_PLAN.md: Phase 1 complete, CommandChoiceHelpers fully protoless
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add Scala overloads to BattalionNameFilter and BattleFilter
- Add Scala GameState overload to BattalionNameFilter using FactionUtils.provinces
- Add Scala overload to BattleFilter using FactionUtils.hostilityStatus
- Update GameStateViewFilter Scala overload to use the new Scala sub-filters
- Add shardok_battle visibility for view_filters package
- Update DEPROTO_PLAN.md to reflect completed work
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add Scala overloads to Visibility.scala for hasFullVisibility using
Vector[FactionT] instead of proto GameState
- Add Scala overload to HeroViewFilter.filteredHeroView taking HeroT
and ScalaGameState with proper type conversions
- Add Scala overload to FactionViewFilter.filteredFactionView taking
FactionT and ScalaGameState
- Update GameStateViewFilter Scala overload to use Scala sub-filters
where available (HeroViewFilter, FactionViewFilter)
- Update BUILD.bazel files for visibility and exports
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
DigitalOcean free tier limits to 5 repositories. Use the existing
shardok-server repository with arm64-prefixed tags instead of a
separate shardok-server-arm64 repository.
- shardok-server:latest (x86)
- shardok-server:arm64-latest (ARM64)
Also adds HETZNER_SETUP.md with infrastructure setup instructions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The user database was being written to ~/eagle0/eagle/save/ but Docker
only mounted ./saves to /app/saves. This caused user data (including
displayName) to be lost on every container restart.
Add EAGLE_SAVE_DIR and EAGLE_ARCHIVE_DIR environment variables to
SaveDirectory, defaulting to the existing paths for local development.
Docker compose now sets these to /app/saves and /app/archived which
are properly mounted to persistent volumes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When the admin panel requests game history with limit=0, it only needs
the total count for pagination. Previously this would load all entries
and serialize each one to JSON, causing timeouts on games with many
actions.
Now when limit <= 0, we return just the count with empty entries,
making the count request fast regardless of history size.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The download handler was using ParseInt which fails for game IDs that
have the high bit set (appear as negative when formatted as signed).
The gameID is already parsed and validated in handleGameRoutes using
ParseUint, so just pass it through instead of re-parsing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add `filteredGameState(gs: ScalaGameState, factionId: Option[FactionId])`
overload that converts internally for now (sub-filters still need proto)
- Update HumanPlayerClientConnectionState to pass Scala GameState directly,
removing 3 wasteful toProto conversions at call sites
- Add visibility for view_filters to access GameStateConverter
- Export Scala GameState from game_state_view_filter target
- Update DEPROTO_PLAN.md to reflect Phase 4 progress
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Use DigitalOcean registry for ARM64 images instead of GitHub Container
Registry for consistency with all other images (Eagle, Shardok x86,
admin, auth, jfr-sidecar).
- Update ci/BUILD.bazel: shardok_server_push_arm64 → registry.digitalocean.com
- Update shardok_arm64_build.yml: use DO_REGISTRY_TOKEN auth
- Update docs to reflect the registry change
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When a user sets their display name, the current JWT still has the old
(blank) displayName. This caused games to be created with blank usernames.
Fix: SetDisplayName now returns a new access token with the updated
displayName claim. The client stores this new token so subsequent requests
(like joining games) use the correct displayName.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add TLS and token authentication support to Shardok server
Enables secure communication between Eagle and remote Shardok instances:
- TLS support: Read SSL cert/key from config paths, create SslServerCredentials
- Token auth: Validate "Authorization: Bearer <token>" header on all RPCs
- Config: Added authTokenPath to ServerConfiguration
- TokenValidator class reads token from file and validates requests
- Graceful fallback: If TLS not configured, uses insecure credentials
Configuration options (via eagle0.conf or environment):
- sslCertPath: Path to TLS certificate (e.g., Let's Encrypt fullchain.pem)
- sslPrivateKeyPath: Path to TLS private key
- authTokenPath: Path to file containing the auth token
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update implementation plan to mark completed tasks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add TLS and token authentication support to Eagle's Shardok client
Add ShardokSecurityConfig to configure TLS and auth token for connecting
to remote Shardok instances. When useTls is enabled, uses system trust
store for Let's Encrypt certificates. When authToken is set, adds Bearer
token to all gRPC requests via BearerTokenInterceptor.
This enables Eagle to connect securely to Shardok instances running on
Hetzner Cloud with Let's Encrypt TLS and token-based authentication.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Manages the lifecycle of Shardok instances on Hetzner Cloud:
- Activity-based spin-up when players connect
- Automatic shutdown after idle timeout (default 60 minutes)
- State machine tracking: Stopped -> Starting -> Ready -> InUse
- Cloud-init script generation for Docker container setup
- Health checking during startup
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Phase 2 of OAuth extraction: Unity client now routes OAuth RPCs
(GetOAuthUrl, CheckOAuthStatus, RefreshToken) directly to the Go
auth service on port 40033, while user RPCs (SetDisplayName,
GetCurrentUser, Logout) continue to go to Eagle.
Changes:
- AuthClient: Use separate gRPC channels for auth service and Eagle
- OAuthManager: Accept both authServiceUrl and eagleUrl parameters
- ConnectionHandler: Pass both URLs when configuring OAuthManager
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Previously, getGameHistory loaded all action results with history.all
then paginated in-memory. For games with many actions, this caused
timeouts.
Now uses history.since(startIndex).take(limit) which efficiently loads
only the save files containing the requested range. The since() method
in PersistedHistory already calculates which partial game files to read.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Eliminate GameState toProto() conversion in LLM pipeline
Phase 1 of deproto optimization: Make LlmRequestWithGameState use Scala
GameState instead of proto GameState, eliminating wasteful round-trip
conversion in UnrequestedTextHandler (the heaviest proto conversion in profiles).
Changes:
- LlmResolver: Change LlmRequestWithGameState.gameState to Scala GameState
- UnrequestedTextHandler: Remove GameStateConverter.toProto() calls, pass
gameHistory.stateAfter() directly
- Update all prompt generators (~38 files) to use Scala model types:
- FactionT instead of proto Faction
- HeroT instead of proto Hero
- ProvinceT instead of proto Province
- Scala enums (Gender.Male, Profession.Mage) instead of proto enums
- DivineMessagePromptGenerator: Rewrite quest pattern matching to use
Scala QuestC types instead of proto quest types
- ChronicleUpdatePromptGenerator: Inline province lookup, remove
LegacyFactionUtils dependency
- BattalionDescriptions: Add Scala BattalionT and BattalionTypeId overloads
- Update test files to use concrete Scala types (HeroC, FactionC, ProvinceC)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Make AttackCommandChooser fully protoless
- Add `estimatedPower(BattalionViewC)` to `BattalionPower` for handling recon
data with optional training/armament (defaults to 50.0 when unknown)
- Update `AttackCommandChooser.chosenAttackCommand` to use Scala `BattalionViewC`
instead of proto `BattalionView`
- Update `MidGameAIClient` to pass battalions directly without proto conversion
- Remove `LegacyBattalionUtils` and proto `battalion_view_scala_proto` dependencies
- Update DEPROTO_PLAN.md: Phase 1 complete, CommandChoiceHelpers fully protoless
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>