JSON Nexus Visualizer Phase 2: Quality Banner, Schema Error Inspector & Auto-Revalidation

Phase 1 hardened the runtime: Web Worker offloading, IndexedDB persistence, responsive layout. Phase 2 surfaces the intelligence that was already running silently.

In Phase 1, we made the visualizer fast and stable. In Phase 2, we make it observable. The profiler, schema validator, and quality engine have always produced rich diagnostics—but they lived in a collapsed drawer. Phase 2 elevates them:

  • Quality Banner: Critical signals (missing data, outliers, schema drift, duplicate keys, schema violations) now appear in a dismissible banner directly above the table—no drawer click required.
  • Schema Error Inspector: JSON Schema violations are mapped from JSON Pointer paths to exact table cells via a new pointer-utils.js module. Invalid cells show inline badges; the drawer panel groups violations by cell with human-readable messages.
  • Automatic Re-validation on Share Restore: Opening a shared link with an embedded schema now re-runs validation immediately, so the banner and cell highlights appear without user action.

All three upgrades are live at webdevelopmentor.com/json-nexus/ and in every share link (/s/{token}/).


Upgrade 1: Quality Banner — From Drawer to Toolbar

The Problem

The profiler’s qualitySnapshot() computes null rates, IQR-based numeric outliers, schema drift, duplicate JSON keys, and a composite issueCount. The schema validator produces violations with JSON Pointer paths. But both rendered only inside the Data Quality & Schema drawer disclosure—hidden by default, easy to miss.

The Fix: Elevated Banner Component


Signal Aggregation Logic

function renderQualityBanner(quality, schemaValidation) {
  if (!els.qualityBanner || !els.qualityBannerText) return;

  const signals = [];
  const details = [];

  if (quality) {
    // Null/missing columns
    const nullCols = (quality.nullRates || []).filter(item => item.missingRate > 0);
    if (nullCols.length) {
      const highNulls = nullCols.filter(item => item.missingRate > 0.5).length;
      signals.push(`${nullCols.length} column${nullCols.length === 1 ? '' : 's'} with missing data${highNulls ? ` (${highNulls} >50%)` : ''}`);
      details.push({ label: 'Missing', value: `${nullCols.length} columns` });
    }

    // Numeric outliers (IQR)
    if (quality.numericOutliers?.length) {
      const totalOutliers = quality.numericOutliers.reduce((sum, item) => sum + item.count, 0);
      signals.push(`${totalOutliers} numeric outlier${totalOutliers === 1 ? '' : 's'} (IQR)`);
      details.push({ label: 'Outliers', value: totalOutliers });
    }

    // Schema drift
    if (quality.schemaDrift?.changed) {
      const added = quality.schemaDrift.added?.length || 0;
      const removed = quality.schemaDrift.removed?.length || 0;
      signals.push(`Schema drift: +${added} / −${removed} columns`);
      details.push({ label: 'Drift', value: `+${added} added, −${removed} removed` });
    }

    // Duplicate keys
    if (quality.duplicateKeyCount) {
      signals.push(`${quality.duplicateKeyCount} duplicate key${quality.duplicateKeyCount === 1 ? '' : 's'}`);
      details.push({ label: 'Dupes', value: quality.duplicateKeyCount });
    }
  }

  // Schema validation violations
  if (schemaValidation.validSchema && schemaValidation.result?.violations?.length) {
    const violations = schemaValidation.result.violations.length;
    signals.push(`${violations} schema violation${violations === 1 ? '' : 's'}`);
    details.push({ label: 'Schema', value: violations });
  }

  if (signals.length) {
    els.qualityBannerText.textContent = signals.join(' · ');
    els.qualityBanner.hidden = false;
    els.qualityBannerDetails.innerHTML = details.map(d => 
      `${d.label}${d.value}`
    ).join('');
    els.qualityBannerDetails.hidden = false;
  } else {
    els.qualityBanner.hidden = true;
  }
}

// Called from renderQualityPanel() and applySchemaToCurrentPayload()
function renderQualityPanel() {
  // ... existing drawer rendering ...
  renderQualityBanner(activeProfile?.quality, schemaValidation);
}

Styling

.quality-banner {
  border: 1px solid var(--warning);
  background: color-mix(in srgb, var(--warning) 12%, var(--panel));
  border-radius: 10px;
  margin: 10px 16px;
  box-shadow: 0 4px 16px rgba(0,0,0,.15);
  animation: qualityBannerSlide 240ms ease-out;
}
@keyframes qualityBannerSlide {
  from { opacity: 0; transform: translateY(-8px); }
  to { opacity: 1; transform: translateY(0); }
}
.quality-banner-content {
  display: flex; align-items: center; gap: 10px; padding: 12px 14px;
}
.quality-banner-icon { color: var(--warning); font-size: 14px; }
.quality-banner-text { flex: 1; font-size: 11px; line-height: 1.5; }
.quality-banner-dismiss {
  width: 28px; height: 28px; border-radius: 6px;
  border: 1px solid var(--line); background: var(--panel-2);
  display: grid; place-items: center; font-size: 14px;
}
.quality-banner-details {
  border-top: 1px solid var(--line); padding: 12px 14px;
  font-size: 10px; color: var(--muted);
}
.quality-banner-details .quality-detail-row {
  display: flex; gap: 16px; flex-wrap: wrap; margin-top: 8px;
}
.quality-detail-item {
  display: flex; align-items: center; gap: 6px;
  padding: 4px 8px; border: 1px solid var(--line);
  border-radius: 6px; background: var(--panel-2);
}
.quality-detail-label { color: var(--muted); font-size: 9px; text-transform: uppercase; letter-spacing: .07em; }
.quality-detail-value { font-weight: 600; color: var(--text); }

