Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changelog/NEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@

## Fixed

- AI discovery now rejects unsafe batch settings, limits background runs to one per family database, and lets an active run be cancelled without leaving provider work behind.
- **[issue-158] Unknown API routes now return JSON errors** — Requests to unrecognized `/api` paths receive a stable 404 error envelope instead of the browser app's HTML, while client-side navigation continues to use the SPA fallback.
- Search results now keep their alphabetical ordering. The batch person-loader (`getPersonsBatch`) re-orders rows back to the requested order, fixing a regression where SQLite's `WHERE person_id IN (...)` returned rows in table order and silently discarded the search query's `ORDER BY display_name` (so the default, unsorted search view appeared randomly ordered).
- Platform comparison now treats equivalent place spellings as matches: "Dallas, Texas, USA" vs "Dallas, Texas, United States" (and U.S.A. / United States of America / state abbreviations like TX vs Texas, UK vs United Kingdom, etc.) — no longer flagged as `different`. Place containment is now suffix-based, so "Texas" no longer falsely matches "Texarkana"
Expand Down
7 changes: 6 additions & 1 deletion client/src/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,11 @@ export const api = {
body: JSON.stringify(options || {})
}),

cancelDiscovery: (dbId: string) =>
fetchJson<{ runId: string; message: string }>(`/ai-discovery/${dbId}/cancel`, {
method: 'POST',
}),

getDiscoveryProgress: (runId: string) =>
fetchJson<DiscoveryProgress>(`/ai-discovery/progress/${runId}`),

Expand Down Expand Up @@ -1066,7 +1071,7 @@ export interface DiscoveryResult {
}

export interface DiscoveryProgress {
status: 'pending' | 'running' | 'completed' | 'failed';
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
totalPersons: number;
analyzedPersons: number;
candidatesFound: number;
Expand Down
11 changes: 11 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ Backend runs on port 6374 by default. Application JSON endpoints use the envelop
| GET | `/api/favorites/db/:dbId/tags` | Get tags for database |
| GET | `/api/favorites/db/:dbId/sparse-tree` | Get sparse tree data |

## AI Discovery

| Method | Path | Description |
|--------|------|-------------|
| POST | `/api/ai-discovery/:dbId/quick` | Analyze a small ancestor sample synchronously |
| POST | `/api/ai-discovery/:dbId/start` | Start one bounded background discovery run for this database |
| POST | `/api/ai-discovery/:dbId/cancel` | Cancel the active background discovery run |
| GET | `/api/ai-discovery/progress/:runId` | Get background discovery progress |

`POST /api/ai-discovery/:dbId/start` accepts optional `{ batchSize, maxPersons }` values. Both must be positive integers; `batchSize` is capped at 100 and `maxPersons` at 1000. Defaults are 50 and 500 respectively. A second active run for the same database returns `409` with the active run ID.

## Augmentation

| Method | Path | Description |
Expand Down
53 changes: 42 additions & 11 deletions server/src/routes/ai-discovery.routes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { Router, Request, Response } from 'express';
import { aiDiscoveryService } from '../services/ai-discovery.service.js';
import {
aiDiscoveryService,
DiscoveryInputError,
DiscoveryRunConflictError,
normalizeFullDiscoveryOptions,
} from '../services/ai-discovery.service.js';
import { favoritesService } from '../services/favorites.service.js';
import { logger } from '../lib/logger.js';
import { asyncHandler } from '../utils/asyncHandler.js';
Expand Down Expand Up @@ -52,21 +57,47 @@ router.post('/:dbId/quick', asyncHandler(async (req: Request, res: Response) =>
*/
router.post('/:dbId/start', asyncHandler(async (req: Request, res: Response) => {
const { dbId } = req.params;
const { batchSize, maxPersons } = req.body;

const result = await aiDiscoveryService.startDiscovery(dbId, {
batchSize,
maxPersons,
}).catch(err => {
res.status(500).json({ success: false, error: err.message });
return null;
});
let options;
try {
options = normalizeFullDiscoveryOptions(req.body ?? {});
} catch (err) {
const message = err instanceof DiscoveryInputError ? err.message : 'Invalid discovery options';
logger.warn('ai-discovery', `Rejected full discovery request dbId=${dbId}: ${message}`);
res.status(400).json({ success: false, error: message });
return;
}

if (result !== null) {
try {
const result = await aiDiscoveryService.startDiscovery(dbId, options);
res.json({ success: true, data: result });
} catch (err) {
if (err instanceof DiscoveryRunConflictError) {
logger.warn('ai-discovery', `Rejected concurrent full discovery dbId=${dbId}, activeRunId=${err.runId}`);
res.status(409).json({ success: false, error: err.message, data: { runId: err.runId } });
return;
}
const message = err instanceof Error ? err.message : 'Unable to start discovery';
logger.error('ai-discovery', `Failed to start full discovery dbId=${dbId}: ${message}`);
res.status(500).json({ success: false, error: message });
}
}));

/**
* Cancel the active full AI discovery run for a database.
* POST /api/ai-discovery/:dbId/cancel
*/
router.post('/:dbId/cancel', (req: Request, res: Response) => {
const { dbId } = req.params;
const cancelled = aiDiscoveryService.cancelDiscovery(dbId);

if (!cancelled) {
res.status(404).json({ success: false, error: 'No active discovery run for this database' });
return;
}

res.json({ success: true, data: { ...cancelled, message: 'Cancellation requested' } });
});

/**
* Get progress of a discovery run
* GET /api/ai-discovery/progress/:runId
Expand Down
Loading
Loading