Goal: bring Invar to feature parity with RVTools.
This plan is derived from docs/RVTOOLS-SHEETS-AND-COLUMNS.md (the column spec),
read against the code actually in this repo as of the fork. It is a roadmap, not
a schedule: sizes are relative, and every sheet still has to satisfy the ground
rule in CLAUDE.md, namely that no vCenter property path gets written without
being queried live first.
A visual version of this plan, with per-sheet coverage across all 27 sheets,
is in docs/parity-roadmap.html. Open it in a browser.
Six of RVTools' 27 sheets exist, five of them as UI sheets and one export-only.
| Sheet | RVTools cols | Invar cols | State |
|---|---|---|---|
| vInfo | 89 | 25 | Partial |
| vHost | 70 | 49 | Partial |
| vDisk | 40 | 25 | Partial |
| vSnapshot | 22 | 13 | Partial |
| vHealth | 3 | 3 (5 of 7 checks) | Partial |
| vMetaData | 4 | 4 (export only) | Done |
| All 27 sheets | Implemented |
Missing entirely: vCPU, vMemory, vPartition, vNetwork, vCD, vUSB, vTools, vSource, vRP, vCluster, vHBA, vNIC, vSwitch, vPort, dvSwitch, dvPort, vSC_VMK, vDatastore, vMultiPath, vLicense, vFileInfo.
Two things are already at parity and should not be re-litigated:
- xlsx formatting. Fonts, number formats, freeze panes, autofilter, sheet
order and the
True/Falsetext booleans all match a real RVTools export. See the format table indocs/RVTOOLS-SHEETS-AND-COLUMNS.md. - Sheet plumbing. The frontend is generic: it calls
list_sheetsand renders whateverfetch_sheetreturns.export.rsalready carries the full 27-nameRVTOOLS_SHEET_ORDERand skips what does not exist yet. Adding a sheet costs one module plus one line inlist_sheetsand one infetch_all_tables(src-tauri/src/lib.rs:52,:83). No UI work per sheet.
Invar is also already ahead of RVTools in three places, and none of this is parity work: multi-vCenter aggregation in one view, the Insights dashboard, and the HTML topology report.
Every sheet today runs its own full inventory walk. vinfo, vdisk,
vsnapshot and vhealth each call retrieve("VirtualMachine", ...)
independently, and common.rs adds two more walks for host names and per-host VM
totals. An export therefore does ten full inventory passes for five sheets.
That number was measured live on 2026-09-03 by instrumenting
SoapClient::retrieve, not estimated: each sheet pays a HostSystem name walk
plus its own VirtualMachine walk, and vHost pays two.
Ten of the 27 sheets are derived from VirtualMachine properties. Adding them
the current way means ten more full walks per export, against an API whose
sessions already need careful management. This is the single decision that
governs how expensive the rest of the plan is.
The fix: fetch once per vCenter per export, into a snapshot that sheets read from.
InventorySnapshot {
vms: Vec<ManagedObject>, // union of every sheet's VM property set
hosts: Vec<ManagedObject>, // union of every sheet's host property set
datastores, clusters, resource_pools, dvswitches, ...
}
Each sheet becomes a pure function fn rows(&InventorySnapshot) -> Vec<Vec<Cell>>.
Two consequences beyond speed:
- Sheets become unit-testable against one captured XML fixture, with no live
vCenter. Today only
vhealth(8 tests) andvsnapshot(1 test) have any. fetch_sheetfor a single sheet can still request a narrow snapshot, so the interactive path does not pay for the export path.
| Item | Why | Size | State |
|---|---|---|---|
InventorySnapshot and sheets as pure functions over it |
Sets the marginal cost of the remaining 21 sheets | L | Done |
| Test harness for sheets without a vCenter | Every later sheet ships with a parse test instead of a live-only check | M | Done |
| Inventory path index: Datacenter, Cluster, Folder | Cross-cutting columns on ~20 sheets, appended generically the way VI SDK Server already is (data/mod.rs). Needs parent walks over Folder, Datacenter, ComputeResource, ClusterComputeResource, which is a different query shape than the flat reads used so far |
M | Done |
| Real captured XML fixtures per object type | Replaces the hand-written fragments the tests use today with real responses | M | Done |
Phase 0 adds no sheets. It is still the right first move: the path index alone adds three columns to twenty sheets, and doing it after the sheets exist means editing twenty modules instead of one.
src-tauri/src/data/snapshot.rs holds InventorySnapshot, which is fetched once
per vCenter with the union of the requested sheets' property sets, and
SheetSpec, which is what a sheet now is:
pub struct SheetSpec {
pub name: &'static str,
pub columns: fn() -> Vec<Column>,
pub vm_props: &'static [&'static [&'static str]],
pub host_props: &'static [&'static [&'static str]],
pub rows: fn(&InventorySnapshot) -> Result<Vec<Vec<Cell>>, String>,
}Consequences:
- Inventory walks per export dropped from 10 to 3. One
VirtualMachineretrieve and oneHostSystemretrieve now serve all five sheets. Adding the seven Phase 1 sheets adds zero walks: they widen the same property union. data::SHEETSis the single registry.list_sheets,fetch_sheetand the export all drive off it, so a new sheet is one module plus one line.lib.rsno longer has a per-sheet match arm.- A single sheet fetches only its own properties. Opening one tab does not pay for the export's full union.
- Property sets compose. A sheet declares
&[VM_CONTEXT_PROPS, VM_PROPS]rather than restating the shared context, so there is one definition of it. - Sheets are pure, so they are testable.
snapshot::test_supportbuildsManagedObjects from XML fragments and assembles a snapshot; tests look up columns by RVTools label rather than by index, so a new column cannot silently shift them. Test count went from 9 to 23.
Run against vcf-mgmt-vc91.vcf.soultec.lab (vCenter 9.1.0.0300 build 25629530;
3 hosts, 161 VMs incl. 7 templates). Both commits were built and run on the same
vCenter minutes apart, via examples/parity_probe (the five per-sheet fetchers,
which is exactly what fetch_all_tables did at cf626b8) and
examples/union_probe (the shared-snapshot path).
| Sheet | vCenter (derived from raw XML) | cf626b8 | 1ef59b3 per-sheet | 1ef59b3 shared snapshot |
|---|---|---|---|---|
| vInfo | 161 | 161 | 161 | 161 |
| vHost | 3 | 3 | 3 | 3 |
| vDisk | 345 | 345 | 345 | 345 |
| vSnapshot | 3 | 3 | 3 | 3 |
| vHealth | 166 | 166 | 166 | 166 |
The first column is not the app: it was derived independently from captured
RetrievePropertiesEx responses, replicating each sheet's row logic. So the two
builds agree with each other and with vCenter.
Comparison went further than row counts — every cell was diffed, joining rows on
a natural key. All five sheets are identical in all three pairings
(old/new, new-per-sheet/new-shared, old/new-shared) except for live counters
(CPU Usage (%), Memory Usage (%), CPU usage %), which drift between any two
runs. Column names and counts are identical, no sheet emitted a warning, the
xlsx exports match sheet-for-sheet and row-for-row, and all 23 unit tests pass.
Caveat worth keeping: this lab runs vSphere Supervisor, so ephemeral Kubernetes pod VMs appear and disappear between runs. Comparisons were bracketed to distinguish that churn from a code difference; the only key-level differences seen were such VMs, never a value difference on a VM present in both runs.
Still unverified: the GUI itself has not been run on Windows — the fetch and export paths were driven through the library API, not the Tauri window.
Datacenter, Cluster and Folder are now appended generically, the way
VI SDK Server already was, so the ~20 sheets still to be written inherit them
for free.
The query shape was confirmed live before any property path was written.
parent comes back as
<val type="Folder" xsi:type="ManagedObjectReference">group-v4</val> — xsi:type
only says "this is a reference", while the plain type attribute carries the
managed-object type the walk needs. Reading that beats inferring a type from the
moref prefix, which would work right up until it did not.
Two facts made this cheaper than the plan assumed:
CreateContainerViewtakes a repeating<type>, and oneRetrievePropertiesExcan carry onepropSetper type against that single view. Folder + Datacenter + ComputeResource therefore cost one walk, not three. Measured: a full five-sheet export now does 3 walks (HostSystem, VirtualMachine, containers), up from 2.- A
ComputeResourceview also returnsClusterComputeResource, its subclass, so clusters need no separate query.
The tree is not uniform, and the code follows it rather than flattening it: a VM
reaches its folder through parent, but its cluster through
runtime.host → the host's parent, because folders and compute live in
separate branches of the inventory. A host outside a cluster still has a
ComputeResource parent, but that is a container vSphere invents rather than a
cluster anyone named, so Cluster is left empty rather than inventing one.
Column placement follows RVTools rather than being applied uniformly: VM sheets
get Datacenter, Cluster and Folder; vHost gets Datacenter and Cluster but no
Folder, since a host's folder is the datacenter's host folder that RVTools
does not show; vHealth gets none, being three columns wide. SheetSpec declares
which via RowSource, and Table::extend_from does the work once.
Verified live: all three columns populate on every row (161 vInfo, 345 vDisk,
3 vSnapshot, 3 vHost), with real folder names — vSpherePods, vm,
vcf-management-services, ESX Agents — and row counts unchanged.
src-tauri/src/data/fixtures/ holds five real RetrievePropertiesEx
<objects> elements — four VMs and one host — sanitised so the public repo
carries no lab identifiers while element names, xsi:type, nesting and ordering
stay byte-identical to what vCenter sent. snapshot::test_support::captured_snapshot
assembles them into a snapshot, and every sheet now has tests that run over real
responses. Test count went from 23 to 46.
The payoff was immediate: an assertion written from imagination said a
snapshot's state matches the VM's current power state. The capture disagreed —
state records the VM's state when the snapshot was taken, so a running VM
carries a poweredOff snapshot. A hand-written fixture would have encoded the
wrong meaning and passed.
Five paths stay synthetic because the lab cannot produce them: a host without
NTP, nested childSnapshotList snapshots, vCLS- VMs, a VM with no name, and
RDM-backed disks. src-tauri/src/data/fixtures/README.md records that, so a
green suite is not mistaken for coverage of those cases.
All seven read from the same VM snapshot. No new query pattern, no new object type. This is the cheapest large block of parity in the whole plan.
vCPU (30 cols), vMemory (34), vTools (30), vNetwork (27),
vPartition (22), vCD (21), vUSB (26).
Takes the app from 6 sheets to 13. Size: M in total, and mostly S per sheet once Phase 0 exists.
All seven sheets exist: vCPU, vMemory, vTools, vNetwork, vPartition, vCD, vUSB. The app is at 13 of 27 sheets, and the promise that they would add no inventory walks held — measured, not assumed. A full 12-sheet export does the same 3 walks a 5-sheet export did; only the VM property union grew, 33 → 67.
Live row counts: vInfo/vCPU/vMemory/vTools 162 each, vDisk 345, vPartition 791, vNetwork 234, vCD 23, vUSB 1, vSnapshot 5, vHost 3, vHealth 169.
Three columns are deliberately absent rather than shipped empty, each because a
live query said so: vMemory's Overhead (runtime.memoryOverhead returned for
no VM), vNetwork's Switch (needs DistributedVirtualSwitch, which is Phase 2)
and vTools' Required Version (no observed source property).
Two shapes the lab could not test were created rather than assumed. It had
no nested snapshot and no VirtualUSB device, so invar-fixture-01 was built
to carry both. That immediately caught a real defect: a VirtualUSB reports
connected on the device itself, not inside the connectable block that
CD-ROMs and NICs use, so the first implementation produced an empty cell and no
error. Hand-written XML would have been written to match the wrong code.
Test count 51 → 80.
Five come out of the host snapshot (config.storageDevice, config.network):
vHBA (11), vNIC (12), vSwitch (21), vPort (20), vSC_VMK (13).
Two need new object types, but still plain flat retrieves:
dvSwitch (27) via DistributedVirtualSwitch, dvPort (38) via
DistributedVirtualPortgroup.
Takes the app to 20 sheets. Size: M. Done — see below.
All seven sheets exist: vHBA, vNIC, vSwitch, vPort, vSC_VMK, dvSwitch, dvPort. The app is at 20 of 27 sheets.
Walks went 3 to 5. The five host sheets add none — they widen the existing
HostSystem fetch (41 to 47 properties). The two extra are the new object types,
which genuinely need their own queries: a DistributedVirtualSwitch and a
DistributedVirtualPortgroup carry different properties, so unlike the
container walk they cannot share one propSet.
DistributedVirtualSwitch also joined the container walk (name and parent
only), because dvPort references its switch by moref and a Network view does
not return switches — only portgroups, which are a Network subclass. Without
that, dvPort's Switch column would have shown dvs-20.
Live row counts: vHBA 3, vNIC 18, vSwitch 1, vPort 1, dvSwitch 1, dvPort 60, vSC_VMK 18.
vSwitch and vPort had nothing to parse, because these hosts run entirely on
a distributed switch and both properties returned empty arrays. Rather than ship
two sheets verified only by hand-written XML, an isolated standard switch and
port group were created on one host (invar-vSwitch / invar-pg, no
uplinks, see docs/LAB-ENVIRONMENT.md). Both sheets now have a real row.
That immediately corrected the port-group reader. A HostPortGroup carries
computedPolicy — the effective settings, including what it inherits from the
switch — alongside spec/policy, which holds only what was explicitly set. The
first implementation read the spec, so a group inheriting its teaming showed an
empty Policy. Also learned: numPorts on a switch is the elastic count ESXi
allocated, not the number requested.
A host's view of a distributed switch is a proxySwitch and is deliberately
not counted as a standard switch — RVTools separates them, and merging would
double-report the same networking.
Partial columns, all explained rather than left to look like bugs: vNIC's Speed,
Duplex, Switch and Uplink port are populated for 6 of 18 NICs, because only two
per host are cabled and linkSpeed is sent only for a link that is up;
vSC_VMK's Port Group is 9 of 18, because the NSX vxlan and hyperbus
VMkernel ports sit on no port group.
Test count 80 to 107.
| Sheet | Source | Note |
|---|---|---|
vCluster (32) |
ClusterComputeResource |
topology.rs already retrieves this type |
vDatastore (27) |
Datastore |
topology.rs already retrieves this type |
vRP (46) |
ResourcePool |
New type, flat read |
vSource (12) |
ServiceContent.about |
Already fetched by test_connection (lib.rs:36); it is a direct call, not a ContainerView |
vLicense (8) |
LicenseManager |
Direct moref call, not a ContainerView; retrieve() cannot express it as written |
Takes the app to 25 sheets. Size: M. vSource is close to free.
vSource, vRP, vCluster, vDatastore and vLicense. The app is at 25 of 27 sheets; only vMultiPath and vFileInfo remain, both Phase 5 blockers.
Two of these do not fit the ContainerView shape, so the SOAP client grew two
methods: retrieve_moref reaches an object by its own moref (LicenseManager is
a singleton in no container), and service_content wraps
RetrieveServiceContent (the vCenter is not an object in its own inventory).
Live, all 24 tables, zero warnings: vInfo/vCPU/vMemory/vTools 164 each, vDisk 348, vPartition 793, vNetwork 236, vCD 23, vUSB 1, vSnapshot 5, vSource 1, vRP 43, vCluster 1, vHost 3, vHBA 3, vNIC 18, vSwitch 1, vPort 1, dvSwitch 1, dvPort 59, vSC_VMK 18, vDatastore 4, vLicense 1, vHealth 171.
licenses is a LicenseManagerLicenseInfo[], so its elements carry the type
name — a first pass looking for the field name found zero and would have
shipped an empty sheet with no error. An evaluation licence reports total = 0
meaning unlimited rather than exhausted, and is passed through as stated.
A licence key is a credential, so the fixture masks it, and that rule now lives in the sanitiser rather than in someone's memory.
cargo run --example property_audit re-checks every path the app declares
against a live vCenter, reading the property sets straight off data::SHEETS
so there is no list to keep in sync. Ground rule 1 governs the moment code is
written; this applies it continuously, because an upgrade can retire a path
that was verified perfectly well against the previous build, and a retired path
does not error — it silently empties a column.
It earned that immediately. Against 9.1.0.0300.25629530 and again against 9.1.1.0.25712839 after the VCF upgrade:
summary.currentEVCModeKeyreturns for no host. Expected: EVC is off.hardware.systemInfo.serialNumberreturned for every host on 9.1.0.0300 and for none on 9.1.1 — the upgrade retired it. The mapping was deliberately left alone while the upgrade was in flight, re-audited once 9.1.1 was stable, and only then changed:Serial numbernow falls back tootherIdentifyingInfo/SerialNumberTag, which reports the same value. The direct field is still preferred when present, since older vCenters populate it. Verified live on 9.1.1: the column readsCZ20300KTXagain.
Re-run after the VCF upgrade, all 24 tables, zero warnings: vInfo/vCPU/vMemory/ vTools 164 each, vDisk 347, vPartition 802, vNetwork 236, vCD 23, vUSB 1, vSnapshot 5, vSource 1, vRP 43, vCluster 1, vHost 3, vHBA 3, vNIC 18, vSwitch 1, vPort 1, dvSwitch 1, dvPort 59, vSC_VMK 18, vDatastore 4, vLicense 1, vHealth 171.
The audit found 122 properties, 120 still returning — every
VirtualMachine, DistributedVirtualSwitch, DistributedVirtualPortgroup and
ClusterComputeResource path survived the upgrade untouched. The two that did
not are summary.currentEVCModeKey (EVC is off, expected) and the serial number
above.
ESXi hosts were still on 9.1.0.0200 at that point, so host remediation had not
run and invar-vSwitch survived. That may change when it does.
Flat property additions, gated on extending
docs/VCENTER-PROPERTY-REFERENCE.md (89 verified paths today, parity needs more).
vInfo 25 to 89, vHost 49 to 70, vDisk 25 to 40, vSnapshot 13 to 22.
Size: M, and it parallelises well across people because the sheets do not interact.
Column depth on the four sheets that existed before Phase 1, all against vCenter 9.1.1:
| Sheet | Columns before | after |
|---|---|---|
| vInfo | 29 | 53 |
| vHost | 52 | 63 |
| vDisk | 29 | 33 |
| vSnapshot | 17 | 19 |
(Counts include the generic Datacenter / Cluster / Folder / VI SDK Server.)
No path was written from RVTools' column names alone. probe_candidates
bisects a list of candidate paths against a live vCenter, which matters because
vim25 faults the entire retrieve if one path does not exist on the type —
a single bad guess empties every sheet, not one column. It separated three
outcomes:
- Invalid, never to be written:
summary.guest.guestState,config.hardware.numMonitors,config.hardware.videoRamSizeInKB,guest.ipStack.dnsConfig.hostName,guest.ipStack.dnsConfig.domainName,config.vmfsDatastore,config.sslThumbprint. All look plausible; none exist. - Valid but populated on nothing here, so not claimed as columns:
runtime.memoryOverhead(which confirms dropping vMemory'sOverheadwas right),config.cpuAffinity.affinitySet,config.scheduledHardwareUpgradeInfo.versionKey,parentVApp,summary.tpmAttestation.status,hardware.biosInfo.vendor,hardware.biosInfo.firmwareMajorRelease,config.consoleReservation.serviceConsoleReserved. - Valid and populated: everything now shipped.
ResourcePool joined the container walk, so vInfo's Resource pool shows a
name rather than a moref. Walk count is unchanged.
Verified live: every new column carries data, none is empty. The partial ones
are all explainable — PowerOn 98/164 (running VMs only), DAS protection
71/164 (HA-protected VMs), Resource pool 156/164 (templates have none).
Still short of RVTools' full 89 on vInfo. What remains is mostly per-NIC
Network #1..#8 columns, FT detail this lab does not use, and cluster-rule
membership, which needs ClusterComputeResource rule objects rather than VM
properties.
| Item | Needs | Unlocks |
|---|---|---|
| Datastore file browsing | HostDatastoreBrowser + SearchDatastoreSubFolders |
vFileInfo (5 cols) and the vHealth Zombie check |
| Storage path enumeration | HostMultipathInfo / config.storageDevice.multipathInfo + scsiLun |
vMultiPath (32 cols) |
Both are L, and the datastore walk is genuinely expensive at runtime, so it wants to be opt-in rather than part of every export.
The seventh vHealth check, Performance tip, has no determinable trigger from
the reference export. Either reverse-engineer it or ship 6 of 7 with a documented
gap. Do not invent a trigger.
Completing Phase 5 reaches 27 of 27 sheets.
The plan sized this as an L, on the belief it needed HostMultipathInfo from an
API area the app could not reach. It did not. config.storageDevice.multipathInfo
and config.storageDevice.scsiLun both come off the HostSystem fetch that
vHBA and vNIC already do, so the sheet adds no inventory walk at all. It was
one module, like every sheet since Phase 1.
The assumption was never tested until property_audit was pointed at the host
properties and both came back 3/3. Worth remembering the next time a plan calls
something a blocker: the cost of checking was one query.
The two properties are shaped differently, and the pair is a compact example of
the rule in CLAUDE.md. scsiLun is a top-level array, so its elements carry
the declared type name — <ScsiLun xsi:type="HostScsiDisk">. multipathInfo is
a single object, so the LUNs beneath it repeat the field name <lun>, and each
LUN's paths repeat <path>. Reading either the wrong way yields no rows and no
error.
A row is a path, not a device: the sheet exists to show that a LUN is reachable
more than one way and which way is live. Live: 18 rows across 3 hosts, every
column populated. The lab's disks are local SAS with one path each, so Working path is true throughout — correct, but not much of a test of the multi-path
case this sheet is named for.
26 of 27 sheets. Only vFileInfo remains, and that one is a real blocker:
it needs HostDatastoreBrowser and SearchDatastoreSubFolders, a genuinely
different API area, and the walk is expensive enough that it should be opt-in
rather than part of every export. It also unlocks vHealth's Zombie check.
Unlike vMultiPath, this one really was a blocker, and the measurement is the reason it is built the way it is:
| Datastore | Size | Walk |
|---|---|---|
| 3 x VMFS | 1.3 TiB each | ~1.1s each, 136 files |
| vSAN | 32.7 TiB, 164 VMs | did not finish in 10 minutes |
So the sheet is opt-in. It is excluded from the export, and each datastore gets a 15-second budget. That is deliberately short: a longer wait would be justified if waiting eventually worked, and here it does not — the choice is between answering quickly with an honest gap and blocking for minutes to reach the same gap. A datastore that overruns is named in a warning on this sheet, and the others are still shown.
Two mistakes were made building it and are worth recording. The first version
returned Err when a datastore overran, which threw away the datastores that
had answered — the exact "fail everything" behaviour the architecture notes warn
against. The second attached the warning to all 25 tables, telling 24 sheets
about a datastore they never read; noise is how a real warning gets ignored.
Both were caught by running the thing rather than by reading it.
SearchDatastoreSubFolders_Task is also the first task the app makes, so
SoapClient grew wait_task alongside search_datastore.
All 27 sheets now exist. vHealth's Zombie check is the remaining piece of
RVTools behaviour that this unlocks and which is still unimplemented.
Sheets are not the whole product. RVTools also does these.
Landed 2026-09-10:
- Headless export.
invar-export, a second binary sharing the desktop app'sconfig.json.--xlsx,--csv,--sheets,--include-file-info, and an exit status that distinguishes a clean run from one that was written but is short of a vCenter.default-run = "invar"keepstauri devworking. - CSV export per sheet, in the GUI and the CLI, sharing the workbook's cell rendering so the two exports of one inventory cannot disagree. Formula-leading cells are defused: VM annotations are free text, and a CSV of them is an injection vector into whoever opens it in Excel.
- Grid search, filter and column sort in the UI. This was listed as missing
and had in fact been built (
src/main.js,sortedRows/filteredRows); the entry was stale rather than the feature absent.
Still open:
- Connect straight to a standalone ESXi host, not only to vCenter. Needs a live ESXi host to verify against: the REST session endpoint the app logs in through is a vCenter endpoint, and what an ESXi host offers instead has not been checked here.
- Custom attributes and vSphere tags as columns. Custom attributes come off
availableField/customValue; tags need the REST tagging API and a per-object association lookup. Neither property path has been queried live, so neither may be written yet (ground rule 1). - vHealth's
Zombiecheck. The evidence for how RVTools does it is inreference/RVTools_export_all_2024-08-18_15.54.15.xlsx: that export hasvFileInfoswitched off entirely, one row reading "This tab page is empty when GetFileInfo option is not set", and still reports 6 Zombie rows. So the check is not driven by the full datastore walk. All six name files inside VM folders ([NFS-01-500] WIN-19-181/Cloud_Volume_E.vmdk), which points at a per-VM-folder search rather than a per-datastore one, bounded by VM count instead of datastore size.SoapClient::search_datastorealready takes an arbitrarydatastorePath, so no new request shape is needed. It still wants a live run before it ships: a false "Zombie" claim about a real VMDK is worse than a missing check, and linked clones, first-class disks underfcd/and VMs registered on another vCenter are all plausible false positives. - Email delivery of a finished export over SMTP.
- Zip and password-protect the export.
Both were named as inherited defects in CLAUDE.md.
- Stored vCenter passwords are cleartext. Fixed 2026-09-10. They now go to
the OS credential store (
src-tauri/src/vcenter/secrets.rs): Keychain, Credential Manager, Secret Service.config.jsonholds hosts, usernames and the certificate policy only,passwordisskip_serializingso it reaches neither the file nor the webview, and an old file's cleartext passwords are migrated on first read. Headless machines with no credential store useINVAR_PASSWORD_<n>. - If the Linux web-service mode in Phase 6 happens, authenticate it. The
reference implementation bound
0.0.0.0with no auth and an endpoint that returned stored vCenter credentials in cleartext. Do not inherit that. - Certificate verification was off with no way to turn it on. Fixed
2026-09-10.
src/main.jshardcodedskip_cert_verify: trueon every saved connection, so every connection the app ever made was open to interception. It now defaults to false and is a per-connection checkbox.
One blocker stands in front of Phase 1, and it is not solved by writing Rust. The second item here is settled and recorded so it stays settled.
-
A vCenter to develop against: settled as of 2026-09-03.
vcf-mgmt-vc91.vcf.soultec.lab, a VCF 9 management domain running vSphere Supervisor — 3 HPE DL380 Gen10 hosts, 161 VMs, vSAN-backed.docs/LAB-ENVIRONMENT.mdis re-documented against it and records the connection, how credentials are kept out of this public repo, and which empty results are expected there rather than bugs. The previous entry pointed at the upstream author's lab, which does not resolve here.Ground rule 1 in
CLAUDE.mdstill holds and is now cheap to satisfy: no new property path gets written before it has been queried against this vCenter. -
Which RVTools version is the parity target: settled, it is 4.6. The column spec in
docs/RVTOOLS-SHEETS-AND-COLUMNS.mdis derived fromreference/RVTools_export_all_2024-08-18_15.54.15.xlsx, and that export stays the reference. Every sheet and column count in this plan is sized against it. A newer RVTools release is explicitly out of scope; if that changes, the lists have to be re-derived from a fresh export before any remaining phase is sized.
Phase 0 items 1 and 2, then all of Phase 1.
That is one architectural change plus seven sheets that share a single code path, and it moves the app from 6 sheets to 13 while adding Datacenter, Cluster and Folder to every sheet at once. It also produces the fixture corpus that makes Phases 2 to 4 testable without a live vCenter for every change.