Upgrade 2: JSON Schema Error Inspector — Pointer to Cell Mapping

The Problem

The schema validator returns violations with JSON Pointer paths (e.g., /items/2/email). The table model uses flattened dot-notation keys (e.g., email or items.2.email). Bridging this gap required a dedicated utility.

pointer-utils.js — New Module

// assets/bbs/js/pointer-utils.js
function jsonPointerToPath(pointer) {
  // "/items/2/email" -> ['items', '2', 'email']
  if (!pointer || pointer === '#') return [];
  return pointer.split('/').slice(1).map(seg => 
    seg.replace(/~1/g, '/').replace(/~0/g, '~')
  );
}

function pathToCellKey(pathSegments) {
  // ['items', '2', 'email'] -> 'items.2.email' (matches flattened column key)
  if (!pathSegments.length) return null;
  return pathSegments.join('.');
}

function groupViolationsByCell(violations, rowCount) {
  const cellViolations = new Map();
  violations.forEach(violation => {
    const path = jsonPointerToPath(violation.instanceLocation || violation.path);
    let rowIdx = 0;
    for (let i = 0; i < path.length; i++) {
      if (/^\d+$/.test(path[i])) { rowIdx = parseInt(path[i], 10); break; }
    }
    if (rowIdx >= rowCount) rowIdx = rowCount - 1;
    const cellKey = pathToCellKey(path);
    if (!cellViolations.has(cellKey)) cellViolations.set(cellKey, []);
    cellViolations.get(cellKey).push({ ...violation, rowIndex: rowIdx });
  });
  return cellViolations;
}

function formatViolationMessage(violation) {
  // Human-readable messages per keyword
  const keyword = violation.keyword || 'validation';
  switch (keyword) {
    case 'type': return `Expected ${violation.expected || 'valid type'}, got ${violation.actual || 'invalid'}`;
    case 'required': return `Missing required property: ${violation.missingProperty || 'unknown'}`;
    case 'format': return `Invalid ${violation.format || 'format'}: ${violation.message}`;
    case 'minimum': return `Value must be >= ${violation.limit || violation.minimum}`;
    case 'maximum': return `Value must be <= ${violation.limit || violation.maximum}`;
    case 'minLength': return `Must be at least ${violation.limit} characters`;
    case 'maxLength': return `Must be at most ${violation.limit} characters`;
    case 'pattern': return `Must match pattern: ${violation.pattern}`;
    case 'enum': return `Must be one of: ${(violation.allowedValues || []).join(', ')}`;
    default: return violation.message || 'Validation failed';
  }
}

// Exported to window.JSONNexusPointer

Integration: deriveSchemaHighlights

// In app.js — called from applySchemaToCurrentPayload()
function deriveSchemaHighlights(result) {
  const cells = new Set();
  const rows = new Set();
  if (!result?.violations?.length) return { cells, rows };

  const violationsByCell = window.JSONNexusPointer.groupViolationsByCell(
    result.violations, tableModel.rows.length
  );

  violationsByCell.forEach((violations, cellKey) => {
    cells.add(cellKey);
    violations.forEach(v => rows.add(v.rowIndex));
  });
  return { cells, rows };
}

// In renderTable() — apply visual treatment to flagged cells
function renderTable() {
  // ... for each cell ...
  if (schemaValidation.cells.has(schemaCellKey(rowIndex, column))) {
    td.classList.add('schema-invalid-cell');
    td.title = 'Schema violation: ' + violationsForCell.map(v => 
      window.JSONNexusPointer.formatViolationMessage(v)
    ).join('; ');
  }
}

// In renderQualitySchemaValidation() — show grouped violations
function renderQualitySchemaValidation() {
  // ... existing logic ...
  if (result?.valid) { /* PASS */ }
  else {
    const violationsByCell = window.JSONNexusPointer.groupViolationsByCell(
      result.violations, tableModel.rows.length
    );
    let html = '';
    violationsByCell.forEach((violations, cellKey) => {
      html += `
${cellKey} (row ${violations[0].rowIndex + 1})
    ${violations.map(v => `
  • ${window.JSONNexusPointer.formatViolationMessage(v)}
  • `).join('')}
`; }); els.qualitySchemaValidation.innerHTML = html; } }

Visual Treatment

