feat(cluster): add cluster configuration support across server, CLI, and admin UI

This commit is contained in:
2026-03-22 10:31:19 +00:00
parent 038ceb976f
commit 32bf9bae0e
13 changed files with 422 additions and 38 deletions

View File

@@ -36,12 +36,84 @@ export class ObjstViewConfig extends DeesElement {
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css`
.sectionSpacer {
margin-top: 32px;
}
.infoPanel {
margin-top: 32px;
padding: 24px;
border-radius: 8px;
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a2e')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#2a2a4a')};
}
.infoPanel h2 {
margin: 0 0 16px 0;
font-size: 18px;
font-weight: 600;
color: ${cssManager.bdTheme('#333', '#ccc')};
}
.infoPanel p {
margin: 0 0 16px 0;
font-size: 14px;
color: ${cssManager.bdTheme('#666', '#999')};
line-height: 1.5;
}
.infoPanel .row {
display: flex;
align-items: center;
margin-bottom: 8px;
font-size: 14px;
}
.infoPanel .label {
min-width: 260px;
font-family: monospace;
font-weight: 500;
color: ${cssManager.bdTheme('#1565c0', '#64b5f6')};
padding: 4px 8px;
border-radius: 4px;
background: ${cssManager.bdTheme('#e3f2fd', '#1a237e30')};
}
.infoPanel .value {
color: ${cssManager.bdTheme('#666', '#999')};
margin-left: 12px;
}
.driveList {
margin-top: 16px;
}
.driveList .driveItem {
display: flex;
align-items: center;
margin-bottom: 8px;
font-size: 14px;
}
.driveList .driveIndex {
width: 32px;
height: 32px;
border-radius: 6px;
background: ${cssManager.bdTheme('#e8eaf6', '#1a237e40')};
color: ${cssManager.bdTheme('#3f51b5', '#7986cb')};
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 13px;
margin-right: 12px;
}
.driveList .drivePath {
font-family: monospace;
color: ${cssManager.bdTheme('#333', '#e0e0e0')};
padding: 6px 12px;
border-radius: 4px;
background: ${cssManager.bdTheme('#e8e8e8', '#252540')};
}
`,
];
public render(): TemplateResult {
const config = this.configState.config;
const tiles: IStatsTile[] = [
const serverTiles: IStatsTile[] = [
{
id: 'objstPort',
title: 'Storage API Port',
@@ -92,20 +164,166 @@ export class ObjstViewConfig extends DeesElement {
},
];
const clusterTiles: IStatsTile[] = [
{
id: 'clusterStatus',
title: 'Cluster Status',
value: config?.clusterEnabled ? 'Enabled' : 'Disabled',
type: 'text',
icon: 'lucide:network',
color: config?.clusterEnabled ? '#4caf50' : '#ff9800',
},
{
id: 'nodeId',
title: 'Node ID',
value: config?.clusterNodeId || '(auto)',
type: 'text',
icon: 'lucide:fingerprint',
color: '#607d8b',
},
{
id: 'quicPort',
title: 'QUIC Port',
value: config?.clusterQuicPort ?? 4433,
type: 'number',
icon: 'lucide:radio',
color: '#00bcd4',
},
{
id: 'seedNodes',
title: 'Seed Nodes',
value: config?.clusterSeedNodes?.length ?? 0,
type: 'number',
icon: 'lucide:gitBranch',
color: '#3f51b5',
description: config?.clusterSeedNodes?.length
? config.clusterSeedNodes.join(', ')
: 'No seed nodes configured',
},
{
id: 'heartbeatInterval',
title: 'Heartbeat Interval',
value: `${config?.clusterHeartbeatIntervalMs ?? 5000}ms`,
type: 'text',
icon: 'lucide:heartPulse',
color: '#e91e63',
},
{
id: 'heartbeatTimeout',
title: 'Heartbeat Timeout',
value: `${config?.clusterHeartbeatTimeoutMs ?? 30000}ms`,
type: 'text',
icon: 'lucide:timer',
color: '#ff5722',
},
];
const erasureTiles: IStatsTile[] = [
{
id: 'dataShards',
title: 'Data Shards',
value: config?.erasureDataShards ?? 4,
type: 'number',
icon: 'lucide:layers',
color: '#2196f3',
},
{
id: 'parityShards',
title: 'Parity Shards',
value: config?.erasureParityShards ?? 2,
type: 'number',
icon: 'lucide:shieldCheck',
color: '#4caf50',
},
{
id: 'chunkSize',
title: 'Chunk Size',
value: this.formatBytes(config?.erasureChunkSizeBytes ?? 4194304),
type: 'text',
icon: 'lucide:puzzle',
color: '#9c27b0',
description: `${config?.erasureDataShards ?? 4}+${config?.erasureParityShards ?? 2} = ${Math.round(((config?.erasureParityShards ?? 2) / (config?.erasureDataShards ?? 4)) * 100)}% overhead`,
},
];
const drivePaths = config?.drivePaths?.length
? config.drivePaths
: config?.storageDirectory
? [config.storageDirectory]
: ['/data'];
const driveTiles: IStatsTile[] = [
{
id: 'driveCount',
title: 'Drive Count',
value: drivePaths.length,
type: 'number',
icon: 'lucide:hardDrive',
color: '#3f51b5',
},
];
const refreshAction = {
name: 'Refresh',
iconName: 'lucide:refreshCw',
action: async () => {
await appstate.configStatePart.dispatchAction(appstate.fetchConfigAction, null);
},
};
return html`
<objst-sectionheading>Configuration</objst-sectionheading>
<objst-sectionheading>Server Configuration</objst-sectionheading>
<dees-statsgrid
.tiles=${tiles}
.gridActions=${[
{
name: 'Refresh',
iconName: 'lucide:refreshCw',
action: async () => {
await appstate.configStatePart.dispatchAction(appstate.fetchConfigAction, null);
},
},
]}
.tiles=${serverTiles}
.gridActions=${[refreshAction]}
></dees-statsgrid>
<div class="sectionSpacer">
<objst-sectionheading>Cluster Configuration</objst-sectionheading>
</div>
<dees-statsgrid .tiles=${clusterTiles}></dees-statsgrid>
${config?.clusterEnabled ? html`
<div class="sectionSpacer">
<objst-sectionheading>Erasure Coding</objst-sectionheading>
</div>
<dees-statsgrid .tiles=${erasureTiles}></dees-statsgrid>
` : ''}
<div class="sectionSpacer">
<objst-sectionheading>Storage Drives</objst-sectionheading>
</div>
<dees-statsgrid .tiles=${driveTiles}></dees-statsgrid>
<div class="driveList">
${drivePaths.map((path, i) => html`
<div class="driveItem">
<div class="driveIndex">${i + 1}</div>
<span class="drivePath">${path}</span>
</div>
`)}
</div>
<div class="infoPanel">
<h2>Configuration Reference</h2>
<p>Cluster and drive settings are applied at server startup. To change them, set the environment variables and restart the server.</p>
<div class="row"><span class="label">OBJST_CLUSTER_ENABLED</span><span class="value">Enable clustering (true/false)</span></div>
<div class="row"><span class="label">OBJST_CLUSTER_NODE_ID</span><span class="value">Unique node identifier</span></div>
<div class="row"><span class="label">OBJST_CLUSTER_QUIC_PORT</span><span class="value">QUIC transport port (default: 4433)</span></div>
<div class="row"><span class="label">OBJST_CLUSTER_SEED_NODES</span><span class="value">Comma-separated seed node addresses</span></div>
<div class="row"><span class="label">OBJST_DRIVE_PATHS</span><span class="value">Comma-separated drive mount paths</span></div>
<div class="row"><span class="label">OBJST_ERASURE_DATA_SHARDS</span><span class="value">Data shards for erasure coding (default: 4)</span></div>
<div class="row"><span class="label">OBJST_ERASURE_PARITY_SHARDS</span><span class="value">Parity shards for erasure coding (default: 2)</span></div>
<div class="row"><span class="label">OBJST_ERASURE_CHUNK_SIZE</span><span class="value">Chunk size in bytes (default: 4194304)</span></div>
<div class="row"><span class="label">OBJST_HEARTBEAT_INTERVAL_MS</span><span class="value">Heartbeat interval in ms (default: 5000)</span></div>
<div class="row"><span class="label">OBJST_HEARTBEAT_TIMEOUT_MS</span><span class="value">Heartbeat timeout in ms (default: 30000)</span></div>
</div>
`;
}
private formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
}
}