Skip to content
Draft
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
41 changes: 28 additions & 13 deletions plans/simplification_execution_waves.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ Turn the audit into a deletion-first simplification program that:
- old `src/utils/cloudSync.ts` entrypoint was removed
- remaining sync compatibility read paths were deleted

- Wave 6 is complete:
- `src/utils/indexedDBOperations.ts` is now the winning shared IndexedDB CRUD entrypoint
- the Finder-owned generic `dbOperations` wrapper was removed
- Finder content reads, `useFilesStore`, and `useDisplaySettingsStore` now use the shared helper
- remaining `ensureIndexedDBInitialized()` call sites are specialized backup, migration, or sync flows rather than routine CRUD

- Wave 5 is partially complete:
- `useThemeFlags()` exists
- repeated theme booleans were replaced in key touched UI/app files
Expand All @@ -43,29 +49,34 @@ Turn the audit into a deletion-first simplification program that:
- room and song API paths were unified through `src/api/*`
- some internal API calls still bypass the shared client layer and use direct `abortableFetch`

- Wave 7 is partially complete:
- auth-related client calls now go through `src/api/auth.ts`
- remaining room typing, room message delete, and user search client calls now go through `src/api/*`
- other internal APIs still need wrappers or migrations before the wave is complete

- Wave 3 is only partially complete:
- backup/restore serialization was unified
- `indexedDBOperations.ts` was not fully narrowed to a single winning API shape
- multiple direct IndexedDB transaction paths still exist
- `indexedDBOperations.ts` is now narrowed to the shared winning CRUD shape
- backup/export/restore and sync still keep specialized bulk or cursor-based IndexedDB logic

- Wave 4 is only partially complete at the domain-file granularity:
- `src/sync/domains.ts` and `src/sync/types.ts` now exist
- the sync monolith was moved out of `src/utils/cloudSync.ts`
- domain logic is still grouped inside one large `src/sync/domains.ts` file rather than split into per-domain modules

### Known unrelated noise still visible during sync verification
### Recently resolved cleanup noise

- `useFilesStore` rehydrate still logs a null-spread failure in `withRequiredRootDirectories()`
- this does not currently fail the focused sync suite, but it should be cleaned up
- `useFilesStore` rehydrate no longer logs the null-spread failure in `withRequiredRootDirectories()`
- malformed or partial `filesystem.json` payloads are normalized before root-directory reconciliation

## Recommended next agent order

1. Finish IndexedDB unification
2. Finish internal API client unification
3. Split `src/sync/domains.ts` into per-domain modules
4. Unify style tokens between `src/index.css` and `src/styles/themes.css`
5. Reduce legacy Windows CSS runtime surface
6. Fix `useFilesStore` rehydrate noise
1. Finish internal API client unification
2. Split `src/sync/domains.ts` into per-domain modules
3. Unify style tokens between `src/index.css` and `src/styles/themes.css`
4. Reduce legacy Windows CSS runtime surface
5. Return to backup/restore dedupe in control panels if more IndexedDB cleanup is needed
6. Audit remaining sync test-only language/display noise if it still matters

## Wave order

Expand Down Expand Up @@ -272,15 +283,19 @@ Replace repeated theme booleans and reduce styling drift before it grows further

### Wave 6 - finish IndexedDB winner selection

**Status**

Complete.

**Intent**

Collapse the remaining competing IndexedDB entrypoints into one obvious API.

**Scope**

- decide whether `src/utils/indexedDBOperations.ts` or Finder-owned `dbOperations` wins
- migrate `useDisplaySettingsStore`, Finder, and any remaining direct transaction code to the winner
- reduce `ensureIndexedDBInitialized()` call sites outside the chosen shared module
- migrate `useDisplaySettingsStore`, Finder, and the routine CRUD call sites to the winner
- reduce `ensureIndexedDBInitialized()` call sites outside the chosen shared module for non-specialized flows
- remove dead or duplicate helpers after migration

### Wave 7 - finish internal API client unification
Expand Down
80 changes: 80 additions & 0 deletions src/api/ai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { abortableFetch } from "@/utils/abortableFetch";
import { getApiUrl } from "@/utils/platform";
import { apiRequest } from "@/api/core";

export async function extractConversationMemories(payload: {
timeZone: string;
messages: Array<{
role: string;
parts?: unknown;
metadata?: { createdAt?: string | number };
}>;
userTimeZone?: string;
}): Promise<{
extracted: number;
dailyNotes?: number;
analyzed?: number;
message?: string;
}> {
return apiRequest<
{
extracted: number;
dailyNotes?: number;
analyzed?: number;
message?: string;
},
typeof payload
>({
path: "/api/ai/extract-memories",
method: "POST",
body: payload,
headers: payload.userTimeZone
? { "X-User-Timezone": payload.userTimeZone }
: undefined,
timeout: 15000,
retry: { maxAttempts: 1, initialDelayMs: 250 },
});
}

export async function requestRyoReply(payload: {
roomId: string;
prompt: string;
systemState?: {
chatRoomContext?: {
recentMessages?: string;
mentionedMessage?: string;
roomId?: string | null;
};
};
}): Promise<{ message: unknown }> {
return apiRequest<{ message: unknown }, typeof payload>({
path: "/api/ai/ryo-reply",
method: "POST",
body: payload,
timeout: 20000,
retry: { maxAttempts: 1, initialDelayMs: 250 },
});
}

export async function fetchProactiveGreeting(options?: {
signal?: AbortSignal;
}): Promise<{ greeting?: string }> {
const response = await abortableFetch(getApiUrl("/api/chat"), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
messages: [],
proactiveGreeting: true,
}),
timeout: 20000,
signal: options?.signal,
throwOnHttpError: false,
retry: { maxAttempts: 1, initialDelayMs: 250 },
});

