feat: LibreOffice Calc extension, ODS library, AI description, audit design
Calc extension (pkg/calc/):
- Python UNO ProtocolHandler with 8 toolbar commands
- SiloClient HTTP client adapted from FreeCAD workbench
- Pull BOM/Project: populates sheets with 28-col format, hidden property
columns, row hash tracking, auto project tagging
- Push: row classification, create/update items, conflict detection
- Completion wizard: 3-step category/description/fields with PN conflict
resolution dialog
- OpenRouter AI integration: generate standardized descriptions from seller
text, configurable model/instructions, review dialog
- Settings: JSON persistence, env var fallbacks, OpenRouter fields
- 31 unit tests (no UNO/network required)
Go ODS library (internal/ods/):
- Pure Go ODS read/write (ZIP of XML, no headless LibreOffice)
- Writer, reader, 10 round-trip tests
Server ODS endpoints (internal/api/ods.go):
- GET /api/items/export.ods, template.ods, POST import.ods
- GET /api/items/{pn}/bom/export.ods
- GET /api/projects/{code}/sheet.ods
- POST /api/sheets/diff
Documentation:
- docs/CALC_EXTENSION.md: extension progress report
- docs/COMPONENT_AUDIT.md: web audit tool design with weighted scoring,
assembly computed fields, batch AI assistance plan
This commit is contained in:
523
docs/COMPONENT_AUDIT.md
Normal file
523
docs/COMPONENT_AUDIT.md
Normal file
@@ -0,0 +1,523 @@
|
||||
# Component Audit Tool
|
||||
|
||||
**Last Updated:** 2026-02-01
|
||||
**Status:** Design
|
||||
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
The parts database has grown organically. Many items were created with only a
|
||||
part number, description, and category. The property schema defines dozens of
|
||||
fields per category (material, finish, manufacturer, supplier, cost, etc.) but
|
||||
most items have few or none of these populated. There is no way to see which
|
||||
items are missing data or to prioritize what needs filling in.
|
||||
|
||||
Currently, adding or updating properties requires either:
|
||||
- Editing each item individually through the web UI detail panel
|
||||
- Bulk CSV export, manual editing, re-import
|
||||
- The Calc extension (new, not yet widely used)
|
||||
|
||||
None of these approaches give visibility into what's missing across the
|
||||
database. Engineers don't know which items need attention until they encounter
|
||||
a blank field during a design review or procurement cycle.
|
||||
|
||||
---
|
||||
|
||||
## Goals
|
||||
|
||||
1. Show a per-item completeness score based on the property schema
|
||||
2. Surface the least-complete items so they can be prioritized
|
||||
3. Let users fill in missing fields directly from the audit view
|
||||
4. Filter by project, category, completeness threshold
|
||||
5. Track improvement over time
|
||||
|
||||
---
|
||||
|
||||
## Design
|
||||
|
||||
The audit tool is a new page in the existing web UI (`/audit`), built with
|
||||
the same server-rendered Go templates + vanilla JS approach as the items and
|
||||
projects pages. It adds one new API endpoint for the completeness data and
|
||||
reuses existing endpoints for updates.
|
||||
|
||||
### Completeness Scoring
|
||||
|
||||
Each item's completeness is computed against its category's property schema.
|
||||
The schema defines both **global defaults** (12 fields, all categories) and
|
||||
**category-specific properties** (varies: 9 fields for fasteners, 20+ for
|
||||
motion components, etc.).
|
||||
|
||||
**Score formula:**
|
||||
|
||||
```
|
||||
score = sum(weight for each filled field) / sum(weight for all applicable fields)
|
||||
```
|
||||
|
||||
Score is 0.0 to 1.0, displayed as a percentage. Fields are weighted
|
||||
differently depending on sourcing type.
|
||||
|
||||
**Purchased parts (`sourcing_type = "purchased"`):**
|
||||
|
||||
| Weight | Fields | Rationale |
|
||||
|--------|--------|-----------|
|
||||
| 3 | manufacturer_pn, sourcing_link | Can't procure without these |
|
||||
| 2 | manufacturer, supplier, supplier_pn, standard_cost | Core procurement data |
|
||||
| 1 | description, sourcing_type, lead_time_days, minimum_order_qty, lifecycle_status | Important but less blocking |
|
||||
| 1 | All category-specific properties | Engineering detail |
|
||||
| 0.5 | rohs_compliant, country_of_origin, notes, long_description | Nice to have |
|
||||
|
||||
**Manufactured parts (`sourcing_type = "manufactured"`):**
|
||||
|
||||
| Weight | Fields | Rationale |
|
||||
|--------|--------|-----------|
|
||||
| 3 | has_bom (at least one BOM child) | Can't manufacture without a BOM |
|
||||
| 2 | description, standard_cost | Core identification |
|
||||
| 1 | All category-specific properties | Engineering detail |
|
||||
| 0.5 | manufacturer, supplier, notes, long_description | Less relevant for in-house |
|
||||
|
||||
The `has_bom` check for manufactured parts queries the `relationships`
|
||||
table for at least one `rel_type = 'component'` child. This is not a
|
||||
property field -- it's a structural check. A manufactured part with no BOM
|
||||
children is flagged as critically incomplete regardless of how many other
|
||||
fields are filled.
|
||||
|
||||
**Assemblies (categories A01-A07):**
|
||||
|
||||
Assembly scores are partially computed from children:
|
||||
|
||||
| Field | Source | Notes |
|
||||
|-------|--------|-------|
|
||||
| weight | Sum of child weights | Computed if all children have weight |
|
||||
| standard_cost | Sum of child (cost * qty) | Computed from BOM |
|
||||
| component_count | Count of BOM children | Always computable |
|
||||
| has_bom | BOM children exist | Required (weight 3) |
|
||||
|
||||
A computed field counts as "filled" if the data needed to compute it is
|
||||
available. If a computed value exists, it is shown alongside the stored
|
||||
value so engineers can verify or override.
|
||||
|
||||
Assembly-specific properties that cannot be computed (assembly_time,
|
||||
test_procedure, ip_rating, dimensions) are scored normally.
|
||||
|
||||
**Field filled criteria:**
|
||||
|
||||
- String fields: non-empty after trimming
|
||||
- Number fields: non-null and non-zero
|
||||
- Boolean fields: non-null (false is a valid answer)
|
||||
- has_bom: at least one component relationship exists
|
||||
|
||||
Item-level fields (`description`, `sourcing_type`, `sourcing_link`,
|
||||
`standard_cost`, `long_description`) are checked on the items table.
|
||||
Property fields (`manufacturer`, `material`, etc.) are checked on the
|
||||
current revision's `properties` JSONB column. BOM existence is checked
|
||||
on the `relationships` table.
|
||||
|
||||
### Tiers
|
||||
|
||||
Items are grouped into completeness tiers for dashboard display:
|
||||
|
||||
| Tier | Range | Color | Label |
|
||||
|------|-------|-------|-------|
|
||||
| Critical | 0-25% | Red | Missing critical data |
|
||||
| Low | 25-50% | Orange | Needs attention |
|
||||
| Partial | 50-75% | Yellow | Partially complete |
|
||||
| Good | 75-99% | Light green | Nearly complete |
|
||||
| Complete | 100% | Green | All fields populated |
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
### `GET /api/audit/completeness`
|
||||
|
||||
Returns completeness scores for all items (or filtered subset).
|
||||
|
||||
**Query parameters:**
|
||||
|
||||
| Param | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `project` | string | Filter by project code |
|
||||
| `category` | string | Filter by category prefix (e.g. `F`, `F01`) |
|
||||
| `max_score` | float | Only items below this score (e.g. `0.5`) |
|
||||
| `min_score` | float | Only items above this score |
|
||||
| `sort` | string | `score_asc` (default), `score_desc`, `part_number`, `updated_at` |
|
||||
| `limit` | int | Pagination limit (default 100) |
|
||||
| `offset` | int | Pagination offset |
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"part_number": "F01-0042",
|
||||
"description": "M3x10 Socket Head Cap Screw",
|
||||
"category": "F01",
|
||||
"category_name": "Screws and Bolts",
|
||||
"sourcing_type": "purchased",
|
||||
"projects": ["3DX10", "PROTO"],
|
||||
"score": 0.41,
|
||||
"weighted_filled": 12.5,
|
||||
"weighted_total": 30.5,
|
||||
"has_bom": false,
|
||||
"bom_children": 0,
|
||||
"missing_critical": ["manufacturer_pn", "sourcing_link"],
|
||||
"missing": [
|
||||
"manufacturer_pn",
|
||||
"sourcing_link",
|
||||
"supplier",
|
||||
"supplier_pn",
|
||||
"finish",
|
||||
"strength_grade",
|
||||
"torque_spec"
|
||||
],
|
||||
"updated_at": "2026-01-15T10:30:00Z"
|
||||
},
|
||||
{
|
||||
"part_number": "A01-0003",
|
||||
"description": "3DX10 Line Assembly",
|
||||
"category": "A01",
|
||||
"category_name": "Mechanical Assembly",
|
||||
"sourcing_type": "manufactured",
|
||||
"projects": ["3DX10"],
|
||||
"score": 0.68,
|
||||
"weighted_filled": 15.0,
|
||||
"weighted_total": 22.0,
|
||||
"has_bom": true,
|
||||
"bom_children": 12,
|
||||
"computed_fields": {
|
||||
"standard_cost": 7538.61,
|
||||
"component_count": 12,
|
||||
"weight": null
|
||||
},
|
||||
"missing_critical": [],
|
||||
"missing": ["assembly_time", "test_procedure", "weight", "ip_rating"],
|
||||
"updated_at": "2026-01-28T14:20:00Z"
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total_items": 847,
|
||||
"avg_score": 0.42,
|
||||
"manufactured_without_bom": 31,
|
||||
"by_tier": {
|
||||
"critical": 123,
|
||||
"low": 298,
|
||||
"partial": 251,
|
||||
"good": 142,
|
||||
"complete": 33
|
||||
},
|
||||
"by_category": {
|
||||
"F": {"count": 156, "avg_score": 0.51},
|
||||
"C": {"count": 89, "avg_score": 0.38},
|
||||
"R": {"count": 201, "avg_score": 0.29}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/audit/completeness/{partNumber}`
|
||||
|
||||
Single-item detail with field-by-field breakdown.
|
||||
|
||||
```json
|
||||
{
|
||||
"part_number": "F01-0042",
|
||||
"description": "M3x10 Socket Head Cap Screw",
|
||||
"category": "F01",
|
||||
"sourcing_type": "purchased",
|
||||
"score": 0.41,
|
||||
"has_bom": false,
|
||||
"fields": [
|
||||
{"key": "description", "source": "item", "weight": 1, "value": "M3x10 Socket Head Cap Screw", "filled": true},
|
||||
{"key": "sourcing_type", "source": "item", "weight": 1, "value": "purchased", "filled": true},
|
||||
{"key": "standard_cost", "source": "item", "weight": 2, "value": 0.12, "filled": true},
|
||||
{"key": "sourcing_link", "source": "item", "weight": 3, "value": "", "filled": false},
|
||||
{"key": "manufacturer", "source": "property", "weight": 2, "value": null, "filled": false},
|
||||
{"key": "manufacturer_pn", "source": "property", "weight": 3, "value": null, "filled": false},
|
||||
{"key": "supplier", "source": "property", "weight": 2, "value": null, "filled": false},
|
||||
{"key": "supplier_pn", "source": "property", "weight": 2, "value": null, "filled": false},
|
||||
{"key": "material", "source": "property", "weight": 1, "value": "18-8 Stainless Steel", "filled": true},
|
||||
{"key": "finish", "source": "property", "weight": 1, "value": null, "filled": false},
|
||||
{"key": "thread_size", "source": "property", "weight": 1, "value": "M3", "filled": true},
|
||||
{"key": "thread_pitch", "source": "property", "weight": 1, "value": null, "filled": false},
|
||||
{"key": "length", "source": "property", "weight": 1, "value": "10mm", "filled": true},
|
||||
{"key": "head_type", "source": "property", "weight": 1, "value": "Socket", "filled": true}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
For assemblies, the detail response includes a `computed_fields` section
|
||||
showing values derived from children (cost rollup, weight rollup,
|
||||
component count). These are presented alongside stored values in the UI
|
||||
so engineers can compare and choose to accept the computed value.
|
||||
|
||||
Existing `PUT /api/items/{pn}` and revision property updates handle writes.
|
||||
|
||||
---
|
||||
|
||||
## Web UI
|
||||
|
||||
### Audit Page (`/audit`)
|
||||
|
||||
New page accessible from the top navigation bar (fourth tab after Items,
|
||||
Projects, Schemas).
|
||||
|
||||
**Layout:**
|
||||
|
||||
```
|
||||
+------------------------------------------------------------------+
|
||||
| Items | Projects | Schemas | Audit |
|
||||
+------------------------------------------------------------------+
|
||||
| [Project: ___] [Category: ___] [Max Score: ___] [Search] |
|
||||
+------------------------------------------------------------------+
|
||||
| Summary Bar |
|
||||
| [===Critical: 123===|===Low: 298===|==Partial: 251==|Good|Done] |
|
||||
+------------------------------------------------------------------+
|
||||
| Score | PN | Description | Category | Missing|
|
||||
|-------|-----------|--------------------------|----------|--------|
|
||||
| 12% | R01-0003 | Bearing, Deep Groove 6205| Bearings | 18 |
|
||||
| 15% | E14-0001 | NTC Thermistor 10K | Sensors | 16 |
|
||||
| 23% | C03-0012 | 1/4" NPT Ball Valve SS | Valves | 14 |
|
||||
| 35% | F01-0042 | M3x10 Socket Head Cap | Screws | 7 |
|
||||
| ... | | | | |
|
||||
+------------------------------------------------------------------+
|
||||
```
|
||||
|
||||
**Interactions:**
|
||||
|
||||
- Click a row to open an inline edit panel (right side, same split-panel
|
||||
pattern as the items page)
|
||||
- The edit panel shows all applicable fields for the category, with empty
|
||||
fields highlighted
|
||||
- Editing a field and pressing Enter/Tab saves immediately via API
|
||||
- Score updates live after each save
|
||||
- Summary bar updates as items are completed
|
||||
- Click a tier segment in the summary bar to filter to that tier
|
||||
|
||||
### Inline Edit Panel
|
||||
|
||||
```
|
||||
+----------------------------------+
|
||||
| F01-0042 Score: 35% |
|
||||
| M3x10 Socket Head Cap Screw |
|
||||
+----------------------------------+
|
||||
| -- Required -- |
|
||||
| Description [M3x10 Socket H..] |
|
||||
| Sourcing [purchased v ] |
|
||||
+----------------------------------+
|
||||
| -- Procurement -- |
|
||||
| Manufacturer [________________] |
|
||||
| Mfr PN [________________] |
|
||||
| Supplier [________________] |
|
||||
| Supplier PN [________________] |
|
||||
| Cost [$0.12 ] |
|
||||
| Sourcing Link[________________] |
|
||||
| Lead Time [____ days ] |
|
||||
+----------------------------------+
|
||||
| -- Fastener Properties -- |
|
||||
| Material [18-8 Stainless ] |
|
||||
| Finish [________________] |
|
||||
| Thread Size [M3 ] |
|
||||
| Thread Pitch [________________] |
|
||||
| Length [10mm ] |
|
||||
| Head Type [Socket ] |
|
||||
| Drive Type [________________] |
|
||||
| Strength [________________] |
|
||||
| Torque Spec [________________] |
|
||||
+----------------------------------+
|
||||
| [Save All] |
|
||||
+----------------------------------+
|
||||
```
|
||||
|
||||
Fields are grouped into sections: Required, Procurement (global defaults),
|
||||
and category-specific properties. Empty fields have a subtle red left border.
|
||||
Filled fields have a green left border. The score bar at the top updates as
|
||||
fields are filled in.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: API endpoint and scoring engine
|
||||
|
||||
New file: `internal/api/audit_handlers.go`
|
||||
|
||||
- `HandleAuditCompleteness` -- query items, join current revision properties,
|
||||
compute scores against schema, return paginated JSON
|
||||
- `HandleAuditItemDetail` -- single item with field-by-field breakdown
|
||||
- Scoring logic in a helper function that takes item fields + revision
|
||||
properties + category schema and returns score + missing list
|
||||
|
||||
Register routes:
|
||||
- `GET /api/audit/completeness` (viewer role)
|
||||
- `GET /api/audit/completeness/{partNumber}` (viewer role)
|
||||
|
||||
### Phase 2: Web UI page
|
||||
|
||||
New template: `internal/api/templates/audit.html`
|
||||
|
||||
- Same base template, Catppuccin Mocha theme, nav bar with Audit tab
|
||||
- Summary bar with tier counts (colored segments)
|
||||
- Sortable, filterable table
|
||||
- Split-panel detail view on row click
|
||||
- Vanilla JS fetch calls to audit and item update endpoints
|
||||
|
||||
Update `internal/api/web.go`:
|
||||
- Add `HandleAuditPage` handler
|
||||
- Register `GET /audit` route
|
||||
|
||||
Update `internal/api/templates/base.html`:
|
||||
- Add Audit tab to navigation
|
||||
|
||||
### Phase 3: Inline editing
|
||||
|
||||
- Field save on blur/Enter via `PUT /api/items/{pn}` for item fields
|
||||
- Property updates via `POST /api/items/{pn}/revisions` with updated
|
||||
properties map
|
||||
- Live score recalculation after save (re-fetch from audit detail endpoint)
|
||||
- Batch "Save All" button for multiple field changes
|
||||
|
||||
### Phase 4: Tracking and reporting
|
||||
|
||||
- Store periodic score snapshots (daily cron or on-demand) in a new
|
||||
`audit_snapshots` table for trend tracking
|
||||
- Dashboard chart showing completeness improvement over time
|
||||
- Per-project completeness summary on the projects page
|
||||
- CSV export of audit results for offline review
|
||||
|
||||
### Phase 5: Batch AI assistance
|
||||
|
||||
Server-side OpenRouter integration for bulk property inference from existing
|
||||
sourcing data. This extends the Calc extension's AI client pattern to the
|
||||
backend.
|
||||
|
||||
**Workflow:**
|
||||
|
||||
1. Audit page shows items with sourcing links but missing properties
|
||||
2. Engineer selects items (or filters to a category/project) and clicks
|
||||
"AI Fill Properties"
|
||||
3. Server fetches each item's sourcing link page content (or uses the
|
||||
seller description from the item's metadata)
|
||||
4. OpenRouter API call per item: system prompt describes the category's
|
||||
property schema, user prompt provides the scraped/stored description
|
||||
5. AI returns structured JSON with suggested property values
|
||||
6. Results shown in a review table: item, field, current value, suggested
|
||||
value, confidence indicator
|
||||
7. Engineer checks/unchecks suggestions, clicks "Apply Selected"
|
||||
8. Server writes accepted values as property updates (new revision)
|
||||
|
||||
**AI prompt structure:**
|
||||
|
||||
```
|
||||
System: You are a parts data specialist. Given a product description
|
||||
and a list of property fields with types, extract values for as many
|
||||
fields as possible. Return JSON only.
|
||||
|
||||
User:
|
||||
Category: F01 (Screws and Bolts)
|
||||
Product: {seller_description or scraped page text}
|
||||
|
||||
Fields to extract:
|
||||
- material (string): Material specification
|
||||
- finish (string): Surface finish
|
||||
- thread_size (string): Thread size designation
|
||||
- thread_pitch (string): Thread pitch
|
||||
- length (string): Fastener length with unit
|
||||
- head_type (string): Head style
|
||||
- drive_type (string): Drive type
|
||||
- strength_grade (string): Strength/property class
|
||||
```
|
||||
|
||||
**Rate limiting:** Queue items and process in batches of 10 with 1s delay
|
||||
between batches to stay within OpenRouter rate limits. Show progress bar
|
||||
in the UI.
|
||||
|
||||
**Cost control:** Use `openai/gpt-4.1-nano` by default (cheapest). Show
|
||||
estimated cost before starting batch. Allow model override in settings.
|
||||
|
||||
---
|
||||
|
||||
## Database Changes
|
||||
|
||||
### Phase 1: None
|
||||
|
||||
Completeness is computed at query time from existing `items` +
|
||||
`revisions.properties` data joined against the in-memory schema definition.
|
||||
No new tables needed for the core feature.
|
||||
|
||||
### Phase 4: New table
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS audit_snapshots (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
captured_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
total_items INTEGER NOT NULL,
|
||||
avg_score DECIMAL(5,4) NOT NULL,
|
||||
by_tier JSONB NOT NULL,
|
||||
by_category JSONB NOT NULL,
|
||||
by_project JSONB NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
### Phase 5: None
|
||||
|
||||
AI suggestions are ephemeral (computed per request, not stored). Accepted
|
||||
suggestions are written through the existing revision/property update path.
|
||||
|
||||
---
|
||||
|
||||
## Scoring Examples
|
||||
|
||||
### Purchased Fastener (F01)
|
||||
|
||||
**Weighted total: ~30.5 points**
|
||||
|
||||
| Field | Weight | Filled? | Points |
|
||||
|-------|--------|---------|--------|
|
||||
| manufacturer_pn | 3 | no | 0/3 |
|
||||
| sourcing_link | 3 | no | 0/3 |
|
||||
| manufacturer | 2 | no | 0/2 |
|
||||
| supplier | 2 | no | 0/2 |
|
||||
| supplier_pn | 2 | no | 0/2 |
|
||||
| standard_cost | 2 | yes | 2/2 |
|
||||
| description | 1 | yes | 1/1 |
|
||||
| sourcing_type | 1 | yes | 1/1 |
|
||||
| material | 1 | yes | 1/1 |
|
||||
| thread_size | 1 | yes | 1/1 |
|
||||
| length | 1 | yes | 1/1 |
|
||||
| head_type | 1 | yes | 1/1 |
|
||||
| drive_type | 1 | no | 0/1 |
|
||||
| finish | 1 | no | 0/1 |
|
||||
| ... (remaining) | 0.5-1 | no | 0/... |
|
||||
|
||||
**Score: 8/30.5 = 26%** -- "Low" tier, flagged because weight-3 fields
|
||||
(manufacturer_pn, sourcing_link) are missing.
|
||||
|
||||
### Manufactured Assembly (A01)
|
||||
|
||||
**Weighted total: ~22 points**
|
||||
|
||||
| Field | Weight | Source | Points |
|
||||
|-------|--------|--------|--------|
|
||||
| has_bom | 3 | BOM query | 3/3 (12 children) |
|
||||
| description | 2 | item | 2/2 |
|
||||
| standard_cost | 2 | computed from children | 2/2 |
|
||||
| component_count | 1 | computed (= 12) | 1/1 |
|
||||
| weight | 1 | computed (needs children) | 0/1 (not all children have weight) |
|
||||
| assembly_time | 1 | property | 0/1 |
|
||||
| test_procedure | 1 | property | 0/1 |
|
||||
| dimensions | 1 | property | 0/1 |
|
||||
| ip_rating | 1 | property | 0/1 |
|
||||
| ... (globals) | 0.5-1 | property | .../... |
|
||||
|
||||
**Score: ~15/22 = 68%** -- "Partial" tier, mostly complete because BOM
|
||||
and cost are covered through children.
|
||||
|
||||
### Motor (R01) -- highest field count
|
||||
|
||||
30+ applicable fields across global defaults + motion-specific properties
|
||||
(load, speed, power, voltage, current, torque, encoder, gear ratio...).
|
||||
A motor with only description + cost + sourcing_type scores under 10%
|
||||
because of the large denominator. Motors are the category most likely to
|
||||
benefit from batch AI extraction from datasheets.
|
||||
Reference in New Issue
Block a user