mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-28 05:46:58 +02:00
* feat(analytics): add daily client usage reporting (MUL-5125) Co-authored-by: multica-agent <github@multica.ai> * fix(analytics): clarify daily usage semantics (MUL-5125) Co-authored-by: multica-agent <github@multica.ai> * fix(analytics): resolve usage review blockers (MUL-5125) Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai>
156 lines
4.9 KiB
TypeScript
156 lines
4.9 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useMemo } from "react";
|
|
import { ApiClient } from "../api/client";
|
|
import { installFreezeWatchdog } from "../diagnostics/freeze-watchdog";
|
|
import { setApiInstance, setSchemaLogger } from "../api";
|
|
import { createAuthStore, registerAuthStore } from "../auth";
|
|
import { createChatStore, registerChatStore } from "../chat";
|
|
import {
|
|
I18nProvider,
|
|
LocaleAdapterProvider,
|
|
UserLocaleSync,
|
|
} from "../i18n/react";
|
|
import { WSProvider } from "../realtime";
|
|
import { QueryProvider } from "../provider";
|
|
import { createLogger } from "../logger";
|
|
import { defaultStorage } from "./storage";
|
|
import { AuthInitializer } from "./auth-initializer";
|
|
import type { CoreProviderProps, ClientIdentity } from "./types";
|
|
import type { StorageAdapter } from "../types/storage";
|
|
import { ClientUsageReporter } from "../client-usage";
|
|
import {
|
|
configureShortcutPlatform,
|
|
configureShortcutRuntime,
|
|
} from "../shortcuts/platform";
|
|
|
|
// Module-level singletons — created once at first render, never recreated.
|
|
// Vite HMR preserves module-level state, so these survive hot reloads.
|
|
let initialized = false;
|
|
let authStore: ReturnType<typeof createAuthStore>;
|
|
let chatStore: ReturnType<typeof createChatStore>;
|
|
function initCore(
|
|
apiBaseUrl: string,
|
|
storage: StorageAdapter,
|
|
onLogin?: () => void,
|
|
onLogout?: () => void,
|
|
cookieAuth?: boolean,
|
|
identity?: ClientIdentity,
|
|
) {
|
|
if (initialized) return;
|
|
|
|
configureShortcutPlatform(
|
|
identity?.os === "macos" ||
|
|
identity?.os === "windows" ||
|
|
identity?.os === "linux" ||
|
|
identity?.os === "unknown"
|
|
? identity.os
|
|
: null,
|
|
);
|
|
// Authoritative override; before this runs (module-eval store hydration)
|
|
// detectShortcutRuntime() reads the preload globals and already agrees.
|
|
configureShortcutRuntime(
|
|
identity?.platform === "desktop" ? "desktop" : null,
|
|
);
|
|
|
|
const api = new ApiClient(apiBaseUrl, {
|
|
logger: createLogger("api"),
|
|
onUnauthorized: () => {
|
|
storage.removeItem("multica_token");
|
|
},
|
|
identity,
|
|
});
|
|
setApiInstance(api);
|
|
setSchemaLogger(createLogger("api-schema"));
|
|
|
|
// In token mode, hydrate token from storage.
|
|
if (!cookieAuth) {
|
|
const token = storage.getItem("multica_token");
|
|
if (token) api.setToken(token);
|
|
}
|
|
// Workspace identity is URL-driven: the [workspaceSlug] layout resolves
|
|
// the slug and calls setCurrentWorkspace(slug, wsId) on mount. The api
|
|
// client reads the slug from that singleton for the X-Workspace-Slug
|
|
// header. No boot-time hydration from storage is required.
|
|
|
|
authStore = createAuthStore({ api, storage, onLogin, onLogout, cookieAuth });
|
|
registerAuthStore(authStore);
|
|
|
|
chatStore = createChatStore({ storage });
|
|
registerChatStore(chatStore);
|
|
|
|
initialized = true;
|
|
}
|
|
|
|
export function CoreProvider({
|
|
children,
|
|
apiBaseUrl = "",
|
|
wsUrl = "ws://localhost:8080/ws",
|
|
storage = defaultStorage,
|
|
cookieAuth,
|
|
onLogin,
|
|
onLogout,
|
|
identity,
|
|
locale,
|
|
resources,
|
|
localeAdapter,
|
|
}: CoreProviderProps) {
|
|
// Initialize singletons on first render only. Dependencies are read-once:
|
|
// apiBaseUrl, storage, and callbacks are set at app boot and never change at runtime.
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
useMemo(() => initCore(apiBaseUrl, storage, onLogin, onLogout, cookieAuth, identity), []);
|
|
|
|
// Client-only freeze watchdog — shared by web and desktop. No-op on the
|
|
// server and idempotent, so mounting it here covers both apps in one place.
|
|
useEffect(() => {
|
|
installFreezeWatchdog();
|
|
}, []);
|
|
|
|
// I18nProvider wraps everything else: server and client must use the same
|
|
// (locale, resources) to avoid hydration mismatch. Language switching goes
|
|
// through window.location.reload(), never client-side changeLanguage.
|
|
const tree = (
|
|
<QueryProvider>
|
|
<AuthInitializer
|
|
onLogin={onLogin}
|
|
onLogout={onLogout}
|
|
storage={storage}
|
|
cookieAuth={cookieAuth}
|
|
identity={identity}
|
|
>
|
|
{/* Desktop's reporter owns both activity and runtime state so it must
|
|
be the only writer for that installation. */}
|
|
{identity?.platform !== "desktop" && (
|
|
<ClientUsageReporter storage={storage} identity={identity} />
|
|
)}
|
|
<WSProvider
|
|
wsUrl={wsUrl}
|
|
authStore={authStore}
|
|
storage={storage}
|
|
cookieAuth={cookieAuth}
|
|
identity={identity}
|
|
>
|
|
{children}
|
|
</WSProvider>
|
|
</AuthInitializer>
|
|
</QueryProvider>
|
|
);
|
|
|
|
// UserLocaleSync requires a LocaleAdapter to persist; only mount it when
|
|
// the host app provides one (web layout + desktop App both do).
|
|
const withAdapter = localeAdapter ? (
|
|
<LocaleAdapterProvider adapter={localeAdapter}>
|
|
<UserLocaleSync />
|
|
{tree}
|
|
</LocaleAdapterProvider>
|
|
) : (
|
|
tree
|
|
);
|
|
|
|
return (
|
|
<I18nProvider locale={locale} resources={resources}>
|
|
{withAdapter}
|
|
</I18nProvider>
|
|
);
|
|
}
|