mirror of
				https://github.com/wanderer-industries/wanderer
				synced 2025-11-04 00:14:52 +00:00 
			
		
		
		
	Compare commits
	
		
			79 Commits
		
	
	
		
			tracking-c
			...
			v1.70.1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					3c064baf8a | ||
| 
						 | 
					9e11b10d74 | ||
| 
						 | 
					2fc45e00b4 | ||
| 
						 | 
					45eb08fc3a | ||
| 
						 | 
					fbcfae0200 | ||
| 
						 | 
					9c4ce013ec | ||
| 
						 | 
					5dba7c12f0 | ||
| 
						 | 
					db793c80c8 | ||
| 
						 | 
					aa4a3f1aa9 | ||
| 
						 | 
					3424639309 | ||
| 
						 | 
					0f309a29ba | ||
| 
						 | 
					e13b8846b8 | ||
| 
						 | 
					d5ea4d6129 | ||
| 
						 | 
					9d50bfedbd | ||
| 
						 | 
					b03410083c | ||
| 
						 | 
					a314b1e448 | ||
| 
						 | 
					e8a51a85c4 | ||
| 
						 | 
					d4074f966c | ||
| 
						 | 
					1413b41824 | ||
| 
						 | 
					379c1edec3 | ||
| 
						 | 
					58b5bade9e | ||
| 
						 | 
					71aee4cd3e | ||
| 
						 | 
					10bab0cfa1 | ||
| 
						 | 
					698350b0f7 | ||
| 
						 | 
					a97cf25031 | ||
| 
						 | 
					8302d088bd | ||
| 
						 | 
					64788e73de | ||
| 
						 | 
					114fd471e8 | ||
| 
						 | 
					b24a3120d3 | ||
| 
						 | 
					c5f93b3d0a | ||
| 
						 | 
					79290e4721 | ||
| 
						 | 
					984e126f23 | ||
| 
						 | 
					cd1ad31aed | ||
| 
						 | 
					1e3f6cf9e7 | ||
| 
						 | 
					9c6ccd9a8a | ||
| 
						 | 
					681ba21d39 | ||
| 
						 | 
					aef62189ee | ||
| 
						 | 
					09f70ac817 | ||
| 
						 | 
					1eacb22143 | ||
| 
						 | 
					8524bad377 | ||
| 
						 | 
					9d899243d1 | ||
| 
						 | 
					9acf20a639 | ||
| 
						 | 
					71ef6b2e82 | ||
| 
						 | 
					5e34d95dd2 | ||
| 
						 | 
					25a809c064 | ||
| 
						 | 
					f760498150 | ||
| 
						 | 
					328301a375 | ||
| 
						 | 
					f28e7ebbbb | ||
| 
						 | 
					bfa84af71e | ||
| 
						 | 
					42cd1ba976 | ||
| 
						 | 
					88cba866fd | ||
| 
						 | 
					af2876a84b | ||
| 
						 | 
					c5b15bfa78 | ||
| 
						 | 
					85f00a63c2 | ||
| 
						 | 
					05f427bcd7 | ||
| 
						 | 
					69f4c41534 | ||
| 
						 | 
					30b9239a8b | ||
| 
						 | 
					2061a83c59 | ||
| 
						 | 
					24e723de07 | ||
| 
						 | 
					5a927e5ba5 | ||
| 
						 | 
					10fafcf59f | ||
| 
						 | 
					be87591801 | ||
| 
						 | 
					086d4378d3 | ||
| 
						 | 
					e982275905 | ||
| 
						 | 
					77c02703e9 | ||
| 
						 | 
					0ef27d4f95 | ||
| 
						 | 
					5edc27744e | ||
| 
						 | 
					02ff887fee | ||
| 
						 | 
					3a30eeb59f | ||
| 
						 | 
					79af8fb601 | ||
| 
						 | 
					f9f00faa0e | ||
| 
						 | 
					a3c41e84e4 | ||
| 
						 | 
					7f21f33351 | ||
| 
						 | 
					568f682cee | ||
| 
						 | 
					901c4c8ca4 | ||
| 
						 | 
					3dbba97f9c | ||
| 
						 | 
					3475620267 | ||
| 
						 | 
					8936a5e5d8 | ||
| 
						 | 
					719e34f9bc | 
							
								
								
									
										199
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										199
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -2,6 +2,205 @@
 | 
			
		||||
 | 
			
		||||
<!-- changelog -->
 | 
			
		||||
 | 
			
		||||
## [v1.70.1](https://github.com/wanderer-industries/wanderer/compare/v1.70.0...v1.70.1) (2025-06-14)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes:
 | 
			
		||||
 | 
			
		||||
* resolve api issue with custom name
 | 
			
		||||
 | 
			
		||||
## [v1.70.0](https://github.com/wanderer-industries/wanderer/compare/v1.69.1...v1.70.0) (2025-06-11)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features:
 | 
			
		||||
 | 
			
		||||
* Core: Fix admin page error
 | 
			
		||||
 | 
			
		||||
## [v1.69.1](https://github.com/wanderer-industries/wanderer/compare/v1.69.0...v1.69.1) (2025-06-11)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## [v1.69.0](https://github.com/wanderer-industries/wanderer/compare/v1.68.6...v1.69.0) (2025-06-11)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features:
 | 
			
		||||
 | 
			
		||||
* Core: Added multiple tracking pools support
 | 
			
		||||
 | 
			
		||||
## [v1.68.6](https://github.com/wanderer-industries/wanderer/compare/v1.68.5...v1.68.6) (2025-06-10)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## [v1.68.5](https://github.com/wanderer-industries/wanderer/compare/v1.68.4...v1.68.5) (2025-06-10)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes:
 | 
			
		||||
 | 
			
		||||
* Core: Fixed updating map options
 | 
			
		||||
 | 
			
		||||
## [v1.68.4](https://github.com/wanderer-industries/wanderer/compare/v1.68.3...v1.68.4) (2025-06-10)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## [v1.68.3](https://github.com/wanderer-industries/wanderer/compare/v1.68.2...v1.68.3) (2025-06-09)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## [v1.68.2](https://github.com/wanderer-industries/wanderer/compare/v1.68.1...v1.68.2) (2025-06-09)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes:
 | 
			
		||||
 | 
			
		||||
* Core: Fixed character auth with wallet (on characters page)
 | 
			
		||||
 | 
			
		||||
## [v1.68.1](https://github.com/wanderer-industries/wanderer/compare/v1.68.0...v1.68.1) (2025-06-09)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes:
 | 
			
		||||
 | 
			
		||||
* Core: Fixed auth from welcome page if invites disabled
 | 
			
		||||
 | 
			
		||||
## [v1.68.0](https://github.com/wanderer-industries/wanderer/compare/v1.67.5...v1.68.0) (2025-06-09)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features:
 | 
			
		||||
 | 
			
		||||
* Core: Added invites store support
 | 
			
		||||
 | 
			
		||||
## [v1.67.5](https://github.com/wanderer-industries/wanderer/compare/v1.67.4...v1.67.5) (2025-06-08)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes:
 | 
			
		||||
 | 
			
		||||
* Core: Added back ARM docker image build
 | 
			
		||||
 | 
			
		||||
## [v1.67.4](https://github.com/wanderer-industries/wanderer/compare/v1.67.3...v1.67.4) (2025-06-08)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes:
 | 
			
		||||
 | 
			
		||||
* Core: Fixed issue with system splash updates
 | 
			
		||||
 | 
			
		||||
## [v1.67.3](https://github.com/wanderer-industries/wanderer/compare/v1.67.2...v1.67.3) (2025-06-08)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes:
 | 
			
		||||
 | 
			
		||||
* Core: Fixed issue with system splash updates
 | 
			
		||||
 | 
			
		||||
## [v1.67.2](https://github.com/wanderer-industries/wanderer/compare/v1.67.1...v1.67.2) (2025-06-08)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## [v1.67.1](https://github.com/wanderer-industries/wanderer/compare/v1.67.0...v1.67.1) (2025-06-08)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## [v1.67.0](https://github.com/wanderer-industries/wanderer/compare/v1.66.25...v1.67.0) (2025-06-08)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features:
 | 
			
		||||
 | 
			
		||||
* Core: Added support for WANDERER_CHARACTER_TRACKING_PAUSE_DISABLED env variable to pause inactive character trackers
 | 
			
		||||
 | 
			
		||||
## [v1.66.25](https://github.com/wanderer-industries/wanderer/compare/v1.66.24...v1.66.25) (2025-06-08)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes:
 | 
			
		||||
 | 
			
		||||
* Core: Disabled kills fetching based on env settings
 | 
			
		||||
 | 
			
		||||
## [v1.66.24](https://github.com/wanderer-industries/wanderer/compare/v1.66.23...v1.66.24) (2025-06-08)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## [v1.66.23](https://github.com/wanderer-industries/wanderer/compare/v1.66.22...v1.66.23) (2025-06-08)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## [v1.66.22](https://github.com/wanderer-industries/wanderer/compare/v1.66.21...v1.66.22) (2025-06-07)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## [v1.66.21](https://github.com/wanderer-industries/wanderer/compare/v1.66.20...v1.66.21) (2025-06-07)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes:
 | 
			
		||||
 | 
			
		||||
* Core: Fixed kills fetching based on env settings
 | 
			
		||||
 | 
			
		||||
## [v1.66.20](https://github.com/wanderer-industries/wanderer/compare/v1.66.19...v1.66.20) (2025-06-07)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## [v1.66.19](https://github.com/wanderer-industries/wanderer/compare/v1.66.18...v1.66.19) (2025-06-07)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes:
 | 
			
		||||
 | 
			
		||||
* Core: Added check for offline characters timeouts
 | 
			
		||||
 | 
			
		||||
## [v1.66.18](https://github.com/wanderer-industries/wanderer/compare/v1.66.17...v1.66.18) (2025-06-07)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## [v1.66.17](https://github.com/wanderer-industries/wanderer/compare/v1.66.16...v1.66.17) (2025-06-07)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes:
 | 
			
		||||
 | 
			
		||||
* Core: Increased tracking pause timeout for offline characters up to 10 hours
 | 
			
		||||
 | 
			
		||||
## [v1.66.16](https://github.com/wanderer-industries/wanderer/compare/v1.66.15...v1.66.16) (2025-06-07)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes:
 | 
			
		||||
 | 
			
		||||
* Core: Increased tracking pause timeout for offline characters up to 10 hours
 | 
			
		||||
 | 
			
		||||
## [v1.66.15](https://github.com/wanderer-industries/wanderer/compare/v1.66.14...v1.66.15) (2025-06-07)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -57,8 +57,8 @@ export const Characters = ({ data }: CharactersProps) => {
 | 
			
		||||
          <>
 | 
			
		||||
            <span
 | 
			
		||||
              className={clsx(
 | 
			
		||||
                'absolute top-[2px] left-[2px] w-[9px] h-[9px]',
 | 
			
		||||
                'text-yellow-500 text-[9px] rounded-[1px] z-10 hover:hidden',
 | 
			
		||||
                'absolute flex flex-col  p-[2px]  top-[0px] left-[0px] w-[35px] h-[35px]',
 | 
			
		||||
                'text-yellow-500 text-[9px] z-10 bg-gray-800/40',
 | 
			
		||||
                'pi',
 | 
			
		||||
                PrimeIcons.PAUSE,
 | 
			
		||||
              )}
 | 
			
		||||
 
 | 
			
		||||
@@ -117,7 +117,12 @@ config :wanderer_app,
 | 
			
		||||
  admins: admins,
 | 
			
		||||
  corp_id: System.get_env("WANDERER_CORP_ID", "-1") |> String.to_integer(),
 | 
			
		||||
  corp_wallet: System.get_env("WANDERER_CORP_WALLET", ""),
 | 
			
		||||
  corp_wallet_eve_id: System.get_env("WANDERER_CORP_WALLET_EVE_ID", "-1"),
 | 
			
		||||
  public_api_disabled: public_api_disabled,
 | 
			
		||||
  active_tracking_pool: System.get_env("WANDERER_ACTIVE_TRACKING_POOL", "default"),
 | 
			
		||||
  character_tracking_pause_disabled:
 | 
			
		||||
    System.get_env("WANDERER_CHARACTER_TRACKING_PAUSE_DISABLED", "true")
 | 
			
		||||
    |> String.to_existing_atom(),
 | 
			
		||||
  character_api_disabled:
 | 
			
		||||
    System.get_env("WANDERER_CHARACTER_API_DISABLED", "true") |> String.to_existing_atom(),
 | 
			
		||||
  zkill_preload_disabled:
 | 
			
		||||
@@ -170,11 +175,31 @@ config :ueberauth, WandererApp.Ueberauth.Strategy.Eve.OAuth,
 | 
			
		||||
  client_id: {WandererApp.Ueberauth, :client_id},
 | 
			
		||||
  client_secret: {WandererApp.Ueberauth, :client_secret},
 | 
			
		||||
  client_id_default: System.get_env("EVE_CLIENT_ID", "<EVE_CLIENT_ID>"),
 | 
			
		||||
  client_id_1: System.get_env("EVE_CLIENT_ID_1", ""),
 | 
			
		||||
  client_id_2: System.get_env("EVE_CLIENT_ID_2", ""),
 | 
			
		||||
  client_id_3: System.get_env("EVE_CLIENT_ID_3", ""),
 | 
			
		||||
  client_id_4: System.get_env("EVE_CLIENT_ID_4", ""),
 | 
			
		||||
  client_id_5: System.get_env("EVE_CLIENT_ID_5", ""),
 | 
			
		||||
  client_id_6: System.get_env("EVE_CLIENT_ID_6", ""),
 | 
			
		||||
  client_id_7: System.get_env("EVE_CLIENT_ID_7", ""),
 | 
			
		||||
  client_id_8: System.get_env("EVE_CLIENT_ID_8", ""),
 | 
			
		||||
  client_id_9: System.get_env("EVE_CLIENT_ID_9", ""),
 | 
			
		||||
  client_id_10: System.get_env("EVE_CLIENT_ID_10", ""),
 | 
			
		||||
  client_id_with_wallet:
 | 
			
		||||
    System.get_env("EVE_CLIENT_WITH_WALLET_ID", "<EVE_CLIENT_WITH_WALLET_ID>"),
 | 
			
		||||
  client_id_with_corp_wallet:
 | 
			
		||||
    System.get_env("EVE_CLIENT_WITH_CORP_WALLET_ID", "<EVE_CLIENT_WITH_CORP_WALLET_ID>"),
 | 
			
		||||
  client_secret_default: System.get_env("EVE_CLIENT_SECRET", "<EVE_CLIENT_SECRET>"),
 | 
			
		||||
  client_secret_1: System.get_env("EVE_CLIENT_SECRET_1", ""),
 | 
			
		||||
  client_secret_2: System.get_env("EVE_CLIENT_SECRET_2", ""),
 | 
			
		||||
  client_secret_3: System.get_env("EVE_CLIENT_SECRET_3", ""),
 | 
			
		||||
  client_secret_4: System.get_env("EVE_CLIENT_SECRET_4", ""),
 | 
			
		||||
  client_secret_5: System.get_env("EVE_CLIENT_SECRET_5", ""),
 | 
			
		||||
  client_secret_6: System.get_env("EVE_CLIENT_SECRET_6", ""),
 | 
			
		||||
  client_secret_7: System.get_env("EVE_CLIENT_SECRET_7", ""),
 | 
			
		||||
  client_secret_8: System.get_env("EVE_CLIENT_SECRET_8", ""),
 | 
			
		||||
  client_secret_9: System.get_env("EVE_CLIENT_SECRET_9", ""),
 | 
			
		||||
  client_secret_10: System.get_env("EVE_CLIENT_SECRET_10", ""),
 | 
			
		||||
  client_secret_with_wallet:
 | 
			
		||||
    System.get_env("EVE_CLIENT_WITH_WALLET_SECRET", "<EVE_CLIENT_WITH_WALLET_SECRET>"),
 | 
			
		||||
  client_secret_with_corp_wallet:
 | 
			
		||||
 
 | 
			
		||||
