Implement SQLite-based event persistence as specified in sqlite-event-manager.md.
## Changes
### Backend
- **EventStorage** (`src/classes/event_storage.py`): New SQLite storage layer
- Cursor-based pagination with compound cursor `{month_stamp}_{rowid}`
- Avatar filtering (single and pair queries)
- Major/minor event separation
- Cleanup API with `keep_major` and `before_month_stamp` filters
- **EventManager** (`src/classes/event_manager.py`): Refactored to use SQLite
- Delegates to EventStorage for persistence
- Memory fallback mode for testing
- New `get_events_paginated()` method
- **API** (`src/server/main.py`):
- `GET /api/events` - Paginated event retrieval with filtering
- `DELETE /api/events/cleanup` - User-triggered cleanup
### Frontend
- **EventPanel.vue**: Scroll-to-load pagination, dual-person filter UI
- **world.ts**: Event state management with pagination
- **game.ts**: New API client methods
### Testing
- 81 new tests for EventStorage, EventManager, and API
- Added `pytest-asyncio` and `httpx` to requirements.txt
## Known Issues: Save/Load is Currently Broken
After loading a saved game, the following issues occur:
1. **Wrong database used**: API returns events from the startup database instead
of the loaded save's `_events.db` file
2. **Events from wrong time period**: Shows events from year 115 when loaded
save is at year 114
3. **Pagination broken after load**: `has_more` returns `False` despite hundreds
of events in the saved database
4. **Filter functionality broken**: Character selection filter stops working
after loading a game
Root cause: `load_game.py` does not properly switch the EventManager's database
connection to the loaded save's events database.
218 lines
5.5 KiB
TypeScript
218 lines
5.5 KiB
TypeScript
import { httpClient } from './http';
|
|
import type {
|
|
InitialStateDTO,
|
|
MapResponseDTO,
|
|
DetailResponseDTO,
|
|
SaveFileDTO
|
|
} from '../types/api';
|
|
|
|
export interface HoverParams {
|
|
type: string;
|
|
id: string;
|
|
}
|
|
|
|
// --- New Types ---
|
|
|
|
export interface GameDataDTO {
|
|
sects: Array<{ id: number; name: string; alignment: string }>;
|
|
personas: Array<{ id: number; name: string; desc: string; rarity: string }>;
|
|
realms: string[];
|
|
techniques: Array<{ id: number; name: string; grade: string; attribute: string; sect: string | null }>;
|
|
weapons: Array<{ id: number; name: string; grade: string; type: string }>;
|
|
auxiliaries: Array<{ id: number; name: string; grade: string }>;
|
|
alignments: Array<{ value: string; label: string }>;
|
|
}
|
|
|
|
export interface SimpleAvatarDTO {
|
|
id: string;
|
|
name: string;
|
|
sect_name: string;
|
|
realm: string;
|
|
gender: string;
|
|
age: number;
|
|
}
|
|
|
|
export interface CreateAvatarParams {
|
|
surname?: string;
|
|
given_name?: string;
|
|
gender?: string;
|
|
age?: number;
|
|
level?: number;
|
|
sect_id?: number;
|
|
persona_ids?: number[];
|
|
pic_id?: number;
|
|
technique_id?: number;
|
|
weapon_id?: number;
|
|
auxiliary_id?: number;
|
|
alignment?: string;
|
|
appearance?: number;
|
|
relations?: Array<{ target_id: string; relation: string }>;
|
|
}
|
|
|
|
export interface PhenomenonDTO {
|
|
id: number;
|
|
name: string;
|
|
desc: string;
|
|
rarity: string;
|
|
duration_years: number;
|
|
effect_desc: string;
|
|
}
|
|
|
|
export interface LLMConfigDTO {
|
|
base_url: string;
|
|
api_key: string;
|
|
model_name: string;
|
|
fast_model_name: string;
|
|
mode: string;
|
|
}
|
|
|
|
// --- Events Pagination ---
|
|
|
|
export interface EventDTO {
|
|
id: string;
|
|
text: string;
|
|
content: string;
|
|
year: number;
|
|
month: number;
|
|
month_stamp: number;
|
|
related_avatar_ids: string[];
|
|
is_major: boolean;
|
|
is_story: boolean;
|
|
}
|
|
|
|
export interface EventsResponseDTO {
|
|
events: EventDTO[];
|
|
next_cursor: string | null;
|
|
has_more: boolean;
|
|
}
|
|
|
|
export interface FetchEventsParams {
|
|
avatar_id?: string;
|
|
avatar_id_1?: string;
|
|
avatar_id_2?: string;
|
|
cursor?: string;
|
|
limit?: number;
|
|
}
|
|
|
|
export const gameApi = {
|
|
// --- World State ---
|
|
|
|
fetchInitialState() {
|
|
return httpClient.get<InitialStateDTO>('/api/state');
|
|
},
|
|
|
|
fetchMap() {
|
|
return httpClient.get<MapResponseDTO>('/api/map');
|
|
},
|
|
|
|
fetchAvatarMeta() {
|
|
return httpClient.get<{ males: number[]; females: number[] }>('/api/meta/avatars');
|
|
},
|
|
|
|
fetchPhenomenaList() {
|
|
return httpClient.get<{ phenomena: PhenomenonDTO[] }>('/api/meta/phenomena');
|
|
},
|
|
|
|
setPhenomenon(id: number) {
|
|
return httpClient.post('/api/control/set_phenomenon', { id });
|
|
},
|
|
|
|
// --- Information ---
|
|
|
|
fetchDetailInfo(params: HoverParams) {
|
|
const query = new URLSearchParams(Object.entries(params));
|
|
return httpClient.get<DetailResponseDTO>(`/api/detail?${query}`);
|
|
},
|
|
|
|
// --- Actions ---
|
|
|
|
setLongTermObjective(avatarId: string, content: string) {
|
|
return httpClient.post('/api/action/set_long_term_objective', {
|
|
avatar_id: avatarId,
|
|
content
|
|
});
|
|
},
|
|
|
|
clearLongTermObjective(avatarId: string) {
|
|
return httpClient.post('/api/action/clear_long_term_objective', {
|
|
avatar_id: avatarId
|
|
});
|
|
},
|
|
|
|
// --- Controls ---
|
|
|
|
pauseGame() {
|
|
return httpClient.post('/api/control/pause', {});
|
|
},
|
|
|
|
resumeGame() {
|
|
return httpClient.post('/api/control/resume', {});
|
|
},
|
|
|
|
// --- Saves ---
|
|
|
|
fetchSaves() {
|
|
return httpClient.get<{ saves: SaveFileDTO[] }>('/api/saves');
|
|
},
|
|
|
|
saveGame(filename?: string) {
|
|
return httpClient.post<{ status: string; filename: string }>('/api/game/save', { filename });
|
|
},
|
|
|
|
loadGame(filename: string) {
|
|
return httpClient.post<{ status: string; message: string }>('/api/game/load', { filename });
|
|
},
|
|
|
|
// --- Avatar Management ---
|
|
|
|
fetchGameData() {
|
|
return httpClient.get<GameDataDTO>('/api/meta/game_data');
|
|
},
|
|
|
|
fetchAvatarList() {
|
|
return httpClient.get<{ avatars: SimpleAvatarDTO[] }>('/api/meta/avatar_list');
|
|
},
|
|
|
|
createAvatar(params: CreateAvatarParams) {
|
|
return httpClient.post<{ status: string; message: string; avatar_id: string }>('/api/action/create_avatar', params);
|
|
},
|
|
|
|
deleteAvatar(avatarId: string) {
|
|
return httpClient.post<{ status: string; message: string }>('/api/action/delete_avatar', { avatar_id: avatarId });
|
|
},
|
|
|
|
// --- LLM Config ---
|
|
|
|
fetchLLMConfig() {
|
|
return httpClient.get<LLMConfigDTO>('/api/config/llm');
|
|
},
|
|
|
|
testLLMConnection(config: LLMConfigDTO) {
|
|
return httpClient.post<{ status: string; message: string }>('/api/config/llm/test', config);
|
|
},
|
|
|
|
saveLLMConfig(config: LLMConfigDTO) {
|
|
return httpClient.post<{ status: string; message: string }>('/api/config/llm/save', config);
|
|
},
|
|
|
|
// --- Events Pagination ---
|
|
|
|
fetchEvents(params: FetchEventsParams = {}) {
|
|
const query = new URLSearchParams();
|
|
if (params.avatar_id) query.set('avatar_id', params.avatar_id);
|
|
if (params.avatar_id_1) query.set('avatar_id_1', params.avatar_id_1);
|
|
if (params.avatar_id_2) query.set('avatar_id_2', params.avatar_id_2);
|
|
if (params.cursor) query.set('cursor', params.cursor);
|
|
if (params.limit) query.set('limit', String(params.limit));
|
|
const qs = query.toString();
|
|
return httpClient.get<EventsResponseDTO>(`/api/events${qs ? '?' + qs : ''}`);
|
|
},
|
|
|
|
cleanupEvents(keepMajor = true, beforeMonthStamp?: number) {
|
|
const query = new URLSearchParams();
|
|
query.set('keep_major', String(keepMajor));
|
|
if (beforeMonthStamp !== undefined) query.set('before_month_stamp', String(beforeMonthStamp));
|
|
return httpClient.delete<{ deleted: number }>(`/api/events/cleanup?${query}`);
|
|
}
|
|
};
|