mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-10-28 15:51:26 +00:00
feat(mobile): Containr companion app — Expo/RN thin client
Phone app v1 (Phase I): PAT auth against any instance, secure-store credentials, dark UI matching the design system. - Overview: service status counts, per-project health dots, deploy feed - Services/Projects: status badges, drill-down detail - Service detail: start/stop/restart/redeploy/sleep/wake, env-doctor issues, deployment history + rollback - Logs: 300-line tail, 5s poll, stderr highlighted - Databases: status, start/stop/restart, backup-now - Settings: server info, disconnect CI: mobile job (tsc + expo lint) gated on a dedicated app/mobile paths filter so non-mobile PRs skip it entirely. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
1054679ca2
commit
925199633b
31 files changed
+13064
-62
No files matched your search
@@ -0,0 +1,83 @@
|
||||
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import { Api } from './api';
|
||||
|
||||
const KEY_URL = 'containr.server_url';
|
||||
const KEY_TOKEN = 'containr.token';
|
||||
|
||||
interface Session {
|
||||
ready: boolean;
|
||||
api: Api | null;
|
||||
serverUrl: string | null;
|
||||
signIn: (serverUrl: string, token: string) => Promise<void>;
|
||||
signOut: () => Promise<void>;
|
||||
}
|
||||
|
||||
const Ctx = createContext<Session>({
|
||||
ready: false,
|
||||
api: null,
|
||||
serverUrl: null,
|
||||
signIn: async () => {},
|
||||
signOut: async () => {},
|
||||
});
|
||||
|
||||
export function normalizeServerUrl(raw: string): string {
|
||||
let u = raw.trim();
|
||||
if (!/^https?:\/\//i.test(u)) u = `https://${u}`;
|
||||
return u.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
export function SessionProvider({ children }: { children: React.ReactNode }) {
|
||||
const [ready, setReady] = useState(false);
|
||||
const [serverUrl, setServerUrl] = useState<string | null>(null);
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const [u, t] = await Promise.all([
|
||||
SecureStore.getItemAsync(KEY_URL),
|
||||
SecureStore.getItemAsync(KEY_TOKEN),
|
||||
]);
|
||||
setServerUrl(u);
|
||||
setToken(t);
|
||||
} finally {
|
||||
setReady(true);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const value = useMemo<Session>(
|
||||
() => ({
|
||||
ready,
|
||||
serverUrl,
|
||||
api: serverUrl && token ? new Api(serverUrl, token) : null,
|
||||
signIn: async (url, tok) => {
|
||||
const normalized = normalizeServerUrl(url);
|
||||
await SecureStore.setItemAsync(KEY_URL, normalized);
|
||||
await SecureStore.setItemAsync(KEY_TOKEN, tok.trim());
|
||||
setServerUrl(normalized);
|
||||
setToken(tok.trim());
|
||||
},
|
||||
signOut: async () => {
|
||||
await SecureStore.deleteItemAsync(KEY_URL);
|
||||
await SecureStore.deleteItemAsync(KEY_TOKEN);
|
||||
setServerUrl(null);
|
||||
setToken(null);
|
||||
},
|
||||
}),
|
||||
[ready, serverUrl, token],
|
||||
);
|
||||
|
||||
return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
|
||||
}
|
||||
|
||||
export function useSession() {
|
||||
return useContext(Ctx);
|
||||
}
|
||||
|
||||
export function useApi(): Api {
|
||||
const { api } = useSession();
|
||||
if (!api) throw new Error('not signed in');
|
||||
return api;
|
||||
}
|
||||
Reference in new issue
Block a user