Compare commits

...

14 Commits

Author SHA1 Message Date
CI
a549e70e1a chore: release version v1.98.0 2026-04-06 22:42:56 +00:00
Dmitry Popov
74e8f45265 feat(core): added character profile pages support 2026-04-07 00:38:24 +02:00
CI
c61b8a9942 chore: [skip ci] 2026-03-26 01:02:39 +00:00
CI
0891706489 chore: release version v1.97.5 2026-03-26 01:02:39 +00:00
Dmitry Popov
7d720dcfb5 Merge branch 'main' of github.com:wanderer-industries/wanderer 2026-03-26 02:02:08 +01:00
Dmitry Popov
63b40b9c75 fix(core): Fixed character re-auth issues 2026-03-26 02:02:04 +01:00
CI
fc167fafaf chore: [skip ci] 2026-03-26 00:11:47 +00:00
CI
b9197880f0 chore: release version v1.97.4 2026-03-26 00:11:47 +00:00
Dmitry Popov
88f027facd Merge branch 'main' of github.com:wanderer-industries/wanderer 2026-03-26 01:11:01 +01:00
Dmitry Popov
d62ad709ab fix(core): Fixed character re-auth issues 2026-03-26 01:10:57 +01:00
CI
15aeb8eb85 chore: [skip ci] 2026-03-25 23:41:47 +00:00
CI
6970db438d chore: release version v1.97.3 2026-03-25 23:41:47 +00:00
Dmitry Popov
9ab7fcc46e fix(core): Fixed character re-auth issues 2026-03-26 00:41:07 +01:00
CI
931a8e629d chore: [skip ci] 2026-03-23 11:20:01 +00:00
38 changed files with 1753 additions and 150 deletions

View File

@@ -2,6 +2,42 @@
<!-- changelog -->
## [v1.98.0](https://github.com/wanderer-industries/wanderer/compare/v1.97.5...v1.98.0) (2026-04-06)
### Features:
* core: added character profile pages support
## [v1.97.5](https://github.com/wanderer-industries/wanderer/compare/v1.97.4...v1.97.5) (2026-03-26)
### Bug Fixes:
* core: Fixed character re-auth issues
## [v1.97.4](https://github.com/wanderer-industries/wanderer/compare/v1.97.3...v1.97.4) (2026-03-26)
### Bug Fixes:
* core: Fixed character re-auth issues
## [v1.97.3](https://github.com/wanderer-industries/wanderer/compare/v1.97.2...v1.97.3) (2026-03-25)
### Bug Fixes:
* core: Fixed character re-auth issues
## [v1.97.2](https://github.com/wanderer-industries/wanderer/compare/v1.97.1...v1.97.2) (2026-03-23)

View File

@@ -1,5 +1,6 @@
@layer tailwind-base, primereact, tailwind-utilities;
@import 'quill/dist/quill.snow.css';
@import 'primereact/resources/themes/arya-blue/theme.css' layer(primereact);
/*@import 'primereact/resources/themes/bootstrap4-dark-blue/theme.css' layer(primereact);*/
@@ -1025,3 +1026,77 @@ body > div:first-of-type {
transform: translateY(0);
box-shadow: 0 2px 10px rgba(236, 72, 153, 0.3);
}
/* Quill editor dark theme overrides */
.ql-toolbar.ql-snow {
border-color: #44403c;
background-color: #1c1917;
}
.ql-container.ql-snow {
border-color: #44403c;
background-color: #0c0a09;
color: #e7e5e4;
min-height: 200px;
}
.ql-editor.ql-blank::before {
color: #78716c;
font-style: italic;
}
.ql-snow .ql-stroke {
stroke: #a8a29e;
}
.ql-snow .ql-fill,
.ql-snow .ql-stroke.ql-fill {
fill: #a8a29e;
}
.ql-snow .ql-picker {
color: #a8a29e;
}
.ql-snow .ql-picker-options {
background-color: #1c1917;
border-color: #44403c;
}
.ql-snow .ql-picker-label:hover,
.ql-snow .ql-picker-label.ql-active,
.ql-snow .ql-picker-item:hover,
.ql-snow .ql-picker-item.ql-selected {
color: #e7e5e4;
}
.ql-snow .ql-picker-label:hover .ql-stroke,
.ql-snow .ql-picker-label.ql-active .ql-stroke,
.ql-snow button:hover .ql-stroke,
.ql-snow button.ql-active .ql-stroke {
stroke: #e7e5e4;
}
.ql-snow .ql-picker-label:hover .ql-fill,
.ql-snow .ql-picker-label.ql-active .ql-fill,
.ql-snow button:hover .ql-fill,
.ql-snow button.ql-active .ql-fill {
fill: #e7e5e4;
}
.ql-snow a {
color: #60a5fa;
}
.ql-tooltip {
background-color: #1c1917 !important;
border-color: #44403c !important;
color: #e7e5e4 !important;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3) !important;
}
.ql-tooltip input[type="text"] {
background-color: #0c0a09;
border-color: #44403c;
color: #e7e5e4;
}

View File

@@ -9,6 +9,7 @@ import DownloadJson from './downloadJson';
import NewVersionUpdate from './newVersionUpdate';
import MapAction from './maps/mapAction';
import ShowCharactersAddAlert from './showCharactersAddAlert';
import WysiwygEditor from './wysiwygEditor';
export default {
DownloadJson,
@@ -22,4 +23,5 @@ export default {
CopyToClipboard,
NewVersionUpdate,
ShowCharactersAddAlert,
WysiwygEditor,
};

View File

@@ -0,0 +1,46 @@
import Quill from "quill";
import TurndownService from "turndown";
const WysiwygEditor = {
mounted() {
const view = this as any;
const editorContainer = view.el.querySelector(".ql-editor-container");
if (!editorContainer) return;
const toolbarOptions = [
["bold", "italic", "underline", "strike"],
["blockquote", "link"],
[{ list: "ordered" }, { list: "bullet" }],
[{ header: [1, 2, 3, false] }],
["clean"],
];
const quill = new Quill(editorContainer, {
theme: "snow",
modules: { toolbar: toolbarOptions },
});
const initialContent = editorContainer.getAttribute("data-initial-content");
if (initialContent) {
quill.clipboard.dangerouslyPasteHTML(initialContent);
}
quill.on("text-change", () => {
view.pushEvent("content-text-change", { content: quill.getText() });
});
view.handleEvent("request_editor_content", () => {
const html = quill.root.innerHTML;
if (quill.getText().trim() === "") {
view.pushEvent("editor_content_markdown", { markdown: "" });
} else {
const turndownService = new TurndownService();
const markdown = turndownService.turndown(html);
view.pushEvent("editor_content_markdown", { markdown: markdown });
}
});
},
};
export default WysiwygEditor;

View File

@@ -43,6 +43,8 @@
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.1",
"tailwindcss": "^3.3.6",
"quill": "^2.0.3",
"turndown": "^7.2.0",
"topbar": "^3.0.0",
"use-local-storage-state": "^19.3.1"
},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 285 KiB