if (!response.ok) {
throw new Error(`Failed to fetch proactive greeting (${response.status})`);
}

return (await response.json()) as { greeting?: string };
}
56 changes: 56 additions & 0 deletions src/api/airdrop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { apiRequest } from "@/api/core";

export async function sendAirDropHeartbeat(): Promise<{ success: boolean }> {
return apiRequest<{ success: boolean }>({
path: "/api/airdrop/heartbeat",
method: "POST",
});
}

export async function discoverAirDropUsers(): Promise<{ users: string[] }> {
return apiRequest<{ users: string[] }>({
path: "/api/airdrop/discover",
method: "GET",
});
}

export async function sendAirDropFile(payload: {
recipient: string;
fileName: string;
fileType?: string;
content: string;
}): Promise<{ success: boolean; transferId: string }> {
return apiRequest<{ success: boolean; transferId: string }, typeof payload>({
path: "/api/airdrop/send",
method: "POST",
body: payload,
});
}

export async function respondToAirDropTransfer(payload: {
transferId: string;
accept: boolean;
}): Promise<{
success: boolean;
declined?: boolean;
fileName?: string;
fileType?: string;
content?: string;
sender?: string;
}> {
return apiRequest<
{
success: boolean;
declined?: boolean;
fileName?: string;
fileType?: string;
content?: string;
sender?: string;
},
typeof payload
>({
path: "/api/airdrop/respond",
method: "POST",
body: payload,
});
}
67 changes: 67 additions & 0 deletions src/api/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,27 @@ export interface RegisterResponse {
};
}

export interface SessionResponse {
authenticated: boolean;
username?: string;
expired?: boolean;
}

export interface PasswordStatusResponse {
hasPassword: boolean;
username: string;
}

export interface PasswordSetResponse {
success: boolean;
}

export interface LogoutAllDevicesResponse {
success: boolean;
message: string;
deletedCount: number;
}

export async function loginWithPassword(params: {
username: string;
password: string;
Expand Down Expand Up @@ -66,3 +87,49 @@ export async function logoutUser(): Promise<{ success: boolean }> {
method: "POST",
});
}

export async function getSession(params?: {
legacyToken?: string | null;
username?: string;
}): Promise<SessionResponse> {
const headers = new Headers();

if (params?.legacyToken) {
headers.set("Authorization", `Bearer ${params.legacyToken}`);
}
if (params?.username) {
headers.set("X-Username", params.username);
}

return apiRequest<SessionResponse>({
path: "/api/auth/session",
method: "GET",
headers,
retry: { maxAttempts: 2, initialDelayMs: 500 },
timeout: 10000,
});
}

export async function checkPassword(): Promise<PasswordStatusResponse> {
return apiRequest<PasswordStatusResponse>({
path: "/api/auth/password/check",
method: "GET",
});
}

export async function setPassword(params: {
password: string;
}): Promise<PasswordSetResponse> {
return apiRequest<PasswordSetResponse, { password: string }>({
path: "/api/auth/password/set",
method: "POST",
body: { password: params.password },
});
}

export async function logoutAllDevices(): Promise<LogoutAllDevicesResponse> {
return apiRequest<LogoutAllDevicesResponse>({
path: "/api/auth/logout-all",
method: "POST",
});
}
64 changes: 64 additions & 0 deletions src/api/iframeCheck.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { abortableFetch } from "@/utils/abortableFetch";

export type IframeCheckMode = "check" | "proxy" | "ai" | "list-cache";

export function buildIframeCheckPath(params: {
url: string;
mode?: IframeCheckMode;
year?: string;
month?: string;
theme?: string;
}): string {
const query = new URLSearchParams({
url: params.url,
});

if (params.mode) query.set("mode", params.mode);
if (params.year) query.set("year", params.year);
if (params.month) query.set("month", params.month);
if (params.theme) query.set("theme", params.theme);

return `/api/iframe-check?${query.toString()}`;
}

export async function listIframeCachedYears(
url: string
): Promise<{ years: string[] }> {
const response = await abortableFetch(
buildIframeCheckPath({ url, mode: "list-cache" }),
{
timeout: 15000,
retry: { maxAttempts: 2, initialDelayMs: 500 },
}
);

if (!response.ok) {
throw new Error(`Failed to fetch cached years (${response.status})`);
}

return (await response.json()) as { years: string[] };
}

export async function fetchIframeAiSnapshot(
params: {
url: string;
year: string;
theme?: string;
},
options?: { signal?: AbortSignal }
): Promise<Response> {
return abortableFetch(
buildIframeCheckPath({
url: params.url,
mode: "ai",
year: params.year,
theme: params.theme,
}),
{
signal: options?.signal,
timeout: 15000,
throwOnHttpError: false,
retry: { maxAttempts: 1, initialDelayMs: 250 },
}
);
}
19 changes: 19 additions & 0 deletions src/api/media.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { abortableFetch } from "@/utils/abortableFetch";
import { getApiUrl } from "@/utils/platform";

export async function transcribeAudio(formData: FormData): Promise<{ text: string }> {
const response = await abortableFetch(getApiUrl("/api/audio-transcribe"), {
method: "POST",
body: formData,
timeout: 30000,
throwOnHttpError: false,
retry: { maxAttempts: 1, initialDelayMs: 250 },
});

if (!response.ok) {
const errorData = (await response.json()) as { error?: string };
throw new Error(errorData.error || "Transcription failed");
}

return (await response.json()) as { text: string };
}
Loading
Loading