Versions
Release history — what changed in each version of Open Gauge, and why.
Open Gauge tracks one version number across the frontend and backend. It's displayed at the
bottom-left of the sidebar (v4.1.0 · self-hosted) and in the OpenAPI schema. This page mirrors
VERSIONS.md at the repo root.
History below 1.0.0 is retrofitted from the git commit log, numbered by semantic-versioning
impact (0.x while the product was pre-release/unstable, 1.0.0 at the point it became a
documented, licensed, self-hostable product). Patch releases (x.y.Z) break out the smaller
fixes and incremental additions that landed between each minor version.
4.17.0
Added
- Unified template management: base, default, and active/inactive. The admin Templates
section is now a collapsible sidebar group with two panels — Calibration Reports and
Procedures — sharing one row layout and one set of actions instead of the previous flat,
certificate-only section. Every kind now has a permanent, non-deletable, non-editable base
template ("Open Gauge (default)"), lazily created the first time its panel is opened or its kind
is generated with nothing configured — no migration seeding, so it works on a fresh install with
zero setup. Every template — base or custom, LaTeX or editor-authored — has two independent
controls: Default (exactly one per kind is used when a document is generated without an
explicit template choice; for Calibration Reports this is a single default shared across legacy
.texand editor templates) and Active (an inactive template drops out of every download picker while staying in the list, editable — distinct from Delete, which removes it from the list entirely). Disabling or deleting the current default falls back to the base template automatically; the base template itself can't be disabled while it's the only default. - Calibration report resolution now includes the admin-set default from either template
system. Previously the org/global fallback (the last step before the built-in template, when
nothing is assigned to the calibration or its sensor channel) only ever considered legacy
.texcertificate templates — an editor (WYSIWYG) certificate template could never be the fallback default. It now resolves to whichever template (either kind) is marked default in Admin → Templates → Calibration Reports. - Procedure downloads gained the same treatment: downloading with no template chosen now renders with the template marked default in Admin → Templates → Procedures (falling back to the base template) instead of always using an in-code fallback document with no way to change it.
4.16.0
Added
- Download a procedure as PDF. Every procedure's detail page gained a Download PDF button
next to Edit — it live-renders the procedure's current data (metadata, required equipment,
materials, environmental conditions, safety instructions, steps, and acceptance criteria) into a
PDF, using a published Calibration Procedure document template (the same WYSIWYG editor
certificate templates use). If one or more procedure templates are published, the button's
chevron menu lets you pick which one to render with for that download (a one-off choice, not a
saved default); with none published yet, it falls back to a new built-in default procedure
template that covers every field on the Procedure model, the same way certificate generation
has always had a built-in fallback — so the button works on a fresh install before anyone has
designed a custom layout. New endpoint:
GET /procedures/{id}/download(optionaldocument_template_idquery param). - The document template editor's "Calibration Procedure" variable catalog is now complete.
Added the procedure fields it was missing (
procedure.description,procedure.author,procedure.duration_min,procedure.tags,procedure.physical_quantity,procedure.difficulty) and a new Materials collection (material.name/material.quantity, matching the procedure'smaterialslist) that had no variable representation at all before. The server-side renderer's collection support was extended to match —required_equipment,materials,environmental_conditions,steps,acceptance_criteria, andsafety_instructionsnow all resolve to real repeating-table rows (previously onlymeasurements, a certificate-only collection, was wired up server-side), and the admin "Preview Data" mode for a procedure template now uses real procedure-shaped sample data instead of certificate sample data.
4.15.1
Fixed
- The
channel.accuracycertificate/report variable no longer repeats the unit. It rendered as e.g.±1 %FS % of full scale—accuracy_unit(%FS) and a human label ofaccuracy_type(% of full scale) were both appended, describing the same thing twice. It now renders as±1 %FS, matching the accuracy display already used on the asset detail page.
4.15.0
Changed
- Redesigned the Admin → Templates section's creation flow. The three stacked inline forms (upload LaTeX / create from scratch / import JSON) are replaced by a single "+ New template" button opening a modal with three clear options — Import LaTeX file, Import JSON template, Create new template — each importer showing a live, client-side preview of the selected file's contents plus Name/Version fields before submitting.
- The template list now uses one unified row layout for both LaTeX and editor (JSON)
templates: preview thumbnail, name, a
v{n}version tag, and right-aligned actions. Edit and Duplicate now only appear for editor templates — LaTeX templates are download/delete only, matching that they can't be opened in the live editor. A Download action was added to every row (raw.texfor LaTeX templates, exported.jsonfor editor templates); clicking a row's preview thumbnail still opens the existing enlarge-to-modal PDF preview for either kind. - LaTeX certificate template uploads can now set a starting version number (defaults to 1,
as before) via a new
versionfield onPOST /certificate-templates.
Added
GET /certificate-templates/{id}/download— streams a LaTeX template's original.texsource as an attachment.POST /document-templates/{id}/preview— renders an editor (JSON) template to PDF using a static sample dataset, the same shape as the live editor's own "Preview Data" mode, so an admin can preview a document template (including unpublished drafts) without generating a real calibration report first.- Document-template import (
POST /document-templates/import) can now override the imported template'snameand startingversion.
4.14.0
Added
- The certificate template context now exposes
is_indicated_style/measured_or_indicated_label, mirroring the web app'sisIndicatedStyle(asset-detail-client.tsx) — true for "reference_vs_indicated" and for "reference_vs_as_found_as_left" with no model. The built-in DAI-based dataset table now uses this to show the same columns, in the same order, as the calibration detail view: an "Indicated" (not "Measured") column, and no "Fitted" column at all for these — a skip_fit calibration's "fitted" value is just the measured value echoed back, not a real fit, so showing it was misleading.
Changed
- The signature pad now draws in a blue-pen tone (
#1e3a8a) instead of near-black (#151f28) — new signature captures only; already-saved signatures are unaffected.
4.13.0
Added
- A residuals chart (reference vs. residual) is now generated for every certificate with
calibration data, mirroring the web app's
ResidualsChart. The existing fitted-curve chart (measured vs. reference with the fit line) now only generates when there's actually an evaluable curve to draw —poly_coefficients, a declared custom formula, or a lookup table — matching the web app'shasEvaluableCurvebehavior. A skip_fit calibration (e.g. "Reference vs Indicated", which has no transfer function by design) previously still got a "Measured vs Reference" scatter with no fit line; it now correctly shows only the residuals chart, same as the calibration detail view.
Changed
- The certificate template's
uncertainty_summary_rowscontext field is now the itemized GUM uncertainty budget — one named row per contribution that actually applies (Fit residuals / Reference standard / Resolution / Sensor nominal accuracy, whichever were supplied), followed by Combined (RSS) and Expanded (±) totals — instead of a collapsed Type A / Type B summary.
Fixed
GET /calibrations/{id}/certificate/downloadno longer rejects calibrations that have no polynomial coefficients by design (skip_fit modes — "Reference vs Indicated" and "Reference vs As-Found/As-Left" with no model — and Lookup Table calibrations, none of which fit a curve, but all of which have real computed statistics). The endpoint's "has this been analyzed" check now looks atmax_error(computed for every analyzed mode) instead ofpoly_coefficients(only ever set when there's an actual curve).
4.12.0
Added
- The certificate template context now exposes an organization's full legal name
(
org_legal_name) alongside the existing short display name (org_name) —Organization.full_namewas already a stored field but was never surfaced to certificate templates. Falls back toorg_name(and, with no organization at all, to "Open Gauge") when a legal name isn't set, so templates that want the registered/legal name on a footer or signature block (rather than the shorter branding name) have a value to use.
4.11.0
Added
- Calibration certificates can now show a "checked by" signature alongside the existing
"prepared by" one.
Calibration.checked_by_user_id/checked_by_namewere already recorded on the calibration but were never surfaced to the hand-authored (.tex) certificate template pipeline —certificate_servicenow resolves the checked-by user's active signature the same way it already does for the performer, and exposeschecked_by_name/checked_by_signature_pathin the template context alongside the existingperformer_name/performer_signature_path. - A compact Type A / Type B / combined / expanded uncertainty summary is now available to
certificate templates (
uncertainty_summary_rows), for templates that want the GUM budget collapsed to these four named totals instead of one row per itemized budget source. Type A is the fit-residuals budget entry; Type B is the RSS of every other entry — both derived from the existinguncertainty_budgetdata, no new fields stored.
Fixed
- A calibration's conformity specification (the tolerance quoted against a "CONFORMS"/"DOES NOT CONFORM" statement) is now expressed with its unit for absolute and manually-overridden tolerances (e.g. "±0.75 °C (absolute)" instead of "±0.75 (absolute)") — a bare number with no unit on a certificate is ambiguous and not acceptable in the units it's presented alongside. Percent-based specifications ("of reading" / "of full scale") were already unambiguous and are unchanged.
4.10.0
Added
- Document Templates now merge into the Admin panel's Templates section alongside LaTeX
certificate templates, gain export/import, and — the missing piece from 4.9.0 — can actually
generate calibration reports. The admin "Templates" section (formerly "Certificate
Templates") lists both authoring paths side by side: upload a
.texfile, or "Create from scratch" in the live editor; an editor template can be exported to a portable JSON file and re-imported (round-trips through the editor, unlike a raw.texupload). A published editor template is rendered to PDF by generating LaTeX from its structured JSON document and compiling it with the existing Tectonic pipeline — reusing the LaTeX toolchain end to end (Jinja escaping,qrcode/matplotlibimage rendering, PAdES signing) instead of adding a new PDF dependency. Each sensor channel can be given a default report template (either kind); a calibration can override it individually via a template picker on its Download button, restricted to users with edit rights — everyone else always downloads whichever template the override, sensor default, or org/global fallback resolves to, with no picker of their own. The old top-level "Document Templates" sidebar entry and its list page are gone (the editor itself is unchanged, still at/templates/[id]); a visit to the old list URL redirects into the admin section.
4.9.0
Added
- A new Document Templates section for designing calibration certificate and procedure
layouts. A three-panel WYSIWYG editor (Tools / A4 page canvas / Variables) lets admins build
reusable, structured document templates — headings, paragraphs, tables (including a
repeating-row binding to a data collection like measurements), a dedicated measurement table,
key/value fields, LaTeX equations, calibration graphs, images, QR codes, signature blocks,
dates, page numbers, columns, and section breaks — with drag-and-drop or click-to-insert from
the tool panel, a contextual text formatting toolbar, and a searchable variable browser scoped
to the template's type (certificate vs. procedure). A "Preview Data" mode swaps every
{{ variable }}token for realistic sample values so the template's final look is obvious before any real data exists. Templates are versioned like calibration history: a published version is immutable, and editing it branches a new draft version rather than mutating it in place. This ships the editor, its structured JSON document model, and backend persistence/versioning — PDF generation from a template is a separate, later task.
4.8.0
Changed
- A saved Lookup Table calibration's detail view didn't match the wizard's own review of the same data. The calibration tab's "General" panel showed a placeholder "Model" equation label (a lookup table has no formula) followed by the generic fit-statistics/uncertainty/conformity layout, most of which is meaningless for an exact interpolant (see the calibration method's own docs). It now shows the same layout Step 3 of the wizard already uses for this method: the lookup table itself (left, 50% width) alongside the Measured-vs-Reference curve and its linearity deviation chart (right, 50%, stacked), with the same hover tooltips and abs/%FS toggle.
Fixed
- A Lookup Table calibration could list several measured values for what was really the same reference point. Repeated readings at (or near) the same nominal reference value were stored as separate rows verbatim, making the delivered table awkward to actually use as a lookup table. Reference values within 1% of the calibration's own reference span of each other are now treated as the same test point and collapsed into a single row (reference and measured both averaged) before the model is built, and the table is always delivered with reference values in ascending order.
- The abs/%FS toggle on the linearity deviation and residuals charts overlapped the plotted points. The toggle was absolutely positioned over the top-left of the chart with no space reserved for it, obscuring points near the top of the plot — on Step 3 of the wizard and, since both charts are shared with it, the calibration detail page too. Both charts now reserve margin above the plotted area for the toggle.
- A Lookup Table's Measured-vs-Reference chart colored points by residual, with a gradient legend, even though its residuals are always ~0 by construction. The color gradient (and the legend explaining it) is meaningful for a real fit, where it highlights which points drove the worst error, but has nothing to show for an exact interpolant. Lookup Table's points now render in a single accent color, matching the line connecting them, with no gradient legend — in both the wizard's Step 3 and the calibration detail page.
4.7.1
Fixed
- A rejected calibration showed a "restore" icon that always failed. Rejected calibrations
share
is_active=falsewith voided ones, so the calibration history list's admin-only action icon treated the two the same and offered a restore action on a rejected record — clicking it always failed server-side, since restore is void-only. Rejected records no longer get an action icon there (they were never restorable — a corrected calibration is a new record, per the calibration philosophy in AGENTS.md). - A channel's output signal range displayed the raw internally-stored value instead of the
user's selected unit.
output_signal_min/maxare stored converted to a base unit (matching every other %FS-capable spec field) and converted back for display everywhere else — except the Specifications panel's "Output range" row, which showed the raw stored number next to the user-facing unit label (e.g. a 4–20 mA signal, stored as 0.004–0.02, showed as "0.004 – 0.02 mA"). It now converts back to the selected unit before display, like the edit form already did. - An admin could approve or reject their own calibration. The checker-or-admin authorization check only verified the decider was the assigned checker or held an admin role — it never checked whether they were also the person who registered the calibration. A second reviewer stops meaning anything if the person being checked can approve themselves, admin override or not, so the endpoint (and the UI's Approve/Reject buttons) now also reject the registrant deciding their own record.
- Duplicating an asset didn't copy its picture, interface images, mechanical table, or CAD files. The asset picture wasn't copied at all, the mechanical interface's picture and table were missing entirely, and the pinout image was copied by reference (pointing the duplicate at the source's own file row) rather than as an independent copy — deleting or replacing either asset's picture would have deleted it out from under the other. Duplicating now copies each file to its own storage object (and CAD files, which aren't columns on the asset, are now copied too) while still leaving calibration history and the serial number behind, as before — a duplicate is a new physical unit's catalog entry, not a clone of its calibration record or nameplate.
- Clicking the mechanical interface picture did nothing. Every other picture in the app opens a preview modal on click; the mechanical interface image never had a click handler wired up at all. It now opens the same shared preview modal. The electrical interface image had the same gap when it has no pinout rows to map (clicking normally opens the pin-mapping tool instead) — it now falls back to the same preview modal in that case too.
4.7.0
Added
- Calibration record detail links the reference standard and procedure. The "General" panel
on a calibration record now shows the Reference standard (linking to the reference asset's
detail page) and Procedure (linking to it in the Procedures register) used for an internal-lab
calibration — previously stored on the record but not surfaced anywhere in the UI. The
Procedures register also accepts a deep link (
/procedures?id=<uuid>) to pre-select a specific procedure, the same convention Sites already supports.
Changed
- Calibration purpose in the General panel now reads like the other fields. It previously rendered as a small colored tag (matching the calibration history nav list, where the tag saves space); in the General panel, where every other field is plain text, that stood out inconsistently. It's now plain translated text there — the nav list's tag is unchanged.
- The "Download Certificate" button no longer shows a template-picker chevron for calibrations with an uploaded certificate. An uploaded PDF certificate is served as-is on download — there's no template to choose — so the split-button's dropdown (which only ever lists certificate generation templates) no longer renders for that case.
Fixed
- A calibration's %FS tolerance was checked against the wrong "full scale." When a sensor
channel's accuracy spec is expressed as "% of Full Scale," conformity (pass/fail) is supposed to
compare the max error against that percentage of the channel's own declared measurement range.
The wizard's default Tolerance box already got this right, but clearing that box (or calling
POST /calibrations/analyzedirectly) fell back to computing "full scale" from the span of this calibration's own reference values instead — silently different whenever the tested range didn't match the sensor's full nominal range./analyzenow accepts the channel's own range (channel_range_min/channel_range_max, sent by the wizard) and uses it for this case, falling back to the previous behavior when it isn't supplied.
4.6.2
Fixed
- A fresh deployment's first admin could never create a location, even after creating an
organization. The "New location" form determined the caller's organization by reading
organization_idoffGET /users/me— a field that no longer exists on the user response since org membership moved to the many-to-manyOrganizationMembertable. The lookup always came back empty, so the form permanently showed "Could not determine your organization. Contact an admin." with no way to proceed, even for a user who had since created and joined an organization. The form now fetches the caller's actual memberships (GET /organizations?mine=true) and offers an Organization picker, the same pattern already used by the asset creation form.
4.6.1
Fixed
- A declared model using a custom formula could never be saved. The calibration wizard's "Confirm & Save" (and "Next" out of Step 2) stayed permanently disabled for a directly-declared model ("Model (transfer function)") using a custom formula, instead of a polynomial — a leftover check depended on an analysis result that the 4.6.0 fake-statistics removal stopped producing. Save-eligibility for this mode now depends only on the wizard's own formula/range validation, which already covers the custom-formula case correctly.
- As-Found/As-Left calibrations built from a Reference vs. Indicated method showed the wrong data table. When the underlying input method was Reference vs. Indicated (no transfer function, no curve fit), the saved calibration's data table still labeled the reading column "Measured" and showed a "Fitted" column — both belong to the curve-fit variant. It now reads "Indicated" and omits "Fitted" for this case, matching the plain Reference vs. Indicated mode's table and the wizard's own Step 3.
4.6.0
Added
- Measured-signal vs. physical-magnitude valid-range entry for a declared model. The calibration wizard's "Model (transfer function)" input method (Step 2) now has a switch next to the valid range fields: enter the range in the measured signal's own terms (x), or in the physical magnitude it maps to (f(x)) — the latter solves for the corresponding measured-signal bounds numerically (bisection), and is only offered when the model is actually invertible (monotonic) over the declared range.
Fixed
- A declared model's uncertainty/conformity numbers were fabricated, not real. Step 3 of the calibration wizard used to run a directly-declared model ("Model (transfer function)") through the same uncertainty-budget/conformity-check machinery as a real dataset, by synthesizing two fake zero-residual points at the declared range's endpoints. That always produced a trivial, meaningless result (0 uncertainty, always conforms) — there's no real data to derive any of that from. Both Step 3 and the saved-calibration view now show just what's actually known for this mode: the model, its declared valid range (measured and the physical magnitude it maps to), and the curve it implies — no uncertainty budget, no conformity check, no fake statistics.
4.5.0
Changed
- Organizations page rebuilt as a single master-detail screen. The list and detail views
used to be separate routes (
/organizationsand/organizations/{id}); they're now one page — a searchable, filterable sidebar (All / Internal / Providers / Customers) next to a detail panel, the same two-panel pattern already used by Locations. Selecting an organization updates the detail panel in place instead of navigating away. Old links and notification links to/organizations/{id}(including the?edit=1deep link used by join-request notifications) still work — they now forward to the merged page. No API or data model changes.
4.4.2
Added
WARM_TECTONIC_CACHEbuild option for theapiimage. The Dockerfile pre-fetches Tectonic's LaTeX package bundle at build time so certificate generation needs no network access at container runtime (a self-hosted/air-gapped requirement) — but that one-time fetch goes throughrelay.fullyjustified.net, which has been observed rate-limiting repeated fetches with HTTP 429. The fetch now also runs against a BuildKit cache mount, so once one build succeeds, later rebuilds on the same host reuse the cached bundle instead of hitting the network again. For the rare case where even the first build is blocked, setWARM_TECTONIC_CACHE=falseininfrastructure/docker/.env(defaulttrue) to skip the pre-fetch — the image still works, it just fetches the bundle lazily on first certificate generation instead of having it baked in. Keep ittruefor any real/air-gapped deployment.
4.4.1
Fixed
- Calibration tab's saved-record view harmonized with the wizard's Step 3. The asset detail
page's Calibration tab had drifted from the calibration wizard's Step 3 analysis panel it was
meant to mirror:
- The model panel used a hand-rolled Equation/Coefficients tab toggle with plain-text
formatting instead of the wizard's KaTeX-rendered symbolic equation + substituted
coefficients. Both views now share the same
ModelPanel/Katexcomponent, so they can't drift apart again. - The statistics panel still showed a "Poly degree" row and a single combined "Valid range" row (reference domain only) — both dropped from the wizard's statistics panel in an earlier release but never updated here. Now shows "Valid range (Measured)" and "Valid range (Reference)" as two separate rows, matching the wizard exactly.
- The metadata panel (calibration date, due date, registered/checked by, type, purpose, lab) used a fixed 2-column grid instead of flowing horizontally across the panel.
- The model panel used a hand-rolled Equation/Coefficients tab toggle with plain-text
formatting instead of the wizard's KaTeX-rendered symbolic equation + substituted
coefficients. Both views now share the same
4.4.0
Added
- Shared
SelectandDatePickercomponents, and app-wide numeric input standardization. UI.md now defines consistent width tiers and behavior for every numeric input, dropdown, and date field in the app:- Numeric inputs — the chevron-button
NumberInputcomponent (previously only used in the Calibration Wizard's Step 3) is now used app-wide, sized to the value rather than the label (w-16to aw-32ceiling). - Dropdowns — a new shared
Selectcomponent replaces the several near-duplicate per-page<select>wrappers. It stays a native<select>(full keyboard nav, mobile picker, screen-reader support) with a restyled chevron; the trigger caps at a width tier (w-20to aw-64ceiling) and ellipsizes long labels with a hover tooltip, while the native option list still auto-sizes to the widest option. - Date pickers — a new shared
DatePickercomponent replaces the native<input type="date">with a themed popover calendar (month navigation,og-accentday markers, a month/year quick-jump view) — a date is always picked from the calendar, never typed, avoiding locale date-format ambiguity. - Applied across every screen that previously hand-rolled its own numeric input,
<select>, or date input (admin, procedures, sites, assets, asset detail, calibration wizard, health tab, interface tab, organization detail).
- Numeric inputs — the chevron-button
4.3.0
Added
- Asset "Interface" and "CAD" tabs. An Electrical panel (connector image + pinout table with
multi-color wire swatches, signal name, description, and a click-to-place pin Mapping
dialog) and a Mechanical panel (drawing + mounting-points table), both read-only until the
page's own Edit button is used — plus a CAD tab for uploading and live-previewing
STL/STEP/IGES/BREP files (STL via
three, the rest triangulated in-browser by theocct-import-jsOpenCascade WASM build), whose upload drop zone works the same way. See Asset registry → Interface tab and → CAD tab.
Fixed
- Asset picture upload/removal (and the new pinout/mechanical-image uploads) no longer blank the header's calibration/location stats — they now reload the full profile after saving instead of trusting the upload endpoint's slimmer response shape directly.
4.2.0
Added
- Procedure export/import. Admin and superadmin users can export procedures as ZIP bundles
(
procedure.yaml+ amedia/steps/{n}/folder per step attachment) and re-import them on another instance —GET /procedures/{id}/export,POST /procedures/export/bulk,POST /procedures/import, andPOST /procedures/import/validateon the backend (app/services/procedure_export.py/procedure_import.py, mirroring the existing asset export/import format). On the Procedures page, Export and Import buttons sit next to + New Procedure for bulk actions, and + New Procedure → Import from file handles a single-procedure ZIP — both gated to admin/superadmin, unlike the asset equivalents which allow any non-viewer. See Procedures → Import & export.
4.1.0
Added
- Calibration purpose tag on each row of the Calibration tab's history list — colored by
purpose (Initial: blue, Routine: green, After Repair: orange, Verification: purple), via a new
CALIBRATION_PURPOSE_STYLEmap inapps/web/src/lib/tokens.tsand atokens.calibrationPurposetranslation namespace, shared with the same tag now also shown in the calibration metadata panel below.
Changed
- Harmonized calibration detail view. The Calibration tab's left-side record view now leads
with a single metadata panel covering calibration date, due date, registered/checked by,
calibration type and purpose, calibration lab (resolved per calibration type: manufacturer
snapshot, external organization, or internal location), environmental conditions, and notes —
each field with a tooltip linking to the matching wizard-field documentation. The small header
line (
v6 · date · by name) and the standalone Conditions & Notes panel are both gone — this one panel replaces them, with the Approve/Reject actions and certificate download button moved into its header row. Below it, the model/equation panel and the statistics/uncertainty/conformity panel are now laid out side by side (model left, statistics right), with the chart/data-table view spanning full width beneath them — mirroring the layout of step 3 of the calibration wizard for the record'sdata_entry_mode. See Viewing a calibration record.
4.0.0
Changed
- Breaking: Frequency response is now a 4th calibration input mechanism, not a separate wizard
step. The original implementation (an optional extra step gated by a Step 1 checkbox, backed
by a
calibration_frequency_pointstable and 5calibrationscolumns) has been fully reverted and reimplemented as a properdata_entry_mode, selected from Step 2's own Input data method dropdown alongside Reference vs Measured/Indicated and Model, open to any Calibration purpose. Enter a sweep of (frequency, reference, measured[, offset]) points; Step 3 shows one sensitivity panel — pick a baseline frequency and Open Gauge computes the sensitivity ratio (measured/reference) there, expressing every other point as a % deviation from it — with a sensitivity-vs-frequency chart (always) and a phase-vs-frequency chart (only with offset enabled), replacing the deleted feature's Method/Uncertainty/Conformity panel stack and dual-axis chart entirely: this mode has no curve fit, uncertainty budget, or conformity check. Internally represented as a polynomial-order-1, no-offset (gain-only) model, the same convention every other mode uses. The oldGET /calibrations/{id}/frequency-pointsendpoint and its backing table/columns are gone (migration035); the newGET /calibrations/{id}/frequency-response-pointsendpoint andcalibration_frequency_response_pointstable replace them (migration036). Any client relying on the old shape must migrate. See the new Frequency response guide page.
3.15.0
Added
- Mermaid diagram support in the Knowledge Center.
apps/docsnow renders```mermaidcode fences as live, theme-aware SVG diagrams (viafumadocs-core'sremarkMdxMermaidplugin and a new client-sideMermaidMDX component that loads themermaidlibrary and re-renders on light/dark theme changes) — available on every guide page going forward, not just the one below. - "Choosing a workflow" section on the Calibration overview guide, with a flowchart tracing every combination of Calibration purpose and Step 2's input data method through to its outcome — what gets a fitted equation, what's residual-only, what's diagnostic-only, and what always feeds the due date/Health tab regardless of path.
3.14.0
Added
- Calibration method for raw data. Step 3 (Analysis) of a raw-data calibration offers a
Calibration method selector alongside the regression degree — Polynomial Fit (default),
Lookup Table (no curve fit; the entered points are the model, linearly interpolated —
R²/RMSE/max error/non-linearity hidden since they're trivially perfect, but the uncertainty
budget and conformity check still compute normally), and Custom Formula (write a formula
shape with free parameters, e.g.
a*x*sin(x) + b, fit via nonlinear regression). Any custom-formula input (this method, andmodel_direct's own) auto-detects free parameter names as you type — see Custom formula syntax. - Lookup Table linearity deviation chart. Since an exact interpolant has no fitted curve, non-linearity doesn't apply — Open Gauge instead fits a straight line through the calibration's own points and charts how far the piecewise-linear interpolation strays from it (absolute/relative %FS), with markers at the entered points. Diagnostic only, not compared against a tolerance.
NumberInputcomponent (@/components/number-input) — theme-aware chevron up/down buttons replacing the browser's native, unstyleable number-input spinner app-wide; typing, arrow keys, and the scroll-wheel all still work. Native date inputs are themed per color scheme too (the calendar icon recolors for light/dark viacolor-scheme, no per-field change needed). UI.md gained a "Compact Numeric Inputs" section documenting the pattern: size inputs to their content, shorten/truncate long labels, useNumberInput.
Changed
- Step 3 (Analysis) reorganized into three panels: Method (raw data only — the
Calibration method dropdown with its dependent field, laid out horizontally), Uncertainty
calculation (distribution, confidence level with a live coverage-factor readout, and opt-in
Sensor nominal accuracy / Reference standard uncertainty toggles, each shown in a fixed unit as
plain text with a refresh icon restoring the channel's own default), and Conformity
assessment (decision rule dropdown, always-visible Error/Uncertainty/Tolerance boxes forming
the real
[error] ± [uncertainty] ≤ [tolerance]expression, an editable Tolerance box that locks to Sensor nominal accuracy while that toggle is on, and a CONFORMS/DOES NOT CONFORM badge). All three are hidden for Lookup Table, whose points are exact by construction. The Statistics panel's valid range now splits into two lines (measured signal, physical quantity) and dropped its redundant polynomial-degree row (already visible in the Model panel). The old "Equation" panel is now "Model" — two theme-aware LaTeX rows (general form, numeric coefficients) sharing one horizontal scroll region between them instead of each row scrolling independently. - Calibration wizard: input-data method selection is now Step 2's own dropdown ("Input data") — Reference vs Measured (renamed from "Raw data," unchanged behavior), Reference vs Indicated, or Model (transfer function) — with the rest of the step rendering immediately below the current selection.
- Reference vs Indicated now uses the channel's physical-quantity unit for its second column instead of the raw output-signal unit, is available for every Calibration purpose (not just Verification), and Step 3 labels that column "Indicated" with no separate "Fitted" column (meaningless when there's no fit).
- As-Found/As-Left is now an automatic consequence of Calibration purpose = After Repair,
applied to Reference vs Measured or Reference vs Indicated, instead of its own
separately-selectable method. Reference vs Measured can now curve-fit both sides independently,
even to different polynomial degrees, since the instrument's behavior may have genuinely
changed across the repair. Step 2's as-found/as-left tables sit side by side, sharing one
Reference/Measured unit row above both; Step 3 shares one Uncertainty calculation and
conformity-criteria (decision rule + tolerance) panel across both sides, with two columns below
showing each side's own criteria readout (
[error] ± [uncertainty] ≤ [tolerance]+ CONFORMS/ DOES NOT CONFORM badge), model (if fitted), and statistics. - The Health tab's curve-comparison dropdown excludes Lookup Table calibrations (no formula/ coefficients to compare, only its own points) — same treatment as reference-vs-indicated/ as-found-as-left modes.
Fixed
- Conformity assessment panel's "locked to sensor accuracy" / "no accuracy spec" explanatory text under the Tolerance box, and the saved-calibration detail view's Lookup Table equation readout for curve-fit As-Found/As-Left records — both had briefly regressed during the panel reworks above.
3.13.0
Added
- Alternative calibration data-entry modes. A calibration's data can now be entered four
ways, selected on the wizard's Step 1 (Data entry mode): the existing raw data
(reference vs. output signal, fitted to a curve); model provided directly — a lab/
manufacturer certificate that states the model itself (a polynomial or a custom formula in
a single variable
x), its valid range, and the same uncertainty-budget inputs raw data collects, with conformity assessed by treating the model as trusted-as-declared; reference vs. indicated value (verification only) — a lab-delivered reference/already-converted-value comparison with no known transfer function, yielding statistics/uncertainty/conformity but no fitted curve, rendered via a residuals-only chart; and reference vs. as-found/as-left value (after repair only) — the same fit-free comparison recorded twice, before and after the repair, with as-left driving the record's official result (due date, approval, Health tab) and as-found kept as read-only diagnostic data. - Custom formula models. A
model_directcalibration's model can be a polynomial or a single-variable formula (+ − * / ^ ( ),sqrt exp log ln sin cos tan abs pow), fully evaluable — it participates in the saved calibration's fitted-curve chart and the Health tab's drift/curve comparison exactly like a polynomial, evaluated independently on each side via a shared, restricted expression whitelist.
Changed
- The Health tab's curve-comparison dropdowns now only offer calibrations that have an evaluable model, and show an explanatory empty state instead of a generic error when a channel has fewer than two.
- Calibration certificates now render a custom formula's equation directly instead of a blank coefficients table. A saved calibration's detail view gained a diagnostic "as-found" panel for after-repair calibrations recorded via the new as-found/as-left mode.
3.12.0
Added
- Residuals chart. A new chart appears under the calibration model's fitted-curve chart on both the wizard's analysis step and a saved calibration's detail view — reference standard measurement on the x-axis, residuals (absolute or relative %, via a toggle) on the y-axis, with a zero reference line and full hover detail. Built as a shared component so both screens stay in sync, and sized so the two stacked charts together match the height of the adjacent statistics panel.
- Environmental condition uncertainty. Temperature, pressure, and humidity readings on a calibration can now each carry their own ± uncertainty, entered on the wizard's Step 1 and shown alongside the reading wherever environmental conditions are displayed. Pressure now defaults to hPa instead of Pa.
- Doc-linked tooltips on every wizard Step 1 field, each pointing at a new field-by-field reference page (Reference → Calibration → "Calibration wizard field reference") with an example for how to fill it.
Changed
- Wizard Step 1 layout. Calibration date and interval now share a row; the "Add frequency response" and "Coefficients only" switches moved to a single row directly above Environmental Conditions; the calibration certificate upload is now a drag-and-drop dropzone, matching the pattern used elsewhere in the wizard.
- Toggle switches no longer show "On"/"Off" text next to the control anywhere in the app — a toggle's own meaningful adjacent label (e.g. "Private", "Include in budget") already says what it does; the redundant state word is gone.
3.11.0
Added
- Calibration type and purpose. Every calibration now records a type — OEM, External Accredited Lab, Internal Lab (In-house), or Customer's Asset — which drives what the Calibration lab field resolves to (a read-only manufacturer snapshot, a picker over external organizations marked as providers/customers, or the existing internal-location picker, respectively) and, for OEM/External Accredited Lab, unlocks an optional PDF Calibration certificate upload (validated by content type and file signature) that takes priority over the system-generated certificate everywhere it's downloaded, and appears — badged, non-deletable — in the asset's Files tab. Every calibration also records a purpose — Initial (defaulted automatically for a channel's first calibration), Routine, After Repair, or Verification. Recording an After Repair calibration captures a repair date/description and adds a before/after-repair period dropdown to the Health tab, so drift and stability metrics can be compared across a repair instead of always spanning the full, repair-mixed history.
3.10.0
Added
- Calibration approval workflow. Every calibration now has a status — valid, pending approval, rejected, or void — instead of just active/voided. Naming an optional Checked by reviewer on the wizard's Step 1 (a new field alongside the renamed Registered by, both now real dropdowns of the asset's organization members instead of a self/free-text toggle) puts the new calibration into pending approval; only a valid calibration counts for the channel's calibration, due-date calculations, and drift analysis — pending and rejected are excluded exactly like voided always was. The assigned checker (or an admin override) approves or rejects it from its row in the Calibration tab, with notifications (in-app and email) to the checker on assignment and to the registrant on decision. Void remains an always-available admin override reachable from any status. The calibration-visibility toggle is renamed "Show Valid Calibrations".
3.9.0
Added
- Internal vs. external organizations. Every organization now has a category — Internal (the existing self-service, joinable workspace, unchanged) or External, a directory entry for an organization outside the system with a calibration relationship to it, typed as a Provider (an OEM or accredited lab supplying calibration services) or a Customer (receiving them). External organizations carry a contact email/phone (distinct from the organization's general email/phone), a structured address (street/city/state/postal code/country, independent of the location tree used for asset locations), and a VAT number, and have no members. Seeing, creating, editing, and deactivating an external organization is restricted to Admin and Super Admin — every other role never sees one, including by direct URL. The Organizations list gained an Internal/External/Both filter (Admin/Super Admin only), and a further Provider/Customer/Both filter when External is selected.
3.8.0
Added
- Frequency response data entry in the calibration wizard. An optional "Add Frequency
Response" checkbox on Step 1 inserts a new step for capturing an amplitude (dB, RMS,
Peak-to-Peak, or Peak) and/or phase (degrees or radians) sweep against frequency, via manual
entry or CSV upload. The step live-plots the entered points with no server round-trip — a
single-axis chart when only amplitude or only phase is captured, or a stacked Bode-style
layout (amplitude on top, phase below, sharing one x-axis) when both are, with automatic
logarithmic frequency-axis detection for sweeps spanning a decade or more. Saved sweeps are
stored in a new
calibration_frequency_pointstable and shown on the calibration detail page in a new "Frequency Response" panel with an interactive chart and data table.
3.7.0
Added
- Unsaved-changes guard on the asset detail page. Opening the calibration wizard while the
asset is in edit mode with unsaved changes now shows a confirmation dialog offering to save
those changes first or discard them, instead of silently losing them. Built on the shared
ConfirmModalcomponent; the guard (guardBeforeLeavingEdit) is written to be reusable for any future action that would otherwise discard an in-progress asset edit.
3.6.1
Changed
- Scrollbars now match the app's own colors instead of the browser default, documented in
UI.md's new Scrollbars section. - Notification bell button now hovers the same way as the language/theme buttons.
- User profile picture in the top bar enlarges on hover.
Fixed
- A user's profile picture on their
/users/{id}detail page didn't open the preview modal on click — it now uses the sharedImageUploadField(read-only mode), matching every other picture in the app. - Tooltips inside a collapsed panel's field rows (asset overview) were clipped.
CollapsibleSectionno longer clips overflow to round its corners — the header button gets conditional rounding instead. - Tooltip "view documentation" links 404'd in demo mode —
tooltip.tsxnow uses the locale-awareLinkfrom@/i18n/navigationinstead of plainnext/link.
3.6.0
Changed
- Assets are now navigated by their human-readable asset ID (e.g.
OG-00042), not the internal UUID. Every place that links to an asset detail page — the asset registry list and grid, dashboard widgets (Upcoming calibrations, Recent assets, Activity), top-bar search, and the "New/Duplicate/Import asset" flows — now builds the URL fromasset_idinstead ofid. QR/sticker labels now encode the sameasset_id-based URL. Every/assets/{ref}endpoint (and its nested resources) resolves{ref}as either theasset_idor the internal UUID, so old bookmarked or previously-printed UUID-based links keep working unchanged.
Fixed
- Demo mode 404'd when clicking an asset from the registry — the registry's list/grid
links were plain
<a href>tags instead of next-intl's locale-awareLink, so the un-prefixed URL 404'd in the demo's static export (no middleware to add the locale prefix). Switched toLinkfrom@/i18n/navigation, matching every other asset link in the app.
3.5.2
Fixed
- Activity register panel's description sat flush against the user column instead of
reading toward the right edge of the panel, and never showed a user's profile picture
even when one was set.
dash_repo.get_activity(apps/api/app/repositories/dashboard.py) never resolvedprofile_picture_idinto a presigned URL the way the full/activitypage'saudit_log_enrich.enrich()already does — soActivityFeedalways fell back to initials.ActivityItem(schema + frontend type) now carriesactor_profile_picture_url, resolved via the shareduser_profile_svc.resolve_picture_url, andActivityFeed's description column is right-aligned to match the "Upcoming calibrations" panel's right-aligned date column.
3.5.1
Fixed
- Upcoming calibrations panel showed overdue calibrations, and some due dates were wrong.
dash_repo.get_calibration_eventspicked each asset's most recently created calibration record to determine its due date; on an asset with multiple independently-scheduled channels (e.g. two sensor channels calibrated at different times), the newest record isn't necessarily the one that governs the asset's current status, so an asset could be flagged overdue even though its actual next due date (the furthest-out among its active calibrations) was still in the future. The query now aggregatesmax(due_date)per asset — the same logic already used byrepositories/asset.py::list_assetsand the dashboard summary's status counts — and only returns assets whose due date is today or later, so the panel no longer double-duties as an overdue list. - Activity register rows dumped all content in one left-aligned column. The dashboard's
ActivityFeedrows now lay out as user (with timestamp underneath) in a fixed-width left column and the action/diff description in the remaining space, instead of a single stacked block; removed the decorative blue dot bullet.
3.5.0
Added
- Google Analytics on project-operated deployments only. A new
<GoogleAnalytics />component (apps/web/src/components/google-analytics.tsx) conditionally loads gtag.js whenNEXT_PUBLIC_GA_MEASUREMENT_IDis set at build time, mounted once in the shared root layout so it covers both the marketing/login page and every route under the demo build. The variable is set only in the build environment of the project's own deployments (the marketing/login site anddemo.opengauge.org's Cloudflare Pages project) — seeapps/web/README.md. It is intentionally absent frominfrastructure/docker/.env.example, so self-hosted Docker Compose installs never load it, keeping the existing Privacy Policy commitment (self-hosted Instances use no third-party analytics) intact. The Privacy Policy page now also discloses this usage, but only renders the disclosure when the variable is actually present at build time. - Cookie consent banner on the marketing/login page. A new
<CookieConsentBanner />(apps/web/src/components/cookie-consent-banner.tsx), translated into all four locales via the newcommon.cookieConsentmessage namespace, gates Google Analytics behind an explicit Accept/Decline choice —<GoogleAnalytics />now waits for"granted"consent (apps/web/src/lib/consent.ts+apps/web/src/hooks/use-consent.ts) before loading gtag.js on that page. A visitor who has already sent a Global Privacy Control or Do Not Track signal is opted out automatically and never sees the banner. The demo build has no login page to show a banner on (visitors skip straight to/dashboard), so it keeps loading analytics unconditionally once the env var is set, unchanged from before — a gap to revisit if the demo needs the same consent flow later.
Fixed
- Demo static export 404'd on
/dashboard(and would have on most other in-app navigation for English specifically). The same root cause as the bare-root 404 above, one layer deeper:localePrefix: "as-needed"means every locale-awareLink/redirect/useRoutercall (@/i18n/navigation) generates an unprefixed URL for the default locale (e.g./dashboard, not/en/dashboard), trusting the middleware to rewrite it — which, again, doesn't run in the static export.apps/web/src/i18n/routing.tsnow forceslocalePrefix: "always"specifically whenNEXT_PUBLIC_DEMO_MODE=true, so every generated href is explicitly prefixed and matches whatgenerateStaticParamsactually emits; the normal server build is unaffected. Two call sites were also bypassing locale-aware routing entirely —components/notification-bell.tsxandapp/[locale]/(app)/assets/page.tsximporteduseRouterstraight fromnext/navigationinstead of@/i18n/navigation— switched to match every other page/component in the app.components/user-summary.tsx(renders anywhere a user is shown in a list: activity entries, member lists, the admin users list) had the same bug with a rawnext/linkto/users/{id}, switched likewise. - Demo static export 404'd on the bare root URL.
localePrefix: "as-needed"(apps/web/src/i18n/routing.ts) relies on thenext-intlmiddleware (apps/web/src/proxy.ts) to rewrite/to the default locale on the fly — but that middleware only runs on the normal server build. The demo's static export (output: "export") has no server to run it, so the bare root produced no file at all and hosts fell through to their 404 page. A newapps/web/src/app/page.tsx(outside the[locale]segment) now covers that gap with aredirect()to the default locale, which works on any static host and is a no-op for the normal build (the middleware already intercepts/before this route would ever be reached there). apps/web/package-lock.jsonwas out of sync withpackage.json, causingnpm cito fail on any clean-install build (Cloudflare Pages included) withEUSAGE/ missing-package errors. Regenerated against a Linux Node 22 environment specifically (matching Cloudflare's build image) rather than Windows, since the drift included Linux-only optional native dependencies (@emnapi/core,@emnapi/runtime) that a Windowsnpm installdoes not resolve.
3.4.0
Added
- Granular, translated activity/audit log. Updates to assets, organizations, procedures,
locations, and (admin) users now record a real field-level diff instead of a generic action
code or a coarse whole-object dump — e.g. an asset update shows exactly which fields changed
and their old/new values, and a sensor channel's own field changes are captured too (e.g.
"channel CH1: Physical quantity — Temperature → Pressure"), not just a channel count.
app/utils/audit_diff.pyprovides the sharedsnapshot()/diff_snapshots()helpers now used across every touched update endpoint; a matched channel getschannel.<id>.<field>keys, and a wholly added/removed channel gets a single summary row instead of a per-field dump. Admin edits to another user's role, organization,is_active, oris_verified(PUT/DELETE /users/{id}) are now audited at all — they previously wrote no log entry whatsoever, despite being privilege-bearing changes. On the frontend, a new sharedActivityDiffcomponent renders these diffs — translated field labels via a newtokens.auditFieldcatalog, and translated values for enum-backed fields (physical quantity, technology, role, etc.) viatranslateAuditFieldValue— in the Activity page, the dashboard activity feed, and the asset detail page's Activity tab. AGENTS.mdgained a Granular Audit Logging policy section: every future update endpoint must capture a real field-level diff the same way, and the frontend must render it throughActivityDiff/tokens.auditFieldrather than a one-off description string.
3.3.0
Added
- Multilingual support. The web app now ships in English (canonical), Spanish, French, and
German, with the architecture designed so adding language #5+ later is a content-only change (a
LocaleMetaentry inapps/web/src/i18n/locales.tsplus amessages/{locale}/*.jsonset) — never an app-code change. Built on next-intl, with non-English locales prefixed in the URL (/es/...,/fr/...,/de/...) while English stays unprefixed at the root, so existing links and bookmarks keep working. A new globe-icon language switcher sits next to the theme toggle in the top bar; the chosen language is written to aNEXT_LOCALEcookie immediately and, once signed in, to the user's profile (PATCH /users/megained a validatedlanguagefield) so it follows a returning user across devices and browsers. Every page and shared component in the authenticated app, the login/register/forgot-password/reset-password/verify-email flow, admin/settings screens, the Privacy Policy and Terms of Service (both the app's and the marketing site's), and every enum-driven dropdown (physical quantities, sensor technologies, calibration status, roles, etc.) are translated. The activity log and audit trail translate each action (asset.created,calibration.voided, …) instead of showing the raw event code, and the dashboard calibration calendar's date tooltip shows the weekday name in the viewer's language. User-entered content (asset names, notes, audit free text) stays untranslated by design — it's shown back exactly as entered, since machine-translating someone else's data would silently change what they wrote. The separate marketing site (landing/, a static HTML/CSS/JS Cloudflare Worker project) gained its own matching language switcher next to its light/dark toggle, full Spanish/French/German copies of the marketing homepage and legal pages, and a first-visitAccept-Language-based redirect in its Worker. - Backend:
Usergained alanguagecolumn (SUPPORTED_LANGUAGES-validated, defaults toen), migrated via028_add_user_language.py, with test coverage for valid/invalid values onUserSelfUpdate. - The Knowledge Center (
apps/docs, and the same content embedded in-app at Documentation) is now translated too, using Fumadocs' built-in i18n: each guide page's Spanish/French/German translation lives as apage.{locale}.mdxsibling next to its English source, with automatic fallback to English for any page not yet translated so coverage can grow incrementally. The standalone docs site follows the same unprefixed-English/prefixed-others URL scheme as the app (/docs/guide,/es/docs/guide, …) and gained its own language switcher in the top nav. The auto-generated API Reference stays English-only in every language, since it's generated directly from the OpenAPI schema rather than hand-written.
3.2.1
Fixed
- The built-in default calibration certificate template printed a "Due Date" field
(
calibration_date+calibration_interval) unconditionally, with no way to know whether the customer had agreed to receive a calibration-interval recommendation — a direct conflict with ISO/IEC 17025 §7.8.4.3 ("A calibration certificate...shall not contain any recommendation on the calibration interval, except where this has been agreed with the customer"), and a contradiction of the certificate-generation docs, which incorrectly claimed no due-date language ever appeared.templates/certificates/default.tex.jinjano longer rendersdue_date— that cell now shows the calibration's location instead.due_dateis still passed into every template's context, so a custom uploaded template can still choose to print it (e.g. once a real customer-agreement flow exists). Found while building the Compliance section of these docs — see ISO/IEC 17025 §7.8.4.3.
3.2.0
Added
- Every place a user appears in a list — organization members, join requests, the Add Member
picker, the admin users list, and activity/audit log entries — now shows their profile picture
and links their name to
/users/{id}, via a new sharedUserSummarycomponent (UserMention, used by activity logs, is now a thin wrapper around it). Backend responses that embed a user (OrganizationMemberResponse,OrganizationJoinRequestResponse,EligibleUserResponse,AuditLogResponse) gained aprofile_picture_urlfield, resolved via a new sharedresolve_picture_urlhelper. Fixed a pre-existing gap along the way: the per-asset audit-log endpoint (GET /assets/{id}/audit-logs) never enrichedactor_name/actor_roleat all (always null) — it now shares the same enrichment as the top-level/audit-logsendpoint. - All checkboxes across the app are replaced with a new shared
ToggleSwitchcomponent — a pill-shaped switch with a smooth on/off color transition and an "On"/"Off" state label. - Login page: a password-visibility eye icon, and a "Stay signed in" toggle — checked (default)
persists the session in
localStorageas before; unchecked usessessionStorageinstead, so signing out is as simple as closing the browser. - Clicking a dashboard pie chart segment now navigates to a filtered register: calibration status → assets filtered by status, sensor/DAQ type → assets filtered by that subtype, procedure physical quantity → the procedures register filtered accordingly.
- Admin → Dashboard → Export/Import database now bundles every MinIO object (certificates,
datasheets, LaTeX templates, profile pictures) alongside the
pg_dumparchive in a single zip, so restoring a backup on a different instance brings its media along instead of leaving file references pointing at nothing. Import still accepts a barepg_dumparchive from older backups (database only, in that case). - Calibration certificate PDFs are now digitally signed with a real, PDF-native signature (PAdES) — any PDF viewer with signature support (Adobe Acrobat, Chrome, Preview) can verify a certificate was issued by its organization and hasn't been altered since, independent of which LaTeX template rendered it. Each organization gets a lazily-generated, self-signed RSA-2048 certificate on its first issued certificate, visible and downloadable from Organizations → (organization) → Certificate signing. See Certificate digital signatures for how to verify one.
- Certificate templates documentation gained an extensive "Recipes" section with copy-pasteable patterns for every placeholder shape, plus a minimal working template skeleton.
Fixed
- The user's profile picture didn't show a pointer cursor on hover even where clicking it does something (opens the preview modal, opens the top-bar avatar dropdown).
3.1.1
Fixed
- High priority: the named-volume-to-bind-mount migration command documented in
Self-hosting → Deployment (for upgrading an install that pre-dates the 3.0.0 bind-mount fix)
silently failed to copy anything when run from Git Bash on Windows: MSYS rewrites any
/-prefixed argument before it reachesdocker.exe, including the container-side paths in-v /host/path:/container/path(not just the host side), mangling them into nonsense likeC:\Program Files\Git\from.docker run ... cp -a /from/. /to/then writes into a bogus location instead ofdata/postgres/data/minio— with no error surfaced, so it looked exactly like the rebuild had wiped the data, when the real cause was the migration step never actually running against the right path. The doc now calls out running the command from PowerShell/cmd.exe instead of Git Bash, or prefixing it withMSYS_NO_PATHCONV=1if Git Bash is unavoidable, plus how to recognize (a stray emptydata/postgres;C-style directory) and recover from a botched prior attempt (the original data is still intact in the old named volume until explicitly removed).- Confirmed the current bind-mount configuration itself (introduced in 3.0.0) is not the source
of any ongoing data loss: a full
docker compose down+up --buildround-trip, both against a freshly-initialized emptydata/directory and against this repo's own populated one, preserves every row and file exactly. - Added
scripts/verify-media-persistence.sh— an isolated, non-destructive check (throwaway Compose project name and temp data directory, safe to run alongside a live deployment) that writes a marker row/object, tears the containers down, rebuilds, and confirms both survive. Exits non-zero on failure, so it can be run after any change todocker-compose.ymlor wired into CI as a regression guard against this class of bug recurring.
- Confirmed the current bind-mount configuration itself (introduced in 3.0.0) is not the source
of any ongoing data loss: a full
3.1.0
Added
- Per-user notification preferences, and a completed notification lifecycle (delete individual/all,
click-to-navigate) for the in-app inbox introduced in 3.0.0.
- New Settings → Notifications section lets each user choose, per category (calibration due, new calibration recorded, organization join request, organization join decision), whether they receive it via email, in-app, both, or neither. New users and untouched categories default to both channels enabled, so nothing needs configuring out of the box.
- The notification bell dropdown gained a Clear all action and a per-notification remove (×) button — previously the inbox could only be marked read, never actually cleared.
- Calibration due-soon/overdue reminders and new-calibration notifications now raise an in-app notification the same way organization join requests already did (in 3.0.0), instead of only ever emailing — the in-app channel doesn't depend on SMTP being configured at all. Both link to the asset's detail page.
- The calibration reminder sweep now marks a reminder as sent once any enabled channel delivers for any recipient (in-app or email), rather than requiring email specifically — a broken SMTP server no longer blocks the guaranteed in-app channel, and won't be retried forever once the recipient has been notified some other way.
3.0.0
Added
- Organizations are now full multi-tenant entities. Any non-Viewer user can create one and becomes
its first admin automatically; a user can belong to any number of organizations, each with its
own
member/adminrole — distinct from the global RBAC role on the user's account. Only Super Admin overrides into organization management the caller isn't a member of; the globaladminrole has no special access to organizations, matching the peer-to-peer model. Viewer is blocked from all organization management regardless of any per-orgadminrole they might hold — the global RBAC restriction wins over the org-level one.- New profile fields:
full_name,website,location_id(primary/HQ location),email,phone,private. - New dedicated Organizations page (list + per-organization detail page, replacing the old Admin → Organizations panel) and a sidebar tab.
- Members can leave, and admins can remove members or change their role — guarded so an organization is never left with zero admins (the last admin must promote someone else first, or deactivate the organization instead).
- Private organizations show only their name (with a lock icon) to non-members, so they stay discoverable enough to request joining; everything else about them — profile, member roster, assets — is members-only.
- Non-members can send a join request; every admin of that organization gets notified via
a new in-app notification inbox (bell icon in the top bar) always, plus email if SMTP is
configured. Approving adds the requester as a
member. - Assets now carry a direct
organization_id(chosen from the creating user's own organizations), independent of the asset's location — shown on the asset detail page and linked back to the organization's page, which in turn shows a clickable asset count filtering the asset register. - The organization page's Members panel gained an Add member button (a modal listing every
user not already an active member, multi-select, added with the
memberrole) and a Danger zone with a confirmed delete action — deleting deactivates the organization, hiding it from everyone except Super Admin. Pending join requests now show inline in the same Members list with a "Pending" status instead of a separate panel, and approving/rejecting doesn't require edit mode; changing a member's role or removing them now does. - A Join / Leave button now appears on every organization's list row and its detail page (beside Edit for admins, in Edit's place for everyone else): "Request to join", a disabled "Request pending" once a request is in flight, or a red "Leave" for members — each behind a confirmation dialog. Leaving as an organization's last admin is guided instead of just rejected: promote another member first, or delete the organization if it's the last member too.
- New shared
ImageUploadFieldUI component (circular picture, click-to-preview, overlaid upload/remove buttons in edit mode) — now used consistently for the organization logo, asset picture, and user profile picture. - A deactivated organization is now visible in the Organizations list to Super Admin only,
shown with a red background and a "Deleted" badge, and can be reactivated with a "Restore
organization" button in its edit form's Danger zone (
POST /organizations/{id}/restore) — restoring only affects visibility, not membership.
- New profile fields:
Changed
- Breaking: Viewer is now read-only across the whole app, not just organizations — they can no
longer create, edit, retire, import, or export assets, procedures, or locations. The
require_not_viewerdependency (already used for signature management) now also gates every mutating endpoint onassets,procedures, andlocations; the corresponding frontend New/Edit/Delete/Import/Export controls are hidden for Viewer accordingly. - Breaking: Teams are removed. Open Gauge now mimics Gogs — only users and organizations,
no team layer between them.
- Dropped the
teamsandteam_memberstables, the/teams*API endpoints, the Settings → Teams self-service join/leave UI, and the Admin → Organizations nested team panel. - Asset ownership (previously
assets.owner, a team reference) is superseded by the directassets.organization_idabove. - Calibration email notifications (new-calibration and due/overdue reminders) now go to every active Technician/Admin/Super Admin who is a member of the asset's organization instead of a team's members; Viewers are never notified. The built-in certificate template's footer drops its separate "Team" line, since the "Organization" line already covers this.
- Dropped the
- Breaking: The redundant
User.is_superuserboolean is removed. It was always ORed withrole == "superadmin"at every permission check; Super Admin capability now comes solely from therolefield. Any account that hadis_superuser=trueis promoted torole=superadminby the migration so no one loses access. - Breaking:
users.organization_id(a single nullable FK, one org per user) is replaced by theorganization_membersmany-to-many table described above. - Consolidated 11+ duplicated
_require_admin/_require_superuserrouter-local helper functions into sharedrequire_admin/require_superadminFastAPI dependencies independencies/deps.py. The frontend's four copies ofROLE_LABELS/ROLE_COLORSare similarly consolidated intolib/roles.ts.
Fixed
- The organization logo displayed as a non-clickable square instead of the circular, click-to-preview picture used everywhere else.
- An organization's website link resolved as relative to the current page when the stored value
had no
http(s)://prefix, sending visitors to a broken/organizations/<website>URL instead of the intended site. Storage is unchanged; only the outgoing link is normalized. - The asset count on an organization's page linked to the asset register but didn't actually
filter it by organization — the register never read
organization_id/organization_namefrom the URL. - High priority: uploaded files and database rows could still appear to be wiped after
docker compose up --buildeven with the project name pinned (previous fix) — a different invocation (cwd,-poverride, or a legacydocker-composev1 binary ignoring thename:key) could still resolve to a differently-named volume. Postgres and MinIO now bind-mount to a fixed host path (infrastructure/docker/data/) instead of a named volume, which can't diverge this way. Existing installs: copy your current named-volume data into./data/postgresand./data/miniobefore upgrading (docker run --rm -v <old_volume>:/from -v $(pwd)/data/X:/to alpine cp -a /from/. /to/), then redeploy. - The organization list page's logo was square instead of the circular treatment used everywhere else.
- The organization picture stayed small (48px) while editing instead of matching the bigger size used when editing an asset or profile picture.
- Landing on the asset register from an organization's (or location's) filtered link didn't apply
the filter until the page was manually refreshed — the register read the filter from
window.location.searchin a mount-only effect, which client-side navigation doesn't re-trigger. Switched to the reactiveuseSearchParams()hook. Separately, dismissing a filter via "View all" only cleared in-memory state, not the URL, so a refresh afterward silently re-applied it — "View all" now also clears the corresponding URL params. - The organization page's "Add member" button was visible outside of edit mode, inconsistent with every other member-management control.
2.4.0
Added
- The signature pad (used when a user sets their approval signature) now has Undo and Erase buttons alongside Clear. Undo reverts the last stroke; Erase toggles an eraser mode that removes ink under the pointer instead of adding it, so a mis-drawn portion of a signature can be corrected without redrawing the whole thing.
- A new 1×0.5 in QR sticker size, alongside the existing 2×2 and 4×2 sizes: QR code on the left half, asset ID and asset name on the right half. Available in PNG, JPG, and PDF from the same asset Sticker dialog.
2.3.0
Changed
- Changelog entries (here and in the repo root's
VERSIONS.md) now show only the version number, not a date — git history is already the record of when something shipped, and the two dates could drift.
Fixed
- The calibration reminder sweep could send real emails during local development and test runs.
email_settingswas documented as a singleton but never enforced as one at the database level; code that queried it with an unordered.first()could silently pick up whatever row existed, including a real, already-configured SMTP row, so a test exercising the reminder sweep could end up delivering a genuine email instead of a no-op. A unique index now makes a second row impossible to create, the same technique already used forcertificate_templates. - The Dangerous zone's Clear database action could delete the account that triggered it.
The access check accepts either the
is_superuserflag orrole == "superadmin", but the reset only preservedis_superuseraccounts — arole == "superadmin"user without that flag could reach the action and then be wiped by it. The calling user is now always preserved regardless of role or flag.
2.2.0
Changed
- Correction to 2.1.0: the landing/marketing page content (features, comparison table, FAQ,
contact form) never belonged in this repository — it's the public
opengauge.orgsite, which lives in a separatelandingrepository. Revertedapps/web/src/app/page.tsxand removed theapps/web/src/components/landing/andapps/web/functions/directories added in 2.1.0; that content was rebuilt in thelandingrepo instead, matching its plain HTML/CSS/JS stack. - Simplified the admin Dangerous zone: removed the "clear selected tables" action added in 2.1.0 (an admin-only endpoint with no UI to grant its own precondition wasn't worth the complexity) and fixed the panel's styling to match the rest of Admin → Dashboard — Import now sits in the same red "Dangerous zone" card as Clear (both genuinely destructive), while the non-destructive Export stays in its own neutral Backup card.
Fixed
POST /admin/database/importfailed withpg_restore: error: could not execute query: ERROR: unrecognized configuration parameter "transaction_timeout". Debian's defaultpostgresql-clientpackage is newer (17) than thedbservice'spostgres:15-alpineserver, and pg_restore synthesizes session-setup statements based on its own client version — v17's aren't understood by a v15 server. The API image now pinspostgresql-client-15via the official PGDG apt repository, matching the server exactly.- Uploaded files (profile pictures, signatures, PDFs, certificate templates) and database rows
could appear to be wiped after
docker compose up --build. Docker Compose derives its project name — and therefore its volume names — from the current directory by default, so invokingdocker composefrom a different location (or a different way) than usual silently creates a second, differently-named stack with fresh, empty volumes.docker-compose.ymlnow pins an explicit projectname, so the same volumes are always used regardless of how Compose is invoked.
2.1.0
Added
- The landing page (
opengauge.org) is now a full marketing page: a refreshed features section, a feature comparison against legacy on-premise calibration software / spreadsheets / generic cloud CMMS, an FAQ, and a contact form tohello@opengauge.com. - A Cloudflare Pages Function (
apps/web/functions/api/contact.ts) handles the contact form's submissions via Cloudflare's own Email Routing "send email" binding — no third-party email API or extra dependency. Only active on the Cloudflare Pages deployment; seeapps/web/functions/README.mdfor the one-time Cloudflare-side setup. Everywhere else (self-hosted Docker, local dev) the form falls back to amailto:link.
Fixed
- The admin Dangerous zone (database export/import/reset) was invisible to any account
promoted to the
superadminrole after the initial install, because the check required the separateis_superuserflag — which no admin-panel UI can ever set on another account. It now also acceptsrole == "superadmin", matching the same convention already used everywhere else in the API for this privilege tier.
2.0.0
Changed
- Breaking: Team membership is now opt-in. Previously the
User.teamfield was a single free-text string with no real membership model behind it; it's replaced by ateam_membersjoin table, so a user can belong to any number of teams and starts in none. Theteam/teamsfield changed shape on the user API (UserResponse.team: string | null→UserResponse.teams: {id, name}[]), andUserCreate/UserUpdate/UserSelfUpdate.teamwere removed — join/leave a team viaPOST/DELETE /teams/{id}/join|leaveinstead. Existingusers.teamvalues are carried over into real membership rows by the migration wherever the text matched a team in the user's own organization. - Creating, renaming, and deleting teams is now only possible from Admin → Organizations. The user's own Settings → Teams tab is self-service only: pick which of your organization's teams to join or leave.
Added
- Dangerous zone moved into Admin → Dashboard (superadmin only): alongside the existing export/import/reset actions, a new Clear selected tables action lets you pick exactly which database tables to empty, rather than only an all-or-nothing reset.
- The PDF preview thumbnail (asset Files section, certificate templates) now shows an eye icon and shadow on hover, to signal it's clickable.
Fixed
- The signature drawing pad's background no longer goes near-black in dark mode, which previously made the dark-ink signature invisible while drawing (and in the saved-signature preview).
1.2.0
Added
- Ratiometric output signal type for sensor channels (e.g. bridge-type load cells), with
mV/VandV/Vunits. See Output signal. - PDF preview for PDF files in the asset Files section — click a file's thumbnail to preview it before downloading, using the same previewer as calibration certificates.
- Reference tables in Adding a sensor covering every supported physical quantity (with technology, type, and units) and every output signal type (with units).
Fixed
- The certificate template dropdown menu text is now visible in dark mode.
- The asset's profile picture no longer appears in the asset's Files list — it's managed only from the Image section, avoiding duplication.
1.1.0
Added
- Database panel (Admin → Database, superadmin only): export a full database backup, restore one, or reset the app to a clean state (deletes all data except superadmin accounts).
Changed
- Fresh installs now start empty. The first account registered on a new install is created
verified and as
superadminautomatically — previously it was created unverified with no admin able to activate it, and the app shipped pre-populated with demo users, assets, locations, and procedures instead of starting empty. See Authentication and Email notifications. - Docker Compose now reads every credential and URL from
infrastructure/docker/.envinstead of hardcoding them indocker-compose.yml— see Docker Compose deployment. - Generated QR codes and asset labels now encode the configurable
FRONTEND_URLinstead of a hardcodedhttp://localhost:3000, so they resolve correctly in production.
Fixed
- The sidebar's "Documentation" section no longer auto-expands when viewing an API Reference page — only clicking "Documentation" itself expands it.
1.0.13
Added
- PDF handling and download for generated calibration certificates.
1.0.12
Added
- User signature management: upload, retrieval, and cryptographic verification.
- LaTeX rendering service and certificate template management.
1.0.11
Fixed
- Logo image dimensions on the sidebar, for proper scaling in light mode.
Added
- Additional documentation links for sensor attributes and asset registry fields.
1.0.10
Added
- Demo mode: in-memory data store with session persistence, for trying Open Gauge without a backend.
Changed
- General functionality and performance improvements across the app.
1.0.9
Added
- Password reset flow and account activation for self-registered users.
Changed
- Cleaned up
package-lock.json, removing unnecessary peer dependencies.
1.0.8
Added
- Dark and light logo SVGs, a new app icon, a calibration-method badge component, and the dark/light theme toggle.
- Radial-gradient backdrop treatment for the dashboard grid background.
1.0.7
Added
- Calibration soft-void functionality (void a calibration without deleting its history).
Changed
- Production start script switched to a custom static server for the Next.js export build.
1.0.6
Added
- Email notifications feature, with SMTP configuration in the admin panel.
1.0.5
Changed
- Broader code-structure refactor for readability and maintainability.
- Removed unnecessary peer dependencies; added
@emnapi/core/@emnapi/runtime.
1.0.4
Added
- Asset import via ZIP upload.
1.0.3
Changed
- Removed outdated standalone
ARCHITECTURE.md,DATABASE.md, andUNITS.mdnow that this documentation site is the source of truth.
1.0.2
Added
- Profile and asset picture uploads.
1.0.1
Changed
- Dashboard component styling and functionality improvements.
- Housekeeping: compiled Python artifacts for storage/utility modules.
1.0.0
The first release considered production-ready: licensed, documented, and covering the full calibration-to-certificate workflow.
Added
- Knowledge Center + API Reference documentation site (this site), with
fumadocs-openapigenerating the API Reference straight from the FastAPI schema. - AGPL-3.0 license, and an expanded
README.md/CONTRIBUTING.md.
0.5.8
Added
- Support for coefficients-only external calibrations (record a calibration without raw point data, using previously-derived coefficients).
0.5.7
Added
measurement_typefield on sensors, with related UI updates.
0.5.6
Added
- Calibration worked examples, an uncertainty-budget breakdown, and reporting utilities.
0.5.5
Added
- Health scoring: health service, scoring tests, and the asset Health tab.
- Health overview display and an enhanced calibration ring card visualization.
0.5.4
Added
- Calibration deletion, restricted to admins.
0.5.3
Changed
- Asset and audit log handling enhanced with actor-based filtering; sensor update fixes.
0.5.2
Changed
- User and audit log detail views now surface actor information (who performed the action), with general UI component improvements.
0.5.1
Added
- Procedures and procedure-distribution summary on the dashboard.
0.5.0
Added
- Pie chart components, with hover effects and shared context management.
Changed
- Calibration lab location retrieval integrated into the Calibration tab, with UI updates for calibration lab display.
0.4.4
Changed
- Dashboard types and API extended with calibration status and recent-assets data.
0.4.3
Added
- Sticker modal for label generation, with preview and download options.
Changed
- Color definitions reorganized; label generation logic cleaned up.
0.4.2
Added
is_calibration_labflag on locations and acalibration_location_idlink on calibrations.
0.4.1
Changed
- Asset and procedure schemas refactored; improved form validation and error handling.
0.4.0
Added
- Label/sticker generation service.
- Activity log.
0.3.7
Added
- Admin panel.
- Calibration report PDF generation.
0.3.6
Added
- User settings page.
Fixed
- Small issues in the calibration wizard and calibration view.
0.3.5
Added
- Procedures page, with file uploads.
- Procedure editing.
0.3.4
Added
- Support for adding new assets.
Fixed
- Asset editing behavior.
0.3.3
Changed
- Calibration chart replaced with Plotly.
- Calibration view updated to support the new calibration graph/table layout.
0.3.2
Added
- Calibration record add workflow.
Changed
- Further location editing refinements.
0.3.1
Added
- Add/edit workflows for locations.
- Asset details edition improvements.
0.3.0
Added
- Locations page, with a site/building/lab hierarchy.
Changed
- Sidebar/topbar background graph styling.
0.2.3
Changed
- Database-backed overview tab panels updated.
Added
- Edit asset overview feature, with initial tests.
0.2.2
Added
- Asset profile page.
0.2.1
Changed
- Assets registry table updated.
0.2.0
Added
- Dark/light mode.
Changed
- Dashboard rebuilt against the new database schema, with new panels.
0.1.2
Changed
- UI elements harmonized across early screens.
0.1.1
Added
- Profile loading on authentication.
0.1.0
Added
- Authentication page and login logic.
- Dashboard screen.
0.0.0
- Initial commit: monorepo scaffolding and base architecture (Next.js frontend, FastAPI backend, Docker Compose infrastructure).