View File

@@ -922,6 +922,11 @@
resolved "https://registry.yarnpkg.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz#775374306116d51c0c500b8c4face0f9a04752d8"
integrity sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==
"@mixmark-io/domino@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@mixmark-io/domino/-/domino-2.2.0.tgz#4e8ec69bf1afeb7a14f0628b7e2c0f35bdb336c3"
integrity sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -3000,6 +3005,11 @@ esutils@^2.0.2:
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
eventemitter3@^5.0.1:
version "5.0.4"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.4.tgz#a86d66170433712dde814707ac52b5271ceb1feb"
integrity sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==
execa@^5.0.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
@@ -3041,7 +3051,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-diff@^1.1.2:
fast-diff@^1.1.2, fast-diff@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0"
integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==
@@ -4321,11 +4331,21 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
lodash-es@^4.17.21:
version "4.18.1"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.18.1.tgz#b962eeb80d9d983a900bf342961fb7418ca10b1d"
integrity sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==
lodash.castarray@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.castarray/-/lodash.castarray-4.4.0.tgz#c02513515e309daddd4c24c60cfddcf5976d9115"
integrity sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==
lodash.clonedeep@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==
lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
@@ -5146,6 +5166,11 @@ package-json-from-dist@^1.0.0:
resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505"
integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==
parchment@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/parchment/-/parchment-3.0.0.tgz#2e3a4ada454e1206ae76ea7afcb50e9fb517e7d6"
integrity sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A==
parent-module@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
@@ -5451,6 +5476,25 @@ queue-microtask@^1.2.2:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
quill-delta@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/quill-delta/-/quill-delta-5.1.0.tgz#1c4bc08f7c8e5cc4bdc88a15a1a70c1cc72d2b48"
integrity sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==
dependencies:
fast-diff "^1.3.0"
lodash.clonedeep "^4.5.0"
lodash.isequal "^4.5.0"
quill@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/quill/-/quill-2.0.3.tgz#752765a31d5a535cdc5717dc49d4e50099365eb1"
integrity sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==
dependencies:
eventemitter3 "^5.0.1"
lodash-es "^4.17.21"
parchment "^3.0.0"
quill-delta "^5.1.0"
react-dom@18.3.1, react-dom@^18.3.1:
version "18.3.1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4"
@@ -5987,16 +6031,7 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -6081,14 +6116,7 @@ stringify-entities@^4.0.0:
character-entities-html4 "^2.0.0"
character-entities-legacy "^3.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -6296,6 +6324,13 @@ ts-jest@^29.1.2:
type-fest "^4.41.0"
yargs-parser "^21.1.1"
turndown@^7.2.0:
version "7.2.4"
resolved "https://registry.yarnpkg.com/turndown/-/turndown-7.2.4.tgz#42d98202aefa8c188c997b586bc6da78bdf27ea2"
integrity sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ==
dependencies:
"@mixmark-io/domino" "^2.2.0"
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
@@ -6618,16 +6653,7 @@ wordwrap@^1.0.0:
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==

View File

@@ -41,6 +41,7 @@ defmodule WandererApp.Api.Character do
)
define(:admin_all, action: :admin_all)
define(:update_description, action: :update_description)
end
actions do
@@ -141,6 +142,11 @@ defmodule WandererApp.Api.Character do
accept([:eve_wallet_balance])
end
update :update_description do
accept([:description])
require_atomic? false
end
end
cloak do
@@ -211,6 +217,11 @@ defmodule WandererApp.Api.Character do
attribute :eve_wallet_balance, :float
attribute :tracking_pool, :string
attribute :description, :string do
allow_nil? true
constraints max_length: 10_000
end
create_timestamp(:inserted_at)
update_timestamp(:updated_at)
end

View File

@@ -22,6 +22,7 @@ defmodule WandererApp.Api.MapTransaction do
define(:by_user, action: :by_user)
define(:create, action: :create)
define(:top_donators, action: :top_donators)
define(:server_top_donators, action: :server_top_donators)
end
actions do
@@ -77,6 +78,31 @@ defmodule WandererApp.Api.MapTransaction do
|> then(&{:ok, &1})
end
end
action :server_top_donators, {:array, :struct} do
argument(:after, :utc_datetime, allow_nil?: true)
run fn input, _context ->
base =
from(t in __MODULE__,
where: t.type == :in and not is_nil(t.user_id),
group_by: [t.user_id],
select: %{user_id: t.user_id, total_amount: sum(t.amount)},
order_by: [desc: sum(t.amount)],
limit: 10
)
query =
case input.arguments[:after] do
nil -> base
after_date -> base |> where([t], t.inserted_at >= ^after_date)
end
query
|> WandererApp.Repo.all()
|> then(&{:ok, &1})
end
end
end
attributes do

View File

