feat: v0.1.0 - geolocation capture, calendar, search, Starlight docs site

- Automatic browser geolocation capture on event creation
- Reverse geocoding via Nominatim API for place names
- Full-text search with SQLite FTS5
- Calendar view for browsing past entries
- DateNavigator component for day navigation
- SearchModal with Ctrl+K shortcut
- QuickAddWidget with Ctrl+J shortcut
- Starlight documentation site with GitHub Pages deployment
- Multiple AI provider support (Groq, OpenAI, Anthropic, Ollama, LM Studio)
- Multi-user registration support

BREAKING: Events now include latitude/longitude/placeName fields
This commit is contained in:
lotherk
2026-03-27 02:27:55 +00:00
parent deaf496a7d
commit 0bdd71a4ed
67 changed files with 15201 additions and 355 deletions

View File

@@ -46,10 +46,6 @@ class ApiClient {
return response.json() as Promise<ApiResponse<T>>;
}
async register(email: string, password: string) {
return this.request<{ user: { id: string; email: string } }>('POST', '/auth/register', { email, password });
}
async login(email: string, password: string) {
return this.request<{ token: string; userId: string }>('POST', '/auth/login', { email, password });
}
@@ -68,7 +64,7 @@ class ApiClient {
}
async getDays() {
return this.request<Array<{ date: string; eventCount: number; hasJournal: boolean }>>('GET', '/days');
return this.request<Array<{ date: string; eventCount: number; hasJournal: boolean; journalTitle?: string; journalExcerpt?: string }>>('GET', '/days');
}
async getDay(date: string) {
@@ -79,8 +75,8 @@ class ApiClient {
return this.request<{ deleted: boolean }>('DELETE', `/days/${date}`);
}
async createEvent(date: string, type: string, content: string, metadata?: object) {
return this.request<Event>('POST', '/events', { date, type, content, metadata });
async createEvent(date: string, type: string, content: string, metadata?: object, location?: { latitude: number; longitude: number; placeName?: string }) {
return this.request<Event>('POST', '/events', { date, type, content, metadata, ...location });
}
async updateEvent(id: string, content: string, metadata?: object) {
@@ -129,14 +125,18 @@ class ApiClient {
return response.json() as Promise<ApiResponse<{ mediaPath: string }>>;
}
async generateJournal(date: string) {
return this.request<{ journal: Journal; task: Task }>('POST', `/journal/generate/${date}`);
async generateJournal(date: string, instructions?: string) {
return this.request<{ journal: Journal; task: Task }>('POST', `/journal/generate/${date}`, { instructions: instructions || '' });
}
async getJournal(date: string) {
return this.request<Journal>('GET', `/journal/${date}`);
}
async getJournals(page: number = 1, limit: number = 10) {
return this.request<{ journals: Journal[]; total: number; page: number; limit: number; totalPages: number }>('GET', `/journals?page=${page}&limit=${limit}`);
}
async getJournalTasks(date: string) {
return this.request<Task[]>('GET', `/journal/${date}/tasks`);
}
@@ -149,9 +149,37 @@ class ApiClient {
return this.request<Settings>('GET', '/settings');
}
async search(query: string) {
return this.request<{ journals: Array<{ date: string; title: string; excerpt: string }>; events: Array<{ date: string; type: string; content: string }> }>('GET', `/search?q=${encodeURIComponent(query)}`);
}
async updateSettings(settings: Partial<Settings>) {
return this.request<Settings>('PUT', '/settings', settings);
}
async exportData() {
return this.request<ExportData>('GET', '/export');
}
async importData(data: ExportData) {
return this.request<ImportResult>('POST', '/import', data);
}
async deleteAccount() {
return this.request<{ deleted: boolean }>('DELETE', '/account');
}
async resetAccount() {
return this.request<{ reset: boolean }>('POST', '/account/reset');
}
async changePassword(currentPassword: string, newPassword: string) {
return this.request<{ changed: boolean }>('POST', '/account/password', { currentPassword, newPassword });
}
async register(email: string, password: string) {
return this.request<{ apiKey: string; userId: string }>('POST', '/auth/register', { email, password });
}
}
export interface Event {
@@ -161,12 +189,16 @@ export interface Event {
content: string;
mediaPath?: string;
metadata?: string;
latitude?: number;
longitude?: number;
placeName?: string;
createdAt: string;
}
export interface Journal {
id: string;
date: string;
title?: string;
content: string;
eventCount: number;
generatedAt: string;
@@ -183,6 +215,7 @@ export interface Task {
request?: string;
response?: string;
error?: string;
title?: string | null;
createdAt: string;
completedAt?: string;
}
@@ -203,4 +236,69 @@ export interface Settings {
}>;
}
export interface ExportData {
version: string;
exportedAt: string;
settings: {
aiProvider: string;
aiApiKey?: string;
aiModel?: string;
aiBaseUrl?: string;
journalPrompt?: string;
language?: string;
timezone?: string;
providerSettings?: string;
journalContextDays?: number;
};
events: Array<{
id: string;
date: string;
type: string;
content: string;
mediaPath?: string;
metadata?: string;
latitude?: number;
longitude?: number;
placeName?: string;
createdAt: string;
}>;
journals: Array<{
id: string;
date: string;
title?: string;
content: string;
eventCount: number;
generatedAt: string;
}>;
tasks: Array<{
id: string;
journalId: string;
date: string;
type: string;
status: string;
provider: string;
model?: string;
prompt?: string;
request?: string;
response?: string;
error?: string;
title?: string;
createdAt: string;
completedAt?: string;
}>;
}
export interface ImportResult {
compatible: boolean;
importedEvents: number;
importedJournals: number;
importedTasks: number;
skippedEvents: number;
skippedJournals: number;
totalEvents: number;
totalJournals: number;
totalTasks: number;
warning?: string;
}
export const api = new ApiClient();