All checks were successful
Deploy / deploy (push) Successful in 3m58s
- 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>
104 lines
2.9 KiB
TypeScript
104 lines
2.9 KiB
TypeScript
import { findTshark, listInterfaces, startCapture, type CaptureHandle } from '@albion/shared';
|
|
import { storeOrders, type CapturedOrder } from './store.js';
|
|
|
|
/**
|
|
* LOCAL sniffer: used only when the server runs on the same PC as the game.
|
|
* When the server is deployed, the Windows agent captures instead and pushes
|
|
* results to /api/ingest/*. Capture + Photon decoding live in @albion/shared.
|
|
*/
|
|
|
|
export interface SnifferStatus {
|
|
tsharkPath: string | null;
|
|
running: boolean;
|
|
packets: number;
|
|
photonEvents: number;
|
|
marketOrdersSeen: number;
|
|
pricesStored: number;
|
|
startedAt: string | null;
|
|
lastError: string | null;
|
|
eventCodeCounts: Record<string, number>;
|
|
recentOrders: CapturedOrder[];
|
|
}
|
|
|
|
let handle: CaptureHandle | null = null;
|
|
const state = {
|
|
packets: 0,
|
|
photonEvents: 0,
|
|
marketOrdersSeen: 0,
|
|
pricesStored: 0,
|
|
startedAt: null as string | null,
|
|
lastError: null as string | null,
|
|
eventCodeCounts: new Map<number, number>(),
|
|
recentOrders: [] as CapturedOrder[],
|
|
};
|
|
|
|
export { listInterfaces };
|
|
|
|
export function startSniffer(interfaceIds?: string[]): { started: boolean; error?: string } {
|
|
if (handle) return { started: false, error: 'sniffer already running' };
|
|
|
|
Object.assign(state, {
|
|
packets: 0,
|
|
photonEvents: 0,
|
|
marketOrdersSeen: 0,
|
|
pricesStored: 0,
|
|
startedAt: new Date().toISOString(),
|
|
lastError: null,
|
|
recentOrders: [],
|
|
});
|
|
state.eventCodeCounts.clear();
|
|
|
|
const result = startCapture(
|
|
{
|
|
onPacket: () => {
|
|
state.packets++;
|
|
},
|
|
onEventCode: (code) => {
|
|
state.photonEvents++;
|
|
state.eventCodeCounts.set(code, (state.eventCodeCounts.get(code) ?? 0) + 1);
|
|
},
|
|
onOrders: (orders) => {
|
|
state.marketOrdersSeen += orders.length;
|
|
void storeOrders(orders)
|
|
.then(({ pricesStored, recent }) => {
|
|
state.pricesStored += pricesStored;
|
|
state.recentOrders = [...recent, ...state.recentOrders].slice(0, 50);
|
|
})
|
|
.catch((e) => {
|
|
state.lastError = `storing sniffed orders failed: ${e instanceof Error ? e.message : e}`;
|
|
});
|
|
},
|
|
onError: (message) => {
|
|
state.lastError = message;
|
|
},
|
|
},
|
|
interfaceIds,
|
|
);
|
|
|
|
if ('error' in result) {
|
|
state.startedAt = null;
|
|
return { started: false, error: result.error };
|
|
}
|
|
handle = result;
|
|
return { started: true };
|
|
}
|
|
|
|
export function stopSniffer(): void {
|
|
handle?.stop();
|
|
handle = null;
|
|
}
|
|
|
|
export function snifferStatus(): SnifferStatus {
|
|
return {
|
|
tsharkPath: findTshark(),
|
|
running: handle != null,
|
|
packets: state.packets,
|
|
photonEvents: state.photonEvents,
|
|
marketOrdersSeen: state.marketOrdersSeen,
|
|
pricesStored: state.pricesStored,
|
|
startedAt: state.startedAt,
|
|
lastError: state.lastError,
|
|
eventCodeCounts: Object.fromEntries([...state.eventCodeCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 40)),
|
|
recentOrders: state.recentOrders,
|
|
};
|
|
}
|