From 7d309aabebdb1c73f9e7d6204d9139f8ce036525 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Sun, 12 Jul 2026 11:22:01 -0400 Subject: [PATCH] Final-pass polish: support contact UX, audit tooltip, doc refresh Support teams: contact management moved from a row expander to a modal (Contacts (N) button per team); application detail Support card and the modal show Email (mailto) and Teams chat buttons for contacts with an SSO, derived as sso@ + a new contact_email_domain site setting (default geaerospace.com, blank hides the buttons). Audit log: hovering a user SSO shows the full name, resolved best-effort from the employee directory in either mode. Docs/hygiene from a standards review: CLAUDE.md active-state, CONTRACT-STABILITY.md and README brought to contract 0.10.0 / 11 plugins / migration head 7d22; get_asset_panels endpoint path fixed in the hook docstring; leftover debug console.logs removed. 781 tests pass; contacts modal, action-button hrefs, and the audit tooltip verified live. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 15 ++ CLAUDE.md | 21 ++- README.md | 7 +- docs/CONFIG.md | 1 + docs/CONTRACT-STABILITY.md | 7 +- frontend/src/assets/style.css | 18 ++ .../src/components/AssetRelationships.vue | 1 - frontend/src/utils/siteSettings.js | 10 ++ .../views/applications/ApplicationDetail.vue | 14 +- frontend/src/views/machines/MachineForm.vue | 1 - frontend/src/views/pcs/PCDetail.vue | 1 - frontend/src/views/settings/AuditLogs.vue | 2 +- frontend/src/views/settings/SiteSettings.vue | 4 +- .../src/views/settings/SupportTeamsList.vue | 161 ++++++++++-------- shopdb/core/api/auditlogs.py | 48 +++++- shopdb/core/api/settings.py | 7 + shopdb/plugins/base.py | 4 +- tests/test_core/test_settings_branding.py | 2 + 18 files changed, 234 insertions(+), 90 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d53e68..f7414d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,15 @@ ADR-007 and ADR-002. ### Added +- Support-team contact UX: the settings Support Teams page now manages each + team's contacts in a per-team "Contacts (N)" modal (name, SSO, active, plus + Add/Edit/Delete) instead of an inline row expander, and the application + detail Support card renders Email (`mailto:`) and Microsoft Teams chat + (`teams.microsoft.com/l/chat`) action buttons for every contact that has an + SSO. Both link targets derive as `sso@` from a new `site` setting + `contact_email_domain` (default `geaerospace.com`; blank hides the buttons), + surfaced in Site & Facility settings under Naming & Patterns and read on the + frontend via `getContactEmailDomain()`. - Plugin `get_permissions` hook (contract 0.10.0) so a plugin declares the RBAC permissions its own routes enforce, instead of core accumulating every plugin's permissions in `Permission.PERMISSIONS` (plugin-is-the-product). The @@ -184,6 +193,12 @@ ADR-007 and ADR-002. ### Fixed +- Audit log: hovering a user's SSO now shows their full name (best-effort, + resolved from the employee directory in either mode). +- Refreshed the internal status docs to match the code (CLAUDE.md active + state, CONTRACT-STABILITY.md and README plugin list at contract 0.10.0), + corrected the get_asset_panels endpoint path in the hook docstring, and + removed leftover debug console.log lines. - Measuring tools wired into the remaining cross-cutting surfaces an audit found them missing from: generic asset serialization (typedata + pluginid, which also fixes relationship-card links to tools), map subtype diff --git a/CLAUDE.md b/CLAUDE.md index 4fe0147..cd0fddd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,14 +16,19 @@ Architecture decisions live in `docs/adr/`. Read those before making schema or c - ADR-004: Deployment topology (per-site instances, not multi-tenant) - ACCEPTED - ADR-005: Equipment vs measuringtools plugin scope - ACCEPTED - ADR-006: Plugin collector contract pattern - ACCEPTED +- ADR-007: Product versioning and releases - ACCEPTED +- ADR-008: Plugin migration ownership (per-plugin chains) - ACCEPTED +- ADR-009: Frontend plugin route gating - ACCEPTED +- ADR-010: Frontend plugin hook contract - ACCEPTED +- ADR-011: Machines rename + modeltypes retyping - ACCEPTED ## Coding convention `CONTRIBUTING.md` defines naming rules (DB tables, columns, Python, JS, Vue, API). Pre-commit hook at `scripts/check-naming-and-style.sh` enforces them. Read `CONTRIBUTING.md` before naming any new identifier. -## Current state (as of 2026-07-10) +## Current state (as of 2026-07-12) -Refactor phases 0-5 landed; phase 6 (multi-site distribution readiness) in progress. +Refactor phases 0-5 landed; phase 6 (multi-site distribution readiness) largely complete; the last big milestone is the legacy-data import + a production pilot. ### Phases done @@ -36,10 +41,11 @@ Refactor phases 0-5 landed; phase 6 (multi-site distribution readiness) in progr ### Active state -- 340 tests passing, naming/style check green, Gitea Actions CI (backend + naming + frontend build) -- `__contract_version__` at 0.6.0 (product `__version__` 0.5.0 - distinct series, ADR-007) +- 781 tests passing, naming/style check green, Gitea Actions CI (backend + naming + frontend build) +- `__contract_version__` at 0.10.0 (product `__version__` 0.6.0, tags v0.5.0/v0.6.0 - distinct series, ADR-007; a large [Unreleased] pile is due a release cut) - 11 bundled plugins all satisfy contract: computers, employees, knowledgebase, machines, measuringtools, network, notifications, printers, slides, usb, warranty -- Single core Alembic chain: baseline `68b3947ae14f` -> head `7d16_directoryemployees` (23 migrations). A fresh site runs `flask db upgrade` from empty; it is reproducible and idempotent. +- Core Alembic chain: baseline `68b3947ae14f` -> head `7d22_apitokens_scopes` (29 core migrations). Each plugin owns its own chain (ADR-008); deploy runs `flask db upgrade` then `flask plugin upgrade-all`. Reproducible + idempotent from empty. +- API is migration-complete: an admin PAT + docs/IMPORT-API.md let a script/LLM import the whole legacy DB (X-Import-Mode preserves timestamps). - Pre-1.0 framework; sister sites should pin tight `core_version` ranges until contract reaches 1.0 ### Deferred @@ -47,8 +53,9 @@ Refactor phases 0-5 landed; phase 6 (multi-site distribution readiness) in progr - Equipment data migration (one-shot script for legacy ASP shopdb -> assets). Per ADR-001, only `category='Equipment' AND machinenumber IS NOT NULL` migrates. Skill `migrating-asset-schema` documents the pattern; the actual one-shot script lives in `scripts/migration/` when run. - Printers retirement: legacy `PrinterData` model + frontend changes. Coordinated with the equipment data migration. - (DONE 2026-07-11) `measuringtools` plugin (ADR-005) is built and bundled; docs/PLUGIN-GUIDE.md narrates its construction as the plugin tutorial. -- Frontend hook contract for asset-detail, map markers, search results -- Alembic per-plugin migration chains (the framework supports them; bundled plugins haven't moved off `db.create_all()` yet) +- (DONE) Frontend plugin hook contract (ADR-010): get_settings_cards / get_asset_panels / get_map_overlays / get_asset_presentation shipped; generic renderers for panels/overlays land incrementally. +- (DONE) Per-plugin Alembic chains (ADR-008): every bundled plugin carries its own chain; no plugin uses db.create_all(). +- Legacy ASP data import against the renamed schema (unblocked; run via docs/IMPORT-API.md) + a production pilot deployment. ## Quick start diff --git a/README.md b/README.md index 5224e1e..45e50bf 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ ShopDB tracks and manages: **Backend:** - Python 3.x with Flask - SQLAlchemy ORM -- MySQL 5.6+ database +- MySQL 5.7+ database (5.6 works with extra utf8mb4 config; see docs/DEPLOY.md) - JWT authentication - Plugin architecture for extensibility @@ -204,11 +204,12 @@ Query parameters for list endpoints: ShopDB supports plugins for extending functionality. See `CONTRIBUTING.md` for plugin development guidelines. -The image bundles ten plugins; only the ones a site installs are loaded: +The image bundles eleven plugins; only the ones a site installs are loaded: - **computers** - Shopfloor PCs and workstations - **employees** - Employee directory -- **machine** - CNC, CMM, and other shop-floor machines +- **machines** - CNC, CMM, and other shop-floor machines +- **measuringtools** - Gage-lab instruments with calibration tracking - **knowledgebase** - Documentation and troubleshooting guides - **network** - Network devices - **notifications** - Shopfloor notifications and recognition feed diff --git a/docs/CONFIG.md b/docs/CONFIG.md index 4de9723..004f860 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -143,6 +143,7 @@ read back through the API. | `pc_access_domain` | `device.geaerospace.net` | Domain appended to a PC hostname for remote-access links. Blank = hostname as-is. | | `employeeid_pattern` | `^\d{9}$` | Regex a search term must match to be treated as an employee id. Invalid regex falls back to the default and never 500s. | | `printer_hostname_template` | `Printer-{ip}.printer.geaerospace.net` | Printer hostname template. `{ip}` is the dash-separated IP address. | +| `contact_email_domain` | `geaerospace.com` | Email domain appended to a support contact's SSO to build email (`sso@domain`) and Teams-chat links. Blank hides the contact action buttons. | | `dualpath_single_machine` | `true` | Treat a Dualpath pair (a dual-bay machine with one controller) as a single machine in the machines list, dashboard/report counts, and the floor map (the secondary bay is hidden). The data model always keeps both bay records; detail pages stay per-bay with a sibling banner. `false` lists and counts both bays separately. | ### branding diff --git a/docs/CONTRACT-STABILITY.md b/docs/CONTRACT-STABILITY.md index d4cf4da..5a84bd0 100644 --- a/docs/CONTRACT-STABILITY.md +++ b/docs/CONTRACT-STABILITY.md @@ -8,7 +8,7 @@ the live code, not aspiration. The authoritative hook reference is ## Current version -The plugin contract is at **0.7.0**, declared in `shopdb/__init__.py` as +The plugin contract is at **0.10.0**, declared in `shopdb/__init__.py` as `__contract_version__`. It is pre-1.0, which under semver means any 0.x minor bump is allowed to break the contract, and this project has used that latitude. @@ -26,8 +26,10 @@ Recorded in the comment block in `shopdb/__init__.py`: | 0.4.0 | Removed the never-implemented `get_searchable_fields` hook (search is a core concern over the asset model) and wired `get_dashboard_widgets` to a real consumer (`/api/dashboard/widgets`) | pre-1.0 contract reduction | | 0.6.0 | Added the `get_reports` hook, consumed by `GET /api/reports` to merge plugin report cards into the Reports hub | additive optional hook (minor) | | 0.7.0 | Added the four ADR-010 frontend-contribution hooks (`get_settings_cards`, `get_asset_panels`, `get_map_overlays`, `get_asset_presentation`), consumed by the `GET /api/pluginui/*` endpoints | additive optional hooks (minor) | +| 0.9.0 | Exposed the dualpath pair-resolution helpers on `shopdb.api` for the machines plugin | additive surface (minor) | +| 0.10.0 | Added the `get_permissions` hook so plugins declare their own RBAC permissions; the catalog is resolved dynamically from core + enabled plugins | additive optional hook (minor) | -The source comment block documents 0.3.0, 0.4.0, 0.6.0, and 0.7.0. Earlier points +The source comment block documents 0.3.0, 0.4.0, 0.6.0, 0.7.0, 0.9.0, and 0.10.0. Earlier points (0.1.x / 0.2.x) predate that recorded rationale; `PluginMeta`'s fallback `core_version` default of `>=0.2.0,<1.0.0` is the only remaining trace of the 0.2 baseline. @@ -50,6 +52,7 @@ land with a new or amended ADR. | `get_navigation_items` | Sidebar menu entries | | `get_dashboard_widgets` | Dashboard widgets, consumed by `/api/dashboard/widgets` | | `get_reports` | Report cards, consumed by `/api/reports` (added 0.6.0) | +| `get_permissions` | Plugin RBAC permissions, merged into the catalog for roles and token scopes (added 0.10.0) | | Frontend-contribution hooks | `get_settings_cards`, `get_asset_panels`, `get_map_overlays`, `get_asset_presentation`, consumed by `/api/pluginui/*` (added 0.7.0, [ADR-010](adr/ADR-010-frontend-plugin-hooks.md)) | | Collector pair | `get_collector_schema` + `apply_collector_payload` per [ADR-006](adr/ADR-006-collector-contract.md) | | Settings helpers | `get_setting` / `set_setting`, namespaced to the plugin | diff --git a/frontend/src/assets/style.css b/frontend/src/assets/style.css index cb67a49..2fcbcdd 100644 --- a/frontend/src/assets/style.css +++ b/frontend/src/assets/style.css @@ -1320,6 +1320,24 @@ td.actions { font-size: 13px; } +/* Small email/Teams action buttons for support contacts */ +.contact-action { + display: inline-block; + margin-left: 0.35rem; + padding: 0.05rem 0.4rem; + font-size: 12px; + line-height: 1.5; + border: 1px solid var(--border); + border-radius: 4px; + color: var(--link); + background: var(--bg); + text-decoration: none; +} +.contact-action:hover { + border-color: var(--primary); + color: var(--primary); +} + /* Feature Tags */ .feature-tag { display: inline-block; diff --git a/frontend/src/components/AssetRelationships.vue b/frontend/src/components/AssetRelationships.vue index 9f4a95f..181c7b6 100644 --- a/frontend/src/components/AssetRelationships.vue +++ b/frontend/src/components/AssetRelationships.vue @@ -344,7 +344,6 @@ async function resolveAssetId() { resolvedAssetId.value = response.data.data?.assetid lookupFailed.value = !resolvedAssetId.value } catch (error) { - console.log('Asset lookup failed for:', props.machineNumber) resolvedAssetId.value = null lookupFailed.value = true loading.value = false diff --git a/frontend/src/utils/siteSettings.js b/frontend/src/utils/siteSettings.js index e240e19..f6078e6 100644 --- a/frontend/src/utils/siteSettings.js +++ b/frontend/src/utils/siteSettings.js @@ -120,6 +120,16 @@ export async function getPrinterHostnameTemplate() { return getSetting('printer_hostname_template', 'Printer-{ip}.printer.geaerospace.net') } +// Email domain appended to a contact SSO to build email/Teams links +// (sso@domain). Unset falls back to geaerospace.com; an explicit blank value +// means "disable contact action buttons", so read the raw setting rather than +// getSetting (which swaps '' for the fallback). +export async function getContactEmailDomain() { + const settings = await loadSettings() + const value = settings['contact_email_domain'] + return (value === undefined || value === null) ? 'geaerospace.com' : value +} + // Apply per-site favicon + brand color at bootstrap. Empty settings keep the // shipped defaults. Do not touch the style.css palette here. export async function applyBranding() { diff --git a/frontend/src/views/applications/ApplicationDetail.vue b/frontend/src/views/applications/ApplicationDetail.vue index 9329edd..180bb46 100644 --- a/frontend/src/views/applications/ApplicationDetail.vue +++ b/frontend/src/views/applications/ApplicationDetail.vue @@ -66,6 +66,10 @@ {{ contact.name }} ({{ contact.sso }}) + @@ -136,6 +140,7 @@ import { ref, onMounted } from 'vue' import { useRoute } from 'vue-router' import { applicationsApi } from '../../api' +import { getContactEmailDomain } from '../../utils/siteSettings' const route = useRoute() @@ -143,9 +148,16 @@ const loading = ref(true) const app = ref(null) const versions = ref([]) const installedOn = ref([]) +const contactEmailDomain = ref('') + +// Build sso@domain for a contact. Assumes contact.sso and domain are set. +function contactEmail(contact) { + return `${contact.sso}@${contactEmailDomain.value}` +} onMounted(async () => { try { + contactEmailDomain.value = await getContactEmailDomain() // Load application details const response = await applicationsApi.get(route.params.id) app.value = response.data.data @@ -155,7 +167,6 @@ onMounted(async () => { const versionsRes = await applicationsApi.getVersions(route.params.id) versions.value = versionsRes.data.data || [] } catch (e) { - console.log('No versions data') } // Load installed on which PCs @@ -163,7 +174,6 @@ onMounted(async () => { const installedRes = await applicationsApi.getInstalledOn(route.params.id) installedOn.value = installedRes.data.data || [] } catch (e) { - console.log('No installed data') } } catch (error) { console.error('Error loading application:', error) diff --git a/frontend/src/views/machines/MachineForm.vue b/frontend/src/views/machines/MachineForm.vue index ca29fc2..e31f34a 100644 --- a/frontend/src/views/machines/MachineForm.vue +++ b/frontend/src/views/machines/MachineForm.vue @@ -523,7 +523,6 @@ onMounted(async () => { } } } catch (e) { - console.log('Could not load relationships') } } } diff --git a/frontend/src/views/pcs/PCDetail.vue b/frontend/src/views/pcs/PCDetail.vue index bf33a02..2907262 100644 --- a/frontend/src/views/pcs/PCDetail.vue +++ b/frontend/src/views/pcs/PCDetail.vue @@ -268,7 +268,6 @@ onMounted(async () => { const appsResponse = await applicationsApi.getMachineApps(route.params.id) installedApps.value = appsResponse.data.data || [] } catch (appError) { - console.log('No installed apps data:', appError.message) } // Warranties load via useWarrantyBadge (watches computer.assetid). } catch (error) { diff --git a/frontend/src/views/settings/AuditLogs.vue b/frontend/src/views/settings/AuditLogs.vue index c5fec50..043a548 100644 --- a/frontend/src/views/settings/AuditLogs.vue +++ b/frontend/src/views/settings/AuditLogs.vue @@ -61,7 +61,7 @@ {{ log.entityname || `#${log.entityid}` }} - {{ log.username || '-' }} + {{ log.username || '-' }} {{ log.ipaddress || '-' }} + + + + + + - + No support teams found @@ -116,6 +75,56 @@ + + +