ledger gemory draft

This commit is contained in:
Sirttas
2026-05-17 12:42:25 +02:00
parent e81fdc24bb
commit f9ae0d142a
35 changed files with 2017 additions and 2763 deletions
+1895 -2479
View File
File diff suppressed because it is too large Load Diff
+14 -16
View File
@@ -11,29 +11,27 @@
},
"dependencies": {
"@heroicons/vue": "^2.0.18",
"@vueuse/components": "^10.5.0",
"@vueuse/core": "^10.2.1",
"@vueuse/integrations": "^10.2.1",
"@vueuse/components": "^14.3.0",
"@vueuse/core": "^14.3.0",
"@vueuse/integrations": "^14.3.0",
"axios": "^1.4.0",
"axios-rate-limit": "^1.3.1",
"gemory": "file:",
"loglevel": "^1.8.1",
"loglevel-plugin-prefix": "^0.8.4",
"oidc-client-ts": "^3.0.1",
"pinia": "^2.1.6",
"pinia": "^3.0.4",
"vue": "^3.3.4",
"vue-router": "^4.2.4"
"vue-router": "^5.0.7"
},
"devDependencies": {
"@types/node": "^20.4.5",
"@vitejs/plugin-vue": "^5.0.4",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.27",
"tailwindcss": "^3.3.3",
"typescript": "^5.0.2",
"vite": "^6.3.5",
"vite-plugin-runtime-env": "^0.1.1",
"vitest": "^3.1.3",
"vue-tsc": "^2.0.18"
"@types/node": "^25.8.0",
"@vitejs/plugin-vue": "^6.0.7",
"tailwindcss": "^4.3.0",
"typescript": "^6.0.3",
"@tailwindcss/vite": "^4.3.0",
"vite": "^8.0.13",
"vite-plugin-runtime-env": "^1.0.0",
"vitest": "^4.1.6",
"vue-tsc": "^3.2.9"
}
}
-8
View File
@@ -1,8 +0,0 @@
export default {
plugins: {
'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {},
},
}
+4 -4
View File
@@ -1,14 +1,12 @@
<script setup lang="ts">
import { useAuthStore } from '@/auth';
import { computed } from 'vue';
import { RouterView, useRoute } from 'vue-router';
import { Sidebar } from './sidebar';
const route = useRoute();
const authStore = useAuthStore();
const hideSidebar = computed(() => {
return !authStore.isLoggedIn || route.name === 'callback' || route.name === 'about';
return route.name === 'callback' || route.name === 'about';
});
</script>
@@ -24,7 +22,9 @@ const hideSidebar = computed(() => {
</template>
</template>
<style scoped lang="postcss">
<style scoped>
@reference "tailwindcss";
div.main-container {
@apply px-4 sm:ml-64;
}
-49
View File
@@ -1,49 +0,0 @@
import log from "loglevel";
import { Log, User, UserManager, WebStorageStateStore } from "oidc-client-ts";
import { defineStore } from "pinia";
import { computed, ref } from "vue";
Log.setLogger(log);
export const useAuthStore = defineStore('auth', () => {
const userManager = new UserManager({
authority: import.meta.env.VITE_AUTH_AUTHORITY,
client_id: import.meta.env.VITE_AUTH_CLIENT_ID,
client_secret: import.meta.env.VITE_AUTH_CLIENT_SECRET,
redirect_uri: import.meta.env.VITE_AUTH_REDIRECT_URI,
scope: import.meta.env.VITE_AUTH_SCOPE,
stateStore: new WebStorageStateStore({ store: window.localStorage }),
userStore: new WebStorageStateStore({ store: window.localStorage })
});
const user = ref<User>();
const isLoggedIn = computed(() => user.value?.expired === false);
const accessToken = computed(() => user.value?.access_token);
const username = computed(() => user.value?.profile.name ?? "");
const userId = computed(() => user.value?.profile.sub ?? "");
const redirect = async () => {
await userManager.signinRedirect();
log.info("Redirecting to login page");
}
const login = async () => {
await userManager.signinCallback();
log.debug("Logged in");
}
const logout = async () => {
await userManager.signoutRedirect();
log.debug("Logged out");
}
const setUser = (u?: User | null) => {
if (u) {
user.value = u;
log.debug("User loaded", u.profile.name);
} else {
user.value = undefined;
}
}
userManager.events.addUserLoaded(setUser);
userManager.getUser().then(setUser);
return { redirect, login, logout, isLoggedIn, accessToken, username, userId };
});
+13 -22
View File
@@ -17,13 +17,21 @@ useEventListener('keyup', e => {
<div class="dropdown" :class="{'dropdown-open': isOpen, 'dropdown-close': !isOpen}" v-on-click-outside="() => isOpen = false">
<button @click="isOpen = !isOpen">
<slot name="button" />
<Transition name="flip">
<Transition
enter-active-class="transition-transform"
enter-from-class="rotate-180"
leave-active-class="hidden"
leave-to-class="rotate-180">
<ChevronDownIcon v-if="!isOpen" class="chevron" />
<ChevronUpIcon v-else class="chevron" />
</Transition>
</button>
<Transition name="fade">
<Transition
enter-active-class="transition-opacity"
enter-from-class="opacity-0"
leave-from-class="transition-opacity"
leave-to-class="opacity-0">
<div v-if="isOpen" class="relative">
<div class="z-10 divide-y rounded-b-md absolute">
<slot />
@@ -33,27 +41,10 @@ useEventListener('keyup', e => {
</div>
</template>
<style scoped lang="postcss">
<style scoped>
@reference "tailwindcss";
.chevron {
@apply w-4 h-4 ms-1;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
.fade-enter-active, .fade-leave-active {
@apply transition-opacity;
}
.flip-enter-from, .flip-leave-to {
transform: rotate(180deg);
}
.flip-enter-active {
@apply transition-transform;
}
.flip-leave-active {
display: none;
}
</style>
+2 -1
View File
@@ -34,7 +34,8 @@ useEventListener('keyup', e => {
</Transition>
</template>
<style scoped lang="postcss">
<style scoped>
@reference "tailwindcss";
.fade-enter-from, .fade-leave-to {
@apply opacity-0;
}
+2 -1
View File
@@ -10,7 +10,8 @@ const modelValue = defineModel({ default: false });
</label>
</template>
<style scoped lang="postcss">
<style scoped>
@reference "tailwindcss";
input:checked ~ span:last-child {
--tw-translate-x: 1.25rem;
}
+2 -1
View File
@@ -32,7 +32,8 @@ const emit = defineEmits<Emit>();
</component>
</template>
<style scoped lang="postcss">
<style scoped>
@reference "tailwindcss";
.sort-header {
@apply relative h-8 pe-3;
}
+2 -1
View File
@@ -67,7 +67,8 @@ const itemHeightStyle = computed(() => {
<slot v-else name="empty" />
</template>
<style scoped lang="postcss">
<style scoped>
@reference "tailwindcss";
div.table-container {
@apply bg-slate-600;
max-height: calc(100vh - v-bind(ypx));
-13
View File
@@ -1,4 +1,3 @@
import { useAuthStore } from "@/auth";
import { createPinia } from 'pinia';
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
@@ -17,18 +16,6 @@ const router = createRouter({
});
app.use(pinia);
const authStore = useAuthStore();
router.beforeEach(async to => {
if (to.name === 'callback') {
await authStore.login();
return { name: 'home' };
} else if (!authStore.isLoggedIn) {
await authStore.redirect();
}
});
app.use(router);
app.mount('#app');
+11
View File
@@ -0,0 +1,11 @@
import { logResource } from "@/service";
import axios from "axios";
export const mammonAxiosInstance = axios.create({
baseURL: import.meta.env.VITE_MAMMON_URL,
headers: {
'Accept': 'application/json',
"Content-Type": "application/json",
},
})
logResource(mammonAxiosInstance)
-3
View File
@@ -1,3 +0,0 @@
export type MarbasObject = {
id: number;
}
-3
View File
@@ -1,3 +0,0 @@
export * from './MarbasObject';
export * from './marbasService';
-56
View File
@@ -1,56 +0,0 @@
import { useAuthStore } from "@/auth";
import { logResource } from "@/service";
import axios from "axios";
export const marbasAxiosInstance = axios.create({
baseURL: import.meta.env.VITE_MARBAS_URL,
headers: {
'Accept': 'application/json',
"Content-Type": "application/json",
},
})
const authStore = useAuthStore();
marbasAxiosInstance.interceptors.request.use(async r => {
if (!authStore.isLoggedIn) {
await authStore.redirect();
}
const accessToken = authStore.accessToken;
if (accessToken) {
r.headers.Authorization = `Bearer ${accessToken}`;
}
if (!r.params?.page_size) {
r.params = { ...r.params, page_size: 250 };
}
return r;
})
logResource(marbasAxiosInstance)
marbasAxiosInstance.interceptors.response.use(async r => {
if (r.status === 401) {
await authStore.redirect();
return marbasAxiosInstance.request(r.config);
}
let next: string = r.data?.next;
let results = r.data?.results;
if (next) {
if (!next.startsWith(import.meta.env.VITE_MARBAS_URL)) { // FIME remove once the API is fixed
next = import.meta.env.VITE_MARBAS_URL + next.replace(/http(s)?:\/\/[^/]+\//g, '');
}
results = results.concat((await marbasAxiosInstance.request({
...r.config,
url: next,
baseURL: '',
})).data);
}
if (results) {
r.data = results;
}
return r;
})
@@ -74,7 +74,8 @@ watchEffect(async () => {
</Tooltip>
</template>
<style scoped lang="postcss">
<style scoped>
@reference "tailwindcss";
.tooltip {
@apply ms-auto;
>:deep(div.header) {
@@ -245,7 +245,8 @@ const total = computed(() => {
</VirtualScrollTable>
</template>
<style scoped lang="postcss">
<style scoped>
@reference "tailwindcss";
div.end {
@apply justify-self-end ms-2;
}
+3 -37
View File
@@ -1,48 +1,16 @@
import { marbasAxiosInstance, MarbasObject } from "@/marbas";
import { AxiosResponse } from "axios";
import log from "loglevel";
import { defineStore } from "pinia";
import { computed, ref } from "vue";
export type AcquiredTypeSource = 'bo' | 'so' | 'prod' | 'misc';
export type MarbasAcquiredType = MarbasObject & {
type: number;
quantity: number;
remaining: number;
price: number;
date: Date;
source: AcquiredTypeSource;
}
type RawMarbasAcquiredType = Omit<MarbasAcquiredType, 'date'> & {
date: string;
}
type InsertableRawMarbasAcquiredType = Omit<MarbasAcquiredType, 'id' | 'date'>;
const mapRawMarbasAcquiredType = (raw: RawMarbasAcquiredType): MarbasAcquiredType => ({
...raw,
date: raw.date ? new Date(raw.date) : new Date()
});
const endpoint = '/api/acquisitions/';
export const useAcquiredTypesStore = defineStore('market-acquisition', () => {
const acquiredTypes = ref<MarbasAcquiredType[]>([]);
const acquiredTypes = ref<any[]>([]); // TODO
const types = computed(() => acquiredTypes.value.filter(item => item.remaining > 0));
const addAcquiredType = async (type: number, quantity: number, price: number, source?: AcquiredTypeSource) => {
const newItem = mapRawMarbasAcquiredType((await marbasAxiosInstance.post<RawMarbasAcquiredType, AxiosResponse<RawMarbasAcquiredType>, InsertableRawMarbasAcquiredType>(endpoint, {
type: type,
quantity: quantity,
remaining: quantity,
price: price,
source: source ?? 'misc',
})).data);
const newItem = [];
acquiredTypes.value = [...acquiredTypes.value, newItem];
log.info(`Acquired type ${newItem.id} with quantity ${newItem.quantity} and price ${newItem.price}`, newItem);
};
const removeAcquiredType = async (id: number, quantity: number) => {
const found = acquiredTypes.value.find(t => t.id === id);
@@ -63,11 +31,9 @@ export const useAcquiredTypesStore = defineStore('market-acquisition', () => {
return i;
}
});
await marbasAxiosInstance.put(`${endpoint}${item.id}/`, item);
log.info(`Acquired type ${item.id} remaining: ${item.remaining}`, item);
};
const refresh = () => marbasAxiosInstance.get<RawMarbasAcquiredType[]>(endpoint).then(res => acquiredTypes.value = res.data.map(mapRawMarbasAcquiredType));
const refresh = () => {}
refresh();
+2 -1
View File
@@ -17,7 +17,8 @@ const { brokerFee, scc } = storeToRefs(useMarketTaxStore());
</div>
</template>
<style scoped lang="postcss">
<style scoped>
@reference "tailwindcss";
div.end {
@apply justify-self-end ms-2;
}
+2 -1
View File
@@ -167,7 +167,8 @@ const getLineColor = (result: Result) => {
</VirtualScrollTable>
</template>
<style scoped lang="postcss">
<style scoped>
@reference "tailwindcss";
div.end {
@apply justify-self-end ms-2;
}
+1 -11
View File
@@ -1,4 +1,3 @@
import { marbasAxiosInstance, MarbasObject } from "@/marbas";
import { EsiMarketOrderHistory, getHistory, MarketType, MarketTypePrice } from "@/market";
import log from "loglevel";
import { defineStore } from "pinia";
@@ -12,21 +11,16 @@ export type TrackingResult = {
orderCount: number,
}
export type MarbasTrackedType = MarbasObject & {
type: number
};
const endpoint = '/api/types_tracking/';
export const useMarketTrackingStore = defineStore('marketTracking', () => {
const trackedTypes = ref<MarbasTrackedType[]>([]);
const trackedTypes = ref<any[]>([]); // TODO
const types = computed(() => trackedTypes.value.map(item => item.type) ?? []);
const addType = async (type: number) => {
const found = trackedTypes.value.find(item => item.type === type);
if (!found) {
trackedTypes.value = [...trackedTypes.value, (await marbasAxiosInstance.post<MarbasTrackedType>(endpoint, { type })).data];
log.info(`Tracking type ${type}`);
}
}
@@ -38,12 +32,8 @@ export const useMarketTrackingStore = defineStore('marketTracking', () => {
}
trackedTypes.value = trackedTypes.value.filter(t => t.id !== found.id);
await marbasAxiosInstance.delete(`${endpoint}${found.id}`);
log.info(`Stopped tracking type ${type}`);
}
marbasAxiosInstance.get<MarbasTrackedType[]>(endpoint).then(res => trackedTypes.value = res.data);
return { types, addType, removeType };
});
+3 -15
View File
@@ -1,5 +1,3 @@
import { marbasAxiosInstance } from "@/marbas";
export type MarketType = {
id: number;
group_id: number;
@@ -18,15 +16,9 @@ export const getMarketTypes = async (types: (string | number)[]): Promise<Market
if (types.length === 0) {
return [];
} else if (types.length === 1 && typeof types[0] === "number") {
return [(await marbasAxiosInstance.get<MarketType>(`/sde/types/${types[0]}/`)).data];
return [];
}
return (await marbasAxiosInstance.post<MarketType[]>("/api/types/search", types.map(t => {
if (typeof t === "number") {
return { id: t };
} else {
return { name: t };
}
}))).data;
return []
}
const blueprintMarketGrous = [ // TODO add all groups
@@ -49,9 +41,5 @@ const blueprintMarketGrous = [ // TODO add all groups
]
export const searchMarketTypes = async (search: string): Promise<MarketType[]> => {
return (await marbasAxiosInstance.post<MarketType[]>("/api/types/search", [{
name__icontains: search,
marketgroup_id___not: null,
marketgroup_id__in___not: blueprintMarketGrous,
}])).data;
return []
}
+2 -1
View File
@@ -104,7 +104,8 @@ watchEffect(async () => {
</div>
</template>
<style scoped lang="postcss">
<style scoped>
@reference "tailwindcss";
.fake-input {
@apply w-96 flex border bg-slate-500 rounded px-1 py-0.5;
+2 -1
View File
@@ -29,7 +29,8 @@ withDefaults(defineProps<Props>(), {
</div>
</template>
<style scoped lang="postcss">
<style scoped>
@reference "tailwindcss";
button:deep(>svg), .button:deep(>svg) {
@apply !w-4 !h-4;
}
+23
View File
@@ -0,0 +1,23 @@
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router';
</script>
<template>
<div class="mt-4">
<div class="flex border-b-2 border-emerald-500">
</div>
<RouterView />
</div>
</template>
<style scoped>
@reference "tailwindcss";
a.tab {
@apply flex items-center px-4 me-2 rounded-t-md bg-slate-600 hover:bg-slate-700;
&.router-link-active {
@apply bg-emerald-500 hover:bg-emerald-700;
}
}
</style>
+3 -1
View File
@@ -20,7 +20,9 @@ import { RouterLink, RouterView } from 'vue-router';
</div>
</template>
<style scoped lang="postcss">
<style scoped>
@reference "tailwindcss";
a.tab {
@apply flex items-center px-4 me-2 rounded-t-md bg-slate-600 hover:bg-slate-700;
&.router-link-active {
+2 -1
View File
@@ -113,7 +113,8 @@ watch(useRoute(), async route => {
<BuyModal ref="buyModal" />
</template>
<style scoped lang="postcss">
<style scoped>
@reference "tailwindcss";
img.type-image {
width: 64px;
height: 64px;
+2 -1
View File
@@ -12,7 +12,8 @@ const modelValue = defineModel({ default: false });
</label>
</template>
<style scoped lang="postcss">
<style scoped>
@reference "tailwindcss";
input:checked ~ span:last-child {
--tw-translate-x: 1.75rem;
}
+1 -5
View File
@@ -1,5 +1,3 @@
import { marbasAxiosInstance } from "@/marbas";
export type ReprocessItemValues = {
typeID: number;
name: string;
@@ -22,7 +20,5 @@ export const reprocess = async (items: string, minerals: string, efficiency?: nu
};
const source = JSON.stringify(sourceJson);
const response = await marbasAxiosInstance.post('/reprocess/', source, {params: {efficiency: efficiency ?? 0.55}});
return response.data;
return []
};
+5 -1
View File
@@ -3,14 +3,18 @@ import { RouteRecordRaw } from 'vue-router';
export const routes: RouteRecordRaw[] = [
{ path: '/', name: 'home', component: () => import('@/pages/Index.vue') },
{ path: '/callback', name: 'callback', component: () => import('@/pages/Index.vue') },
{ path: '/reprocess', component: () => import('@/pages/Reprocess.vue') },
{ path: '/ledger', component: () => import('@/pages/Ledgers.vue') },
{ path: '/market', component: () => import('@/pages/Market.vue'), children: [
{ path: '', redirect: '/market/types' },
{ path: 'types/:type?', name: 'market-types', component: () => import('@/pages/market/TypeInfo.vue') },
{ path: 'tracking', component: () => import('@/pages/market/Tracking.vue') },
{ path: 'acquisitions', component: () => import('@/pages/market/Acquisitions.vue') },
] },
{ path: '/reprocess', component: () => import('@/pages/Reprocess.vue') },
{ path: '/tools', component: () => import('@/pages/Tools.vue') },
{ path: '/characters', component: () => import('@/pages/Characters.vue') },
{ path: '/about', name: 'about', component: () => import('@/pages/About.vue') },
];
+7 -5
View File
@@ -1,18 +1,16 @@
<script setup lang="ts">
import { useAuthStore } from '@/auth';
import { Dropdown } from '@/components';
import { RouterLink } from 'vue-router';
const links = [
{ name: "Ledger", path: "/legers" },
{ name: "Market", path: "/market" },
{ name: "Reprocess", path: "/reprocess" },
{ name: "Tools", path: "/tools" }
];
const authStore = useAuthStore();
const logout = async () => {
await authStore.logout();
}
</script>
@@ -22,7 +20,7 @@ const logout = async () => {
<div class="mb-2 border-b-2 border-emerald-500">
<Dropdown class="mb-2 user-dropdown">
<template #button>
<span>{{ authStore.username }}</span>
<span>NAME</span>
</template>
<ul>
<li>
@@ -48,13 +46,17 @@ const logout = async () => {
</aside>
</template>
<style scoped lang="postcss">
<style scoped>
@reference "tailwindcss";
.sidebar-button {
@apply flex items-center rounded-md hover:bg-slate-800 cursor-pointer;
}
.router-link-active {
@apply bg-emerald-500 hover:bg-emerald-700;
}
.user-dropdown {
@apply w-full;
:deep(>div) {
+3 -3
View File
@@ -1,6 +1,6 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";
@custom-variant search-cancel (&::-webkit-search-cancel-button);
@layer base {
span, table, input, th, tr, td, button, div, hr {
-16
View File
@@ -1,16 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [
require('tailwindcss/plugin')(({ addVariant }) => {
addVariant('search-cancel', '&::-webkit-search-cancel-button');
}),
],
}
+2 -1
View File
@@ -26,7 +26,8 @@
"src/*": [
"./src/*"
]
}
},
"allowSyntheticDefaultImports": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
+2
View File
@@ -2,11 +2,13 @@ import vue from '@vitejs/plugin-vue';
import * as path from "path";
import { defineConfig } from 'vite';
import runtimeEnv from 'vite-plugin-runtime-env';
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
runtimeEnv(),
vue(),
tailwindcss(),
],
resolve: {
alias: {