Converted 10 files: - stores.ts [C:3] — plugins, tasks, selection stores + fetchPlugins/fetchTasks - stores/health.ts [C:3] — health summary with polling dedup + derived failingCount - stores/activity.ts [C:2] — derived active task count for navbar badge - stores/sidebar.ts [C:3] — sidebar expansion, navigation, mobile overlay + localStorage persist - stores/taskDrawer.ts [C:3] — drawer open/close, resource-to-task mapping, auto-open pref - stores/environmentContext.ts [C:3] — environment selector, localStorage persist, derived env - stores/assistantChat.ts [C:3] — panel toggle, conversation binding, seed messages, focus target - stores/datasetReviewSession.ts [C:4] — session CRUD, dirty flag, UiPhase derivation - stores/translationRun.ts [C:4] — WebSocket + polling, uxState FSM, derived active/finished - stores/maintenance.svelte.ts [C:4] — Svelte 5 runes (), WS reconnect, CRUD Build: npm run build passes
158 lines
4.5 KiB
TypeScript
158 lines
4.5 KiB
TypeScript
// #region EnvironmentContextStore [C:3] [TYPE Store] [SEMANTICS environment, context, store, selector, filter]
|
|
// @BRIEF Global selected environment context for navigation, safety cues, and environment-based filtering.
|
|
// @LAYER UI
|
|
// @RELATION DEPENDS_ON -> [ApiModule]
|
|
// @RELATION BINDS_TO -> [SidebarStore]
|
|
// @UX_STATE Loading -> Environments being fetched
|
|
// @UX_STATE Ready -> Environment selector active with environment list
|
|
// @UX_STATE Error -> Failed to load environments
|
|
|
|
import { derived, get, writable, type Writable } from 'svelte/store';
|
|
import { browser } from '$app/environment';
|
|
import { api } from '$lib/api';
|
|
|
|
interface Environment {
|
|
id: string;
|
|
name: string;
|
|
stage?: string;
|
|
is_production?: boolean;
|
|
[k: string]: unknown;
|
|
}
|
|
|
|
interface EnvironmentContextState {
|
|
environments: Environment[];
|
|
selectedEnvId: string;
|
|
isLoading: boolean;
|
|
isLoaded: boolean;
|
|
error: string | null;
|
|
}
|
|
|
|
const INITIAL_STATE: EnvironmentContextState = {
|
|
environments: [],
|
|
selectedEnvId: '',
|
|
isLoading: false,
|
|
isLoaded: false,
|
|
error: null,
|
|
};
|
|
|
|
const SELECTED_ENV_KEY = 'selected_env_id';
|
|
const contextStore: Writable<EnvironmentContextState> = writable(INITIAL_STATE);
|
|
|
|
function getStoredSelectedEnvId(): string {
|
|
if (!browser) return '';
|
|
return localStorage.getItem(SELECTED_ENV_KEY) || '';
|
|
}
|
|
|
|
function persistSelectedEnvId(envId: string): void {
|
|
if (!browser) return;
|
|
if (!envId) {
|
|
localStorage.removeItem(SELECTED_ENV_KEY);
|
|
return;
|
|
}
|
|
localStorage.setItem(SELECTED_ENV_KEY, envId);
|
|
}
|
|
|
|
function hasAuthToken(): boolean {
|
|
if (!browser) return false;
|
|
return Boolean(localStorage.getItem('auth_token'));
|
|
}
|
|
|
|
function resolveSelectedEnvId(environments: Environment[], preferredEnvId: string): string {
|
|
if (!Array.isArray(environments) || environments.length === 0) return '';
|
|
if (preferredEnvId && environments.some((env) => env.id === preferredEnvId)) {
|
|
return preferredEnvId;
|
|
}
|
|
const stored = getStoredSelectedEnvId();
|
|
if (stored && environments.some((env) => env.id === stored)) {
|
|
return stored;
|
|
}
|
|
return environments[0].id;
|
|
}
|
|
|
|
function applySelectedEnvId(selectedEnvId: string): void {
|
|
contextStore.update((state) => {
|
|
const exists = state.environments.some((env) => env.id === selectedEnvId);
|
|
const nextSelectedEnvId = exists ? selectedEnvId : '';
|
|
persistSelectedEnvId(nextSelectedEnvId);
|
|
return { ...state, selectedEnvId: nextSelectedEnvId };
|
|
});
|
|
}
|
|
|
|
async function refreshEnvironmentContext(preferredEnvId: string = ''): Promise<void> {
|
|
if (!hasAuthToken()) {
|
|
contextStore.update((state) => ({
|
|
...state,
|
|
isLoading: false,
|
|
isLoaded: false,
|
|
error: null,
|
|
}));
|
|
return;
|
|
}
|
|
|
|
contextStore.update((state) => ({ ...state, isLoading: true, error: null }));
|
|
try {
|
|
const environments = await api.getEnvironmentsList<Environment[]>();
|
|
const current = get(contextStore).selectedEnvId;
|
|
const selectedEnvId = resolveSelectedEnvId(environments, preferredEnvId || current);
|
|
persistSelectedEnvId(selectedEnvId);
|
|
contextStore.update((state) => ({
|
|
...state,
|
|
environments,
|
|
selectedEnvId,
|
|
isLoading: false,
|
|
isLoaded: true,
|
|
error: null,
|
|
}));
|
|
} catch (error: unknown) {
|
|
const apiError = error as { status?: number; message?: string };
|
|
if (apiError?.status === 401) {
|
|
contextStore.update((state) => ({
|
|
...state,
|
|
isLoading: false,
|
|
isLoaded: false,
|
|
error: null,
|
|
}));
|
|
return;
|
|
}
|
|
console.error('[environmentContext] Failed to refresh environments', error);
|
|
contextStore.update((state) => ({
|
|
...state,
|
|
environments: [],
|
|
selectedEnvId: '',
|
|
isLoading: false,
|
|
isLoaded: true,
|
|
error: apiError?.message || 'Failed to load environments',
|
|
}));
|
|
}
|
|
}
|
|
|
|
async function initializeEnvironmentContext(): Promise<void> {
|
|
const state = get(contextStore);
|
|
if (state.isLoading || state.isLoaded) return;
|
|
await refreshEnvironmentContext();
|
|
}
|
|
|
|
export const environmentContextStore = {
|
|
subscribe: contextStore.subscribe,
|
|
};
|
|
|
|
export function setSelectedEnvironment(envId: string): void {
|
|
applySelectedEnvId(envId);
|
|
}
|
|
|
|
export { refreshEnvironmentContext, initializeEnvironmentContext };
|
|
|
|
export const selectedEnvironmentStore = derived(
|
|
environmentContextStore,
|
|
($context: EnvironmentContextState) =>
|
|
$context.environments.find((env) => env.id === $context.selectedEnvId) || null,
|
|
);
|
|
|
|
export const isProductionContextStore = derived(
|
|
selectedEnvironmentStore,
|
|
($selectedEnvironment: Environment | null) =>
|
|
String($selectedEnvironment?.stage || '').toUpperCase() === 'PROD' ||
|
|
Boolean($selectedEnvironment?.is_production),
|
|
);
|
|
// #endregion EnvironmentContextStore
|