@@ -804,7 +804,8 @@ defmodule WandererApp.Esi.ApiClient do
})
if count >= 3 do
Logger.warning("TOKEN_REFRESH_FAILED: Invalid grant error (#{count}/3, invalidating tokens)",
Logger.warning(
"TOKEN_REFRESH_FAILED: Invalid grant error (#{count}/3, invalidating tokens)",
character_id: character_id,
error_message: error_message,
time_since_expiry_seconds: time_since_expiry,
@@ -861,7 +862,8 @@ defmodule WandererApp.Esi.ApiClient do
expires_at,
_scopes
) do
time_since_expiry = DateTime.diff(DateTime.utc_now(), DateTime.from_unix!(expires_at), :second)
time_since_expiry =
DateTime.diff(DateTime.utc_now(), DateTime.from_unix!(expires_at), :second)
Logger.warning("TOKEN_REFRESH_FAILED: Transient OAuth2 error during token refresh",
character_id: character_id,
@@ -879,7 +881,8 @@ defmodule WandererApp.Esi.ApiClient do
end
defp handle_refresh_token_result(error, _character, character_id, expires_at, _scopes) do
time_since_expiry = DateTime.diff(DateTime.utc_now(), DateTime.from_unix!(expires_at), :second)
time_since_expiry =
DateTime.diff(DateTime.utc_now(), DateTime.from_unix!(expires_at), :second)
Logger.warning("TOKEN_REFRESH_FAILED: Unexpected error during token refresh",
character_id: character_id,
@@ -897,20 +900,48 @@ defmodule WandererApp.Esi.ApiClient do
end
defp invalidate_character_tokens(character, character_id, expires_at, scopes) do
attrs = %{access_token: nil, refresh_token: nil, expires_at: expires_at, scopes: scopes}
with {:ok, _} <- WandererApp.Api.Character.update(character, attrs) do
WandererApp.Character.update_character(character_id, attrs)
# Skip invalidation if the character was recently re-authorized via SSO.
# This protects fresh tokens from being wiped by transient invalid_grant
# errors that can occur shortly after re-auth.
if WandererApp.Cache.lookup!("character:#{character_id}:reauth_grace", false) do
Logger.info(
"[ApiClient] Skipping token invalidation for #{character_id} - within re-auth grace period"
)
else
error ->
Logger.error("Failed to clear tokens for #{character_id}: #{inspect(error)}")
end
# Re-load from DB to avoid race with concurrent re-auth
case WandererApp.Api.Character.by_id(character_id) do
{:ok, current_character} ->
# Only invalidate if tokens haven't been refreshed since we started
if current_character.access_token == character.access_token do
attrs = %{
access_token: nil,
refresh_token: nil,
expires_at: expires_at,
scopes: scopes
}
Phoenix.PubSub.broadcast(
WandererApp.PubSub,
"character:#{character_id}",
:character_token_invalid
)
with {:ok, _} <- WandererApp.Api.Character.update(current_character, attrs) do
WandererApp.Character.update_character(character_id, attrs)
else
error ->
Logger.error("Failed to clear tokens for #{character_id}: #{inspect(error)}")
end
Phoenix.PubSub.broadcast(
WandererApp.PubSub,
"character:#{character_id}",
:character_token_invalid
)
else
Logger.info(
"[ApiClient] Skipping token invalidation for #{character_id} - tokens were refreshed concurrently"
)
end
{:error, _} ->
Logger.error("Failed to load character #{character_id} for token invalidation")
end
end
:ok
end

View File

@@ -336,7 +336,11 @@ defmodule WandererApp.Map.Server.CharactersImpl do
"[CharacterCleanup] Map #{map_id} - untracking settings and removing character #{s.character_id}"
end)
WandererApp.MapCharacterSettingsRepo.untrack!(%{map_id: s.map_id, character_id: s.character_id})
WandererApp.MapCharacterSettingsRepo.untrack!(%{
map_id: s.map_id,
character_id: s.character_id
})
remove_character(map_id, s.character_id)
end)

View File

@@ -278,8 +278,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
),
do:
update_connection(map_id, :update_mass_status, [:mass_status], connection_update, fn
%{mass_status: old_mass_status},
%{mass_status: mass_status} = updated_connection ->
%{mass_status: old_mass_status}, %{mass_status: mass_status} = updated_connection ->
if mass_status != old_mass_status do
maybe_update_linked_signature_mass_status(map_id, updated_connection)
end

View File

@@ -46,14 +46,18 @@ defmodule WandererApp.Ueberauth.Strategy.Eve do
|> with_param(:hl, conn)
|> with_state_param(conn)
opts = oauth_client_options_from_conn(conn, with_wallet, is_admin?)
WandererApp.Cache.put(
"eve_auth_#{params[:state]}",
[with_wallet: with_wallet, is_admin?: is_admin?],
[
with_wallet: with_wallet,
is_admin?: is_admin?,
tracking_pool: Keyword.get(opts, :tracking_pool)
],
ttl: :timer.minutes(30)
)
opts = oauth_client_options_from_conn(conn, with_wallet, is_admin?)
redirect!(conn, WandererApp.Ueberauth.Strategy.Eve.OAuth.authorize_url!(params, opts))
false ->

View File

@@ -68,6 +68,13 @@ defmodule WandererAppWeb do
end
end
def blog_live_view do
live_view(
layout: {WandererAppWeb.Layouts, :blog},
container: {:div, class: ""}
)
end
def live_component do
quote do
use Phoenix.LiveComponent

View File

@@ -206,6 +206,7 @@ defmodule WandererAppWeb.Layouts do
>
<li><a href="/changelog">Changelog</a></li>
<li><a href="/news">News</a></li>
<li :if={@map_subscriptions_enabled}><a href="/sponsors">Sponsors</a></li>
<li><a href="/license">License</a></li>
<li><a href="/contacts">Contact Us</a></li>
</ul>
@@ -236,6 +237,13 @@ defmodule WandererAppWeb.Layouts do
icon="hero-signal-solid"
tip="Characters Tracking"
/>
<.nav_link
:if={@map_subscriptions_enabled}
href="/sponsors"
active={@active_tab == :sponsors}
icon="hero-heart-solid"
tip="Our Sponsors"
/>
</div>
</div>
<div>

View File

@@ -10,6 +10,12 @@ defmodule WandererAppWeb.AuthController do
def callback(%{assigns: %{ueberauth_auth: auth, current_user: user} = _assigns} = conn, _params) do
active_tracking_pool = WandererApp.Character.TrackingConfigUtils.get_active_pool!()
Logger.info(
"[AuthController] SSO callback SUCCESS for eve_id=#{auth.info.email}, " <>
"has_token=#{not is_nil(auth.credentials.token)}, " <>
"has_refresh=#{not is_nil(auth.credentials.refresh_token)}"
)
character_data = %{
eve_id: "#{auth.info.email}",
name: auth.info.name,
@@ -40,8 +46,25 @@ defmodule WandererAppWeb.AuthController do
character
|> WandererApp.Api.Character.update(character_update)
Logger.info(
"[AuthController] Character #{character.id} tokens updated in DB, " <>
"access_token_present=#{not is_nil(character.access_token)}"
)
WandererApp.Character.update_character(character.id, character_update)
# Clear the invalid_grant counter so stale failures don't cause
# premature token invalidation after a successful re-auth
WandererApp.Cache.delete("character:#{character.id}:invalid_grant_count")
# Set a grace period to protect fresh tokens from being wiped by
# in-flight or immediately-subsequent invalid_grant errors
WandererApp.Cache.put(
"character:#{character.id}:reauth_grace",
true,
ttl: :timer.minutes(5)
)
# Update corporation/alliance data from ESI to ensure access control is current
update_character_affiliation(character)
@@ -96,7 +119,16 @@ defmodule WandererAppWeb.AuthController do
end
def callback(conn, _params) do
# This runs when Ueberauth auth FAILED — tokens are NOT updated
ueberauth_failure = conn.assigns[:ueberauth_failure]
Logger.warning(
"[AuthController] SSO callback FAILED - no ueberauth_auth in assigns. " <>
"Failure: #{inspect(ueberauth_failure)}"
)
conn
|> put_flash(:error, "Authorization failed. Please try again.")
|> redirect(to: "/characters")
end

View File

@@ -19,6 +19,7 @@ defmodule WandererAppWeb.RouteBuilderController do
{:error, reason} ->
Logger.warning("[RouteBuilderController] find_closest failed: #{inspect(reason)}")
conn
|> put_status(:bad_gateway)
|> json(%{error: "route_builder_failed"})

View File

@@ -0,0 +1,139 @@
defmodule WandererAppWeb.CharacterProfileLive do
use WandererAppWeb, :live_view
require Logger
@impl true
def mount(%{"eve_id" => eve_id_str}, _session, socket) do
case Integer.parse(eve_id_str) do
{eve_id, ""} ->
case load_character(eve_id) do
{:ok, character} ->
is_owner = owner?(socket.assigns.current_user, eve_id_str)
description_html = render_description(character.description)
{:ok,
assign(socket,
page_title: character.name,
profile: build_profile(character),
is_owner: is_owner,
editing: false,
description_html: description_html,
description_raw: character.description || ""
)}
{:error, _} ->
{:ok,
socket
|> put_flash(:error, "Character not found")
|> redirect(to: "/")}
end
_ ->
{:ok,
socket
|> put_flash(:error, "Invalid character ID")
|> redirect(to: "/")}
end
end
@impl true
def handle_event("toggle_edit", _params, socket) do
if socket.assigns.is_owner do
{:noreply, assign(socket, editing: !socket.assigns.editing)}
else
{:noreply, socket}
end
end
def handle_event("save_description", _params, socket) do
if socket.assigns.is_owner do
{:noreply, push_event(socket, "request_editor_content", %{})}
else
{:noreply, socket}
end
end
def handle_event("cancel_edit", _params, socket) do
{:noreply, assign(socket, editing: false)}
end
def handle_event("content-text-change", _params, socket) do
{:noreply, socket}
end
def handle_event("editor_content_markdown", %{"markdown" => markdown}, socket) do
if socket.assigns.is_owner do
markdown = String.slice(markdown, 0, 10_000)
eve_id_str = socket.assigns.profile.eve_id
case WandererApp.Api.Character.by_eve_id(eve_id_str) do
{:ok, character} ->
case WandererApp.Api.Character.update_description(character, %{description: markdown}) do
{:ok, _updated} ->
Cachex.del(:api_cache, "character_profile_#{eve_id_str}")
description_html = render_description(markdown)
{:noreply,
socket
|> assign(
editing: false,
description_html: description_html,
description_raw: markdown
)
|> put_flash(:info, "Description updated")}
{:error, reason} ->
Logger.error("Failed to update description: #{inspect(reason)}")
{:noreply, put_flash(socket, :error, "Failed to save description")}
end
{:error, _} ->
{:noreply, put_flash(socket, :error, "Character not found")}
end
else
{:noreply, socket}
end
end
defp owner?(current_user, eve_id_str) do
case current_user do
%{characters: characters} when is_list(characters) ->
Enum.any?(characters, fn c -> to_string(c.eve_id) == eve_id_str end)
_ ->
false
end
end
defp load_character(eve_id) do
WandererApp.Api.Character.by_eve_id(eve_id)
end
defp build_profile(character) do
%{
eve_id: character.eve_id,
name: character.name,
corporation_id: character.corporation_id,
corporation_name: character.corporation_name,
corporation_ticker: character.corporation_ticker,
alliance_id: character.alliance_id,
alliance_name: character.alliance_name,
alliance_ticker: character.alliance_ticker,
online: character.online
}
end
defp render_description(nil), do: ""
defp render_description(""), do: ""
defp render_description(markdown) do
case Earmark.as_html(markdown) do
{:ok, html, _} ->
HtmlSanitizeEx.markdown_html(html)
{:error, _, _} ->
""
end
end
end

View File

@@ -0,0 +1,133 @@
<main class="w-full h-full p-4 pl-20 pb-20 overflow-auto">
<article class="ccp-font w-full max-w-2xl mx-auto">
<div class="bg-neutral-900/60 text-stone-200 [text-shadow:0_0px_8px_rgba(0,0,0,0.7)] px-6 py-5 mt-8 rounded-lg">
<div class="flex flex-row items-center gap-4">
<img
src={"https://images.evetech.net/characters/#{@profile.eve_id}/portrait?size=256"}
class="w-20 h-20 rounded-lg flex-shrink-0"
alt={@profile.name}
/>
<div class="flex flex-col gap-1 min-w-0">
<div class="flex items-center gap-2">
<h2 class="text-xl font-bold text-white m-0 truncate">{@profile.name}</h2>
<span
:if={@profile.online}
class="w-2 h-2 rounded-full bg-green-500 flex-shrink-0"
title="Online"
/>
<span
:if={!@profile.online}
class="w-2 h-2 rounded-full bg-gray-500 flex-shrink-0"
title="Offline"
/>
</div>
<div :if={@profile.corporation_name} class="flex items-center gap-2 text-sm">
<img
src={"https://images.evetech.net/corporations/#{@profile.corporation_id}/logo?size=64"}
class="w-5 h-5 rounded"
alt={@profile.corporation_name}
/>
<span class="text-stone-300 truncate">
<span :if={@profile.corporation_ticker} class="text-gray-500">
[{@profile.corporation_ticker}]
</span>
{@profile.corporation_name}
</span>
</div>
<div :if={@profile.alliance_name} class="flex items-center gap-2 text-sm">
<img
src={"https://images.evetech.net/alliances/#{@profile.alliance_id}/logo?size=64"}
class="w-5 h-5 rounded"
alt={@profile.alliance_name}
/>
<span class="text-stone-300 truncate">
<span :if={@profile.alliance_ticker} class="text-gray-500">
[{@profile.alliance_ticker}]
</span>
{@profile.alliance_name}
</span>
</div>
</div>
<div class="flex gap-2 ml-auto flex-shrink-0">
<a
href={"https://zkillboard.com/character/#{@profile.eve_id}/"}
target="_blank"
rel="noopener noreferrer"
>
<.button
type="button"
class="p-button p-component p-button-primary w-full"
style="min-width: 0;"
>
<span class="p-button-label">zKill</span>
</.button>
</a>
<a
href={"https://evewho.com/character/#{@profile.eve_id}"}
target="_blank"
rel="noopener noreferrer"
>
<.button
type="button"
class="p-button p-component p-button-primary w-full"
style="min-width: 0;"
>
<span class="p-button-label">EVE Who</span>
</.button>
</a>
</div>
</div>
<%!-- About / Description section --%>
<div class="mt-4 border-t border-stone-700/50 pt-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-sm font-semibold text-stone-400 m-0 uppercase tracking-wide">About</h3>
<button
:if={@is_owner && !@editing}
phx-click="toggle_edit"
class="h-8 w-8 hover:text-white"
type="button"
>
<.icon name="hero-pencil-square-solid" class="w-6 h-6" />
</button>
</div>
<%!-- View mode --%>
<div :if={!@editing}>
<%= if @description_html != "" do %>
<div class="prose prose-invert prose-sm max-w-none text-stone-300">
{raw(@description_html)}
</div>
<% else %>
<p class="text-stone-500 italic text-sm m-0">No description yet.</p>
<% end %>
</div>
<%!-- Edit mode --%>
<div :if={@editing && @is_owner}>
<div id="wysiwyg-editor" phx-hook="WysiwygEditor" phx-update="ignore">
<div class="ql-editor-container" data-initial-content={@description_html}></div>
</div>
<div class="flex gap-2 mt-3">
<.button
type="button"
phx-click="save_description"
class="p-button p-component p-button-primary w-full"
style="min-width: 0;"
>
<span class="p-button-label">Save</span>
</.button>
<.button
type="button"
phx-click="cancel_edit"
class="p-button p-component p-button-primary w-full"
style="min-width: 0;"
>
<span class="p-button-label">Cancel</span>
</.button>
</div>
</div>
</div>
</div>
</article>
</main>

View File

@@ -22,6 +22,11 @@ defmodule WandererAppWeb.CharactersLive do
"character:#{character_id}:corporation"
)
Phoenix.PubSub.subscribe(
WandererApp.PubSub,
"character:#{character_id}"
)
:ok = WandererApp.Character.TrackerManager.start_tracking(character_id)
end)
@@ -83,12 +88,11 @@ defmodule WandererAppWeb.CharactersLive do
{:ok, _} = WandererApp.MapCharacterSettingsRepo.untrack(settings)
end)
{:ok, updated_character} =
socket.assigns.characters
|> Enum.find(&(&1.id == character_id))
|> WandererApp.Api.Character.mark_as_deleted()
# Load character from DB instead of using plain map from assigns
{:ok, character} = WandererApp.Api.Character.by_id(character_id)
{:ok, _updated_character} = WandererApp.Api.Character.mark_as_deleted(character)
WandererApp.Character.update_character(character_id, updated_character)
WandererApp.Character.update_character(character_id, %{deleted: true, user_id: nil})
{:ok, characters} =
WandererApp.Api.Character.active_by_user(%{user_id: socket.assigns.user_id})
@@ -148,6 +152,18 @@ defmodule WandererAppWeb.CharactersLive do
{:noreply, socket |> assign(characters: characters |> Enum.map(&map_ui_character/1))}
end
@impl true
def handle_info(
event,
socket
)
when event in [:character_token_invalid, :token_updated] do
{:ok, characters} =
WandererApp.Api.Character.active_by_user(%{user_id: socket.assigns.user_id})
{:noreply, socket |> assign(characters: characters |> Enum.map(&map_ui_character/1))}
end
@impl true
def handle_info(
_event,

View File

@@ -197,6 +197,13 @@
</div>
</div>
<div class="card-actions justify-end">
<.link
navigate={~p"/characters/#{character.eve_id}"}
class="tooltip tooltip-bottom"
data-tip="View Profile"
>
<.icon name="hero-user-solid" class="w-4 h-4 hover:text-white" />
</.link>
<.link
patch={~p"/characters/authorize"}
class="tooltip tooltip-bottom"
@@ -236,7 +243,9 @@
</figure>
</:col>
<:col :let={character} label="Name">
{character.name}
<.link navigate={~p"/characters/#{character.eve_id}"} class="hover:text-white underline">
{character.name}
</.link>
</:col>
<:col :let={character} label="Corporation">
{character

View File

@@ -614,7 +614,8 @@ defmodule WandererAppWeb.MapCoreEventHandler do
nil
end
expired_characters = tracked_characters |> Enum.filter(&(&1.access_token == nil)) |> Enum.map(& &1.eve_id)
expired_characters =
tracked_characters |> Enum.filter(&(&1.access_token == nil)) |> Enum.map(& &1.eve_id)
initial_data =
%{

View File

@@ -176,12 +176,14 @@ defmodule WandererAppWeb.MapRoutesEventHandler do
routes_type = Map.get(event, "type", "blueLoot")
security_type = Map.get(event, "securityType", "both")
is_subscription_active? = Map.get(socket.assigns, :is_subscription_active?, false)
routes_limit =
if is_subscription_active? == true do
@paid_routes_limit
else
Map.get(@alpha_routes_limit_by_type, routes_type, @default_alpha_routes_limit)
end
routes_settings =
routes_settings
|> get_routes_settings()

View File

@@ -6,6 +6,7 @@ defmodule WandererAppWeb.Nav do
alias WandererAppWeb.{
AccessListsLive,
CharacterProfileLive,
MapLive,
MapsLive,
CharactersLive,
@@ -64,6 +65,9 @@ defmodule WandererAppWeb.Nav do
{CharactersLive, _} ->
:characters
{CharacterProfileLive, _} ->
:characters
{CharactersTrackingLive, _} ->
:characters_tracking

View File

@@ -0,0 +1,93 @@
defmodule WandererAppWeb.SponsorsLive do
use WandererAppWeb, :live_view
alias BetterNumber, as: Number
require Logger
@cache_key "server_top_donators"
@cache_ttl :timer.minutes(15)
@impl true
def mount(_params, _session, socket) do
if not WandererApp.Env.map_subscriptions_enabled?() do
{:ok, socket |> redirect(to: "/")}
else
top_donators = load_top_donators()
{corporation_id, corporation_info} = load_corporation_info()
{:ok,
assign(socket,
page_title: "Sponsors",
top_donators: top_donators,
corporation_id: corporation_id,
corporation_info: corporation_info
)}
end
end
def format_isk(amount) do
Number.to_human(amount, units: ["", "K", "M", "B", "T", "P"])
end
defp load_top_donators do
case Cachex.get(:api_cache, @cache_key) do
{:ok, nil} ->
donators = fetch_and_enrich()
Cachex.put(:api_cache, @cache_key, donators, ttl: @cache_ttl)
donators
{:ok, cached} ->
cached
_ ->
fetch_and_enrich()
end
end
defp fetch_and_enrich do
after_date = DateTime.utc_now() |> DateTime.add(-30, :day)
case WandererApp.Api.MapTransaction.server_top_donators(%{after: after_date}) do
{:ok, donators} ->
enrich_with_characters(donators)
{:error, reason} ->
Logger.warning("Failed to load server top donators: #{inspect(reason)}")
[]
end
end
defp enrich_with_characters(donators) do
donators
|> Enum.map(fn %{user_id: user_id, total_amount: total_amount} ->
case WandererApp.Api.Character.active_by_user(%{user_id: user_id}) do
{:ok, [character | _]} ->
%{
character_name: character.name,
eve_id: character.eve_id,
corporation_name: character.corporation_name,
corporation_ticker: character.corporation_ticker,
total_amount: total_amount
}
_ ->
nil
end
end)
|> Enum.reject(&is_nil/1)
end
defp load_corporation_info do
corp_eve_id = WandererApp.Env.corp_eve_id()
if corp_eve_id == -1 do
{nil, nil}
else
case WandererApp.Esi.get_corporation_info(corp_eve_id) do
{:ok, info} -> {corp_eve_id, info}
_ -> {nil, nil}
end
end
end
end

View File

@@ -0,0 +1,119 @@
<main class="w-full h-full p-4 pl-20 pb-20 overflow-auto">
<article class="ccp-font w-full max-w-2xl mx-auto">
<h1 class="font-bold text-lg ccp-font text-white mb-1">
Our Sponsors
</h1>
<p class="text-stone-400 text-sm mb-4">
Top 10 ISK donators across all maps in the last 30 days.
</p>
<div class="bg-neutral-900/60 text-stone-200 px-6 py-5 rounded-lg">
<div :if={@top_donators == []} class="text-center text-gray-400 py-8">
No donations found for this period.
</div>
<div :if={@top_donators != []} class="space-y-2">
<div
:for={{donator, index} <- Enum.with_index(@top_donators)}
class="flex flex-row items-center gap-4 p-3 rounded-lg bg-base-200/50"
>
<span class="text-lg font-bold text-gray-500 w-6 text-right flex-shrink-0">
{index + 1}
</span>
<img
src={"https://images.evetech.net/characters/#{donator.eve_id}/portrait?size=64"}
class="w-10 h-10 rounded-lg flex-shrink-0"
alt={donator.character_name}
/>
<div class="flex flex-col gap-0.5 min-w-0">
<.link
navigate={~p"/characters/#{donator.eve_id}"}
class="text-sm font-bold text-white truncate hover:text-blue-400 transition-colors"
>
{donator.character_name}
</.link>
<div :if={donator.corporation_name} class="flex items-center gap-1.5 text-xs">
<span class="text-stone-300 truncate">
<span class="text-gray-500">[{donator.corporation_ticker}]</span>
{donator.corporation_name}
</span>
</div>
</div>
<div class="ml-auto text-right font-mono text-sm text-green-400 flex-shrink-0">
ISK {WandererAppWeb.SponsorsLive.format_isk(donator.total_amount)}
</div>
</div>
</div>
<div class="flex justify-center mt-6">
<.button
type="button"
phx-click={show_modal("donate-modal")}
class="p-button p-component p-button-primary w-full"
style="min-width: 0;"
>
<span class="p-button-label"> Become a Sponsor</span>
</.button>
</div>
</div>
</article>
<.modal id="donate-modal" title="How to become a sponsor?" class="!w-[700px]">
<div :if={is_nil(@corporation_info)} class="w-full max-h-[80vh] overflow-y-auto mx-auto">
It's not available yet :(
</div>
<div :if={@corporation_info} class="w-full max-h-[80vh] overflow-y-auto mx-auto">
<div class="mx-auto p-4 rounded-lg shadow-md">
<div
:if={@corporation_info}
class="w-full flex flex-row items-center justify-between gap-2 p-4 bg-stone-950 bg-opacity-70 rounded-lg"
>
Wanderer EVE Corporation:
<div class="flex flex-row items-center justify-between gap-2 p-4 bg-stone-950 bg-opacity-70 rounded-lg">
<div class="avatar">
<div class="rounded-md w-12 h-12">
<img
src={"https://images.evetech.net/corporations/#{@corporation_id}/logo?size=32"}
alt={@corporation_info["name"]}
/>
</div>
</div>
<span>&nbsp; {@corporation_info["name"]}</span>
</div>
</div>
<h2 class="mt-2 text-2xl font-semibold mb-4 text-white-800">
How to Donate ISK to Wanderer in Eve Online
</h2>
<ol class="list-decimal list-inside mb-4">
<li class="mb-2">
<strong>Open corporations overview:</strong>
Click on the 'Social' and then on 'Corporation' in the Neocom menu to access corporations search.
</li>
<li class="mb-2">
<strong>Search for a Corporation:</strong>
Type in the search bar the name: <b>{@corporation_info["name"]}</b>.
</li>
<li class="mb-2">
<strong>Choose 'Give Money':</strong>
Select the 'Give Money' in the context menu to initiate the transfer.
</li>
<li class="mb-2">
<strong>Specify the Amount:</strong>
Input the amount of ISK you wish to transfer to the corporate account.
</li>
<li class="mb-2">
<strong>Add a Reason (Optional):</strong>
Include a short note or reason for the transfer if desired.
</li>
<li class="mb-2">
<strong>Confirm the Transfer:</strong>
Double-check the recipient's name and the amount, then click 'OK' to complete the transaction.
</li>
</ol>
<p>
The ISK will be transferred instantly to the Wanderer's account. Ensure you enter the correct recipient name to avoid any errors. Fly safe and enjoy your time in Eve Online!
</p>
</div>
</div>
</.modal>
</main>

View File

@@ -7,7 +7,7 @@ defmodule WandererAppWeb.Router do
import WandererAppWeb.UserAuth,
warn: false,
only: [redirect_if_user_is_authenticated: 2]
only: [redirect_if_user_is_authenticated: 2, require_authenticated_user: 2]
import WandererAppWeb.BasicAuth,
warn: false,
@@ -164,6 +164,10 @@ defmodule WandererAppWeb.Router do
plug :put_layout, html: {WandererAppWeb.Layouts, :blog}
end
pipeline :require_auth do
plug :require_authenticated_user
end
pipeline :api do
plug WandererAppWeb.Plugs.ContentNegotiation, accepts: ["json"]
plug :accepts, ["json"]
@@ -417,6 +421,8 @@ defmodule WandererAppWeb.Router do
get "/", BlogController, :license
end
scope "/swaggerui" do
pipe_through [:browser, :api_spec]
@@ -549,6 +555,8 @@ defmodule WandererAppWeb.Router do
live "/tracking", CharactersTrackingLive, :index
live "/characters", CharactersLive, :index
live "/characters/authorize", CharactersLive, :authorize
live "/characters/:eve_id", CharacterProfileLive, :show
live "/sponsors", SponsorsLive, :index
live "/maps/new", MapsLive, :create
live "/maps/:slug/edit", MapsLive, :edit
live "/maps/:slug/settings", MapsLive, :settings

View File

@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
@source_url "https://github.com/wanderer-industries/wanderer"
@version "1.97.2"
@version "1.98.0"
def project do
[
@@ -134,6 +134,8 @@ defmodule WandererApp.MixProject do
{:live_view_events, "~> 0.1.0"},
{:ash_pagify, "~> 1.4.1"},
{:timex, "~> 3.0"},
{:earmark, "~> 1.4"},
{:html_sanitize_ex, "~> 1.4"},
# Test coverage and quality
{:excoveralls, "~> 0.18", only: :test}
]

View File

@@ -57,6 +57,7 @@
"hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"},
"heroicons": {:hex, :heroicons, "0.5.5", "c2bcb05a90f010df246a5a2a2b54cac15483b5de137b2ef0bead77fcdf06e21a", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:phoenix_live_view, ">= 0.18.2", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "2f4bf929440fecd5191ba9f40e5009b0f75dc993d765c0e4d068fcb7026d6da1"},
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.5.0", "ea13a4a92ba0fa17bc6199f1bb7b755a8595ec3b5f763330ea8570d8b5f648e4", [:mix], [{:mochiweb, "~> 2.15 or ~> 3.1", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "4eaa2205ae56fab95d0f25065d709b05f0cba730f3fcec184dfde594acdd4578"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"igniter": {:hex, :igniter, "0.7.0", "6848714fa5afa14258c82924a57af9364745316241a409435cf39cbe11e3ae80", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "1e7254780dbf4b44c9eccd6d86d47aa961efc298d7f520c24acb0258c8e90ba9"},
"inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"},
@@ -78,6 +79,7 @@
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
"mint_web_socket": {:hex, :mint_web_socket, "1.0.4", "0b539116dbb3d3f861cdf5e15e269a933cb501c113a14db7001a3157d96ffafd", [:mix], [{:mint, ">= 1.4.1 and < 2.0.0-0", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "027d4c5529c45a4ba0ce27a01c0f35f284a5468519c045ca15f43decb360a991"},
"mix_audit": {:hex, :mix_audit, "2.1.3", "c70983d5cab5dca923f9a6efe559abfb4ec3f8e87762f02bab00fa4106d17eda", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "8c3987100b23099aea2f2df0af4d296701efd031affb08d0746b2be9e35988ec"},
"mochiweb": {:hex, :mochiweb, "3.3.0", "2898ad0bfeee234e4cbae623c7052abc3ff0d73d499ba6e6ffef445b13ffd07a", [:rebar3], [], "hexpm", "aa85b777fb23e9972ebc424e40b5d35106f19bc998873e026dedd876df8ee50c"},
"mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"},
"nebulex": {:hex, :nebulex, "2.6.2", "0874989db4e382362884662d2ee9f31b4c4862595f4ec300bd279068729dd2d0", [:mix], [{:decorator, "~> 1.4", [hex: :decorator, repo: "hexpm", optional: true]}, {:shards, "~> 1.1", [hex: :shards, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "002a1774d5a187eb631ae4006db13df4bb6b325fe2a3c14cb14a1f3e989042b4"},
"nimble_csv": {:hex, :nimble_csv, "1.2.0", "4e26385d260c61eba9d4412c71cea34421f296d5353f914afe3f2e71cce97722", [:mix], [], "hexpm", "d0628117fcc2148178b034044c55359b26966c6eaa8e2ce15777be3bbc91b12a"},

View File

@@ -3,7 +3,7 @@ title: "Event: Wanderer 2026 Roadmap Reveal",
author: "Wanderer Team",
cover_image_uri: "/images/news/2026/01-01-roadmap/cover.webp",
tags: ~w(event roadmap 2026 announcement community),
description: "JWanderer's 2026 roadmap are ready to reveal! Discover what exciting features and improvements are coming in 2026."
description: "Wanderer's 2026 roadmap are ready to reveal! Discover what exciting features and improvements are coming in 2026."
}
---

View File

@@ -1,36 +0,0 @@
%{
title: "Event: Weekly Giveaway Challenge",
author: "Wanderer Team",
cover_image_uri: "/images/news/2026/01-05-weekly-giveaway/cover.webp",
tags: ~w(event giveaway challenge),
description: "Join our Weekly Giveaway Challenge! Be the fastest to claim your reward!"
}
---
![Weekly Giveaway Challenge](/images/news/2026/01-05-weekly-giveaway/cover.webp "Weekly Giveaway Challenge")
### Event Details
In 2026, we're going to giveaway partnership SKIN codes for our community, every week!
- **Event Name:** Weekly Giveaway Challenge
- **Event Link:** [Join Weekly Giveaway Challenge](https://eventcortex.com/events/invite/Cjo87svZFq6J8cc1cubH4B7AR_VfPmQ4)
---
### Tips for Participants
- **Be Ready:** Know the reveal time and be online a few minutes early.
---
Good luck, and may the fastest capsuleer win!
---
Fly safe,
**Wanderer Team**
---

View File

@@ -1,53 +0,0 @@
%{
title: "EVE Creator Awards - Nominate Wanderer!",
author: "Wanderer Team",
cover_image_uri: "/images/news/2026/02-12-eve-creator-awards/cover.jpg",
tags: ~w(event community awards nomination),
description: "CCP has opened nominations for the EVE Creator Awards! Support Wanderer by voting for Third-Party App of the Year and Developer of the Year."
}
---
![EVE Creator Awards](/images/news/2026/02-12-eve-creator-awards/cover.jpg "EVE Creator Awards")
### EVE Creator Awards - We Need Your Vote!
CCP has opened nominations for the **EVE Creator Awards**, including **Best Third-Party App** and **Developer of the Year**, and you can support us by voting.
---
### How to Nominate Us
You can nominate us for **Third-Party App of the Year**, and choose one of the team as **Developer of the Year**: Dan Sylvest, vvrong, or Gustav Oswaldo.
**App field** may be filled with: `Wanderer` / `https://wanderer.ltd/`
---
### A Bit of Stats
Over the past months, Wanderer has grown to more than **7,000 monthly users**, with pilots joining from all around the world.
---
### Meet the Team
- **Dan Sylvest** — leads frontend, design, and frontend architecture, along with several supporting services.
- **vvrong** (you know him as Demiro) — responsible for backend development, core architecture, and APIs, with additional frontend contributions.
- **Gustav Oswaldo** — contributes across backend and frontend, including zKillboard-related services, APIs, and bots.
---
### Vote Now
- **[Vote for us here](https://eve-creator-awards.paperform.co/)**
- **[Read the announcement](https://www.eveonline.com/news/view/eve-creator-awards)**
---
Thank you for your support!
Fly safe,
**Wanderer Team**
---

View File

@@ -0,0 +1,29 @@
defmodule WandererApp.Repo.Migrations.AddCharacterDescription 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(:maps_v1) do
modify :scopes, {:array, :text}, default: '{wormholes}'
end
alter table(:character_v1) do
add :description, :text
end
end
def down do
alter table(:character_v1) do
remove :description
end
alter table(:maps_v1) do
modify :scopes, {:array, :text}, default: nil
end
end
end

View File

@@ -0,0 +1,135 @@
# Seed script for example sponsor donations.
#
# Creates sample users, characters, a map, and donation transactions
# so the /sponsors page and /characters/:eve_id profile pages have data.
#
# Run with:
# mix run priv/repo/seeds_sponsors.exs
#
require Logger
Logger.info("Seeding sponsor donation data...")
alias WandererApp.Repo
# Well-known EVE character data (real public info, no secrets)
characters_data = [
%{
eve_id: "96734492",
name: "Oz Hasaki",
corporation_id: 98_553_333,
corporation_name: "Wormhole Wanderers",
corporation_ticker: "W.W",
alliance_id: nil,
alliance_name: nil,
alliance_ticker: nil
},
%{
eve_id: "2119543215",
name: "Katya Itzimansen",
corporation_id: 98_681_432,
corporation_name: "Anoikis Explorers",
corporation_ticker: "A.EXP",
alliance_id: 99_011_258,
alliance_name: "Anoikis Coalition",
alliance_ticker: "ANOK"
},
%{
eve_id: "93568202",
name: "Dmitriy Lancel",
corporation_id: 98_712_045,
corporation_name: "Signal Cartel",
corporation_ticker: "1420.",
alliance_id: 99_005_338,
alliance_name: "EvE-Scout Enclave",
alliance_ticker: "SCOUT"
},
%{
eve_id: "94801715",
name: "Heron Explorer",
corporation_id: 98_553_333,
corporation_name: "Wormhole Wanderers",
corporation_ticker: "W.W",
alliance_id: nil,
alliance_name: nil,
alliance_ticker: nil
}
]
# Donation amounts (ISK) for each character — descending order
donation_amounts = [
2_500_000_000.0,
1_200_000_000.0,
800_000_000.0,
350_000_000.0
]
# ---------- Create users, characters, a map, and transactions ----------
Repo.transaction(fn ->
# 1. Create a dummy map for the transactions (needs name + slug)
{:ok, map} =
WandererApp.Api.Map.new(%{
name: "Seed Sponsors Map",
slug: "seed-sponsors-map"
})
Logger.info(" Created map: #{map.id}")
Enum.zip(characters_data, donation_amounts)
|> Enum.each(fn {char_data, amount} ->
# 2. Create a user
{:ok, user} =
WandererApp.Api.User
|> Ash.Changeset.for_create(:create, %{
name: char_data.name,
hash: "seed_sponsor_#{char_data.eve_id}"
})
|> Ash.create()
Logger.info(" Created user: #{user.id} (#{user.name})")
# 3. Create a character linked to that user
{:ok, character} =
WandererApp.Api.Character
|> Ash.Changeset.for_create(:create, %{
eve_id: char_data.eve_id,
name: char_data.name
})
|> Ash.create()
# Assign user
{:ok, character} = WandererApp.Api.Character.assign_user(character, %{user_id: user.id})
# Update corporation info
{:ok, character} =
WandererApp.Api.Character.update_corporation(character, %{
corporation_id: char_data.corporation_id,
corporation_name: char_data.corporation_name,
corporation_ticker: char_data.corporation_ticker
})
# Update alliance info (if present)
if char_data.alliance_id do
WandererApp.Api.Character.update_alliance(character, %{
alliance_id: char_data.alliance_id,
alliance_name: char_data.alliance_name,
alliance_ticker: char_data.alliance_ticker
})
end
Logger.info(" Created character: #{character.eve_id} (#{character.name})")
# 4. Create a donation transaction (type: :in)
{:ok, _txn} =
WandererApp.Api.MapTransaction.create(%{
map_id: map.id,
user_id: user.id,
type: :in,
amount: amount
})
Logger.info(" Created donation: #{amount} ISK from #{char_data.name}")
end)
end)
Logger.info("Sponsor seed data complete!")

View File

@@ -0,0 +1,413 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"gen_random_uuid()\")",
"generated?": false,
"precision": null,
"primary_key?": true,
"references": null,
"scale": null,
"size": null,
"source": "id",
"type": "uuid"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "eve_id",
"type": "text"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "name",
"type": "text"
},
{
"allow_nil?": true,
"default": "false",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "online",
"type": "boolean"
},
{
"allow_nil?": true,
"default": "false",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "deleted",
"type": "boolean"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "scopes",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "character_owner_hash",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "token_type",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "expires_at",
"type": "bigint"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "ship_name",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "ship_item_id",
"type": "bigint"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "corporation_id",
"type": "bigint"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "corporation_name",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "corporation_ticker",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "alliance_id",
"type": "bigint"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "alliance_name",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "alliance_ticker",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "tracking_pool",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "description",
"type": "text"
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "inserted_at",
"type": "utc_datetime_usec"
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "updated_at",
"type": "utc_datetime_usec"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"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": null,
"table": "user_v1"
},
"scale": null,
"size": null,
"source": "user_id",
"type": "uuid"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "encrypted_eve_wallet_balance",
"type": "binary"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "encrypted_location",
"type": "binary"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "encrypted_ship",
"type": "binary"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "encrypted_solar_system_id",
"type": "binary"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "encrypted_structure_id",
"type": "binary"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "encrypted_station_id",
"type": "binary"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "encrypted_access_token",
"type": "binary"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "encrypted_refresh_token",
"type": "binary"
}
],
"base_filter": null,
"check_constraints": [],
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true,
"hash": "B75AEDD6CAB8418E22082401EEB78FDA9E2B9B70883EC7EA0E05EE26695D138E",
"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"
}

View File

@@ -0,0 +1,277 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"gen_random_uuid()\")",
"generated?": false,
"precision": null,
"primary_key?": true,
"references": null,
"scale": null,
"size": null,
"source": "id",
"type": "uuid"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "name",
"type": "text"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "slug",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "description",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "personal_note",
"type": "text"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "public_api_key",
"type": "text"
},
{
"allow_nil?": true,
"default": "[]",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "hubs",
"type": [
"array",
"text"
]
},
{
"allow_nil?": false,
"default": "\"wormholes\"",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "scope",
"type": "text"
},
{
"allow_nil?": true,
"default": "false",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "deleted",
"type": "boolean"
},
{
"allow_nil?": true,
"default": "false",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "only_tracked_characters",
"type": "boolean"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "options",
"type": "text"
},
{
"allow_nil?": false,
"default": "false",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "webhooks_enabled",
"type": "boolean"
},
{
"allow_nil?": false,
"default": "false",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "sse_enabled",
"type": "boolean"
},
{
"allow_nil?": true,
"default": "'{wormholes}'",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "scopes",
"type": [
"array",
"text"
]
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "inserted_at",
"type": "utc_datetime_usec"
},
{
"allow_nil?": false,
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"generated?": false,
"precision": null,
"primary_key?": false,
"references": null,
"scale": null,
"size": null,
"source": "updated_at",
"type": "utc_datetime_usec"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"precision": null,
"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": "maps_v1_owner_id_fkey",
"on_delete": null,
"on_update": null,
"primary_key?": true,
"schema": null,
"table": "character_v1"
},
"scale": null,
"size": null,
"source": "owner_id",
"type": "uuid"
}
],
"base_filter": null,
"check_constraints": [],
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true,
"hash": "17E507A2F8B57193D92DF2E707C6623C68A07B5058227A12DEF1522777BE7B83",
"identities": [
{
"all_tenants?": false,
"base_filter": null,
"index_name": "maps_v1_unique_public_api_key_index",
"keys": [
{
"type": "atom",
"value": "public_api_key"
}
],
"name": "unique_public_api_key",
"nils_distinct?": true,
"where": null
},
{
"all_tenants?": false,
"base_filter": null,
"index_name": "maps_v1_unique_slug_index",
"keys": [
{
"type": "atom",
"value": "slug"
}
],
"name": "unique_slug",
"nils_distinct?": true,
"where": null
}
],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.WandererApp.Repo",
"schema": null,
"table": "maps_v1"
}

View File

@@ -155,7 +155,7 @@ defmodule WandererAppWeb.OpenAPISpecAnalyzer do
# Categorize schemas based on naming patterns
request_schemas = Enum.filter(schema_names, &String.contains?(&1, "Request"))
response_schemas = Enum.filter(schema_names, &String.contains?(&1, "Response"))
shared_schemas = schema_names -- request_schemas -- response_schemas
shared_schemas = schema_names -- (request_schemas -- response_schemas)
%{
total: length(schema_names),