.schema-invalid-cell {
  background: color-mix(in srgb, var(--danger) 14%, var(--panel)) !important;
  box-shadow: inset 3px 0 0 var(--danger), inset 0 0 0 1px color-mix(in srgb, var(--danger) 38%, transparent);
  position: relative;
}
.schema-invalid-cell::after {
  content: '!'; position: absolute; top: 5px; right: 5px;
  width: 15px; height: 15px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--danger); color: #fff; font: 800 9px/1 var(--font);
}
.quality-schema-cell-group {
  margin: 8px 0; padding: 8px; border: 1px solid var(--line);
  border-radius: 8px; background: var(--panel-2);
}
.quality-schema-cell-group strong { color: var(--accent); }
.quality-schema-cell-group ul { margin: 6px 0 0 18px; font-size: 9px; }
.quality-schema-cell-group li { color: var(--muted); line-height: 1.5; }

Upgrade 3: Automatic Schema Re-validation on Share Restore

Share payloads include uiState.schemaInput and uiState.schemaEnabled. Previously, opening a shared link loaded the JSON but left the schema un-validated—the user had to click "Validate schema" manually.

// In loadShareFromPath() — after parsing JSON and hydrating state
await tryParseInput(data.rawJson, { show: true, autoProfile: false });

// Re-apply schema validation if schema was configured
if (state.schemaInput && state.schemaEnabled) {
  console.log('[JNV Share] Re-applying schema validation...');
  await validateSchemaInput({ notify: false });
  console.log('[JNV Share] schemaValidation:', schemaValidation);
}

showToast('Shared visualization restored.', 'success');

Now when a recipient opens a shared link with an embedded schema, the validation runs automatically. The quality banner appears instantly with schema violations, invalid cells are highlighted, and the drawer panel shows grouped violations—no clicks required.


Verification Checklist

  1. Quality banner appears: Drop JSON with nulls/outliers/drift → banner slides down above table with signal summary.
  2. Banner details expand: Click banner → detail chips show "Missing: 3 columns", "Outliers: 12", "Drift: +2 added", etc.
  3. Dismiss works: Click × → banner hides; reload → banner reappears (persistent until issues fixed).
  4. Schema violations highlight cells: Add schema with format: "email" + invalid email → cell gets red badge + ! marker.
  5. Violation grouping in drawer: Open Data Quality & Schema → Schema Validation shows violations grouped by cell with formatted messages.
  6. Share link auto-validates: Open /s/{token}/ with schema → banner + cell highlights appear without clicking Validate.
  7. Pointer mapping accuracy: Violation at /items/2/email highlights row 2, column email in flat table.
  8. Responsive banner: Resize to mobile (≤480px) → banner stacks correctly, details wrap.

Phase 2 Architecture Summary

LayerFilePurpose
Pointer Utilsassets/bbs/js/pointer-utils.jsJSON Pointer ↔ cell key mapping, violation grouping, message formatting
Quality Bannerassets/app.js (renderQualityBanner), templates/visualizer-embed.php, assets/styles.cssToolbar banner with signal summary + detail chips, dismiss, animation
Schema Inspectorassets/app.js (deriveSchemaHighlights, renderQualitySchemaValidation), assets/bbs/js/pointer-utils.jsCell-level violation mapping, inline badges, grouped drawer panel
Auto-Revalidationassets/app.js (loadShareFromPath)Runs validateSchemaInput() on share restore
Stylesassets/styles.cssBanner, badges, detail chips, invalid-cell treatment, responsive
WP Pluginjson-nexus-visualizer.phpEnqueues json-nexus-pointer-utils script

Try the Phase 2 Tool

Drop the JSON below into the visualizer to see the quality banner and schema violations in action:

[
  { "id": 1, "name": "Alice", "email": "alice@example.com", "age": 30, "score": 95.5 },
  { "id": 2, "name": "Bob", "email": "not-an-email", "age": 25, "score": 88.0 },
  { "id": 3, "name": "Carol", "email": "carol@example.com", "age": -5, "score": 92.0 },
  { "id": 4, "name": "Dave", "email": "dave@example.com", "age": 40, "score": 150.0 }
]

Then paste this schema and click Validate schema (or open this pre-configured share link to see auto-validation):

{
  "type": "array",
  "items": {
    "type": "object",
    "properties": {
      "id": { "type": "integer" },
      "name": { "type": "string" },
      "email": { "type": "string", "format": "email" },
      "age": { "type": "integer", "minimum": 0 },
      "score": { "type": "number" }
    },
    "required": ["id", "name", "email", "age", "score"]
  }
}

You'll see: Quality banner (negative age, outlier score, invalid email), red badges on the invalid cells, grouped violations in the drawer, and auto-validation on the share link.

The complete plugin (GPLv3) includes the PHP PDO SQLite backend, 90-day cryptographic share links, REST monitoring, JSON Schema validation, Phase 1 client-side upgrades, and Phase 2 observability features. Download the plugin ZIP from the tool page or clone the repository for self-hosted deployment.

// THE TRANSMISSION LOG : FIELD NOTES

Weekly zero-fluff breakdowns of distributed systems, browser runtime performance, and production post-mortems delivered directly from Stella Sage.