albion/server/src/routes/market.ts
Заид Омар Медхат 337dac78ba
All checks were successful
Deploy / deploy (push) Successful in 3m58s
Client/server split, Postgres, auth, Windows agent, Docker deploy
- Extract world-parse + packet capture into @albion/shared (reused by
  server and a new standalone agent workspace).
- Migrate the data layer from SQLite/better-sqlite3 to shared Postgres
  (async pg with a ?->$n helper; economy engine preloads the item/recipe
  graph so its sync recursion still works).
- API-key auth (ALBION_API_KEY) on all /api routes except health; web UI
  sends it via a fetch wrapper + key-entry gate; server serves the built
  UI with a path-traversal-safe static handler (drops @fastify/static).
- Windows companion agent: pushes world map + captured market orders to
  /api/ingest/*; runs on the gaming PC where the game traffic lives.
- Docker + docker-compose (external proxy + backend networks for Nginx
  Proxy Manager and shared Postgres) and a Forgejo deploy workflow.
- Validated end-to-end against Postgres 16 and a full compose run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 04:10:08 +05:00

97 lines
3 KiB
TypeScript

import type { FastifyInstance } from 'fastify';
import { getMeta, query, setMeta } from '../db/index.js';
import { CITIES, SERVER_HOSTS } from '../market/client.js';
import {
deleteOverride,
getAlbionServer,
getPricesForItem,
getRefreshJob,
marketSummary,
setAlbionServer,
setOverride,
startRefresh,
} from '../market/service.js';
export async function marketRoutes(app: FastifyInstance): Promise<void> {
app.get('/api/market/summary', async () => marketSummary());
app.get('/api/market/cities', async () => ({ cities: CITIES }));
app.get('/api/market/refresh/status', async () => getRefreshJob());
app.post('/api/market/refresh', async (req, reply) => {
const body = (req.body ?? {}) as {
minTier?: number;
maxTier?: number;
cities?: string[];
items?: string[];
};
const result = startRefresh(body);
if (!result.started) return reply.code(409).send({ error: result.reason });
return { started: true };
});
app.get('/api/market/prices/:item', async (req) => {
const { item } = req.params as { item: string };
return { prices: await getPricesForItem(item) };
});
app.put('/api/market/override', async (req, reply) => {
const body = req.body as {
item?: string;
city?: string;
quality?: number;
sellPrice?: number | null;
buyPrice?: number | null;
note?: string | null;
};
if (!body?.item || !body?.city) return reply.code(400).send({ error: 'item and city are required' });
await setOverride({
item: body.item,
city: body.city,
quality: body.quality,
sellPrice: body.sellPrice,
buyPrice: body.buyPrice,
note: body.note,
});
return { ok: true };
});
app.delete('/api/market/override', async (req, reply) => {
const { item, city, quality } = req.query as { item?: string; city?: string; quality?: string };
if (!item || !city) return reply.code(400).send({ error: 'item and city are required' });
await deleteOverride(item, city, quality ? Number(quality) : 1);
return { ok: true };
});
app.get('/api/settings', async () => ({
albionServer: await getAlbionServer(),
availableServers: Object.keys(SERVER_HOSTS),
proxyUrl: (await getMeta('proxy_url')) ?? '',
gamePath: (await getMeta('game_path')) ?? '',
}));
app.put('/api/settings', async (req, reply) => {
const { albionServer, proxyUrl, gamePath } = (req.body ?? {}) as {
albionServer?: string;
proxyUrl?: string;
gamePath?: string;
};
if (albionServer) {
try {
await setAlbionServer(albionServer);
} catch (e) {
return reply.code(400).send({ error: String(e) });
}
}
if (proxyUrl !== undefined) {
if (proxyUrl === '') await query("DELETE FROM meta WHERE key = 'proxy_url'");
else await setMeta('proxy_url', proxyUrl);
}
if (gamePath !== undefined) {
if (gamePath === '') await query("DELETE FROM meta WHERE key = 'game_path'");
else await setMeta('game_path', gamePath);
}
return { ok: true };
});
}