@@ -29,5 +29,6 @@ defmodule WandererApp.Api do
 | 
			
		||||
    resource WandererApp.Api.CorpWalletTransaction
 | 
			
		||||
    resource WandererApp.Api.License
 | 
			
		||||
    resource WandererApp.Api.MapPing
 | 
			
		||||
    resource WandererApp.Api.MapInvite
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,7 @@ defmodule WandererApp.Api.Character do
 | 
			
		||||
    define(:update_alliance, action: :update_alliance)
 | 
			
		||||
    define(:update_wallet_balance, action: :update_wallet_balance)
 | 
			
		||||
    define(:mark_as_deleted, action: :mark_as_deleted)
 | 
			
		||||
    define(:last_active, action: :last_active)
 | 
			
		||||
 | 
			
		||||
    define(:by_id,
 | 
			
		||||
      get_by: [:id],
 | 
			
		||||
@@ -47,7 +48,8 @@ defmodule WandererApp.Api.Character do
 | 
			
		||||
      :access_token,
 | 
			
		||||
      :refresh_token,
 | 
			
		||||
      :expires_at,
 | 
			
		||||
      :scopes
 | 
			
		||||
      :scopes,
 | 
			
		||||
      :tracking_pool
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    defaults [:create, :read, :destroy]
 | 
			
		||||
@@ -72,6 +74,12 @@ defmodule WandererApp.Api.Character do
 | 
			
		||||
      filter(expr(user_id == ^arg(:user_id) and deleted == false))
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    read :last_active do
 | 
			
		||||
      argument(:from, :utc_datetime, allow_nil?: false)
 | 
			
		||||
 | 
			
		||||
      filter(expr(updated_at > ^arg(:from)))
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    update :assign do
 | 
			
		||||
      accept []
 | 
			
		||||
      require_atomic? false
 | 
			
		||||
@@ -85,7 +93,7 @@ defmodule WandererApp.Api.Character do
 | 
			
		||||
 | 
			
		||||
    update :update do
 | 
			
		||||
      require_atomic? false
 | 
			
		||||
      accept([:name, :access_token, :refresh_token, :expires_at, :scopes])
 | 
			
		||||
      accept([:name, :access_token, :refresh_token, :expires_at, :scopes, :tracking_pool])
 | 
			
		||||
 | 
			
		||||
      change(set_attribute(:deleted, false))
 | 
			
		||||
    end
 | 
			
		||||
@@ -198,6 +206,7 @@ defmodule WandererApp.Api.Character do
 | 
			
		||||
    attribute :alliance_name, :string
 | 
			
		||||
    attribute :alliance_ticker, :string
 | 
			
		||||
    attribute :eve_wallet_balance, :float
 | 
			
		||||
    attribute :tracking_pool, :string
 | 
			
		||||
 | 
			
		||||
    create_timestamp(:inserted_at)
 | 
			
		||||
    update_timestamp(:updated_at)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										96
									
								
								lib/wanderer_app/api/map_invite.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								lib/wanderer_app/api/map_invite.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,96 @@
 | 
			
		||||
defmodule WandererApp.Api.MapInvite do
 | 
			
		||||
  @moduledoc false
 | 
			
		||||
 | 
			
		||||
  use Ash.Resource,
 | 
			
		||||
    domain: WandererApp.Api,
 | 
			
		||||
    data_layer: AshPostgres.DataLayer
 | 
			
		||||
 | 
			
		||||
  postgres do
 | 
			
		||||
    repo(WandererApp.Repo)
 | 
			
		||||
    table("map_invites_v1")
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  code_interface do
 | 
			
		||||
    define(:new, action: :new)
 | 
			
		||||
    define(:read, action: :read)
 | 
			
		||||
    define(:destroy, action: :destroy)
 | 
			
		||||
 | 
			
		||||
    define(:by_id,
 | 
			
		||||
      get_by: [:id],
 | 
			
		||||
      action: :read
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    define(:by_map,
 | 
			
		||||
      action: :by_map
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  actions do
 | 
			
		||||
    default_accept [
 | 
			
		||||
      :token
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    defaults [:read, :update, :destroy]
 | 
			
		||||
 | 
			
		||||
    create :new do
 | 
			
		||||
      accept [
 | 
			
		||||
        :map_id,
 | 
			
		||||
        :token,
 | 
			
		||||
        :type,
 | 
			
		||||
        :valid_until
 | 
			
		||||
      ]
 | 
			
		||||
 | 
			
		||||
      primary?(true)
 | 
			
		||||
 | 
			
		||||
      argument :map_id, :uuid, allow_nil?: true
 | 
			
		||||
 | 
			
		||||
      change manage_relationship(:map_id, :map, on_lookup: :relate, on_no_match: nil)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    read :by_map do
 | 
			
		||||
      argument(:map_id, :string, allow_nil?: false)
 | 
			
		||||
 | 
			
		||||
      filter(expr(map_id == ^arg(:map_id)))
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  attributes do
 | 
			
		||||
    uuid_primary_key :id
 | 
			
		||||
 | 
			
		||||
    attribute :token, :string do
 | 
			
		||||
      allow_nil? true
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    attribute :type, :atom do
 | 
			
		||||
      default "user"
 | 
			
		||||
 | 
			
		||||
      constraints(
 | 
			
		||||
        one_of: [
 | 
			
		||||
          :user,
 | 
			
		||||
          :admin
 | 
			
		||||
        ]
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      allow_nil?(false)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    attribute :valid_until, :utc_datetime do
 | 
			
		||||
      allow_nil? true
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    create_timestamp(:inserted_at)
 | 
			
		||||
    update_timestamp(:updated_at)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  relationships do
 | 
			
		||||
    belongs_to :map, WandererApp.Api.Map do
 | 
			
		||||
      attribute_writable? true
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  postgres do
 | 
			
		||||
    references do
 | 
			
		||||
      reference :map, on_delete: :delete
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -27,6 +27,7 @@ defmodule WandererApp.Application do
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        WandererApp.Cache,
 | 
			
		||||
        Supervisor.child_spec({Cachex, name: :esi_auth_cache}, id: :esi_auth_cache_worker),
 | 
			
		||||
        Supervisor.child_spec({Cachex, name: :api_cache}, id: :api_cache_worker),
 | 
			
		||||
        Supervisor.child_spec({Cachex, name: :system_static_info_cache},
 | 
			
		||||
          id: :system_static_info_cache_worker
 | 
			
		||||
@@ -40,6 +41,7 @@ defmodule WandererApp.Application do
 | 
			
		||||
        Supervisor.child_spec({Cachex, name: :tracked_characters},
 | 
			
		||||
          id: :tracked_characters_cache_worker
 | 
			
		||||
        ),
 | 
			
		||||
        WandererApp.Esi.InitClientsTask,
 | 
			
		||||
        WandererApp.Scheduler,
 | 
			
		||||
        {Registry, keys: :unique, name: WandererApp.MapRegistry},
 | 
			
		||||
        {Registry, keys: :unique, name: WandererApp.Character.TrackerRegistry},
 | 
			
		||||
@@ -47,17 +49,16 @@ defmodule WandererApp.Application do
 | 
			
		||||
         child_spec: DynamicSupervisor, name: WandererApp.Map.DynamicSupervisors},
 | 
			
		||||
        {PartitionSupervisor,
 | 
			
		||||
         child_spec: DynamicSupervisor, name: WandererApp.Character.DynamicSupervisors},
 | 
			
		||||
        WandererApp.Zkb.Supervisor,
 | 
			
		||||
        WandererApp.Server.ServerStatusTracker,
 | 
			
		||||
        WandererApp.Server.TheraDataFetcher,
 | 
			
		||||
        {WandererApp.Character.TrackerPoolSupervisor, []},
 | 
			
		||||
        WandererApp.Character.TrackerManager,
 | 
			
		||||
        WandererApp.Map.Manager,
 | 
			
		||||
        WandererApp.Map.ZkbDataFetcher,
 | 
			
		||||
        WandererAppWeb.Presence,
 | 
			
		||||
        WandererAppWeb.Endpoint
 | 
			
		||||
      ] ++
 | 
			
		||||
        maybe_start_corp_wallet_tracker(WandererApp.Env.map_subscriptions_enabled?())
 | 
			
		||||
        maybe_start_corp_wallet_tracker(WandererApp.Env.map_subscriptions_enabled?()) ++
 | 
			
		||||
        maybe_start_zkb(WandererApp.Env.zkill_preload_disabled?())
 | 
			
		||||
 | 
			
		||||
    opts = [strategy: :one_for_one, name: WandererApp.Supervisor]
 | 
			
		||||
 | 
			
		||||
@@ -78,6 +79,12 @@ defmodule WandererApp.Application do
 | 
			
		||||
    :ok
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp maybe_start_zkb(false),
 | 
			
		||||
    do: [WandererApp.Zkb.Supervisor, WandererApp.Map.ZkbDataFetcher]
 | 
			
		||||
 | 
			
		||||
  defp maybe_start_zkb(_),
 | 
			
		||||
    do: []
 | 
			
		||||
 | 
			
		||||
  defp maybe_start_corp_wallet_tracker(true),
 | 
			
		||||
    do: [
 | 
			
		||||
      WandererApp.StartCorpWalletTrackerTask
 | 
			
		||||
 
 | 
			
		||||
@@ -179,9 +179,9 @@ defmodule WandererApp.Character do
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def search(character_id, opts \\ []) do
 | 
			
		||||
    {:ok, %{access_token: access_token, eve_id: eve_id} = _character} =
 | 
			
		||||
    get_character(character_id)
 | 
			
		||||
 | 
			
		||||
    |> case do
 | 
			
		||||
      {:ok, %{access_token: access_token, eve_id: eve_id} = _character} ->
 | 
			
		||||
        case WandererApp.Esi.search(eve_id |> String.to_integer(),
 | 
			
		||||
               access_token: access_token,
 | 
			
		||||
               character_id: character_id,
 | 
			
		||||
@@ -195,6 +195,10 @@ defmodule WandererApp.Character do
 | 
			
		||||
            Logger.warning("#{__MODULE__} failed search: #{inspect(error)}")
 | 
			
		||||
            {:ok, []}
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
      error ->
 | 
			
		||||
        {:ok, []}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def can_track_wallet?(%{scopes: scopes} = _character) when not is_nil(scopes) do
 | 
			
		||||
@@ -203,12 +207,27 @@ defmodule WandererApp.Character do
 | 
			
		||||
 | 
			
		||||
  def can_track_wallet?(_), do: false
 | 
			
		||||
 | 
			
		||||
  def can_track_corp_wallet?(%{scopes: scopes} = _character) when not is_nil(scopes) do
 | 
			
		||||
    scopes |> String.split(" ") |> Enum.member?(@read_corp_wallet_scope)
 | 
			
		||||
  end
 | 
			
		||||
  def can_track_corp_wallet?(%{scopes: scopes} = _character)
 | 
			
		||||
      when not is_nil(scopes),
 | 
			
		||||
      do: scopes |> String.split(" ") |> Enum.member?(@read_corp_wallet_scope)
 | 
			
		||||
 | 
			
		||||
  def can_track_corp_wallet?(_), do: false
 | 
			
		||||
 | 
			
		||||
  @decorate cacheable(
 | 
			
		||||
              cache: WandererApp.Cache,
 | 
			
		||||
              key: "can_pause_tracking-#{character_id}"
 | 
			
		||||
            )
 | 
			
		||||
  def can_pause_tracking?(character_id) do
 | 
			
		||||
    case get_character(character_id) do
 | 
			
		||||
      {:ok, character} when not is_nil(character) ->
 | 
			
		||||
        not WandererApp.Env.character_tracking_pause_disabled?() &&
 | 
			
		||||
          not can_track_corp_wallet?(character)
 | 
			
		||||
 | 
			
		||||
      _ ->
 | 
			
		||||
        true
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def get_ship(%{ship: ship_type_id, ship_name: ship_name} = _character)
 | 
			
		||||
      when not is_nil(ship_type_id) and is_integer(ship_type_id) do
 | 
			
		||||
    ship_type_id
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,7 @@ defmodule WandererApp.Character.Tracker do
 | 
			
		||||
          status: binary()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
  @pause_tracking_timeout :timer.minutes(60 * 24)
 | 
			
		||||
  @pause_tracking_timeout :timer.minutes(60 * 10)
 | 
			
		||||
  @offline_timeout :timer.minutes(5)
 | 
			
		||||
  @online_error_timeout :timer.minutes(2)
 | 
			
		||||
  @ship_error_timeout :timer.minutes(2)
 | 
			
		||||
@@ -61,7 +61,11 @@ defmodule WandererApp.Character.Tracker do
 | 
			
		||||
    WandererApp.Cache.lookup!("character:#{character_id}:last_online_time")
 | 
			
		||||
    |> case do
 | 
			
		||||
      nil ->
 | 
			
		||||
        pause_tracking(character_id)
 | 
			
		||||
        WandererApp.Cache.insert(
 | 
			
		||||
          "character:#{character_id}:last_online_time",
 | 
			
		||||
          DateTime.utc_now()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        :ok
 | 
			
		||||
 | 
			
		||||
      last_online_time ->
 | 
			
		||||
@@ -106,7 +110,8 @@ defmodule WandererApp.Character.Tracker do
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp pause_tracking(character_id) do
 | 
			
		||||
    if not WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused") do
 | 
			
		||||
    if WandererApp.Character.can_pause_tracking?(character_id) &&
 | 
			
		||||
         not WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused") do
 | 
			
		||||
      WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
 | 
			
		||||
      WandererApp.Cache.delete("character:#{character_id}:online_error_time")
 | 
			
		||||
      WandererApp.Cache.delete("character:#{character_id}:ship_error_time")
 | 
			
		||||
@@ -182,8 +187,6 @@ defmodule WandererApp.Character.Tracker do
 | 
			
		||||
                    "character:#{character_id}:last_online_time",
 | 
			
		||||
                    DateTime.utc_now()
 | 
			
		||||
                  )
 | 
			
		||||
                else
 | 
			
		||||
                  WandererApp.Cache.delete("character:#{character_id}:last_online_time")
 | 
			
		||||
                end
 | 
			
		||||
 | 
			
		||||
                WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
 | 
			
		||||
@@ -230,9 +233,7 @@ defmodule WandererApp.Character.Tracker do
 | 
			
		||||
              {:error, :error_limited, headers} ->
 | 
			
		||||
                reset_timeout = get_reset_timeout(headers)
 | 
			
		||||
 | 
			
		||||
                Logger.warning(
 | 
			
		||||
                  "#{__MODULE__} failed to update_online: #{inspect(:error_limited)}"
 | 
			
		||||
                )
 | 
			
		||||
                Logger.warning(".")
 | 
			
		||||
 | 
			
		||||
                WandererApp.Cache.put(
 | 
			
		||||
                  "character:#{character_id}:online_forbidden",
 | 
			
		||||
@@ -319,9 +320,7 @@ defmodule WandererApp.Character.Tracker do
 | 
			
		||||
          {:error, :error_limited, headers} ->
 | 
			
		||||
            reset_timeout = get_reset_timeout(headers)
 | 
			
		||||
 | 
			
		||||
            Logger.warning(
 | 
			
		||||
              "#{__MODULE__} failed to get_character_info: #{inspect(:error_limited)}"
 | 
			
		||||
            )
 | 
			
		||||
            Logger.warning(".")
 | 
			
		||||
 | 
			
		||||
            WandererApp.Cache.put(
 | 
			
		||||
              "character:#{character_id}:info_forbidden",
 | 
			
		||||
@@ -398,7 +397,7 @@ defmodule WandererApp.Character.Tracker do
 | 
			
		||||
              {:error, :error_limited, headers} ->
 | 
			
		||||
                reset_timeout = get_reset_timeout(headers)
 | 
			
		||||
 | 
			
		||||
                Logger.warning("#{__MODULE__} failed to update_ship: #{inspect(:error_limited)}")
 | 
			
		||||
                Logger.warning(".")
 | 
			
		||||
 | 
			
		||||
                WandererApp.Cache.put(
 | 
			
		||||
                  "character:#{character_id}:ship_forbidden",
 | 
			
		||||
@@ -464,8 +463,7 @@ defmodule WandererApp.Character.Tracker do
 | 
			
		||||
      ) do
 | 
			
		||||
    case WandererApp.Character.get_character(character_id) do
 | 
			
		||||
      {:ok, %{eve_id: eve_id, access_token: access_token}} when not is_nil(access_token) ->
 | 
			
		||||
        (WandererApp.Cache.has_key?("character:#{character_id}:location_forbidden") ||
 | 
			
		||||
           WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
 | 
			
		||||
        WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused")
 | 
			
		||||
        |> case do
 | 
			
		||||
          true ->
 | 
			
		||||
            {:error, :skipped}
 | 
			
		||||
@@ -496,9 +494,7 @@ defmodule WandererApp.Character.Tracker do
 | 
			
		||||
                {:error, :skipped}
 | 
			
		||||
 | 
			
		||||
              {:error, :error_limited, headers} ->
 | 
			
		||||
                Logger.warning(
 | 
			
		||||
                  "#{__MODULE__} failed to update_location: #{inspect(:error_limited)}"
 | 
			
		||||
                )
 | 
			
		||||
                Logger.warning(".")
 | 
			
		||||
 | 
			
		||||
                reset_timeout = get_reset_timeout(headers, @location_limit_ttl)
 | 
			
		||||
 | 
			
		||||
@@ -593,9 +589,7 @@ defmodule WandererApp.Character.Tracker do
 | 
			
		||||
                  {:error, :error_limited, headers} ->
 | 
			
		||||
                    reset_timeout = get_reset_timeout(headers)
 | 
			
		||||
 | 
			
		||||
                    Logger.warning(
 | 
			
		||||
                      "#{__MODULE__} failed to update_wallet: #{inspect(:error_limited)}"
 | 
			
		||||
                    )
 | 
			
		||||
                    Logger.warning(".")
 | 
			
		||||
 | 
			
		||||
                    WandererApp.Cache.put(
 | 
			
		||||
                      "character:#{character_id}:wallet_forbidden",
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,8 @@ defmodule WandererApp.Character.TrackerManager.Impl do
 | 
			
		||||
 | 
			
		||||
  @garbage_collection_interval :timer.minutes(15)
 | 
			
		||||
  @untrack_characters_interval :timer.minutes(1)
 | 
			
		||||
  @inactive_character_timeout :timer.minutes(5)
 | 
			
		||||
  @inactive_character_timeout :timer.minutes(10)
 | 
			
		||||
  @untrack_character_timeout :timer.minutes(10)
 | 
			
		||||
 | 
			
		||||
  @logger Application.compile_env(:wanderer_app, :logger)
 | 
			
		||||
 | 
			
		||||
@@ -107,20 +108,27 @@ defmodule WandererApp.Character.TrackerManager.Impl do
 | 
			
		||||
        } = track_settings
 | 
			
		||||
      ) do
 | 
			
		||||
    if track do
 | 
			
		||||
      WandererApp.Cache.insert_or_update(
 | 
			
		||||
        "character_untrack_queue",
 | 
			
		||||
        [],
 | 
			
		||||
        fn untrack_queue ->
 | 
			
		||||
          untrack_queue
 | 
			
		||||
          |> Enum.reject(fn {m_id, c_id} -> m_id == map_id and c_id == character_id end)
 | 
			
		||||
        end
 | 
			
		||||
      )
 | 
			
		||||
      remove_from_untrack_queue(map_id, character_id)
 | 
			
		||||
 | 
			
		||||
      {:ok, character_state} =
 | 
			
		||||
        WandererApp.Character.Tracker.update_settings(character_id, track_settings)
 | 
			
		||||
 | 
			
		||||
      WandererApp.Character.update_character_state(character_id, character_state)
 | 
			
		||||
    else
 | 
			
		||||
      add_to_untrack_queue(map_id, character_id)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    state
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def add_to_untrack_queue(map_id, character_id) do
 | 
			
		||||
    if not WandererApp.Cache.has_key?("#{map_id}:#{character_id}:untrack_requested") do
 | 
			
		||||
      WandererApp.Cache.insert(
 | 
			
		||||
        "#{map_id}:#{character_id}:untrack_requested",
 | 
			
		||||
        DateTime.utc_now()
 | 
			
		||||
      )
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    WandererApp.Cache.insert_or_update(
 | 
			
		||||
      "character_untrack_queue",
 | 
			
		||||
      [{map_id, character_id}],
 | 
			
		||||
@@ -130,7 +138,17 @@ defmodule WandererApp.Character.TrackerManager.Impl do
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
    state
 | 
			
		||||
  def remove_from_untrack_queue(map_id, character_id) do
 | 
			
		||||
    WandererApp.Cache.delete("#{map_id}:#{character_id}:untrack_requested")
 | 
			
		||||
 | 
			
		||||
    WandererApp.Cache.insert_or_update(
 | 
			
		||||
      "character_untrack_queue",
 | 
			
		||||
      [],
 | 
			
		||||
      fn untrack_queue ->
 | 
			
		||||
        untrack_queue
 | 
			
		||||
        |> Enum.reject(fn {m_id, c_id} -> m_id == map_id and c_id == character_id end)
 | 
			
		||||
      end
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def get_characters(
 | 
			
		||||
@@ -208,10 +226,28 @@ defmodule WandererApp.Character.TrackerManager.Impl do
 | 
			
		||||
      ) do
 | 
			
		||||
    Process.send_after(self(), :untrack_characters, @untrack_characters_interval)
 | 
			
		||||
 | 
			
		||||
    WandererApp.Cache.get_and_remove!("character_untrack_queue", [])
 | 
			
		||||
    WandererApp.Cache.lookup!("character_untrack_queue", [])
 | 
			
		||||
    |> Task.async_stream(
 | 
			
		||||
      fn {map_id, character_id} ->
 | 
			
		||||
        if not character_is_present(map_id, character_id) do
 | 
			
		||||
        untrack_timeout_reached =
 | 
			
		||||
          if WandererApp.Cache.has_key?("#{map_id}:#{character_id}:untrack_requested") do
 | 
			
		||||
            untrack_requested =
 | 
			
		||||
              WandererApp.Cache.lookup!(
 | 
			
		||||
                "#{map_id}:#{character_id}:untrack_requested",
 | 
			
		||||
                DateTime.utc_now()
 | 
			
		||||
              )
 | 
			
		||||
 | 
			
		||||
            duration = DateTime.diff(DateTime.utc_now(), untrack_requested, :millisecond)
 | 
			
		||||
            duration >= @untrack_character_timeout
 | 
			
		||||
          else
 | 
			
		||||
            false
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
        Logger.debug(fn -> "Untrack timeout reached: #{inspect(untrack_timeout_reached)}" end)
 | 
			
		||||
 | 
			
		||||
        if untrack_timeout_reached do
 | 
			
		||||
          remove_from_untrack_queue(map_id, character_id)
 | 
			
		||||
 | 
			
		||||
          WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:solar_system_id")
 | 
			
		||||
          WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:station_id")
 | 
			
		||||
          WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:structure_id")
 | 
			
		||||
@@ -235,6 +271,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
          WandererApp.Character.update_character_state(character_id, character_state)
 | 
			
		||||
          WandererApp.Map.Server.Impl.broadcast!(map_id, :untrack_character, character_id)
 | 
			
		||||
        end
 | 
			
		||||
      end,
 | 
			
		||||
      max_concurrency: System.schedulers_online(),
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,10 @@ defmodule WandererApp.Character.TrackerPool do
 | 
			
		||||
 | 
			
		||||
  @update_location_interval :timer.seconds(1)
 | 
			
		||||
  @update_online_interval :timer.seconds(5)
 | 
			
		||||
  @check_online_errors_interval :timer.seconds(30)
 | 
			
		||||
  @check_offline_characters_interval :timer.minutes(2)
 | 
			
		||||
  @check_online_errors_interval :timer.minutes(1)
 | 
			
		||||
  @check_ship_errors_interval :timer.minutes(1)
 | 
			
		||||
  @check_location_errors_interval :timer.minutes(1)
 | 
			
		||||
  @update_ship_interval :timer.seconds(2)
 | 
			
		||||
  @update_info_interval :timer.minutes(1)
 | 
			
		||||
  @update_wallet_interval :timer.minutes(1)
 | 
			
		||||
@@ -130,7 +133,10 @@ defmodule WandererApp.Character.TrackerPool do
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    Process.send_after(self(), :update_online, 100)
 | 
			
		||||
    Process.send_after(self(), :check_online_errors, @check_online_errors_interval)
 | 
			
		||||
    Process.send_after(self(), :check_online_errors, :timer.seconds(60))
 | 
			
		||||
    Process.send_after(self(), :check_ship_errors, :timer.seconds(90))
 | 
			
		||||
    Process.send_after(self(), :check_location_errors, :timer.seconds(120))
 | 
			
		||||
    Process.send_after(self(), :check_offline_characters, @check_offline_characters_interval)
 | 
			
		||||
    Process.send_after(self(), :update_location, 300)
 | 
			
		||||
    Process.send_after(self(), :update_ship, 500)
 | 
			
		||||
    Process.send_after(self(), :update_info, 1500)
 | 
			
		||||
@@ -217,6 +223,50 @@ defmodule WandererApp.Character.TrackerPool do
 | 
			
		||||
    {:noreply, state}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_info(
 | 
			
		||||
        :check_offline_characters,
 | 
			
		||||
        %{
 | 
			
		||||
          characters: characters
 | 
			
		||||
        } =
 | 
			
		||||
          state
 | 
			
		||||
      ) do
 | 
			
		||||
    Process.send_after(self(), :check_offline_characters, @check_offline_characters_interval)
 | 
			
		||||
 | 
			
		||||
    try do
 | 
			
		||||
      characters
 | 
			
		||||
      |> Task.async_stream(
 | 
			
		||||
        fn character_id ->
 | 
			
		||||
          if WandererApp.Character.can_pause_tracking?(character_id) do
 | 
			
		||||
            WandererApp.TaskWrapper.start_link(
 | 
			
		||||
              WandererApp.Character.Tracker,
 | 
			
		||||
              :check_offline,
 | 
			
		||||
              [
 | 
			
		||||
                character_id
 | 
			
		||||
              ]
 | 
			
		||||
            )
 | 
			
		||||
          else
 | 
			
		||||
            :ok
 | 
			
		||||
          end
 | 
			
		||||
        end,
 | 
			
		||||
        timeout: :timer.seconds(15),
 | 
			
		||||
        max_concurrency: System.schedulers_online(),
 | 
			
		||||
        on_timeout: :kill_task
 | 
			
		||||
      )
 | 
			
		||||
      |> Enum.each(fn
 | 
			
		||||
        {:ok, _result} -> :ok
 | 
			
		||||
        {:error, reason} -> @logger.error("Error in check_offline: #{inspect(reason)}")
 | 
			
		||||
      end)
 | 
			
		||||
    rescue
 | 
			
		||||
      e ->
 | 
			
		||||
        Logger.error("""
 | 
			
		||||
        [Tracker Pool] check_offline => exception: #{Exception.message(e)}
 | 
			
		||||
        #{Exception.format_stacktrace(__STACKTRACE__)}
 | 
			
		||||
        """)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    {:noreply, state}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_info(
 | 
			
		||||
        :check_online_errors,
 | 
			
		||||
        %{
 | 
			
		||||
@@ -257,6 +307,86 @@ defmodule WandererApp.Character.TrackerPool do
 | 
			
		||||
    {:noreply, state}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_info(
 | 
			
		||||
        :check_ship_errors,
 | 
			
		||||
        %{
 | 
			
		||||
          characters: characters
 | 
			
		||||
        } =
 | 
			
		||||
          state
 | 
			
		||||
      ) do
 | 
			
		||||
    Process.send_after(self(), :check_ship_errors, @check_ship_errors_interval)
 | 
			
		||||
 | 
			
		||||
    try do
 | 
			
		||||
      characters
 | 
			
		||||
      |> Task.async_stream(
 | 
			
		||||
        fn character_id ->
 | 
			
		||||
          WandererApp.TaskWrapper.start_link(
 | 
			
		||||
            WandererApp.Character.Tracker,
 | 
			
		||||
            :check_ship_errors,
 | 
			
		||||
            [
 | 
			
		||||
              character_id
 | 
			
		||||
            ]
 | 
			
		||||
          )
 | 
			
		||||
        end,
 | 
			
		||||
        timeout: :timer.seconds(15),
 | 
			
		||||
        max_concurrency: System.schedulers_online(),
 | 
			
		||||
        on_timeout: :kill_task
 | 
			
		||||
      )
 | 
			
		||||
      |> Enum.each(fn
 | 
			
		||||
        {:ok, _result} -> :ok
 | 
			
		||||
        {:error, reason} -> @logger.error("Error in check_ship_errors: #{inspect(reason)}")
 | 
			
		||||
      end)
 | 
			
		||||
    rescue
 | 
			
		||||
      e ->
 | 
			
		||||
        Logger.error("""
 | 
			
		||||
        [Tracker Pool] check_ship_errors => exception: #{Exception.message(e)}
 | 
			
		||||
        #{Exception.format_stacktrace(__STACKTRACE__)}
 | 
			
		||||
        """)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    {:noreply, state}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_info(
 | 
			
		||||
        :check_location_errors,
 | 
			
		||||
        %{
 | 
			
		||||
          characters: characters
 | 
			
		||||
        } =
 | 
			
		||||
          state
 | 
			
		||||
      ) do
 | 
			
		||||
    Process.send_after(self(), :check_location_errors, @check_location_errors_interval)
 | 
			
		||||
 | 
			
		||||
    try do
 | 
			
		||||
      characters
 | 
			
		||||
      |> Task.async_stream(
 | 
			
		||||
        fn character_id ->
 | 
			
		||||
          WandererApp.TaskWrapper.start_link(
 | 
			
		||||
            WandererApp.Character.Tracker,
 | 
			
		||||
            :check_location_errors,
 | 
			
		||||
            [
 | 
			
		||||
              character_id
 | 
			
		||||
            ]
 | 
			
		||||
          )
 | 
			
		||||
        end,
 | 
			
		||||
        timeout: :timer.seconds(15),
 | 
			
		||||
        max_concurrency: System.schedulers_online(),
 | 
			
		||||
        on_timeout: :kill_task
 | 
			
		||||
      )
 | 
			
		||||
      |> Enum.each(fn
 | 
			
		||||
        {:ok, _result} -> :ok
 | 
			
		||||
        {:error, reason} -> @logger.error("Error in check_location_errors: #{inspect(reason)}")
 | 
			
		||||
      end)
 | 
			
		||||
    rescue
 | 
			
		||||
      e ->
 | 
			
		||||
        Logger.error("""
 | 
			
		||||
        [Tracker Pool] check_location_errors => exception: #{Exception.message(e)}
 | 
			
		||||
        #{Exception.format_stacktrace(__STACKTRACE__)}
 | 
			
		||||
        """)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    {:noreply, state}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_info(
 | 
			
		||||
        :update_location,
 | 
			
		||||
        %{
 | 
			
		||||
 
 | 
			
		||||
@@ -54,7 +54,8 @@ defmodule WandererApp.Character.TransactionsTracker.Impl do
 | 
			
		||||
 | 
			
		||||
    {:ok, latest_transactions} = WandererApp.Api.CorpWalletTransaction.latest()
 | 
			
		||||
 | 
			
		||||
    case WandererApp.Character.can_track_corp_wallet?(character) do
 | 
			
		||||
    case character.eve_id == WandererApp.Env.corp_wallet_eve_id() &&
 | 
			
		||||
           WandererApp.Character.can_track_corp_wallet?(character) do
 | 
			
		||||
      true ->
 | 
			
		||||
        Process.send_after(self(), :update_corp_wallets, 500)
 | 
			
		||||
        Process.send_after(self(), :check_wallets, 500)
 | 
			
		||||
@@ -163,7 +164,7 @@ defmodule WandererApp.Character.TransactionsTracker.Impl do
 | 
			
		||||
        {:error, :forbidden}
 | 
			
		||||
 | 
			
		||||
      {:error, :error_limited, _headers} ->
 | 
			
		||||
        Logger.warning("#{__MODULE__} failed to get_wallet_journal: error_limited")
 | 
			
		||||
        Logger.warning(".")
 | 
			
		||||
        {:error, :error_limited}
 | 
			
		||||
 | 
			
		||||
      {:error, error} ->
 | 
			
		||||
@@ -192,7 +193,7 @@ defmodule WandererApp.Character.TransactionsTracker.Impl do
 | 
			
		||||
        {:error, :forbidden}
 | 
			
		||||
 | 
			
		||||
      {:error, :error_limited, _headers} ->
 | 
			
		||||
        Logger.warning("#{__MODULE__} failed to update_corp_wallets: error_limited")
 | 
			
		||||
        Logger.warning(".")
 | 
			
		||||
        {:error, :error_limited}
 | 
			
		||||
 | 
			
		||||
      {:error, error} ->
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,13 @@ defmodule WandererApp.Env do
 | 
			
		||||
  def invites, do: get_key(:invites, false)
 | 
			
		||||
  def map_subscriptions_enabled?, do: get_key(:map_subscriptions_enabled, false)
 | 
			
		||||
  def public_api_disabled?, do: get_key(:public_api_disabled, false)
 | 
			
		||||
 | 
			
		||||
  @decorate cacheable(
 | 
			
		||||
              cache: WandererApp.Cache,
 | 
			
		||||
              key: "active_tracking_pool"
 | 
			
		||||
            )
 | 
			
		||||
  def active_tracking_pool, do: get_key(:active_tracking_pool, "default")
 | 
			
		||||
  def character_tracking_pause_disabled?, do: get_key(:character_tracking_pause_disabled, true)
 | 
			
		||||
  def character_api_disabled?, do: get_key(:character_api_disabled, false)
 | 
			
		||||
  def zkill_preload_disabled?, do: get_key(:zkill_preload_disabled, false)
 | 
			
		||||
  def wallet_tracking_enabled?, do: get_key(:wallet_tracking_enabled, false)
 | 
			
		||||
@@ -23,6 +30,7 @@ defmodule WandererApp.Env do
 | 
			
		||||
  def admin_username, do: get_key(:admin_username)
 | 
			
		||||
  def admin_password, do: get_key(:admin_password)
 | 
			
		||||
  def corp_wallet, do: get_key(:corp_wallet, "")
 | 
			
		||||
  def corp_wallet_eve_id, do: get_key(:corp_wallet_eve_id, "-1")
 | 
			
		||||
  def corp_eve_id, do: get_key(:corp_id, -1)
 | 
			
		||||
  def subscription_settings, do: get_key(:subscription_settings)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -536,7 +536,6 @@ defmodule WandererApp.Esi.ApiClient do
 | 
			
		||||
          {:error, :not_found}
 | 
			
		||||
 | 
			
		||||
        {:ok, %{status: 420, headers: headers} = _error} ->
 | 
			
		||||
          Logger.warning("#{path} error_limited error: #{inspect(headers)}")
 | 
			
		||||
          {:error, :error_limited, headers}
 | 
			
		||||
 | 
			
		||||
        {:ok, %{status: status} = _error} when status in [401, 403] ->
 | 
			
		||||
@@ -593,7 +592,6 @@ defmodule WandererApp.Esi.ApiClient do
 | 
			
		||||
          {:error, :forbidden}
 | 
			
		||||
 | 
			
		||||
        {:ok, %{status: 420, headers: headers} = _error} ->
 | 
			
		||||
          Logger.warning("#{url} error_limited error: #{inspect(headers)}")
 | 
			
		||||
          {:error, :error_limited, headers}
 | 
			
		||||
 | 
			
		||||
        {:ok, %{status: status}} ->
 | 
			
		||||
@@ -632,7 +630,6 @@ defmodule WandererApp.Esi.ApiClient do
 | 
			
		||||
          {:error, :forbidden}
 | 
			
		||||
 | 
			
		||||
        {:ok, %{status: 420, headers: headers} = _error} ->
 | 
			
		||||
          Logger.warning("#{url} error_limited error: #{inspect(headers)}")
 | 
			
		||||
          {:error, :error_limited, headers}
 | 
			
		||||
 | 
			
		||||
        {:ok, %{status: status}} ->
 | 
			
		||||
@@ -674,13 +671,20 @@ defmodule WandererApp.Esi.ApiClient do
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp refresh_token(character_id) do
 | 
			
		||||
    {:ok, %{expires_at: expires_at, refresh_token: refresh_token, scopes: scopes} = character} =
 | 
			
		||||
    {:ok,
 | 
			
		||||
     %{
 | 
			
		||||
       expires_at: expires_at,
 | 
			
		||||
       refresh_token: refresh_token,
 | 
			
		||||
       scopes: scopes,
 | 
			
		||||
       tracking_pool: tracking_pool
 | 
			
		||||
     } = character} =
 | 
			
		||||
      WandererApp.Character.get_character(character_id)
 | 
			
		||||
 | 
			
		||||
    refresh_token_result =
 | 
			
		||||
      WandererApp.Ueberauth.Strategy.Eve.OAuth.get_refresh_token([],
 | 
			
		||||
        with_wallet: WandererApp.Character.can_track_wallet?(character),
 | 
			
		||||
        is_admin?: WandererApp.Character.can_track_corp_wallet?(character),
 | 
			
		||||
        tracking_pool: tracking_pool,
 | 
			
		||||
        token: %OAuth2.AccessToken{refresh_token: refresh_token}
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ defmodule WandererApp.StartCorpWalletTrackerTask do
 | 
			
		||||
 | 
			
		||||
      user_hash ->
 | 
			
		||||
        user_hash
 | 
			
		||||
        |> _get_user_characters()
 | 
			
		||||
        |> get_user_characters()
 | 
			
		||||
        |> maybe_start_corp_wallet_tracker()
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
@@ -25,6 +25,7 @@ defmodule WandererApp.StartCorpWalletTrackerTask do
 | 
			
		||||
    admin_character =
 | 
			
		||||
      user_characters
 | 
			
		||||
      |> Enum.find(fn character ->
 | 
			
		||||
        character.eve_id == WandererApp.Env.corp_wallet_eve_id() &&
 | 
			
		||||
          WandererApp.Character.can_track_corp_wallet?(character)
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
@@ -41,12 +42,12 @@ defmodule WandererApp.StartCorpWalletTrackerTask do
 | 
			
		||||
 | 
			
		||||
  def maybe_start_corp_wallet_tracker(_), do: :ok
 | 
			
		||||
 | 
			
		||||
  defp _get_user_characters(user_hash) when not is_nil(user_hash) and is_binary(user_hash) do
 | 
			
		||||
  defp get_user_characters(user_hash) when not is_nil(user_hash) and is_binary(user_hash) do
 | 
			
		||||
    case WandererApp.Api.User.by_hash(user_hash, load: :characters) do
 | 
			
		||||
      {:ok, user} -> {:ok, user.characters}
 | 
			
		||||
      {:error, _} -> {:ok, []}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp _get_user_characters(_), do: {:ok, []}
 | 
			
		||||
  defp get_user_characters(_), do: {:ok, []}
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -294,14 +294,16 @@ defmodule WandererApp.Map do
 | 
			
		||||
    map_id
 | 
			
		||||
    |> update_map(%{characters_limit: characters_limit, hubs_limit: hubs_limit})
 | 
			
		||||
 | 
			
		||||
    map
 | 
			
		||||
    map_id
 | 
			
		||||
    |> get_map!()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def update_options!(%{map_id: map_id} = map, options) do
 | 
			
		||||
    map_id
 | 
			
		||||
    |> update_map(%{options: options})
 | 
			
		||||
 | 
			
		||||
    map
 | 
			
		||||
    map_id
 | 
			
		||||
    |> get_map!()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def add_systems!(map, []), do: map
 | 
			
		||||
 
 | 
			
		||||
@@ -76,6 +76,11 @@ defmodule WandererApp.Map.Operations do
 | 
			
		||||
          {:ok, map()} | {:skip, :exists} | {:error, String.t()}
 | 
			
		||||
  defdelegate create_connection(map_id, attrs, char_id), to: Connections
 | 
			
		||||
 | 
			
		||||
  @doc "Create a connection from a Plug.Conn"
 | 
			
		||||
  @spec create_connection(Plug.Conn.t(), map()) ::
 | 
			
		||||
          {:ok, :created} | {:skip, :exists} | {:error, atom()}
 | 
			
		||||
  defdelegate create_connection(conn, attrs), to: Connections
 | 
			
		||||
 | 
			
		||||
  @doc "Update a connection"
 | 
			
		||||
  @spec update_connection(String.t(), String.t(), map()) ::
 | 
			
		||||
          {:ok, map()} | {:error, String.t()}
 | 
			
		||||
 
 | 
			
		||||
@@ -212,7 +212,7 @@ defmodule WandererApp.Map.Server do
 | 
			
		||||
    do:
 | 
			
		||||
      map_id
 | 
			
		||||
      |> map_pid!
 | 
			
		||||
      |> GenServer.call({&Impl.update_subscription_settings/2, [settings]})
 | 
			
		||||
      |> GenServer.cast({&Impl.update_subscription_settings/2, [settings]})
 | 
			
		||||
 | 
			
		||||
  def delete_connection(map_id, connection_info) when is_binary(map_id),
 | 
			
		||||
    do:
 | 
			
		||||
 
 | 
			
		||||
@@ -5,9 +5,10 @@ defmodule WandererApp.Map.Operations.Connections do
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  require Logger
 | 
			
		||||
  alias WandererApp.Map.Server.{ConnectionsImpl, Server}
 | 
			
		||||
  alias WandererApp.Map.Server
 | 
			
		||||
  alias Ash.Error.Invalid
 | 
			
		||||
  alias WandererApp.MapConnectionRepo
 | 
			
		||||
  alias WandererApp.CachedInfo
 | 
			
		||||
 | 
			
		||||
  # Connection type constants
 | 
			
		||||
  @connection_type_wormhole 0
 | 
			
		||||
@@ -20,7 +21,7 @@ defmodule WandererApp.Map.Operations.Connections do
 | 
			
		||||
  @xlarge_ship_size 3
 | 
			
		||||
 | 
			
		||||
  # System class constants
 | 
			
		||||
  @c1_system_class "C1"
 | 
			
		||||
  @c1_system_class 1
 | 
			
		||||
 | 
			
		||||
  @doc """
 | 
			
		||||
  Creates a connection between two systems, applying special rules for C1 wormholes.
 | 
			
		||||
@@ -34,8 +35,8 @@ defmodule WandererApp.Map.Operations.Connections do
 | 
			
		||||
  defp do_create(attrs, map_id, char_id) do
 | 
			
		||||
    with {:ok, source} <- parse_int(attrs["solar_system_source"], "solar_system_source"),
 | 
			
		||||
         {:ok, target} <- parse_int(attrs["solar_system_target"], "solar_system_target"),
 | 
			
		||||
         {:ok, src_info} <- ConnectionsImpl.get_system_static_info(source),
 | 
			
		||||
         {:ok, tgt_info} <- ConnectionsImpl.get_system_static_info(target) do
 | 
			
		||||
         {:ok, src_info} <- CachedInfo.get_system_static_info(source),
 | 
			
		||||
         {:ok, tgt_info} <- CachedInfo.get_system_static_info(target) do
 | 
			
		||||
      build_and_add_connection(attrs, map_id, char_id, src_info, tgt_info)
 | 
			
		||||
    else
 | 
			
		||||
      {:error, reason} -> handle_precondition_error(reason, attrs)
 | 
			
		||||
@@ -45,6 +46,12 @@ defmodule WandererApp.Map.Operations.Connections do
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp build_and_add_connection(attrs, map_id, char_id, src_info, tgt_info) do
 | 
			
		||||
    Logger.debug("[Connections] build_and_add_connection called with src_info: #{inspect(src_info)}, tgt_info: #{inspect(tgt_info)}")
 | 
			
		||||
 | 
			
		||||
    # Guard against nil info
 | 
			
		||||
    if is_nil(src_info) or is_nil(tgt_info) do
 | 
			
		||||
      {:error, :invalid_system_info}
 | 
			
		||||
    else
 | 
			
		||||
      info = %{
 | 
			
		||||
        solar_system_source_id: src_info.solar_system_id,
 | 
			
		||||
        solar_system_target_id: tgt_info.solar_system_id,
 | 
			
		||||
@@ -62,6 +69,7 @@ defmodule WandererApp.Map.Operations.Connections do
 | 
			
		||||
        other               -> Logger.error("[add_connection] unexpected: #{inspect(other)}"); {:error, :unexpected_error}
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp resolve_ship_size(attrs, src_info, tgt_info) do
 | 
			
		||||
    type = parse_type(attrs["type"])
 | 
			
		||||
 
 | 
			
		||||
@@ -86,19 +86,24 @@ defmodule WandererApp.Map.Server.CharactersImpl do
 | 
			
		||||
    end)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def untrack_characters(map_id, character_ids),
 | 
			
		||||
    do:
 | 
			
		||||
  def untrack_characters(map_id, character_ids) do
 | 
			
		||||
    character_ids
 | 
			
		||||
    |> Enum.each(fn character_id ->
 | 
			
		||||
        if is_character_map_active?(map_id, character_id) do
 | 
			
		||||
      is_character_map_active?(map_id, character_id)
 | 
			
		||||
      |> untrack_character(map_id, character_id)
 | 
			
		||||
    end)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def untrack_character(true, map_id, character_id) do
 | 
			
		||||
    WandererApp.Character.TrackerManager.update_track_settings(character_id, %{
 | 
			
		||||
      map_id: map_id,
 | 
			
		||||
      track: false
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
          Impl.broadcast!(map_id, :untrack_character, character_id)
 | 
			
		||||
  end
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
  def untrack_character(_is_character_map_active, _map_id, character_id) do
 | 
			
		||||
    :ok
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def is_character_map_active?(map_id, character_id) do
 | 
			
		||||
    case WandererApp.Character.get_character_state(character_id) do
 | 
			
		||||
@@ -219,8 +224,7 @@ defmodule WandererApp.Map.Server.CharactersImpl do
 | 
			
		||||
      Task.start_link(fn ->
 | 
			
		||||
        character_updates =
 | 
			
		||||
          maybe_update_online(map_id, character_id) ++
 | 
			
		||||
            maybe_update_tracking_status(map_id, character_id)
 | 
			
		||||
 | 
			
		||||
            maybe_update_tracking_status(map_id, character_id) ++
 | 
			
		||||
            maybe_update_location(map_id, character_id) ++
 | 
			
		||||
            maybe_update_ship(map_id, character_id) ++
 | 
			
		||||
            maybe_update_alliance(map_id, character_id) ++
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@ defmodule WandererApp.Map.Server.Impl do
 | 
			
		||||
 | 
			
		||||
  @pubsub_client Application.compile_env(:wanderer_app, :pubsub_client)
 | 
			
		||||
  @backup_state_timeout :timer.minutes(1)
 | 
			
		||||
  @update_presence_timeout :timer.seconds(1)
 | 
			
		||||
  @update_presence_timeout :timer.seconds(5)
 | 
			
		||||
  @update_characters_timeout :timer.seconds(1)
 | 
			
		||||
  @update_tracked_characters_timeout :timer.seconds(1)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -20,17 +20,9 @@ defmodule WandererApp.Ueberauth.Strategy.Eve do
 | 
			
		||||
    is_admin? = Map.get(params, "admin", "false") in ~w(true 1)
 | 
			
		||||
    invite_token = Map.get(params, "invite", nil)
 | 
			
		||||
 | 
			
		||||
    invite_token_valid =
 | 
			
		||||
      case WandererApp.Env.invites() do
 | 
			
		||||
        true ->
 | 
			
		||||
          case invite_token do
 | 
			
		||||
            nil -> false
 | 
			
		||||
            token -> WandererApp.Cache.lookup!("invite_#{token}", false)
 | 
			
		||||
          end
 | 
			
		||||
    {invite_token_valid, invite_type} = check_invite_valid(invite_token)
 | 
			
		||||
 | 
			
		||||
        _ ->
 | 
			
		||||
          true
 | 
			
		||||
      end
 | 
			
		||||
    is_admin? = is_admin? || invite_type == :admin
 | 
			
		||||
 | 
			
		||||
    case invite_token_valid do
 | 
			
		||||
      true ->
 | 
			
		||||
@@ -200,10 +192,13 @@ defmodule WandererApp.Ueberauth.Strategy.Eve do
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp oauth_client_options_from_conn(conn, with_wallet, is_admin?) do
 | 
			
		||||
    tracking_pool = WandererApp.Env.active_tracking_pool()
 | 
			
		||||
 | 
			
		||||
    base_options = [
 | 
			
		||||
      redirect_uri: callback_url(conn),
 | 
			
		||||
      with_wallet: with_wallet,
 | 
			
		||||
      is_admin?: is_admin?
 | 
			
		||||
      is_admin?: is_admin?,
 | 
			
		||||
      tracking_pool: tracking_pool
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    request_options = conn.private[:ueberauth_request_options].options
 | 
			
		||||
@@ -218,4 +213,33 @@ defmodule WandererApp.Ueberauth.Strategy.Eve do
 | 
			
		||||
  defp option(conn, key) do
 | 
			
		||||
    Keyword.get(options(conn), key, Keyword.get(default_options(), key))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp check_invite_valid(invite_token) do
 | 
			
		||||
    case invite_token do
 | 
			
		||||
      token when not is_nil(token) and token != "" ->
 | 
			
		||||
        check_token_valid(token)
 | 
			
		||||
 | 
			
		||||
      _ ->
 | 
			
		||||
        {not WandererApp.Env.invites(), :user}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp check_token_valid(token) do
 | 
			
		||||
    WandererApp.Cache.lookup!("invite_#{token}", false)
 | 
			
		||||
    |> case do
 | 
			
		||||
      true -> {true, :user}
 | 
			
		||||
      _ -> check_map_token_valid(token)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def check_map_token_valid(token) do
 | 
			
		||||
    {:ok, invites} = WandererApp.Api.MapInvite.read()
 | 
			
		||||
 | 
			
		||||
    invites
 | 
			
		||||
    |> Enum.find(fn invite -> invite.token == token end)
 | 
			
		||||
    |> case do
 | 
			
		||||
      nil -> {false, nil}
 | 
			
		||||
      invite -> {true, invite.type}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										84
									
								
								lib/wanderer_app/ueberauth/strategy/eve/init_configs_task.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								lib/wanderer_app/ueberauth/strategy/eve/init_configs_task.ex
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,84 @@
 | 
			
		||||
defmodule WandererApp.Esi.InitClientsTask do
 | 
			
		||||
  use Task
 | 
			
		||||
 | 
			
		||||
  require Logger
 | 
			
		||||
 | 
			
		||||
  def start_link(arg) do
 | 
			
		||||
    Task.start_link(__MODULE__, :run, [arg])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def run(_arg) do
 | 
			
		||||
    Logger.info("starting")
 | 
			
		||||
 | 
			
		||||
    Cachex.put(
 | 
			
		||||
      :esi_auth_cache,
 | 
			
		||||
      :active_config,
 | 
			
		||||
      "config_#{WandererApp.Env.active_tracking_pool()}"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    cache_clients()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp cache_clients() do
 | 
			
		||||
    config = Application.get_env(:ueberauth, WandererApp.Ueberauth.Strategy.Eve.OAuth, [])
 | 
			
		||||
 | 
			
		||||
    cache_client("default", %{
 | 
			
		||||
      client_id: config[:client_id_default],
 | 
			
		||||
      client_secret: config[:client_secret_default]
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    Enum.each(1..10, fn index ->
 | 
			
		||||
      client_id = config["client_id_#{index}" |> String.to_atom()]
 | 
			
		||||
      client_secret = config["client_secret_#{index}" |> String.to_atom()]
 | 
			
		||||
 | 
			
		||||
      if client_id != "" && client_secret != "" do
 | 
			
		||||
        cache_client(index, %{
 | 
			
		||||
          client_id: client_id,
 | 
			
		||||
          client_secret: client_secret
 | 
			
		||||
        })
 | 
			
		||||
      end
 | 
			
		||||
    end)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp cache_client(id, config) do
 | 
			
		||||
    config_uuid = UUID.uuid4()
 | 
			
		||||
 | 
			
		||||
    config =
 | 
			
		||||
      config
 | 
			
		||||
      |> Map.merge(%{
 | 
			
		||||
        id: id,
 | 
			
		||||
        uuid: config_uuid
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
    Cachex.put(
 | 
			
		||||
      :esi_auth_cache,
 | 
			
		||||
      "config_#{id}",
 | 
			
		||||
      config
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    Cachex.put(
 | 
			
		||||
      :esi_auth_cache,
 | 
			
		||||
      config_uuid,
 | 
			
		||||
      config
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # Cachex.put(
 | 
			
		||||
    #   :esi_auth_cache,
 | 
			
		||||
    #   "config_uuid_#{id}",
 | 
			
		||||
    #   config_uuid
 | 
			
		||||
    # )
 | 
			
		||||
 | 
			
		||||
    configs_total_count =
 | 
			
		||||
      if id == "default" do
 | 
			
		||||
        0
 | 
			
		||||
      else
 | 
			
		||||
        id
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    Cachex.put(
 | 
			
		||||
      :esi_auth_cache,
 | 
			
		||||
      "configs_total_count",
 | 
			
		||||
      configs_total_count
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -2,26 +2,50 @@ defmodule WandererApp.Ueberauth do
 | 
			
		||||
  @moduledoc false
 | 
			
		||||
 | 
			
		||||
  def client_id(opts \\ []) do
 | 
			
		||||
    config = _get_config()
 | 
			
		||||
    config = get_config()
 | 
			
		||||
    tracking_pool = Keyword.get(opts, :tracking_pool)
 | 
			
		||||
 | 
			
		||||
    cond do
 | 
			
		||||
      Keyword.get(opts, :is_admin?) -> config[:client_id_with_corp_wallet]
 | 
			
		||||
      Keyword.get(opts, :with_wallet) -> config[:client_id_with_wallet]
 | 
			
		||||
      not is_nil(tracking_pool) -> get_settings(tracking_pool)[:client_id]
 | 
			
		||||
      true -> config[:client_id_default]
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def client_secret(opts \\ []) do
 | 
			
		||||
    config = _get_config()
 | 
			
		||||
    config = get_config()
 | 
			
		||||
    tracking_pool = Keyword.get(opts, :tracking_pool)
 | 
			
		||||
 | 
			
		||||
    cond do
 | 
			
		||||
      Keyword.get(opts, :is_admin?) -> config[:client_secret_with_corp_wallet]
 | 
			
		||||
      Keyword.get(opts, :with_wallet) -> config[:client_secret_with_wallet]
 | 
			
		||||
      not is_nil(tracking_pool) -> get_settings(tracking_pool)[:client_secret]
 | 
			
		||||
      true -> config[:client_secret_default]
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp _get_config() do
 | 
			
		||||
  defp get_settings(nil) do
 | 
			
		||||
    {:ok, esi_config} =
 | 
			
		||||
      Cachex.get(
 | 
			
		||||
        :esi_auth_cache,
 | 
			
		||||
        "config_default"
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    esi_config
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp get_settings(tracking_pool) do
 | 
			
		||||
    {:ok, esi_config} =
 | 
			
		||||
      Cachex.get(
 | 
			
		||||
        :esi_auth_cache,
 | 
			
		||||
        "config_#{tracking_pool}"
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    esi_config
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp get_config() do
 | 
			
		||||
    Application.get_env(:ueberauth, WandererApp.Ueberauth.Strategy.Eve.OAuth, [])
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -62,6 +62,9 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
 | 
			
		||||
  Returns `{:ok, kills, updated_state}` on success, or `{:error, reason, updated_state}`.
 | 
			
		||||
  """
 | 
			
		||||
  def fetch_kills_for_system(system_id, since_hours, state, opts \\ []) do
 | 
			
		||||
    zkill_preload_disabled = WandererApp.Env.zkill_preload_disabled?()
 | 
			
		||||
 | 
			
		||||
    if not zkill_preload_disabled do
 | 
			
		||||
      limit = Keyword.get(opts, :limit, nil)
 | 
			
		||||
      force? = Keyword.get(opts, :force, false)
 | 
			
		||||
 | 
			
		||||
@@ -119,6 +122,9 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
 | 
			
		||||
            {:error, error, state}
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
    else
 | 
			
		||||
      raise ":kills_disabled"
 | 
			
		||||
    end
 | 
			
		||||
  rescue
 | 
			
		||||
    e ->
 | 
			
		||||
      Logger.error("[Fetcher] EXCEPTION in fetch_kills_for_system => #{Exception.message(e)}")
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,8 @@ defmodule WandererAppWeb.AuthController do
 | 
			
		||||
      access_token: auth.credentials.token,
 | 
			
		||||
      refresh_token: auth.credentials.refresh_token,
 | 
			
		||||
      expires_at: auth.credentials.expires_at,
 | 
			
		||||
      scopes: auth.credentials.scopes
 | 
			
		||||
      scopes: auth.credentials.scopes,
 | 
			
		||||
      tracking_pool: WandererApp.Env.active_tracking_pool()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    %{
 | 
			
		||||
@@ -29,7 +30,8 @@ defmodule WandererAppWeb.AuthController do
 | 
			
		||||
            access_token: auth.credentials.token,
 | 
			
		||||
            refresh_token: auth.credentials.refresh_token,
 | 
			
		||||
            expires_at: auth.credentials.expires_at,
 | 
			
		||||
            scopes: auth.credentials.scopes
 | 
			
		||||
            scopes: auth.credentials.scopes,
 | 
			
		||||
            tracking_pool: WandererApp.Env.active_tracking_pool()
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          {:ok, character} =
 | 
			
		||||
 
 | 
			
		||||
@@ -266,6 +266,10 @@ defmodule WandererAppWeb.MapConnectionAPIController do
 | 
			
		||||
        conn
 | 
			
		||||
        |> put_status(:bad_request)
 | 
			
		||||
        |> json(%{error: reason})
 | 
			
		||||
      {:error, :precondition_failed, _reason} ->
 | 
			
		||||
        conn
 | 
			
		||||
        |> put_status(:bad_request)
 | 
			
		||||
        |> json(%{error: "Invalid request parameters"})
 | 
			
		||||
      _other ->
 | 
			
		||||
        conn
 | 
			
		||||
        |> put_status(:internal_server_error)
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,7 @@ defmodule WandererAppWeb.MapSystemAPIController do
 | 
			
		||||
      solar_system_id: %Schema{type: :integer, description: "EVE solar system ID"},
 | 
			
		||||
      solar_system_name: %Schema{type: :string, description: "EVE solar system name"},
 | 
			
		||||
      region_name: %Schema{type: :string, description: "EVE region name"},
 | 
			
		||||
      custom_name: %Schema{type: :string, nullable: true, description: "Custom name for the system"},
 | 
			
		||||
      position_x: %Schema{type: :integer, description: "X coordinate"},
 | 
			
		||||
      position_y: %Schema{type: :integer, description: "Y coordinate"},
 | 
			
		||||
      status: %Schema{
 | 
			
		||||
@@ -137,6 +138,7 @@ defmodule WandererAppWeb.MapSystemAPIController do
 | 
			
		||||
            solar_system_id: 30_000_142,
 | 
			
		||||
            solar_system_name: "Jita",
 | 
			
		||||
            region_name: "The Forge",
 | 
			
		||||
            custom_name: "Trade Hub Central",
 | 
			
		||||
            position_x: 100.5,
 | 
			
		||||
            position_y: 200.3,
 | 
			
		||||
            status: "active",
 | 
			
		||||
@@ -179,6 +181,7 @@ defmodule WandererAppWeb.MapSystemAPIController do
 | 
			
		||||
        solar_system_id: 30_000_142,
 | 
			
		||||
        solar_system_name: "Jita",
 | 
			
		||||
        region_name: "The Forge",
 | 
			
		||||
        custom_name: "Trade Hub Central",
 | 
			
		||||
        position_x: 100.5,
 | 
			
		||||
        position_y: 200.3,
 | 
			
		||||
        status: "active",
 | 
			
		||||
 
 | 
			
		||||
@@ -262,13 +262,18 @@ defmodule WandererAppWeb.Helpers.APIUtils do
 | 
			
		||||
 | 
			
		||||
  @spec map_system_to_json(struct()) :: map()
 | 
			
		||||
  def map_system_to_json(system) do
 | 
			
		||||
    original = get_original_name(system.solar_system_id)
 | 
			
		||||
 | 
			
		||||
    # Determine the actual custom_name: if name differs from original, use it as custom_name
 | 
			
		||||
    actual_custom_name = if system.name != original and system.name not in [nil, ""], do: system.name, else: system.custom_name
 | 
			
		||||
 | 
			
		||||
    base =
 | 
			
		||||
      Map.take(system, ~w(
 | 
			
		||||
        id map_id solar_system_id custom_name temporary_name description tag labels
 | 
			
		||||
        id map_id solar_system_id temporary_name description tag labels
 | 
			
		||||
        locked visible status position_x position_y inserted_at updated_at
 | 
			
		||||
      )a)
 | 
			
		||||
      |> Map.put(:custom_name, actual_custom_name)
 | 
			
		||||
 | 
			
		||||
    original = get_original_name(system.solar_system_id)
 | 
			
		||||
    name = pick_name(system)
 | 
			
		||||
 | 
			
		||||
    base
 | 
			
		||||
@@ -283,11 +288,15 @@ defmodule WandererAppWeb.Helpers.APIUtils do
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp pick_name(%{temporary_name: t, custom_name: c, solar_system_id: id}) do
 | 
			
		||||
  defp pick_name(%{temporary_name: t, custom_name: c, name: n, solar_system_id: id} = system) do
 | 
			
		||||
    original = get_original_name(id)
 | 
			
		||||
 | 
			
		||||
    cond do
 | 
			
		||||
      t not in [nil, ""] -> t
 | 
			
		||||
      c not in [nil, ""] -> c
 | 
			
		||||
      true -> get_original_name(id)
 | 
			
		||||
      # If name differs from original, it's a custom name
 | 
			
		||||
      n not in [nil, ""] and n != original -> n
 | 
			
		||||
      true -> original
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ defmodule WandererAppWeb.AdminLive do
 | 
			
		||||
    corp_wallet_character =
 | 
			
		||||
      socket.assigns.current_user.characters
 | 
			
		||||
      |> Enum.find(fn character ->
 | 
			
		||||
        character.eve_id == WandererApp.Env.corp_wallet_eve_id() &&
 | 
			
		||||
          WandererApp.Character.can_track_corp_wallet?(character)
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
@@ -58,10 +59,10 @@ defmodule WandererAppWeb.AdminLive do
 | 
			
		||||
     socket
 | 
			
		||||
     |> assign(
 | 
			
		||||
       active_map_subscriptions: active_map_subscriptions,
 | 
			
		||||
       show_invites?: WandererApp.Env.invites(),
 | 
			
		||||
       user_character_ids: user_character_ids,
 | 
			
		||||
       user_id: user_id,
 | 
			
		||||
       invite_link: nil,
 | 
			
		||||
       tracker_stats: [],
 | 
			
		||||
       map_subscriptions_enabled?: WandererApp.Env.map_subscriptions_enabled?(),
 | 
			
		||||
       restrict_maps_creation?: WandererApp.Env.restrict_maps_creation?()
 | 
			
		||||
     )}
 | 
			
		||||
@@ -77,21 +78,6 @@ defmodule WandererAppWeb.AdminLive do
 | 
			
		||||
    {:noreply, apply_action(socket, socket.assigns.live_action, params, uri)}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_event("generate-invite-link", _params, socket) do
 | 
			
		||||
    uuid = UUID.uuid4(:default)
 | 
			
		||||
    WandererApp.Cache.put("invite_#{uuid}", true, ttl: @invite_link_ttl)
 | 
			
		||||
 | 
			
		||||
    invite_link =
 | 
			
		||||
      socket.assigns.uri
 | 
			
		||||
      |> Map.put(:path, "/welcome")
 | 
			
		||||
      |> Map.put(:query, URI.encode_query(%{invite: uuid}))
 | 
			
		||||
      |> URI.to_string()
 | 
			
		||||
 | 
			
		||||
    {:noreply,
 | 
			
		||||
     socket
 | 
			
		||||
     |> assign(invite_link: invite_link)}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def handle_event("update-eve-db-data", _params, socket) do
 | 
			
		||||
    WandererApp.EveDataService.update_eve_data()
 | 
			
		||||
@@ -224,6 +210,58 @@ defmodule WandererAppWeb.AdminLive do
 | 
			
		||||
     |> push_navigate(to: ~p"/maps/new")}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_event("validate", %{"form" => params}, socket) do
 | 
			
		||||
    form = AshPhoenix.Form.validate(socket.assigns.form, params)
 | 
			
		||||
    {:noreply, assign(socket, form: form)}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_event("generate-invite-link", _params, socket) do
 | 
			
		||||
    token = UUID.uuid4()
 | 
			
		||||
    new_params = Map.put(socket.assigns.form.params || %{}, "token", token)
 | 
			
		||||
    form = AshPhoenix.Form.validate(socket.assigns.form, new_params)
 | 
			
		||||
 | 
			
		||||
    invite_link =
 | 
			
		||||
      socket.assigns.uri
 | 
			
		||||
      |> get_invite_link(token)
 | 
			
		||||
 | 
			
		||||
    {:noreply, assign(socket, form: form, invite_link: invite_link)}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_event(
 | 
			
		||||
        "add_invite_link",
 | 
			
		||||
        %{"form" => %{"type" => type, "valid_until" => valid_until}},
 | 
			
		||||
        socket
 | 
			
		||||
      ) do
 | 
			
		||||
    %{
 | 
			
		||||
      type: type |> String.to_existing_atom(),
 | 
			
		||||
      valid_until: get_valid_until(valid_until),
 | 
			
		||||
      token: UUID.uuid4(),
 | 
			
		||||
      map_id: nil
 | 
			
		||||
    }
 | 
			
		||||
    |> WandererApp.Api.MapInvite.new()
 | 
			
		||||
    |> case do
 | 
			
		||||
      {:ok, _invite} ->
 | 
			
		||||
        {:noreply, socket |> push_patch(to: ~p"/admin")}
 | 
			
		||||
 | 
			
		||||
      error ->
 | 
			
		||||
        {:noreply, socket |> put_flash(:error, "Failed to add invite. Try again.")}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_event(
 | 
			
		||||
        "delete-invite",
 | 
			
		||||
        %{"id" => id},
 | 
			
		||||
        socket
 | 
			
		||||
      ) do
 | 
			
		||||
    id
 | 
			
		||||
    |> WandererApp.Api.MapInvite.by_id!()
 | 
			
		||||
    |> WandererApp.Api.MapInvite.destroy!()
 | 
			
		||||
 | 
			
		||||
    {:ok, invites} = WandererApp.Api.MapInvite.read()
 | 
			
		||||
 | 
			
		||||
    {:noreply, socket |> assign(:invites, invites)}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def handle_event(event, body, socket) do
 | 
			
		||||
    Logger.warning(fn -> "unhandled event: #{event} #{inspect(body)}" end)
 | 
			
		||||
@@ -255,6 +293,10 @@ defmodule WandererAppWeb.AdminLive do
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp apply_action(socket, :index, _params, uri) do
 | 
			
		||||
    {:ok, invites} = WandererApp.Api.MapInvite.read()
 | 
			
		||||
 | 
			
		||||
    {:ok, tracker_stats} = load_tracker_stats()
 | 
			
		||||
 | 
			
		||||
    socket
 | 
			
		||||
    |> assign(:active_page, :admin)
 | 
			
		||||
    |> assign(:uri, URI.parse(uri))
 | 
			
		||||
@@ -268,6 +310,113 @@ defmodule WandererAppWeb.AdminLive do
 | 
			
		||||
    ])
 | 
			
		||||
    |> assign(:form, to_form(%{"amount" => 500_000_000}))
 | 
			
		||||
    |> assign(:unlink_character_form, to_form(%{}))
 | 
			
		||||
    |> assign(:invites, invites)
 | 
			
		||||
    |> assign(:tracker_stats, tracker_stats)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp apply_action(socket, :add_invite_link, _params, uri) do
 | 
			
		||||
    socket
 | 
			
		||||
    |> assign(:active_page, :admin)
 | 
			
		||||
    |> assign(:uri, URI.parse(uri))
 | 
			
		||||
    |> assign(:page_title, "Add Invite Link")
 | 
			
		||||
    |> assign(:invite_types, [%{label: "User", id: :user}, %{label: "Admin", id: :admin}])
 | 
			
		||||
    |> assign(:valid_types, [
 | 
			
		||||
      %{label: "1D", id: 1},
 | 
			
		||||
      %{label: "1W", id: 7},
 | 
			
		||||
      %{label: "1M", id: 30},
 | 
			
		||||
      %{label: "1Y", id: 365}
 | 
			
		||||
    ])
 | 
			
		||||
    |> assign(:unlink_character_form, to_form(%{}))
 | 
			
		||||
    |> assign(:character_search_options, [])
 | 
			
		||||
    |> assign(:amounts, [
 | 
			
		||||
      %{label: "500M", value: 500_000_000},
 | 
			
		||||
      %{label: "1B", value: 1_000_000_000},
 | 
			
		||||
      %{label: "5B", value: 5_000_000_000},
 | 
			
		||||
      %{label: "10B", value: 10_000_000_000}
 | 
			
		||||
    ])
 | 
			
		||||
    |> assign(:form, to_form(%{"amount" => 500_000_000}))
 | 
			
		||||
    |> assign(:invite_token, UUID.uuid4())
 | 
			
		||||
    |> assign(
 | 
			
		||||
      :form,
 | 
			
		||||
      AshPhoenix.Form.for_create(WandererApp.Api.MapInvite, :new,
 | 
			
		||||
        forms: [
 | 
			
		||||
          auto?: true
 | 
			
		||||
        ]
 | 
			
		||||
      )
 | 
			
		||||
      |> to_form()
 | 
			
		||||
    )
 | 
			
		||||
    |> assign(:invites, [])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp load_tracker_stats() do
 | 
			
		||||
    {:ok, characters} =
 | 
			
		||||
      WandererApp.Api.Character.last_active(%{
 | 
			
		||||
        from:
 | 
			
		||||
          DateTime.utc_now()
 | 
			
		||||
          |> DateTime.add(-1 * 60 * 24 * 7, :minute)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
    admins_count =
 | 
			
		||||
      characters |> Enum.filter(&WandererApp.Character.can_track_corp_wallet?/1) |> Enum.count()
 | 
			
		||||
 | 
			
		||||
    with_wallets_count =
 | 
			
		||||
      characters
 | 
			
		||||
      |> Enum.filter(
 | 
			
		||||
        &(WandererApp.Character.can_track_wallet?(&1) and
 | 
			
		||||
            not WandererApp.Character.can_track_corp_wallet?(&1))
 | 
			
		||||
      )
 | 
			
		||||
      |> Enum.count()
 | 
			
		||||
 | 
			
		||||
    default_count =
 | 
			
		||||
      characters
 | 
			
		||||
      |> Enum.filter(
 | 
			
		||||
        &(is_nil(&1.tracking_pool) and not WandererApp.Character.can_track_wallet?(&1) and
 | 
			
		||||
            not WandererApp.Character.can_track_corp_wallet?(&1))
 | 
			
		||||
      )
 | 
			
		||||
      |> Enum.count()
 | 
			
		||||
 | 
			
		||||
    result = [
 | 
			
		||||
      %{title: "Admins", value: admins_count},
 | 
			
		||||
      %{title: "With Wallet", value: with_wallets_count},
 | 
			
		||||
      %{title: "Default", value: default_count}
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    {:ok, pools_count} =
 | 
			
		||||
      Cachex.get(
 | 
			
		||||
        :esi_auth_cache,
 | 
			
		||||
        "configs_total_count"
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    result =
 | 
			
		||||
      if not is_nil(pools_count) && pools_count != 0 do
 | 
			
		||||
        pools =
 | 
			
		||||
          1..pools_count
 | 
			
		||||
          |> Enum.map(fn pool_id ->
 | 
			
		||||
            pools_character_count =
 | 
			
		||||
              characters
 | 
			
		||||
              |> Enum.filter(
 | 
			
		||||
                &(&1.tracking_pool == "#{pool_id}" and
 | 
			
		||||
                    not WandererApp.Character.can_track_wallet?(&1) and
 | 
			
		||||
                    not WandererApp.Character.can_track_corp_wallet?(&1))
 | 
			
		||||
              )
 | 
			
		||||
              |> Enum.count()
 | 
			
		||||
 | 
			
		||||
            %{title: "Pool #{pool_id}", value: pools_character_count}
 | 
			
		||||
          end)
 | 
			
		||||
 | 
			
		||||
        result ++ pools
 | 
			
		||||
      else
 | 
			
		||||
        result
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    {:ok, result}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp get_invite_link(uri, token) do
 | 
			
		||||
    uri
 | 
			
		||||
    |> Map.put(:path, "/auth/eve")
 | 
			
		||||
    |> Map.put(:query, URI.encode_query(%{invite: token}))
 | 
			
		||||
    |> URI.to_string()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp search(search) do
 | 
			
		||||
@@ -290,11 +439,29 @@ defmodule WandererAppWeb.AdminLive do
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <span :if={@option.value == :loading} <span class="loading loading-spinner loading-xs"></span>
 | 
			
		||||
        <%= @option.label %>
 | 
			
		||||
        {@option.label}
 | 
			
		||||
    </div>
 | 
			
		||||
    """
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp get_valid_until("1") do
 | 
			
		||||
    DateTime.utc_now() |> DateTime.add(24 * 3600, :second)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp get_valid_until("7") do
 | 
			
		||||
    DateTime.utc_now() |> DateTime.add(24 * 3600 * 7, :second)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp get_valid_until("30") do
 | 
			
		||||
    DateTime.utc_now() |> DateTime.add(24 * 3600 * 30, :second)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp get_valid_until("365") do
 | 
			
		||||
    DateTime.utc_now() |> DateTime.add(24 * 3600 * 365, :second)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp get_valid_until(_), do: get_valid_until("1")
 | 
			
		||||
 | 
			
		||||
  def search_member_icon_url(%{character: true} = option),
 | 
			
		||||
    do: member_icon_url(%{eve_character_id: option.value})
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -112,27 +112,29 @@
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div :if={@show_invites?} class="card dark:bg-zinc-800 dark:border-zinc-600">
 | 
			
		||||
        <div class="card dark:bg-zinc-800 dark:border-zinc-600">
 | 
			
		||||
          <div class="card-body">
 | 
			
		||||
            <div class="col-span-6">
 | 
			
		||||
              <span class="text-gray-400 dark:text-gray-400">Invite Link</span>
 | 
			
		||||
              <span class="text-gray-400 dark:text-gray-400">Invites</span>
 | 
			
		||||
              <h4 class="my-4 font-medium text-gray-800 text-4xl  dark:text-gray-100">
 | 
			
		||||
                <.button class="btn btn-primary" phx-click="generate-invite-link">
 | 
			
		||||
                  Generate
 | 
			
		||||
                </.button>
 | 
			
		||||
 | 
			
		||||
                <div :if={not is_nil(@invite_link)} class="join">
 | 
			
		||||
                  <input
 | 
			
		||||
                    class="input input-bordered join-item"
 | 
			
		||||
                    readonly
 | 
			
		||||
                    type="text"
 | 
			
		||||
                    value={@invite_link}
 | 
			
		||||
                  />
 | 
			
		||||
                <.link class="btn mt-2 w-full btn-neutral rounded-none" patch={~p"/admin/invite"}>
 | 
			
		||||
                  <.icon name="hero-plus-solid" class="w-6 h-6" />
 | 
			
		||||
                  <h3 class="card-title text-center text-md">New Invite</h3>
 | 
			
		||||
                </.link>
 | 
			
		||||
              </h4>
 | 
			
		||||
              <.table
 | 
			
		||||
                id="invites"
 | 
			
		||||
                rows={@invites}
 | 
			
		||||
                class="!max-h-[40vh] !overflow-y-auto"
 | 
			
		||||
              >
 | 
			
		||||
                <:col :let={invite} label="Link">
 | 
			
		||||
                  <div class="flex items-center">
 | 
			
		||||
                  <div class="w-[200px] no-wrap truncate"><%= get_invite_link(@uri, invite.token) %></div>
 | 
			
		||||
                  <.button
 | 
			
		||||
                    phx-hook="CopyToClipboard"
 | 
			
		||||
                    id="copy-to-clipboard"
 | 
			
		||||
                    class="copy-link btn join-item rounded-r-full"
 | 
			
		||||
                    data-url={@invite_link}
 | 
			
		||||
                    class="copy-link btn btn-neutral rounded-none"
 | 
			
		||||
                    data-url={get_invite_link(@uri, invite.token)}
 | 
			
		||||
                  >
 | 
			
		||||
                    Copy
 | 
			
		||||
                    <div class="absolute w-[100px] !mr-[-170px] link-copied hidden">
 | 
			
		||||
@@ -140,7 +142,48 @@
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </.button>
 | 
			
		||||
                  </div>
 | 
			
		||||
              </h4>
 | 
			
		||||
                </:col>
 | 
			
		||||
                <:col :let={invite} label="Type">
 | 
			
		||||
                  <%= invite.type %>
 | 
			
		||||
                </:col>
 | 
			
		||||
                <:col :let={invite} label="Valid Until">
 | 
			
		||||
                  <div>
 | 
			
		||||
                    <p class="mb-0 text-xs text-gray-600 dark:text-zinc-100 whitespace-nowrap">
 | 
			
		||||
                      <.local_time id={invite.id} at={invite.valid_until} />
 | 
			
		||||
                    </p>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </:col>
 | 
			
		||||
                <:action :let={invite}>
 | 
			
		||||
                  <.button
 | 
			
		||||
                    phx-click="delete-invite"
 | 
			
		||||
                    phx-value-id={invite.id}
 | 
			
		||||
                    data={[confirm: "Please confirm to delete invite!"]}
 | 
			
		||||
                    class="hover:text-white"
 | 
			
		||||
                  >
 | 
			
		||||
                    <.icon name="hero-trash-solid" class="w-4 h-4" />
 | 
			
		||||
                  </.button>
 | 
			
		||||
                </:action>
 | 
			
		||||
              </.table>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="card dark:bg-zinc-800 dark:border-zinc-600">
 | 
			
		||||
          <div class="card-body">
 | 
			
		||||
            <div class="col-span-6">
 | 
			
		||||
              <span class="text-gray-400 dark:text-gray-400">Tracking Pools</span>
 | 
			
		||||
                <.table
 | 
			
		||||
                  id="tracking_pools"
 | 
			
		||||
                  rows={@tracker_stats}
 | 
			
		||||
                  class="!max-h-[40vh] !overflow-y-auto"
 | 
			
		||||
                >
 | 
			
		||||
                  <:col :let={stat} label="Pool">
 | 
			
		||||
                    <div class="w-[200px] no-wrap truncate">{stat.title}</div>
 | 
			
		||||
                  </:col>
 | 
			
		||||
                  <:col :let={stat} label="Count">
 | 
			
		||||
                    {stat.value}
 | 
			
		||||
                  </:col>
 | 
			
		||||
                </.table>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
@@ -261,4 +304,40 @@
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <.modal
 | 
			
		||||
    :if={@live_action in [:add_invite_link]}
 | 
			
		||||
    title={"New Invite"}
 | 
			
		||||
    class="!w-[500px]"
 | 
			
		||||
    id="add_invite_link_modal"
 | 
			
		||||
    show
 | 
			
		||||
    on_cancel={JS.patch(~p"/admin")}
 | 
			
		||||
  >
 | 
			
		||||
 | 
			
		||||
    <.form :let={f} for={@form} phx-change="validate" phx-submit={@live_action}>
 | 
			
		||||
      <.input
 | 
			
		||||
        type="select"
 | 
			
		||||
        field={f[:type]}
 | 
			
		||||
        class="select h-8 min-h-[10px] !pt-1 !pb-1 text-sm bg-neutral-900"
 | 
			
		||||
        wrapper_class="mt-2"
 | 
			
		||||
        label="Type"
 | 
			
		||||
        options={Enum.map(@invite_types, fn invite_type -> {invite_type.label, invite_type.id} end)}
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <.input
 | 
			
		||||
        type="select"
 | 
			
		||||
        field={f[:valid_until]}
 | 
			
		||||
        class="select h-8 min-h-[10px] !pt-1 !pb-1 text-sm bg-neutral-900"
 | 
			
		||||
        wrapper_class="mt-2"
 | 
			
		||||
        label="Valid"
 | 
			
		||||
        options={Enum.map(@valid_types, fn valid_type -> {valid_type.label, valid_type.id} end)}
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <!-- API Key Section with grid layout -->
 | 
			
		||||
      <div class="modal-action">
 | 
			
		||||
        <.button class="mt-2" type="submit" phx-disable-with="Saving...">
 | 
			
		||||
          <%= (@live_action == :add_invite_link && "Add") || "Save" %>
 | 
			
		||||
        </.button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </.form>
 | 
			
		||||
  </.modal>
 | 
			
		||||
</main>
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,8 @@ defmodule WandererAppWeb.CharactersLive do
 | 
			
		||||
 | 
			
		||||
  alias BetterNumber, as: Number
 | 
			
		||||
 | 
			
		||||
  @active_tracking_pool 1
 | 
			
		||||
 | 
			
		||||
  def mount(_params, %{"user_id" => user_id} = _session, socket)
 | 
			
		||||
      when not is_nil(user_id) do
 | 
			
		||||
    {:ok, characters} = WandererApp.Api.Character.active_by_user(%{user_id: user_id})
 | 
			
		||||
@@ -56,17 +58,25 @@ defmodule WandererAppWeb.CharactersLive do
 | 
			
		||||
  @impl true
 | 
			
		||||
  def handle_event("authorize", form, socket) do
 | 
			
		||||
    track_wallet = form |> Map.get("track_wallet", false)
 | 
			
		||||
    token = UUID.uuid4(:default)
 | 
			
		||||
    WandererApp.Cache.put("invite_#{token}", true, ttl: :timer.minutes(30))
 | 
			
		||||
 | 
			
		||||
    {:noreply, socket |> push_navigate(to: ~p"/auth/eve?invite=#{token}&w=#{track_wallet}")}
 | 
			
		||||
    {:ok, esi_config} =
 | 
			
		||||
      Cachex.get(
 | 
			
		||||
        :esi_auth_cache,
 | 
			
		||||
        "config_#{WandererApp.Env.active_tracking_pool()}"
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    WandererApp.Cache.put("invite_#{esi_config.uuid}", true, ttl: :timer.minutes(30))
 | 
			
		||||
 | 
			
		||||
    {:noreply,
 | 
			
		||||
     socket |> push_navigate(to: ~p"/auth/eve?invite=#{esi_config.uuid}&w=#{track_wallet}")}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def handle_event("delete", %{"character_id" => character_id}, socket) do
 | 
			
		||||
    WandererApp.Character.TrackerManager.stop_tracking(character_id)
 | 
			
		||||
 | 
			
		||||
    {:ok, map_user_settings} = WandererApp.Api.MapCharacterSettings.tracked_by_character(%{character_id: character_id})
 | 
			
		||||
    {:ok, map_user_settings} =
 | 
			
		||||
      WandererApp.Api.MapCharacterSettings.tracked_by_character(%{character_id: character_id})
 | 
			
		||||
 | 
			
		||||
    map_user_settings
 | 
			
		||||
    |> Enum.each(fn settings ->
 | 
			
		||||
@@ -87,6 +97,15 @@ defmodule WandererAppWeb.CharactersLive do
 | 
			
		||||
    {:noreply, socket |> assign(characters: characters |> Enum.map(&map_ui_character/1))}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def handle_event(
 | 
			
		||||
        "validate",
 | 
			
		||||
        params,
 | 
			
		||||
        socket
 | 
			
		||||
      ) do
 | 
			
		||||
    {:noreply, assign(socket, form: params)}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def handle_event("show_table", %{"value" => "on"}, socket) do
 | 
			
		||||
    {:noreply, socket |> assign(mode: :table)}
 | 
			
		||||
@@ -148,7 +167,7 @@ defmodule WandererAppWeb.CharactersLive do
 | 
			
		||||
    socket
 | 
			
		||||
    |> assign(:active_page, :characters)
 | 
			
		||||
    |> assign(:page_title, "Authorize Character - Characters")
 | 
			
		||||
    |> assign(:form, to_form(%{"track_wallet" => false}))
 | 
			
		||||
    |> assign(:form, to_form(%{}))
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp map_ui_character(character) do
 | 
			
		||||
 
 | 
			
		||||
@@ -242,10 +242,13 @@
 | 
			
		||||
    show
 | 
			
		||||
    on_cancel={JS.patch(~p"/characters")}
 | 
			
		||||
  >
 | 
			
		||||
    <.form :let={f} for={@form} phx-submit="authorize">
 | 
			
		||||
      <div :if={@wallet_tracking_enabled?} class="pb-2 -mt-8">
 | 
			
		||||
  <div class="flex flex-col gap-3">
 | 
			
		||||
 | 
			
		||||
    <.form :let={f} for={@form} phx-submit="authorize" phx-change="validate">
 | 
			
		||||
      <div :if={@wallet_tracking_enabled?} class="pb-2">
 | 
			
		||||
        <.input
 | 
			
		||||
          type="checkbox"
 | 
			
		||||
 | 
			
		||||
          field={f[:track_wallet]}
 | 
			
		||||
          label="Access to character wallet information"
 | 
			
		||||
        />
 | 
			
		||||
@@ -254,5 +257,6 @@
 | 
			
		||||
        <.button type="submit">AUTHORIZE</.button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </.form>
 | 
			
		||||
    </div>
 | 
			
		||||
  </.modal>
 | 
			
		||||
</main>
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ defmodule WandererAppWeb.ServerStatusLive do
 | 
			
		||||
        "server_status"
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    {:ok, socket |> assign(server_online: false)}
 | 
			
		||||
    {:ok, socket |> assign(server_online: true)}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
 
 | 
			
		||||
@@ -230,7 +230,11 @@ defmodule WandererAppWeb.Router do
 | 
			
		||||
    delete "/connections", MapConnectionAPIController, :delete
 | 
			
		||||
    delete "/systems", MapSystemAPIController, :delete
 | 
			
		||||
    resources "/systems", MapSystemAPIController, only: [:index, :show, :create, :update, :delete]
 | 
			
		||||
    resources "/connections", MapConnectionAPIController, only: [:index, :show, :create, :update, :delete], param: "id"
 | 
			
		||||
 | 
			
		||||
    resources "/connections", MapConnectionAPIController,
 | 
			
		||||
      only: [:index, :show, :create, :update, :delete],
 | 
			
		||||
      param: "id"
 | 
			
		||||
 | 
			
		||||
    resources "/structures", MapSystemStructureAPIController, except: [:new, :edit]
 | 
			
		||||
    get "/structure-timers", MapSystemStructureAPIController, :structure_timers
 | 
			
		||||
    resources "/signatures", MapSystemSignatureAPIController, except: [:new, :edit]
 | 
			
		||||
@@ -238,8 +242,6 @@ defmodule WandererAppWeb.Router do
 | 
			
		||||
    get "/tracked-characters", MapAPIController, :show_tracked_characters
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  #
 | 
			
		||||
  # Other API routes
 | 
			
		||||
  #
 | 
			
		||||
@@ -359,6 +361,7 @@ defmodule WandererAppWeb.Router do
 | 
			
		||||
        WandererAppWeb.Nav
 | 
			
		||||
      ] do
 | 
			
		||||
      live("/", AdminLive, :index)
 | 
			
		||||
      live("/invite", AdminLive, :add_invite_link)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    error_tracker_dashboard("/errors",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								mix.exs
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								mix.exs
									
									
									
									
									
								
							@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
 | 
			
		||||
 | 
			
		||||
  @source_url "https://github.com/wanderer-industries/wanderer"
 | 
			
		||||
 | 
			
		||||
  @version "1.66.15"
 | 
			
		||||
  @version "1.70.1"
 | 
			
		||||
 | 
			
		||||
  def project do
 | 
			
		||||
    [
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										40
									
								
								priv/repo/migrations/20250608220906_add_map_invites.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								priv/repo/migrations/20250608220906_add_map_invites.exs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
defmodule WandererApp.Repo.Migrations.AddMapInvites do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  Updates resources based on their most recent snapshots.
 | 
			
		||||
 | 
			
		||||
  This file was autogenerated with `mix ash_postgres.generate_migrations`
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use Ecto.Migration
 | 
			
		||||
 | 
			
		||||
  def up do
 | 
			
		||||
    create table(:map_invites_v1, primary_key: false) do
 | 
			
		||||
      add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true
 | 
			
		||||
      add :token, :text
 | 
			
		||||
      add :valid_until, :utc_datetime
 | 
			
		||||
 | 
			
		||||
      add :inserted_at, :utc_datetime_usec,
 | 
			
		||||
        null: false,
 | 
			
		||||
        default: fragment("(now() AT TIME ZONE 'utc')")
 | 
			
		||||
 | 
			
		||||
      add :updated_at, :utc_datetime_usec,
 | 
			
		||||
        null: false,
 | 
			
		||||
        default: fragment("(now() AT TIME ZONE 'utc')")
 | 
			
		||||
 | 
			
		||||
      add :map_id,
 | 
			
		||||
          references(:maps_v1,
 | 
			
		||||
            column: :id,
 | 
			
		||||
            name: "map_invites_v1_map_id_fkey",
 | 
			
		||||
            type: :uuid,
 | 
			
		||||
            prefix: "public",
 | 
			
		||||
            on_delete: :delete_all
 | 
			
		||||
          )
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def down do
 | 
			
		||||
    drop constraint(:map_invites_v1, "map_invites_v1_map_id_fkey")
 | 
			
		||||
 | 
			
		||||
    drop table(:map_invites_v1)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										21
									
								
								priv/repo/migrations/20250608221356_add_map_invite_type.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								priv/repo/migrations/20250608221356_add_map_invite_type.exs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
defmodule WandererApp.Repo.Migrations.AddMapInviteType do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  Updates resources based on their most recent snapshots.
 | 
			
		||||
 | 
			
		||||
  This file was autogenerated with `mix ash_postgres.generate_migrations`
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use Ecto.Migration
 | 
			
		||||
 | 
			
		||||
  def up do
 | 
			
		||||
    alter table(:map_invites_v1) do
 | 
			
		||||
      add :type, :text, null: false, default: "user"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def down do
 | 
			
		||||
    alter table(:map_invites_v1) do
 | 
			
		||||
      remove :type
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -0,0 +1,21 @@
 | 
			
		||||
defmodule WandererApp.Repo.Migrations.AddCharacterTrackingPool do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  Updates resources based on their most recent snapshots.
 | 
			
		||||
 | 
			
		||||
  This file was autogenerated with `mix ash_postgres.generate_migrations`
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use Ecto.Migration
 | 
			
		||||
 | 
			
		||||
  def up do
 | 
			
		||||
    alter table(:character_v1) do
 | 
			
		||||
      add :tracking_pool, :text
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def down do
 | 
			
		||||
    alter table(:character_v1) do
 | 
			
		||||
      remove :tracking_pool
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										343
									
								
								priv/resource_snapshots/repo/character_v1/20250611074436.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										343
									
								
								priv/resource_snapshots/repo/character_v1/20250611074436.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,343 @@
 | 
			
		||||
{
 | 
			
		||||
  "attributes": [
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": false,
 | 
			
		||||
      "default": "fragment(\"gen_random_uuid()\")",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": true,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "id",
 | 
			
		||||
      "type": "uuid"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": false,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "eve_id",
 | 
			
		||||
      "type": "text"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": false,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "name",
 | 
			
		||||
      "type": "text"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "false",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "online",
 | 
			
		||||
      "type": "boolean"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "false",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "deleted",
 | 
			
		||||
      "type": "boolean"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "scopes",
 | 
			
		||||
      "type": "text"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "character_owner_hash",
 | 
			
		||||
      "type": "text"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "token_type",
 | 
			
		||||
      "type": "text"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "expires_at",
 | 
			
		||||
      "type": "bigint"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "ship_name",
 | 
			
		||||
      "type": "text"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "ship_item_id",
 | 
			
		||||
      "type": "bigint"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "corporation_id",
 | 
			
		||||
      "type": "bigint"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "corporation_name",
 | 
			
		||||
      "type": "text"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "corporation_ticker",
 | 
			
		||||
      "type": "text"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "alliance_id",
 | 
			
		||||
      "type": "bigint"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "alliance_name",
 | 
			
		||||
      "type": "text"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "alliance_ticker",
 | 
			
		||||
      "type": "text"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "tracking_pool",
 | 
			
		||||
      "type": "text"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": false,
 | 
			
		||||
      "default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "inserted_at",
 | 
			
		||||
      "type": "utc_datetime_usec"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": false,
 | 
			
		||||
      "default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "updated_at",
 | 
			
		||||
      "type": "utc_datetime_usec"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": {
 | 
			
		||||
        "deferrable": false,
 | 
			
		||||
        "destination_attribute": "id",
 | 
			
		||||
        "destination_attribute_default": null,
 | 
			
		||||
        "destination_attribute_generated": null,
 | 
			
		||||
        "index?": false,
 | 
			
		||||
        "match_type": null,
 | 
			
		||||
        "match_with": null,
 | 
			
		||||
        "multitenancy": {
 | 
			
		||||
          "attribute": null,
 | 
			
		||||
          "global": null,
 | 
			
		||||
          "strategy": null
 | 
			
		||||
        },
 | 
			
		||||
        "name": "character_v1_user_id_fkey",
 | 
			
		||||
        "on_delete": null,
 | 
			
		||||
        "on_update": null,
 | 
			
		||||
        "primary_key?": true,
 | 
			
		||||
        "schema": "public",
 | 
			
		||||
        "table": "user_v1"
 | 
			
		||||
      },
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "user_id",
 | 
			
		||||
      "type": "uuid"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "encrypted_eve_wallet_balance",
 | 
			
		||||
      "type": "binary"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "encrypted_location",
 | 
			
		||||
      "type": "binary"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "encrypted_ship",
 | 
			
		||||
      "type": "binary"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "encrypted_solar_system_id",
 | 
			
		||||
      "type": "binary"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "encrypted_structure_id",
 | 
			
		||||
      "type": "binary"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "encrypted_station_id",
 | 
			
		||||
      "type": "binary"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "encrypted_access_token",
 | 
			
		||||
      "type": "binary"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "encrypted_refresh_token",
 | 
			
		||||
      "type": "binary"
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "base_filter": null,
 | 
			
		||||
  "check_constraints": [],
 | 
			
		||||
  "custom_indexes": [],
 | 
			
		||||
  "custom_statements": [],
 | 
			
		||||
  "has_create_action": true,
 | 
			
		||||
  "hash": "4C6974D764592ED3279CFD3FF85CE89B146C7E16628F0A4278E319A4A5E5FD8C",
 | 
			
		||||
  "identities": [
 | 
			
		||||
    {
 | 
			
		||||
      "all_tenants?": false,
 | 
			
		||||
      "base_filter": null,
 | 
			
		||||
      "index_name": "character_v1_unique_eve_id_index",
 | 
			
		||||
      "keys": [
 | 
			
		||||
        {
 | 
			
		||||
          "type": "atom",
 | 
			
		||||
          "value": "eve_id"
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "name": "unique_eve_id",
 | 
			
		||||
      "nils_distinct?": true,
 | 
			
		||||
      "where": null
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "multitenancy": {
 | 
			
		||||
    "attribute": null,
 | 
			
		||||
    "global": null,
 | 
			
		||||
    "strategy": null
 | 
			
		||||
  },
 | 
			
		||||
  "repo": "Elixir.WandererApp.Repo",
 | 
			
		||||
  "schema": null,
 | 
			
		||||
  "table": "character_v1"
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,98 @@
 | 
			
		||||
{
 | 
			
		||||
  "attributes": [
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": false,
 | 
			
		||||
      "default": "fragment(\"gen_random_uuid()\")",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": true,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "id",
 | 
			
		||||
      "type": "uuid"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "token",
 | 
			
		||||
      "type": "text"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "valid_until",
 | 
			
		||||
      "type": "utc_datetime"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": false,
 | 
			
		||||
      "default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "inserted_at",
 | 
			
		||||
      "type": "utc_datetime_usec"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": false,
 | 
			
		||||
      "default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "updated_at",
 | 
			
		||||
      "type": "utc_datetime_usec"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": {
 | 
			
		||||
        "deferrable": false,
 | 
			
		||||
        "destination_attribute": "id",
 | 
			
		||||
        "destination_attribute_default": null,
 | 
			
		||||
        "destination_attribute_generated": null,
 | 
			
		||||
        "index?": false,
 | 
			
		||||
        "match_type": null,
 | 
			
		||||
        "match_with": null,
 | 
			
		||||
        "multitenancy": {
 | 
			
		||||
          "attribute": null,
 | 
			
		||||
          "global": null,
 | 
			
		||||
          "strategy": null
 | 
			
		||||
        },
 | 
			
		||||
        "name": "map_invites_v1_map_id_fkey",
 | 
			
		||||
        "on_delete": "delete",
 | 
			
		||||
        "on_update": null,
 | 
			
		||||
        "primary_key?": true,
 | 
			
		||||
        "schema": "public",
 | 
			
		||||
        "table": "maps_v1"
 | 
			
		||||
      },
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "map_id",
 | 
			
		||||
      "type": "uuid"
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "base_filter": null,
 | 
			
		||||
  "check_constraints": [],
 | 
			
		||||
  "custom_indexes": [],
 | 
			
		||||
  "custom_statements": [],
 | 
			
		||||
  "has_create_action": true,
 | 
			
		||||
  "hash": "B4122C666C9DCF20E420209F604765AB1A3C4979D4134916F7EF9292162B250C",
 | 
			
		||||
  "identities": [],
 | 
			
		||||
  "multitenancy": {
 | 
			
		||||
    "attribute": null,
 | 
			
		||||
    "global": null,
 | 
			
		||||
    "strategy": null
 | 
			
		||||
  },
 | 
			
		||||
  "repo": "Elixir.WandererApp.Repo",
 | 
			
		||||
  "schema": null,
 | 
			
		||||
  "table": "map_invites_v1"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										108
									
								
								priv/resource_snapshots/repo/map_invites_v1/20250608221356.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								priv/resource_snapshots/repo/map_invites_v1/20250608221356.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,108 @@
 | 
			
		||||
{
 | 
			
		||||
  "attributes": [
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": false,
 | 
			
		||||
      "default": "fragment(\"gen_random_uuid()\")",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": true,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "id",
 | 
			
		||||
      "type": "uuid"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "token",
 | 
			
		||||
      "type": "text"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": false,
 | 
			
		||||
      "default": "\"user\"",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "type",
 | 
			
		||||
      "type": "text"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "valid_until",
 | 
			
		||||
      "type": "utc_datetime"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": false,
 | 
			
		||||
      "default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "inserted_at",
 | 
			
		||||
      "type": "utc_datetime_usec"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": false,
 | 
			
		||||
      "default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "updated_at",
 | 
			
		||||
      "type": "utc_datetime_usec"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": {
 | 
			
		||||
        "deferrable": false,
 | 
			
		||||
        "destination_attribute": "id",
 | 
			
		||||
        "destination_attribute_default": null,
 | 
			
		||||
        "destination_attribute_generated": null,
 | 
			
		||||
        "index?": false,
 | 
			
		||||
        "match_type": null,
 | 
			
		||||
        "match_with": null,
 | 
			
		||||
        "multitenancy": {
 | 
			
		||||
          "attribute": null,
 | 
			
		||||
          "global": null,
 | 
			
		||||
          "strategy": null
 | 
			
		||||
        },
 | 
			
		||||
        "name": "map_invites_v1_map_id_fkey",
 | 
			
		||||
        "on_delete": "delete",
 | 
			
		||||
        "on_update": null,
 | 
			
		||||
        "primary_key?": true,
 | 
			
		||||
        "schema": "public",
 | 
			
		||||
        "table": "maps_v1"
 | 
			
		||||
      },
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "map_id",
 | 
			
		||||
      "type": "uuid"
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "base_filter": null,
 | 
			
		||||
  "check_constraints": [],
 | 
			
		||||
  "custom_indexes": [],
 | 
			
		||||
  "custom_statements": [],
 | 
			
		||||
  "has_create_action": true,
 | 
			
		||||
  "hash": "99662C7392D745B0B2D22445A3A703DC2287EBBA185BB7818D58F472B5D033D3",
 | 
			
		||||
  "identities": [],
 | 
			
		||||
  "multitenancy": {
 | 
			
		||||
    "attribute": null,
 | 
			
		||||
    "global": null,
 | 
			
		||||
    "strategy": null
 | 
			
		||||
  },
 | 
			
		||||
  "repo": "Elixir.WandererApp.Repo",
 | 
			
		||||
  "schema": null,
 | 
			
		||||
  "table": "map_invites_v1"
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user