diff --git a/README.md b/README.md index a5978c3..58c2e80 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Kindred Silo is an R&D-oriented item database with: - **Role-based access control** (admin > editor > viewer) with API tokens and sessions - **ODS import/export** for items, BOMs, and project sheets - **Audit/completeness scoring** with weighted per-category property validation -- **Web UI** with htmx-based item browser, project management, and schema editing +- **Web UI** — React SPA (Vite + TypeScript, Catppuccin Mocha theme) for item browsing, project management, schema editing, and audit - **CAD integration** via REST API ([silo-mod](https://git.kindred-systems.com/kindred/silo-mod), [silo-calc](https://git.kindred-systems.com/kindred/silo-calc)) - **Physical inventory** tracking with hierarchical locations (schema ready) @@ -22,24 +22,33 @@ Kindred Silo is an R&D-oriented item database with: ``` silo/ ├── cmd/ -│ ├── silo/ # CLI tool -│ └── silod/ # API server +│ ├── silo/ # CLI tool +│ └── silod/ # API server ├── internal/ -│ ├── api/ # HTTP handlers, routes, templates (76 endpoints) -│ ├── auth/ # Authentication (local, LDAP, OIDC) -│ ├── config/ # Configuration loading -│ ├── db/ # PostgreSQL repositories -│ ├── migration/ # Property migration utilities -│ ├── odoo/ # Odoo ERP integration -│ ├── ods/ # ODS spreadsheet library -│ ├── partnum/ # Part number generation -│ ├── schema/ # YAML schema parsing -│ └── storage/ # MinIO file storage -├── migrations/ # Database migrations (10 files) -├── schemas/ # Part numbering schemas (YAML) -├── deployments/ # Docker Compose and systemd configs -├── scripts/ # Deployment and setup scripts -└── docs/ # Documentation +│ ├── api/ # HTTP handlers and routes (75 endpoints) +│ ├── auth/ # Authentication (local, LDAP, OIDC) +│ ├── config/ # Configuration loading +│ ├── db/ # PostgreSQL repositories +│ ├── migration/ # Property migration utilities +│ ├── odoo/ # Odoo ERP integration +│ ├── ods/ # ODS spreadsheet library +│ ├── partnum/ # Part number generation +│ ├── schema/ # YAML schema parsing +│ ├── storage/ # MinIO file storage +│ └── testutil/ # Test helpers +├── web/ # React SPA (Vite + TypeScript) +│ └── src/ +│ ├── api/ # API client and type definitions +│ ├── components/ # Reusable UI components +│ ├── context/ # Auth context provider +│ ├── hooks/ # Custom React hooks +│ ├── pages/ # Page components (Items, Projects, Schemas, Settings, Audit, Login) +│ └── styles/ # Catppuccin Mocha theme and global styles +├── migrations/ # Database migrations (11 files) +├── schemas/ # Part numbering schemas (YAML) +├── deployments/ # Docker Compose and systemd configs +├── scripts/ # Deployment and setup scripts +└── docs/ # Documentation ``` ## Quick Start @@ -95,12 +104,16 @@ The server provides the REST API and ODS endpoints consumed by these clients. | Document | Description | |----------|-------------| -| [docs/AUTH.md](docs/AUTH.md) | Authentication system design | -| [docs/AUTH_USER_GUIDE.md](docs/AUTH_USER_GUIDE.md) | User guide for login, tokens, and roles | -| [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) | Production deployment guide | | [docs/SPECIFICATION.md](docs/SPECIFICATION.md) | Full design specification and API reference | | [docs/STATUS.md](docs/STATUS.md) | Implementation status | -| [ROADMAP.md](ROADMAP.md) | Feature roadmap and gap analysis | +| [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) | Production deployment guide | +| [docs/CONFIGURATION.md](docs/CONFIGURATION.md) | Configuration reference (all `config.yaml` options) | +| [docs/AUTH.md](docs/AUTH.md) | Authentication system design | +| [docs/AUTH_USER_GUIDE.md](docs/AUTH_USER_GUIDE.md) | User guide for login, tokens, and roles | +| [docs/GAP_ANALYSIS.md](docs/GAP_ANALYSIS.md) | Gap analysis and revision control roadmap | +| [docs/COMPONENT_AUDIT.md](docs/COMPONENT_AUDIT.md) | Component audit tool design | +| [ROADMAP.md](ROADMAP.md) | Feature roadmap and SOLIDWORKS PDM comparison | +| [frontend-spec.md](frontend-spec.md) | React SPA frontend specification | ## License diff --git a/web/src/components/items/MainTab.tsx b/web/src/components/items/MainTab.tsx index 14a0d99..18e4ff0 100644 --- a/web/src/components/items/MainTab.tsx +++ b/web/src/components/items/MainTab.tsx @@ -1,6 +1,6 @@ -import { useState, useEffect } from 'react'; -import { get, post, del } from '../../api/client'; -import type { Item, Project, Revision } from '../../api/types'; +import { useState, useEffect } from "react"; +import { get, post, del } from "../../api/client"; +import type { Item, Project, Revision } from "../../api/types"; interface MainTabProps { item: Item; @@ -9,8 +9,14 @@ interface MainTabProps { } function formatDate(s: string) { - if (!s) return '—'; - return new Date(s).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); + if (!s) return "—"; + return new Date(s).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); } function formatFileSize(bytes: number) { @@ -21,19 +27,23 @@ function formatFileSize(bytes: number) { } export function MainTab({ item, onReload, isEditor }: MainTabProps) { - const [itemProjects, setItemProjects] = useState([]); + const [itemProjects, setItemProjects] = useState([]); const [allProjects, setAllProjects] = useState([]); const [latestRev, setLatestRev] = useState(null); - const [addProject, setAddProject] = useState(''); + const [addProject, setAddProject] = useState(""); useEffect(() => { - get(`/api/items/${encodeURIComponent(item.part_number)}/projects`) + get( + `/api/items/${encodeURIComponent(item.part_number)}/projects`, + ) .then(setItemProjects) .catch(() => setItemProjects([])); - get('/api/projects') + get("/api/projects") .then(setAllProjects) .catch(() => {}); - get(`/api/items/${encodeURIComponent(item.part_number)}/revisions`) + get( + `/api/items/${encodeURIComponent(item.part_number)}/revisions`, + ) .then((revs) => { if (revs.length > 0) setLatestRev(revs[revs.length - 1]!); }) @@ -43,67 +53,144 @@ export function MainTab({ item, onReload, isEditor }: MainTabProps) { const handleAddProject = async () => { if (!addProject) return; try { - await post(`/api/items/${encodeURIComponent(item.part_number)}/projects`, { projects: [addProject] }); - setItemProjects((prev) => [...prev, addProject]); - setAddProject(''); + await post( + `/api/items/${encodeURIComponent(item.part_number)}/projects`, + { projects: [addProject] }, + ); + const proj = allProjects.find((p) => p.code === addProject); + if (proj) setItemProjects((prev) => [...prev, proj]); + setAddProject(""); onReload(); } catch (e) { - alert(e instanceof Error ? e.message : 'Failed to add project'); + alert(e instanceof Error ? e.message : "Failed to add project"); } }; const handleRemoveProject = async (code: string) => { try { - await del(`/api/items/${encodeURIComponent(item.part_number)}/projects/${encodeURIComponent(code)}`); - setItemProjects((prev) => prev.filter((p) => p !== code)); + await del( + `/api/items/${encodeURIComponent(item.part_number)}/projects/${encodeURIComponent(code)}`, + ); + setItemProjects((prev) => prev.filter((p) => p.code !== code)); onReload(); } catch (e) { - alert(e instanceof Error ? e.message : 'Failed to remove project'); + alert(e instanceof Error ? e.message : "Failed to remove project"); } }; const row = (label: string, value: React.ReactNode) => ( -
- {label} - {value} +
+ + {label} + + {value}
); return (
- {row('Part Number', {item.part_number})} - {row('Description', item.description)} - {row('Type', item.item_type)} - {row('Sourcing', item.sourcing_type || '—')} - {item.sourcing_link && row('Source Link', {item.sourcing_link})} - {item.standard_cost != null && row('Std Cost', `$${item.standard_cost.toFixed(2)}`)} - {row('Revision', `Rev ${item.current_revision}`)} - {row('Created', formatDate(item.created_at))} - {row('Updated', formatDate(item.updated_at))} + {row( + "Part Number", + + {item.part_number} + , + )} + {row("Description", item.description)} + {row("Type", item.item_type)} + {row("Sourcing", item.sourcing_type || "—")} + {item.sourcing_link && + row( + "Source Link", + + {item.sourcing_link} + , + )} + {item.standard_cost != null && + row("Std Cost", `$${item.standard_cost.toFixed(2)}`)} + {row("Revision", `Rev ${item.current_revision}`)} + {row("Created", formatDate(item.created_at))} + {row("Updated", formatDate(item.updated_at))} {item.long_description && ( -
-
Long Description
-
{item.long_description}
+
+
+ Long Description +
+
{item.long_description}
)} {/* Project Tags */} -
-
Projects
-
- {itemProjects.map((code) => ( - - {code} +
+
+ Projects +
+
+ {itemProjects.map((proj) => ( + + {proj.code} {isEditor && ( @@ -116,22 +203,36 @@ export function MainTab({ item, onReload, isEditor }: MainTabProps) { value={addProject} onChange={(e) => setAddProject(e.target.value)} style={{ - padding: '0.1rem 0.3rem', fontSize: '0.75rem', - backgroundColor: 'var(--ctp-surface0)', border: '1px solid var(--ctp-surface1)', - borderRadius: '0.3rem', color: 'var(--ctp-text)', + padding: "0.1rem 0.3rem", + fontSize: "0.75rem", + backgroundColor: "var(--ctp-surface0)", + border: "1px solid var(--ctp-surface1)", + borderRadius: "0.3rem", + color: "var(--ctp-text)", }} > {allProjects - .filter((p) => !itemProjects.includes(p.code)) - .map((p) => )} + .filter((p) => !itemProjects.some((ip) => ip.code === p.code)) + .map((p) => ( + + ))} {addProject && ( - )} @@ -142,21 +243,58 @@ export function MainTab({ item, onReload, isEditor }: MainTabProps) { {/* File Info */} {latestRev?.file_key && ( -
-
File Attachment (Rev {latestRev.revision_number})
-
- {latestRev.file_size != null && {formatFileSize(latestRev.file_size)}} +
+
+ File Attachment (Rev {latestRev.revision_number}) +
+
+ {latestRev.file_size != null && ( + {formatFileSize(latestRev.file_size)} + )} {latestRev.file_checksum && ( - + SHA256: {latestRev.file_checksum.substring(0, 12)}... )}