feat: enhance network configuration settings and UI
- Added new fields for proxy URL, session affinity, and session affinity TTL in usage tracking. - Updated tauri configuration to increase window width. - Expanded Japanese and Chinese translations for new error messages and notices related to network settings. - Introduced new network configuration panel in the UI, allowing users to set port, LAN access, proxy URL, and session affinity options. - Implemented validation for port input and added error handling for network settings. - Refactored API access logic to support new provider record management functions. - Updated navigation to ensure the config page is always accessible. - Added tests for new provider record functionalities and network settings.
This commit is contained in:
1 parent
be2336320d
commit
ea4adbcba8
12 files changed
+1117
-160
No files matched your search
+65
-16
@@ -529,7 +529,10 @@ export const buildProviderRecord = (
|
||||
return applyAdvancedFields(next, section, draft);
|
||||
};
|
||||
|
||||
const providerIdentityMatches = (row: ProviderRow, record: Record<string, unknown>) => {
|
||||
const providerIdentityMatches = (
|
||||
row: ProviderRecordIdentity,
|
||||
record: Record<string, unknown>,
|
||||
) => {
|
||||
if (definitionFor(row.section).openAi) {
|
||||
return readString(record, 'name') === row.name;
|
||||
}
|
||||
@@ -539,6 +542,56 @@ const providerIdentityMatches = (row: ProviderRow, record: Record<string, unknow
|
||||
);
|
||||
};
|
||||
|
||||
type ProviderRecordIdentity = Pick<
|
||||
ProviderRow,
|
||||
'section' | 'index' | 'name' | 'apiKey' | 'baseUrl'
|
||||
>;
|
||||
|
||||
const providerPrimaryIdentityMatches = (
|
||||
row: ProviderRecordIdentity,
|
||||
record: Record<string, unknown>,
|
||||
) => definitionFor(row.section).openAi
|
||||
? readString(record, 'name') === row.name
|
||||
: readString(record, 'api-key', 'apiKey') === row.apiKey;
|
||||
|
||||
export const resolveProviderRecordIndex = (
|
||||
records: Record<string, unknown>[],
|
||||
row: ProviderRecordIdentity,
|
||||
) => {
|
||||
const exactIndex = records.findIndex((record) => providerIdentityMatches(row, record));
|
||||
if (exactIndex >= 0) return exactIndex;
|
||||
|
||||
const indexedRecord = records[row.index];
|
||||
if (indexedRecord && providerPrimaryIdentityMatches(row, indexedRecord)) {
|
||||
return row.index;
|
||||
}
|
||||
|
||||
const primaryMatches = records
|
||||
.map((record, index) => ({ record, index }))
|
||||
.filter(({ record }) => providerPrimaryIdentityMatches(row, record));
|
||||
return primaryMatches.length === 1 ? primaryMatches[0].index : -1;
|
||||
};
|
||||
|
||||
export const providerRecordWithDisabledState = (
|
||||
section: ProviderSection,
|
||||
record: Record<string, unknown>,
|
||||
disabled: boolean,
|
||||
) => {
|
||||
const nextRecord = stripResponseFields(record);
|
||||
if (definitionFor(section).openAi) {
|
||||
nextRecord.disabled = disabled;
|
||||
return nextRecord;
|
||||
}
|
||||
|
||||
const excludedModels = Array.isArray(nextRecord['excluded-models'])
|
||||
? nextRecord['excluded-models'].map(String).filter((model) => model.trim() !== '*')
|
||||
: [];
|
||||
if (disabled) excludedModels.push('*');
|
||||
if (excludedModels.length > 0) nextRecord['excluded-models'] = excludedModels;
|
||||
else delete nextRecord['excluded-models'];
|
||||
return nextRecord;
|
||||
};
|
||||
|
||||
export function ApiAccessPage() {
|
||||
const { t } = useI18n();
|
||||
const [records, setRecords] = useState(emptyRecords);
|
||||
@@ -682,9 +735,7 @@ export function ApiAccessPage() {
|
||||
let nextList: Record<string, unknown>[];
|
||||
|
||||
if (editingRow) {
|
||||
const targetIndex = current.findIndex((record) =>
|
||||
providerIdentityMatches(editingRow, record),
|
||||
);
|
||||
const targetIndex = resolveProviderRecordIndex(current, editingRow);
|
||||
if (targetIndex < 0) {
|
||||
throw new Error(t('apiAccess.error.stale'));
|
||||
}
|
||||
@@ -749,7 +800,7 @@ export function ApiAccessPage() {
|
||||
try {
|
||||
const latestConfig = await managementApi.get('/config');
|
||||
const latestRows = sectionRecordsFromConfig(latestConfig, row.section);
|
||||
const targetIndex = latestRows.findIndex((record) => providerIdentityMatches(row, record));
|
||||
const targetIndex = resolveProviderRecordIndex(latestRows, row);
|
||||
if (targetIndex < 0) {
|
||||
throw new Error(t('apiAccess.error.stale'));
|
||||
}
|
||||
@@ -765,17 +816,15 @@ export function ApiAccessPage() {
|
||||
value: { disabled: !currentlyDisabled },
|
||||
});
|
||||
} else {
|
||||
const nextRecord = stripResponseFields(latestRecord);
|
||||
const excludedModels = Array.isArray(nextRecord['excluded-models'])
|
||||
? nextRecord['excluded-models'].map(String).filter((model) => model.trim() !== '*')
|
||||
: [];
|
||||
if (!currentlyDisabled) excludedModels.push('*');
|
||||
if (excludedModels.length > 0) nextRecord['excluded-models'] = excludedModels;
|
||||
else delete nextRecord['excluded-models'];
|
||||
await managementApi.patch(`/${row.section}`, {
|
||||
index: targetIndex,
|
||||
value: nextRecord,
|
||||
});
|
||||
const nextRecord = providerRecordWithDisabledState(
|
||||
row.section,
|
||||
latestRecord,
|
||||
!currentlyDisabled,
|
||||
);
|
||||
const nextRows = latestRows.map((record, index) =>
|
||||
index === targetIndex ? nextRecord : stripResponseFields(record),
|
||||
);
|
||||
await managementApi.put(`/${row.section}`, nextRows);
|
||||
}
|
||||
setNotice(currentlyDisabled ? t('apiAccess.notice.enabled') : t('apiAccess.notice.disabled'));
|
||||
await loadProviders();
|
||||
|
||||
+240
-7
@@ -5,9 +5,12 @@ import {
|
||||
AlertCircle,
|
||||
Check,
|
||||
Copy,
|
||||
Clock3,
|
||||
Eye,
|
||||
EyeOff,
|
||||
KeyRound,
|
||||
Link2,
|
||||
Network,
|
||||
Pencil,
|
||||
Plus,
|
||||
Plug,
|
||||
@@ -17,12 +20,18 @@ import {
|
||||
Trash2,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useCoreRuntime, type CoreStatus } from '../coreRuntime';
|
||||
import { useI18n } from '../i18n';
|
||||
|
||||
type CoreConfigSettings = {
|
||||
apiKeys: CoreApiKey[];
|
||||
port: number;
|
||||
allowLan: boolean;
|
||||
pluginsEnabled: boolean;
|
||||
routingStrategy: string;
|
||||
proxyUrl: string;
|
||||
routingSessionAffinity: boolean;
|
||||
routingSessionAffinityTtl: string;
|
||||
};
|
||||
|
||||
type CoreApiKey = {
|
||||
@@ -30,7 +39,14 @@ type CoreApiKey = {
|
||||
remark: string;
|
||||
};
|
||||
|
||||
type ConfigAction = 'add-key' | 'update-key' | 'delete-key' | 'plugins' | 'routing' | null;
|
||||
type ConfigAction =
|
||||
| 'add-key'
|
||||
| 'update-key'
|
||||
| 'delete-key'
|
||||
| 'plugins'
|
||||
| 'routing'
|
||||
| 'network'
|
||||
| null;
|
||||
type NoticeTone = 'success' | 'error';
|
||||
|
||||
const ROUTING_OPTIONS = [
|
||||
@@ -40,6 +56,7 @@ const ROUTING_OPTIONS = [
|
||||
|
||||
export function ConfigPanelPage() {
|
||||
const { t } = useI18n();
|
||||
const { status: coreStatus, publishStatus, refreshStatus } = useCoreRuntime();
|
||||
const [settings, setSettings] = useState<CoreConfigSettings | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadError, setLoadError] = useState('');
|
||||
@@ -53,6 +70,12 @@ export function ConfigPanelPage() {
|
||||
const [formError, setFormError] = useState('');
|
||||
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
|
||||
const [notice, setNotice] = useState<{ message: string; tone: NoticeTone } | null>(null);
|
||||
const [portDraft, setPortDraft] = useState('8317');
|
||||
const [allowLanDraft, setAllowLanDraft] = useState(false);
|
||||
const [proxyUrlDraft, setProxyUrlDraft] = useState('');
|
||||
const [sessionAffinityDraft, setSessionAffinityDraft] = useState(false);
|
||||
const [sessionTtlDraft, setSessionTtlDraft] = useState('');
|
||||
const [portError, setPortError] = useState('');
|
||||
const noticeTimerRef = useRef<number | null>(null);
|
||||
const copyTimerRef = useRef<number | null>(null);
|
||||
|
||||
@@ -89,12 +112,24 @@ export function ConfigPanelPage() {
|
||||
}, 3200);
|
||||
};
|
||||
|
||||
async function loadSettings() {
|
||||
const applySettings = (result: CoreConfigSettings, syncNetworkDrafts = true) => {
|
||||
setSettings(result);
|
||||
if (syncNetworkDrafts) {
|
||||
setPortDraft(String(result.port));
|
||||
setAllowLanDraft(result.allowLan);
|
||||
setProxyUrlDraft(result.proxyUrl);
|
||||
setSessionAffinityDraft(result.routingSessionAffinity);
|
||||
setSessionTtlDraft(result.routingSessionAffinityTtl);
|
||||
setPortError('');
|
||||
}
|
||||
};
|
||||
|
||||
async function loadSettings(syncNetworkDrafts = true) {
|
||||
setLoading(true);
|
||||
setLoadError('');
|
||||
try {
|
||||
const result = await invoke<CoreConfigSettings>('get_core_config_settings');
|
||||
setSettings(result);
|
||||
applySettings(result, syncNetworkDrafts);
|
||||
} catch (error) {
|
||||
setSettings(null);
|
||||
setLoadError(String(error));
|
||||
@@ -112,13 +147,14 @@ export function ConfigPanelPage() {
|
||||
setBusyAction(action);
|
||||
try {
|
||||
const result = await invoke<CoreConfigSettings>(command, args);
|
||||
setSettings(result);
|
||||
applySettings(result, false);
|
||||
setLoadError('');
|
||||
showNotice(successMessage, 'success');
|
||||
return true;
|
||||
} catch (error) {
|
||||
showNotice(String(error), 'error');
|
||||
void loadSettings();
|
||||
if (settings) applySettings(settings, false);
|
||||
showNotice(t('config.error.saveFailed', { error: String(error) }), 'error');
|
||||
void loadSettings(false);
|
||||
return false;
|
||||
} finally {
|
||||
setBusyAction(null);
|
||||
@@ -250,7 +286,64 @@ export function ConfigPanelPage() {
|
||||
);
|
||||
};
|
||||
|
||||
const saveNetworkRoutingSettings = async () => {
|
||||
if (!settings || busyAction !== null) return;
|
||||
const port = Number(portDraft);
|
||||
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
||||
setPortError(t('config.error.portRange'));
|
||||
showNotice(t('config.error.portRange'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const proxyUrl = proxyUrlDraft.trim();
|
||||
const routingSessionAffinityTtl = sessionTtlDraft.trim();
|
||||
const networkChanged = port !== settings.port || allowLanDraft !== settings.allowLan;
|
||||
setPortError('');
|
||||
setBusyAction('network');
|
||||
try {
|
||||
const result = await invoke<CoreConfigSettings>('save_network_routing_settings', {
|
||||
settings: {
|
||||
port,
|
||||
allowLan: allowLanDraft,
|
||||
proxyUrl,
|
||||
routingSessionAffinity: sessionAffinityDraft,
|
||||
routingSessionAffinityTtl,
|
||||
},
|
||||
});
|
||||
applySettings(result);
|
||||
setLoadError('');
|
||||
|
||||
if (networkChanged && coreStatus?.running) {
|
||||
try {
|
||||
const status = await invoke<CoreStatus>('restart_core_process');
|
||||
publishStatus(status);
|
||||
showNotice(t('config.notice.networkRestarted'), 'success');
|
||||
} catch (error) {
|
||||
await refreshStatus();
|
||||
showNotice(t('config.error.networkRestartFailed', { error: String(error) }), 'error');
|
||||
}
|
||||
} else if (networkChanged) {
|
||||
showNotice(t('config.notice.networkNextStart'), 'success');
|
||||
} else {
|
||||
showNotice(t('config.notice.networkUpdated'), 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
applySettings(settings);
|
||||
showNotice(t('config.error.saveFailed', { error: String(error) }), 'error');
|
||||
void loadSettings();
|
||||
} finally {
|
||||
setBusyAction(null);
|
||||
}
|
||||
};
|
||||
|
||||
const controlsDisabled = loading || settings === null || busyAction !== null;
|
||||
const networkRoutingDirty = Boolean(settings) && (
|
||||
portDraft !== String(settings?.port)
|
||||
|| allowLanDraft !== settings?.allowLan
|
||||
|| proxyUrlDraft.trim() !== settings?.proxyUrl
|
||||
|| sessionAffinityDraft !== settings?.routingSessionAffinity
|
||||
|| sessionTtlDraft.trim() !== settings?.routingSessionAffinityTtl
|
||||
);
|
||||
const selectedDeleteKey =
|
||||
deleteIndex === null ? '' : settings?.apiKeys[deleteIndex]?.apiKey || '';
|
||||
const deletingLastKey = deleteIndex !== null && settings?.apiKeys.length === 1;
|
||||
@@ -295,7 +388,7 @@ export function ConfigPanelPage() {
|
||||
<AlertCircle size={24} aria-hidden="true" />
|
||||
<strong>{t('config.unavailable')}</strong>
|
||||
<span title={loadError}>{loadError}</span>
|
||||
<button type="button" className="secondary-button compact-button" onClick={loadSettings}>
|
||||
<button type="button" className="secondary-button compact-button" onClick={() => void loadSettings()}>
|
||||
<RefreshCw size={16} aria-hidden="true" />
|
||||
{t('common.retry')}
|
||||
</button>
|
||||
@@ -426,6 +519,146 @@ export function ConfigPanelPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="panel config-network-panel">
|
||||
<div className="config-panel-heading">
|
||||
<div className="config-heading-title">
|
||||
<Network size={18} aria-hidden="true" />
|
||||
<h2>{t('config.network.title')}</h2>
|
||||
</div>
|
||||
<div className="config-heading-actions">
|
||||
<span className={`state-pill ${settings !== null && !networkRoutingDirty ? 'success' : ''}`}>
|
||||
{loading
|
||||
? t('common.loading')
|
||||
: settings === null
|
||||
? t('common.unavailable')
|
||||
: busyAction === 'network'
|
||||
? t('config.network.saving')
|
||||
: networkRoutingDirty
|
||||
? t('config.network.unsaved')
|
||||
: t('config.network.saved')}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="primary-button compact-button"
|
||||
disabled={controlsDisabled || !networkRoutingDirty}
|
||||
onClick={() => void saveNetworkRoutingSettings()}
|
||||
>
|
||||
<Check size={16} aria-hidden="true" />
|
||||
{busyAction === 'network'
|
||||
? t('config.network.saving')
|
||||
: t('config.network.confirmSave')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="config-network-grid">
|
||||
<label className="config-network-field config-network-port-field">
|
||||
<span>{t('config.network.port')}</span>
|
||||
<input
|
||||
className={`config-network-input ${portError ? 'error' : ''}`}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
maxLength={5}
|
||||
value={portDraft}
|
||||
disabled={controlsDisabled}
|
||||
aria-invalid={Boolean(portError)}
|
||||
title={portError || t('config.network.portHint')}
|
||||
onChange={(event) => {
|
||||
setPortDraft(event.currentTarget.value.replace(/\D/g, '').slice(0, 5));
|
||||
setPortError('');
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape' && settings) {
|
||||
setPortDraft(String(settings.port));
|
||||
setPortError('');
|
||||
event.currentTarget.blur();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<small>{t('config.network.portHint')}</small>
|
||||
</label>
|
||||
|
||||
<div className="config-network-field config-network-toggle">
|
||||
<div>
|
||||
<span>{t('config.network.allowLan')}</span>
|
||||
<small>{t('config.network.allowLanHint')}</small>
|
||||
</div>
|
||||
<label className="switch-control" title={t('config.network.allowLan')}>
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label={t('config.network.allowLan')}
|
||||
checked={allowLanDraft}
|
||||
disabled={controlsDisabled}
|
||||
onChange={(event) => setAllowLanDraft(event.currentTarget.checked)}
|
||||
/>
|
||||
<span className="switch-track" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="config-network-field">
|
||||
<span className="config-network-label">
|
||||
<Link2 size={15} aria-hidden="true" />
|
||||
{t('config.network.proxyUrl')}
|
||||
</span>
|
||||
<input
|
||||
className="config-network-input"
|
||||
type="text"
|
||||
value={proxyUrlDraft}
|
||||
disabled={controlsDisabled}
|
||||
placeholder={t('config.network.proxyPlaceholder')}
|
||||
onChange={(event) => setProxyUrlDraft(event.currentTarget.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape' && settings) {
|
||||
setProxyUrlDraft(settings.proxyUrl);
|
||||
event.currentTarget.blur();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<small>{t('config.network.proxyHint')}</small>
|
||||
</label>
|
||||
|
||||
<div className="config-network-field config-network-toggle">
|
||||
<div>
|
||||
<span>{t('config.network.sessionAffinity')}</span>
|
||||
<small>{t('config.network.sessionAffinityHint')}</small>
|
||||
</div>
|
||||
<label className="switch-control" title={t('config.network.sessionAffinity')}>
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label={t('config.network.sessionAffinity')}
|
||||
checked={sessionAffinityDraft}
|
||||
disabled={controlsDisabled}
|
||||
onChange={(event) => setSessionAffinityDraft(event.currentTarget.checked)}
|
||||
/>
|
||||
<span className="switch-track" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="config-network-field">
|
||||
<span className="config-network-label">
|
||||
<Clock3 size={15} aria-hidden="true" />
|
||||
{t('config.network.sessionTtl')}
|
||||
</span>
|
||||
<input
|
||||
className="config-network-input"
|
||||
type="text"
|
||||
value={sessionTtlDraft}
|
||||
disabled={controlsDisabled}
|
||||
placeholder="1h"
|
||||
onChange={(event) => setSessionTtlDraft(event.currentTarget.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape' && settings) {
|
||||
setSessionTtlDraft(settings.routingSessionAffinityTtl);
|
||||
event.currentTarget.blur();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<small>{t('config.network.sessionTtlHint')}</small>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{addDialogOpen ? (
|
||||
<div className="config-dialog-backdrop" onMouseDown={(event) => {
|
||||
if (event.currentTarget === event.target) closeAddDialog();
|
||||
|
||||
+16
-125
@@ -123,11 +123,8 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
|
||||
const [checkingLatest, setCheckingLatest] = useState(Boolean(latestCheckPromise));
|
||||
const [allowLanAccess, setAllowLanAccess] = useState(false);
|
||||
const [customPort, setCustomPort] = useState('8317');
|
||||
const [settingsLoaded, setSettingsLoaded] = useState(false);
|
||||
const [settingsError, setSettingsError] = useState('');
|
||||
const [installing, setInstalling] = useState(false);
|
||||
const [processBusy, setProcessBusy] = useState(false);
|
||||
const [networkBusy, setNetworkBusy] = useState(false);
|
||||
const [processNotice, setProcessNotice] = useState<{
|
||||
message: string;
|
||||
tone: MessageType;
|
||||
@@ -145,8 +142,6 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
|
||||
const [lanIpChecked, setLanIpChecked] = useState(false);
|
||||
const installDialogRef = useRef<HTMLDivElement>(null);
|
||||
const savedPortRef = useRef(8317);
|
||||
const savedAllowLanRef = useRef(false);
|
||||
const settingsSaveRequestRef = useRef(0);
|
||||
const processNoticeTimerRef = useRef<number | null>(null);
|
||||
const copiedApiTimerRef = useRef<number | null>(null);
|
||||
|
||||
@@ -347,13 +342,7 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
|
||||
setAllowLanAccess(settings.allowLan);
|
||||
setCustomPort(String(settings.port));
|
||||
savedPortRef.current = settings.port;
|
||||
savedAllowLanRef.current = settings.allowLan;
|
||||
setSettingsError('');
|
||||
} catch (error) {
|
||||
setSettingsError(String(error));
|
||||
} finally {
|
||||
setSettingsLoaded(true);
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const loadHomeApiKey = async () => {
|
||||
@@ -367,73 +356,6 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
|
||||
}
|
||||
};
|
||||
|
||||
const saveNetworkSettings = async (
|
||||
allowLan: boolean,
|
||||
port: number,
|
||||
restartAfterSave = false,
|
||||
) => {
|
||||
const requestId = ++settingsSaveRequestRef.current;
|
||||
|
||||
try {
|
||||
const settings = await invoke<GuiSettings>('save_gui_settings', {
|
||||
settings: { allowLan, port },
|
||||
});
|
||||
if (requestId !== settingsSaveRequestRef.current) {
|
||||
return;
|
||||
}
|
||||
setAllowLanAccess(settings.allowLan);
|
||||
setCustomPort(String(settings.port));
|
||||
savedPortRef.current = settings.port;
|
||||
savedAllowLanRef.current = settings.allowLan;
|
||||
setSettingsError('');
|
||||
|
||||
if (restartAfterSave) {
|
||||
if (coreStatus?.running) {
|
||||
await runCoreProcessCommand('restart_core_process', {
|
||||
success: t('kernel.notice.networkRestarted'),
|
||||
failure: t('kernel.notice.networkRestartFailed'),
|
||||
});
|
||||
} else {
|
||||
showProcessNotice(t('kernel.notice.networkNextStart'), 'info');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (requestId !== settingsSaveRequestRef.current) {
|
||||
return;
|
||||
}
|
||||
setAllowLanAccess(savedAllowLanRef.current);
|
||||
setCustomPort(String(savedPortRef.current));
|
||||
setSettingsError(String(error));
|
||||
if (restartAfterSave) {
|
||||
showProcessNotice(t('kernel.notice.networkSaveFailed', { error: String(error) }), 'error');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updateAllowLanAccess = (allowLan: boolean) => {
|
||||
const editedPort = Number(customPort);
|
||||
const port =
|
||||
Number.isInteger(editedPort) && editedPort >= 1 && editedPort <= 65535
|
||||
? editedPort
|
||||
: savedPortRef.current;
|
||||
setAllowLanAccess(allowLan);
|
||||
setNetworkBusy(true);
|
||||
void saveNetworkSettings(allowLan, port, true).finally(() => setNetworkBusy(false));
|
||||
};
|
||||
|
||||
const commitCustomPort = () => {
|
||||
const port = Number(customPort);
|
||||
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
||||
setCustomPort(String(savedPortRef.current));
|
||||
setSettingsError(t('kernel.error.port'));
|
||||
return;
|
||||
}
|
||||
|
||||
setNetworkBusy(true);
|
||||
void saveNetworkSettings(allowLanAccess, port, port !== savedPortRef.current)
|
||||
.finally(() => setNetworkBusy(false));
|
||||
};
|
||||
|
||||
const loadPlatform = async () => {
|
||||
try {
|
||||
const result = await invoke<CorePlatform>('detect_core_platform');
|
||||
@@ -746,7 +668,7 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
|
||||
<h2>{t('kernel.control.title')}</h2>
|
||||
</div>
|
||||
<span className={`state-pill ${statusTone}`} title={statusError || undefined}>
|
||||
{processBusy || networkBusy ? t('common.processing') : statusLabel}
|
||||
{processBusy ? t('common.processing') : statusLabel}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -764,48 +686,15 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
|
||||
<dd>{coreStatus?.processId || t('kernel.control.noPid')}</dd>
|
||||
</div>
|
||||
<div className="panel-detail-row">
|
||||
<dt>{t('kernel.control.allowLan')}</dt>
|
||||
<dd className="detail-control-cell">
|
||||
<span className="switch-control" title={t('kernel.control.lanRestartHint')}>
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label={t('kernel.control.allowLanAria')}
|
||||
disabled={!settingsLoaded || networkBusy || processBusy}
|
||||
checked={allowLanAccess}
|
||||
onChange={(event) => updateAllowLanAccess(event.currentTarget.checked)}
|
||||
/>
|
||||
<span className="switch-track" />
|
||||
</span>
|
||||
<dt>{t('kernel.overview.coreVersion')}</dt>
|
||||
<dd>
|
||||
{currentVersion
|
||||
|| (coreInstalled ? t('common.unavailable') : t('kernel.status.notInstalled'))}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="panel-detail-row">
|
||||
<dt>
|
||||
<label htmlFor="custom-port">{t('kernel.control.port')}</label>
|
||||
</dt>
|
||||
<dd className="detail-control-cell">
|
||||
<input
|
||||
id="custom-port"
|
||||
className={`compact-text-input ${settingsError ? 'error' : ''}`}
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
maxLength={5}
|
||||
placeholder={t('kernel.control.portPlaceholder')}
|
||||
title={settingsError || t('kernel.control.portRange')}
|
||||
aria-invalid={Boolean(settingsError)}
|
||||
disabled={!settingsLoaded || networkBusy || processBusy}
|
||||
value={customPort}
|
||||
onChange={(event) =>
|
||||
setCustomPort(event.currentTarget.value.replace(/\D/g, '').slice(0, 5))
|
||||
}
|
||||
onBlur={commitCustomPort}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.currentTarget.blur();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</dd>
|
||||
<dt>{t('kernel.overview.appVersion')}</dt>
|
||||
<dd>{currentAppVersion}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
@@ -813,7 +702,7 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
|
||||
<button
|
||||
type="button"
|
||||
className={coreRunning ? 'danger-button' : 'primary-button'}
|
||||
disabled={!coreInstalled || installing || processBusy || networkBusy}
|
||||
disabled={!coreInstalled || installing || processBusy}
|
||||
onClick={() =>
|
||||
void runCoreProcessCommand(
|
||||
coreRunning ? 'stop_core_process' : 'start_core_process',
|
||||
@@ -826,7 +715,7 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
disabled={!coreInstalled || !coreRunning || installing || processBusy || networkBusy}
|
||||
disabled={!coreInstalled || !coreRunning || installing || processBusy}
|
||||
onClick={() =>
|
||||
void runCoreProcessCommand('restart_core_process', { success: t('kernel.notice.restarted') })
|
||||
}
|
||||
@@ -836,7 +725,7 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
disabled={processBusy || networkBusy}
|
||||
disabled={processBusy}
|
||||
onClick={() => void refreshStatus()}
|
||||
>
|
||||
{t('kernel.control.refresh')}
|
||||
@@ -860,9 +749,11 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
|
||||
: t('appUpdate.phase.checking'))}
|
||||
</p>
|
||||
</div>
|
||||
<span className={`state-pill ${appUpdate?.updateAvailable ? 'update' : appUpdateError ? 'error' : 'success'}`}>
|
||||
{appUpdate?.autoUpdateSupported ? t('appUpdate.portableReady') : t('appUpdate.manualOnly')}
|
||||
</span>
|
||||
{!appUpdate?.autoUpdateSupported && (
|
||||
<span className={`state-pill ${appUpdateError ? 'error' : 'success'}`}>
|
||||
{t('appUpdate.manualFallback')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<dl className="panel-detail-grid software-update-details">
|
||||
|
||||
Reference in new issue
Block a user