feat: native Headlamp integration, TrueNAS API, docs, and CI for v0.2.0

Native Headlamp integrations:
- registerResourceTableColumnsProcessor: add Protocol/Pool/Server columns to
  native StorageClass table and Protocol/Volume Handle to PV table
- registerDetailsViewSection: inject TNS-CSI section into PV detail pages
- registerDetailsViewSection: inject driver role/status into tns-csi Pod pages
- registerDetailsViewHeaderAction: Benchmark shortcut on StorageClass detail
- registerAppBarAction: driver health badge (N/Nc M/Mn, color-coded)
- Trim sidebar from 6 → 4 entries (Overview, Snapshots, Metrics, Benchmark)

TrueNAS API integration:
- src/api/truenas.ts: ConfigStore-backed settings, WebSocket JSON-RPC client
  for pool.query (auth.login_with_api_key + pool.query)
- src/components/TnsCsiSettings.tsx: API key + server override settings UI
  with connection test button
- TnsCsiDataContext: fetch real pool stats (size/allocated/free/status)
- OverviewPage: three-tier pool capacity display (real data → error → metrics
  fallback)

Documentation:
- README, CHANGELOG, CONTRIBUTING, SECURITY
- docs/: architecture, deployment (Helm), getting-started, user-guide,
  troubleshooting

CI:
- .github/workflows/ci.yaml: lint + type-check + test on PR/push
- .github/workflows/release.yaml: workflow_dispatch versioned release

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
2026-02-18 16:37:56 -05:00
parent f2f3c3a87e
commit f1feb5c2f7
30 changed files with 3540 additions and 44 deletions
+83
View File
@@ -58,6 +58,8 @@ export default function OverviewPage() {
persistentVolumeClaims,
controllerPods,
nodePods,
poolStats,
poolStatsError,
loading,
error,
refresh,
@@ -109,6 +111,27 @@ export default function OverviewPage() {
const chartData = protocolChartData(storageClasses);
const totalScs = storageClasses.length;
// Capacity by pool: join volumeCapacityBytes samples (volume_id, protocol)
// with PV volumeHandle → pool name from volumeAttributes.
const capacityByPool: Map<string, number> = React.useMemo(() => {
const map = new Map<string, number>();
if (!metrics) return map;
// Build lookup: volumeHandle → pool name
const handleToPool = new Map<string, string>();
for (const pv of persistentVolumes) {
const handle = pv.spec.csi?.volumeHandle;
const pool = pv.spec.csi?.volumeAttributes?.['pool'];
if (handle && pool) handleToPool.set(handle, pool);
}
for (const sample of metrics.volumeCapacityBytes) {
const volumeId = sample.labels['volume_id'];
if (!volumeId) continue;
const pool = handleToPool.get(volumeId) ?? 'unknown';
map.set(pool, (map.get(pool) ?? 0) + sample.value);
}
return map;
}, [metrics, persistentVolumes]);
return (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
@@ -234,6 +257,66 @@ export default function OverviewPage() {
/>
</SectionBox>
{/* Pool capacity — real data from TrueNAS API when configured */}
{poolStats.length > 0 && (
<SectionBox title="Pool Capacity">
<SimpleTable
columns={[
{ label: 'Pool', getter: (p) => p.name },
{
label: 'Status',
getter: (p) => (
<StatusLabel status={p.status === 'ONLINE' ? 'success' : 'warning'}>
{p.status}
</StatusLabel>
),
},
{ label: 'Total', getter: (p) => formatBytes(p.size) },
{ label: 'Used', getter: (p) => formatBytes(p.allocated) },
{ label: 'Free', getter: (p) => formatBytes(p.free) },
{
label: 'Used %',
getter: (p) => p.size > 0
? `${Math.round((p.allocated / p.size) * 100)}%`
: '—',
},
]}
data={poolStats}
/>
</SectionBox>
)}
{poolStatsError && (
<SectionBox title="Pool Capacity Unavailable">
<NameValueTable
rows={[
{
name: 'Error',
value: <StatusLabel status="warning">{poolStatsError}</StatusLabel>,
},
{
name: 'Note',
value: 'Check your TrueNAS API key and server address in plugin settings.',
},
]}
/>
</SectionBox>
)}
{/* Provisioned capacity by pool (from Prometheus metrics — shown when TrueNAS API not configured) */}
{poolStats.length === 0 && !poolStatsError && capacityByPool.size > 0 && (
<SectionBox title="Provisioned Capacity by Pool">
<NameValueTable
rows={[...capacityByPool.entries()]
.sort(([a], [b]) => a.localeCompare(b))
.map(([pool, bytes]) => ({
name: pool,
value: formatBytes(bytes),
}))}
/>
</SectionBox>
)}
{/* Non-bound PVCs warning */}
{nonBoundPvcs.length > 0 && (
<SectionBox title="Attention: Non-Bound PVCs">