{
  "annotations": {
    "list": [
      {
        "builtIn": 1,
        "datasource": {
          "type": "grafana",
          "uid": "-- Grafana --"
        },
        "enable": true,
        "hide": true,
        "iconColor": "rgba(0, 211, 255, 1)",
        "name": "Annotations & Alerts",
        "type": "dashboard"
      }
    ]
  },
  "editable": true,
  "fiscalYearStartMonth": 0,
  "graphTooltip": 0,
  "id": null,
  "links": [],
  "liveNow": false,
  "panels": [
    {
      "id": 1,
      "type": "volkovlabs-text-panel",
      "title": "SQLite based device management",
      "gridPos": {
        "x": 0,
        "y": 0,
        "h": 20,
        "w": 24
      },
      "fieldConfig": {
        "defaults": {
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          }
        },
        "overrides": []
      },
      "pluginVersion": "6.0.0",
      "targets": [
        {
          "datasource": {
            "type": "marcusolsson-static-datasource",
            "uid": "U0HP2Rv4z"
          },
          "frame": {
            "fields": [
              {
                "config": {},
                "name": "static",
                "type": "string",
                "values": [
                  "data"
                ]
              }
            ],
            "meta": {}
          },
          "refId": "A"
        }
      ],
      "datasource": {
        "type": "marcusolsson-static-datasource",
        "uid": "U0HP2Rv4z"
      },
      "options": {
        "renderMode": "data",
        "editors": [
          "content",
          "afterRender",
          "styles"
        ],
        "editor": {
          "language": "html",
          "format": "auto"
        },
        "wrap": true,
        "contentPartials": [],
        "content": "<!-- SQLite Device Management Dashboard -->\n<div style=\"padding: 20px; font-family: 'Inter', sans-serif;\">\n  <div style=\"margin-bottom: 20px; padding: 15px; background: var(--background-secondary); border-radius: 8px; border-left: 4px solid var(--primary-color);\">\n    <h2 style=\"color: var(--text-primary); margin: 0 0 10px 0; display: flex; align-items: center;\">\n      <i class=\"fas fa-database\" style=\"margin-right: 12px; color: var(--primary-color);\"></i>\n      SQLite Device Management System\n    </h2>\n    <p style=\"color: var(--text-secondary); margin: 5px 0; font-size: 14px;\">\n      Advanced device management with inline editing, CRUD operations, and keyboard navigation.\n      <strong>Adapted for SQLite compatibility.</strong>\n    </p>\n  </div>\n  \n  <div id=\"devices-table\" style=\"\n    background: var(--background-secondary);\n    border: 1px solid var(--border-medium);\n    border-radius: 8px;\n    min-height: 400px;\n    overflow: hidden;\n  \">\n    <div style=\"text-align: center; padding: 60px 20px; color: var(--text-secondary);\">\n      <i class=\"fas fa-spinner fa-spin\" style=\"font-size: 32px; margin-bottom: 15px; color: var(--primary-color);\"></i>\n      <div style=\"font-size: 16px; margin-bottom: 10px;\">Loading SQLite device data...</div>\n      <div style=\"font-size: 14px; opacity: 0.8;\">Click the button below to load and manage devices</div>\n    </div>\n  </div>\n  \n  <div style=\"margin-top: 20px; display: flex; gap: 15px; align-items: center; justify-content: space-between;\">\n    <button id=\"load-devices\" style=\"\n      background: linear-gradient(135deg, var(--primary-color), var(--primary-color)dd);\n      color: var(--primary-text-color);\n      border: none;\n      padding: 12px 24px;\n      border-radius: 8px;\n      font-weight: 600;\n      cursor: pointer;\n      transition: all 0.3s ease;\n      font-size: 14px;\n      box-shadow: 0 2px 8px rgba(0,0,0,0.1);\n      display: flex;\n      align-items: center;\n      gap: 8px;\n    \" onmouseover=\"this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(0,0,0,0.2)'\" onmouseout=\"this.style.transform='translateY(0)'; this.style.boxShadow='0 2px 8px rgba(0,0,0,0.1)'\">\n      <i class=\"fas fa-sync-alt\"></i>\n      Load SQLite Devices\n    </button>\n    \n    <div style=\"font-size: 12px; color: var(--text-secondary);\">\n      <i class=\"fas fa-info-circle\" style=\"margin-right: 5px;\"></i>\n      Database: <code style=\"background: var(--background-canvas); padding: 2px 6px; border-radius: 3px;\">./tmp/devices.db</code>\n    </div>\n  </div>\n  \n  <div style=\"margin-top: 25px; padding: 20px; background: var(--background-canvas); border-radius: 8px; border: 1px solid var(--border-weak);\">\n    <h3 style=\"color: var(--text-primary); margin: 0 0 15px 0; font-size: 16px; display: flex; align-items: center;\">\n      <i class=\"fas fa-keyboard\" style=\"margin-right: 8px; color: var(--success-color);\"></i>\n      Advanced Features\n    </h3>\n    <div style=\"display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 15px; font-size: 13px;\">\n      <div>\n        <strong style=\"color: var(--text-primary);\">✏️ Inline Editing:</strong>\n        <ul style=\"margin: 5px 0; padding-left: 20px; color: var(--text-secondary);\">\n          <li>Click edit icon or any cell to modify</li>\n          <li>Real-time validation with visual feedback</li>\n          <li>Timestamp format detection</li>\n        </ul>\n      </div>\n      <div>\n        <strong style=\"color: var(--text-primary);\">⌨️ Keyboard Navigation:</strong>\n        <ul style=\"margin: 5px 0; padding-left: 20px; color: var(--text-secondary);\">\n          <li><kbd>Tab</kbd> / <kbd>Shift+Tab</kbd> - Navigate cells</li>\n          <li><kbd>Enter</kbd> - Next cell or save row</li>\n          <li><kbd>Ctrl+S</kbd> - Save current row</li>\n          <li><kbd>Esc</kbd> - Cancel editing</li>\n        </ul>\n      </div>\n      <div>\n        <strong style=\"color: var(--text-primary);\">💾 CRUD Operations:</strong>\n        <ul style=\"margin: 5px 0; padding-left: 20px; color: var(--text-secondary);\">\n          <li>Create, Read, Update, Delete records</li>\n          <li>Individual row save/cancel operations</li>\n          <li>Confirmation dialogs for destructive actions</li>\n        </ul>\n      </div>\n      <div>\n        <strong style=\"color: var(--text-primary);\">🎨 UI Features:</strong>\n        <ul style=\"margin: 5px 0; padding-left: 20px; color: var(--text-secondary);\">\n          <li>Frozen columns (hostname, ip_address)</li>\n          <li>Grafana theme integration</li>\n          <li>Smooth animations and transitions</li>\n          <li>Responsive table layout</li>\n        </ul>\n      </div>\n    </div>\n  </div>\n  \n  <div style=\"margin-top: 20px; padding: 15px; background: var(--warning-color)20; border-radius: 6px; border-left: 4px solid var(--warning-color); font-size: 13px;\">\n    <strong style=\"color: var(--warning-text);\">🔧 SQLite Specific Notes:</strong>\n    <ul style=\"margin: 8px 0; padding-left: 20px; color: var(--warning-text);\">\n      <li>Timestamps stored as Unix epoch integers</li>\n      <li>Boolean values stored as 1/0 (SQLite standard)</li>\n      <li>String escaping uses SQLite double-quote syntax</li>\n      <li>Auto-detection of ID fields (id, *_id, uid, etc.)</li>\n    </ul>\n  </div>\n</div>\n\n<!-- Font Awesome CSS -->\n<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css\" integrity=\"sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\">\n\n<style>\nkbd {\n  background: var(--background-canvas);\n  border: 1px solid var(--border-medium);\n  border-radius: 3px;\n  box-shadow: 0 1px 1px rgba(0,0,0,0.2), 0 2px 0 0 rgba(255,255,255,0.7) inset;\n  color: var(--text-primary);\n  display: inline-block;\n  font-size: 0.85em;\n  font-weight: 700;\n  line-height: 1;\n  padding: 2px 4px;\n  white-space: nowrap;\n}\n</style>",
        "defaultContent": "The query didn't return any results.",
        "helpersRemoteUrl": "",
        "helpers": "",
        "afterRenderRemoteUrl": "",
        "afterRender": "// SQLite Device Management - Learning Panel Edit\n// Advanced device management with inline editing, CRUD operations, and keyboard navigation\n// Adapted from PostgreSQL version for SQLite compatibility\n\nlet currentDataSourceUid = 'sqlite-demo'; // SQLite datasource UID\nlet editingCell = null;\nlet originalValues = {};\nlet tableStructure = null; // Store table structure info\nlet currentFocusedInput = null; // Track currently focused input for keyboard navigation\n\nasync function loadDevices() {\n  try {\n    const result = await context.dataSource.sql(currentDataSourceUid, 'SELECT * FROM devices ORDER BY id');\n    const rows = context.dataSource.utils.toObjects(result);\n\n    if (rows.length === 0) {\n      document.getElementById('devices-table').innerHTML = '<p style=\"color: var(--text-secondary);\">No devices found</p>';\n      return;\n    }\n\n    // Store table structure information for better field handling\n    const keys = Object.keys(rows[0]);\n    tableStructure = {\n      fields: keys,\n      idField: detectIdField(keys), // Better ID field detection\n      editableFields: keys.filter(key => !isIdField(key, keys)) // Filter out ID fields\n    };\n\n    // Get Grafana theme for styling\n    const theme = context.grafana.theme;\n\n    // Create scrollable table container with frozen columns\n    let html = `\n    <div style=\"\n      position: relative; \n      overflow-x: auto; \n      overflow-y: visible;\n      border: 1px solid ${theme.colors.border.weak};\n      max-width: 100%;\n    \">\n      <table style=\"\n        width: max-content;\n        min-width: 100%;\n        border-collapse: collapse;\n        background: ${theme.colors.background.primary};\n        color: ${theme.colors.text.primary};\n        font-family: ${theme.typography.fontFamily};\n        font-size: ${theme.typography.body.fontSize};\n        table-layout: fixed;\n      \">\n        <thead>\n          <tr style=\"background: ${theme.colors.primary.main};\">\n            <!-- Actions column (frozen left) -->\n            <th style=\"\n              position: sticky;\n              left: 0;\n              z-index: 20;\n              width: 80px;\n              padding: ${theme.spacing(1, 2)};\n              border: 1px solid ${theme.colors.border.medium};\n              font-weight: ${theme.typography.fontWeightMedium};\n              color: ${theme.colors.primary.contrastText};\n              text-align: center;\n              background: ${theme.colors.primary.main};\n              background-clip: padding-box;\n              box-shadow: 2px 0 8px rgba(0,0,0,0.2), inset 0 0 0 1000px ${theme.colors.primary.main};\n              border-right: 3px solid ${theme.colors.border.strong};\n            \">Actions</th>\n            \n            <!-- Frozen hostname column -->\n            <th style=\"\n              position: sticky;\n              left: 80px;\n              z-index: 19;\n              width: 150px;\n              padding: ${theme.spacing(1, 2)};\n              border: 1px solid ${theme.colors.border.medium};\n              font-weight: ${theme.typography.fontWeightMedium};\n              color: ${theme.colors.primary.contrastText};\n              text-align: left;\n              background: ${theme.colors.primary.main};\n              background-clip: padding-box;\n              box-shadow: 2px 0 8px rgba(0,0,0,0.2), inset 0 0 0 1000px ${theme.colors.primary.main};\n              border-right: 3px solid ${theme.colors.border.strong};\n            \">hostname</th>\n            \n            <!-- Frozen ip_address column -->\n            <th style=\"\n              position: sticky;\n              left: 230px;\n              z-index: 18;\n              width: 140px;\n              padding: ${theme.spacing(1, 2)};\n              border: 1px solid ${theme.colors.border.medium};\n              font-weight: ${theme.typography.fontWeightMedium};\n              color: ${theme.colors.primary.contrastText};\n              text-align: left;\n              background: ${theme.colors.primary.main};\n              background-clip: padding-box;\n              box-shadow: 2px 0 8px rgba(0,0,0,0.2), inset 0 0 0 1000px ${theme.colors.primary.main};\n              border-right: 3px solid ${theme.colors.border.strong};\n            \">ip_address</th>`;\n\n    // Add remaining headers (scrollable)\n    tableStructure.fields.forEach(key => {\n      const isId = key === tableStructure.idField;\n      const isFrozen = key === 'hostname' || key === 'ip_address';\n\n      // Skip ID column and frozen columns (already added)\n      if (isId || isFrozen) return;\n\n      html += `<th style=\"\n        min-width: 120px;\n        padding: ${theme.spacing(1, 2)};\n        border: 1px solid ${theme.colors.border.medium};\n        font-weight: ${theme.typography.fontWeightMedium};\n        color: ${theme.colors.primary.contrastText};\n        text-align: left;\n        background: ${theme.colors.primary.main};\n      \">${key}</th>`;\n    });\n\n    html += `\n          </tr>\n        </thead>\n        <tbody>`;\n\n    // Add rows with Font Awesome icons and frozen columns\n    rows.forEach((row, index) => {\n      // Alternate row colors for better readability\n      const rowBg = index % 2 === 0 ? theme.colors.background.primary : theme.colors.background.secondary;\n\n      html += `<tr data-row-index=\"${index}\" style=\"\n        background: ${rowBg};\n      \" onmouseover=\"\n        this.style.background='${theme.colors.action.hover}';\n        // Update frozen column backgrounds on hover\n        const actionCell = this.children[0];\n        const hostnameCell = this.children[1];\n        const ipCell = this.children[2];\n        if (actionCell) actionCell.style.boxShadow = '2px 0 8px rgba(0,0,0,0.2), inset 0 0 0 1000px ${theme.colors.action.hover}, inset 3px 0 0 ${theme.colors.border.strong}';\n        if (hostnameCell) hostnameCell.style.boxShadow = '2px 0 8px rgba(0,0,0,0.2), inset 0 0 0 1000px ${theme.colors.action.hover}, inset 3px 0 0 ${theme.colors.border.strong}';\n        if (ipCell) ipCell.style.boxShadow = '2px 0 8px rgba(0,0,0,0.2), inset 0 0 0 1000px ${theme.colors.action.hover}, inset 3px 0 0 ${theme.colors.border.strong}';\n      \" onmouseout=\"\n        this.style.background='${rowBg}';\n        // Restore frozen column backgrounds\n        const actionCell = this.children[0];\n        const hostnameCell = this.children[1];\n        const ipCell = this.children[2];\n        if (actionCell) actionCell.style.boxShadow = '2px 0 8px rgba(0,0,0,0.2), inset 0 0 0 1000px ${rowBg}, inset 3px 0 0 ${theme.colors.border.strong}';\n        if (hostnameCell) hostnameCell.style.boxShadow = '2px 0 8px rgba(0,0,0,0.2), inset 0 0 0 1000px ${rowBg}, inset 3px 0 0 ${theme.colors.border.strong}';\n        if (ipCell) ipCell.style.boxShadow = '2px 0 8px rgba(0,0,0,0.2), inset 0 0 0 1000px ${rowBg}, inset 3px 0 0 ${theme.colors.border.strong}';\n      \">`;\n\n      // Get ID value for operations\n      const idValue = row[tableStructure.idField];\n\n      // Actions column (frozen left)\n      html += `<td style=\"\n        position: sticky;\n        left: 0;\n        z-index: 15;\n        width: 80px;\n        padding: ${theme.spacing(1)};\n        border: 1px solid transparent;\n        background: ${rowBg};\n        background-clip: padding-box;\n        text-align: center;\n        box-shadow: 2px 0 8px rgba(0,0,0,0.2), inset 0 0 0 1000px ${rowBg}, inset 3px 0 0 ${theme.colors.border.strong};\n        border-right: 3px solid ${theme.colors.border.strong};\n      \">\n        \n        <!-- Edit Icon -->\n        <i class=\"edit-icon fas fa-pen\" \n           data-device-id=\"${idValue}\" \n           data-row-index=\"${index}\" \n           style=\"\n             color: ${theme.colors.info.main};\n             cursor: pointer;\n             font-size: 14px;\n             margin-right: 8px;\n             padding: 4px;\n             border-radius: 3px;\n             transition: all 0.2s ease;\n           \"\n           onmouseover=\"this.style.background='${theme.colors.info.main}15'; this.style.color='${theme.colors.info.shade}'\"\n           onmouseout=\"this.style.background='transparent'; this.style.color='${theme.colors.info.main}'\"\n           title=\"Edit row\"></i>\n        \n        <!-- Save Icon (hidden initially) -->\n        <i class=\"save-icon fas fa-check\" \n           data-device-id=\"${idValue}\" \n           data-row-index=\"${index}\" \n           style=\"\n             color: ${theme.colors.success.main};\n             cursor: pointer;\n             font-size: 14px;\n             margin-right: 4px;\n             padding: 4px;\n             border-radius: 3px;\n             transition: all 0.2s ease;\n             display: none;\n           \"\n           onmouseover=\"this.style.background='${theme.colors.success.main}15'; this.style.color='${theme.colors.success.shade}'\"\n           onmouseout=\"this.style.background='transparent'; this.style.color='${theme.colors.success.main}'\"\n           title=\"Save changes\"></i>\n        \n        <!-- Cancel Icon (hidden initially) -->\n        <i class=\"cancel-icon fas fa-times\" \n           data-device-id=\"${idValue}\" \n           data-row-index=\"${index}\" \n           style=\"\n             color: ${theme.colors.warning.main};\n             cursor: pointer;\n             font-size: 14px;\n             margin-right: 4px;\n             padding: 4px;\n             border-radius: 3px;\n             transition: all 0.2s ease;\n             display: none;\n           \"\n           onmouseover=\"this.style.background='${theme.colors.warning.main}15'; this.style.color='${theme.colors.warning.shade}'\"\n           onmouseout=\"this.style.background='transparent'; this.style.color='${theme.colors.warning.main}'\"\n           title=\"Cancel edit\"></i>\n        \n        <!-- Delete Icon -->\n        <i class=\"delete-icon fas fa-trash\" \n           data-device-id=\"${idValue}\" \n           data-row-index=\"${index}\" \n           style=\"\n             color: ${theme.colors.error.main};\n             cursor: pointer;\n             font-size: 14px;\n             padding: 4px;\n             border-radius: 3px;\n             transition: all 0.2s ease;\n           \"\n           onmouseover=\"this.style.background='${theme.colors.error.main}15'; this.style.color='${theme.colors.error.shade}'\"\n           onmouseout=\"this.style.background='transparent'; this.style.color='${theme.colors.error.main}'\"\n           title=\"Delete row\"></i>\n      </td>`;\n\n      // Frozen hostname column\n      const hostnameValue = row['hostname'] || '';\n      const hostnameKey = 'hostname';\n      const hostnameCellId = `cell-${index}-hostname`;\n      originalValues[hostnameCellId] = hostnameValue;\n\n      html += `<td class=\"editable-cell\" \n                   data-field=\"${hostnameKey}\" \n                   data-cell-id=\"${hostnameCellId}\" \n                   data-row-index=\"${index}\"\n                   data-original-value=\"${hostnameValue}\"\n                   style=\"\n        position: sticky;\n        left: 80px;\n        z-index: 14;\n        width: 150px;\n        padding: ${theme.spacing(1, 2)};\n        border: 1px solid transparent;\n        color: ${theme.colors.text.primary};\n        cursor: pointer;\n        background: ${rowBg};\n        background-clip: padding-box;\n        box-shadow: 2px 0 8px rgba(0,0,0,0.2), inset 0 0 0 1000px ${rowBg}, inset 3px 0 0 ${theme.colors.border.strong};\n        border-right: 3px solid ${theme.colors.border.strong};\n      \"\n         title=\"Click to edit\">${hostnameValue}</td>`;\n\n      // Frozen ip_address column\n      const ipValue = row['ip_address'] || '';\n      const ipKey = 'ip_address';\n      const ipCellId = `cell-${index}-ip_address`;\n      originalValues[ipCellId] = ipValue;\n\n      html += `<td class=\"editable-cell\" \n                   data-field=\"${ipKey}\" \n                   data-cell-id=\"${ipCellId}\" \n                   data-row-index=\"${index}\"\n                   data-original-value=\"${ipValue}\"\n                   style=\"\n        position: sticky;\n        left: 230px;\n        z-index: 13;\n        width: 140px;\n        padding: ${theme.spacing(1, 2)};\n        border: 1px solid transparent;\n        color: ${theme.colors.text.primary};\n        cursor: pointer;\n        background: ${rowBg};\n        background-clip: padding-box;\n        box-shadow: 2px 0 8px rgba(0,0,0,0.2), inset 0 0 0 1000px ${rowBg}, inset 3px 0 0 ${theme.colors.border.strong};\n        border-right: 3px solid ${theme.colors.border.strong};\n      \"\n         title=\"Click to edit\">${ipValue}</td>`;\n\n      // Add remaining scrollable columns\n      tableStructure.fields.forEach((key, keyIndex) => {\n        const isIdField = key === tableStructure.idField;\n        const isFrozen = key === 'hostname' || key === 'ip_address';\n\n        // Skip ID and frozen columns (already handled)\n        if (isIdField || isFrozen) return;\n\n        const cellId = `cell-${index}-${keyIndex}`;\n        originalValues[cellId] = row[key];\n        const isEditable = !isIdField;\n\n        html += `<td class=\"${isEditable ? 'editable-cell' : 'id-cell'}\" \n                     data-field=\"${key}\" \n                     data-cell-id=\"${cellId}\" \n                     data-row-index=\"${index}\"\n                     data-field-index=\"${keyIndex}\"\n                     data-original-value=\"${row[key]}\"\n                     style=\"\n          min-width: 120px;\n          padding: ${theme.spacing(1, 2)};\n          border: 1px solid ${theme.colors.border.weak};\n          color: ${theme.colors.text.primary};\n          cursor: ${isEditable ? 'pointer' : 'default'};\n          position: relative;\n        \" ${isEditable ? 'title=\"Click to edit\"' : ''}>${formatDisplayValue(key, row[key])}</td>`;\n      });\n\n      html += '</tr>';\n    });\n\n    html += `\n        </tbody>\n      </table>\n    </div>\n    \n    <!-- Font Awesome CSS (if not already included) -->\n    <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css\" integrity=\"sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\">\n    `;\n\n    document.getElementById('devices-table').innerHTML = html;\n\n    // Add event listeners\n    addEventListeners();\n\n  } catch (error) {\n    const theme = context.grafana.theme;\n    document.getElementById('devices-table').innerHTML = `\n      <div style=\"\n        padding: ${theme.spacing(2)};\n        background: ${theme.colors.error.main}15;\n        border: 1px solid ${theme.colors.error.main};\n        border-radius: ${theme.shape.borderRadius(1)}px;\n        color: ${theme.colors.error.text};\n        font-family: ${theme.typography.fontFamily};\n      \">\n        <strong>SQLite Error:</strong> ${error.message}\n      </div>`;\n  }\n}\n\n// Helper function to format display values (especially timestamps)\nfunction formatDisplayValue(fieldName, value) {\n  if (value === null || value === undefined || value === '') {\n    return '<em style=\"color: var(--text-secondary);\">null</em>';\n  }\n\n  // Handle timestamp fields for SQLite (Unix timestamps)\n  const timestampPatterns = [\n    /.*_at$/i, /.*_time$/i, /.*timestamp$/i, /^date$/i, /^time$/i\n  ];\n  \n  const isTimestampField = timestampPatterns.some(pattern => pattern.test(fieldName));\n  \n  if (isTimestampField && /^\\d{10,13}$/.test(String(value))) {\n    // Convert Unix timestamp to readable format\n    const timestamp = parseInt(value);\n    const date = new Date(timestamp > 9999999999 ? timestamp : timestamp * 1000);\n    return `<span title=\"Unix: ${value}\">${date.toLocaleString()}</span>`;\n  }\n\n  return String(value);\n}\n\n// Helper function to detect ID field using common patterns\nfunction detectIdField(fields) {\n  // Common ID field patterns (case-insensitive)\n  const idPatterns = [\n    /^id$/i,           // exact match: \"id\"\n    /^.*_id$/i,        // ends with \"_id\": \"user_id\", \"device_id\"\n    /^id_.*$/i,        // starts with \"id_\": \"id_user\", \"id_device\"\n    /^.*id$/i,         // ends with \"id\": \"userid\", \"deviceid\"\n    /^uid$/i,          // exact match: \"uid\"\n    /^uuid$/i,         // exact match: \"uuid\"\n    /^pk$/i,           // exact match: \"pk\" (primary key)\n    /^primary_key$/i   // exact match: \"primary_key\"\n  ];\n\n  // Find first field matching ID patterns\n  for (const pattern of idPatterns) {\n    const idField = fields.find(field => pattern.test(field));\n    if (idField) return idField;\n  }\n\n  // Fallback: return first field (most common case)\n  return fields[0];\n}\n\n// Helper function to check if a field is an ID field\nfunction isIdField(fieldName, allFields) {\n  return fieldName === detectIdField(allFields);\n}\n\n// Helper function to format SQL values for SQLite\nfunction formatSqlValue(fieldName, value) {\n  // Handle null values\n  if (value === null || value === 'null' || value === '') {\n    return 'NULL';\n  }\n\n  // Convert to string and trim\n  const stringValue = String(value).trim();\n\n  // Handle timestamp fields (SQLite specific)\n  const timestampPatterns = [\n    /.*_at$/i,        // ends with \"_at\": created_at, updated_at, last_seen_at\n    /.*_time$/i,      // ends with \"_time\": start_time, end_time\n    /.*timestamp$/i,  // ends with \"timestamp\"\n    /^date$/i,        // exact: \"date\"\n    /^time$/i         // exact: \"time\"\n  ];\n\n  const isTimestampField = timestampPatterns.some(pattern => pattern.test(fieldName));\n\n  if (isTimestampField) {\n    // Check if it's a Unix timestamp (numbers only, reasonable length)\n    if (/^\\d{10,13}$/.test(stringValue)) {\n      return stringValue; // Keep as number for SQLite\n    }\n\n    // Check if it's already a valid date string\n    const parsedDate = new Date(stringValue);\n    if (!isNaN(parsedDate.getTime())) {\n      // Convert to Unix timestamp for SQLite (milliseconds to seconds)\n      return Math.floor(parsedDate.getTime() / 1000);\n    }\n\n    // If we can't parse it as a date, treat it as NULL\n    return 'NULL';\n  }\n\n  // Handle numeric fields\n  if (/^\\-?\\d+(\\.\\d+)?$/.test(stringValue)) {\n    return stringValue; // No quotes for numbers\n  }\n\n  // Handle boolean fields (SQLite uses 1/0 for boolean)\n  if (stringValue.toLowerCase() === 'true' || stringValue.toLowerCase() === 'false') {\n    return stringValue.toLowerCase() === 'true' ? '1' : '0';\n  }\n\n  // Default: treat as string with proper escaping for SQLite\n  return `'${stringValue.replace(/'/g, \"''\")}'`;\n}\n\nfunction addEventListeners() {\n  const theme = context.grafana.theme;\n\n  // Edit icon listeners\n  document.querySelectorAll('.edit-icon').forEach(icon => {\n    icon.addEventListener('click', function () {\n      const rowIndex = this.getAttribute('data-row-index');\n      enterEditMode(rowIndex);\n    });\n  });\n\n  // Save icon listeners\n  document.querySelectorAll('.save-icon').forEach(icon => {\n    icon.addEventListener('click', async function () {\n      const deviceId = this.getAttribute('data-device-id');\n      const rowIndex = this.getAttribute('data-row-index');\n      await saveRow(deviceId, rowIndex);\n    });\n  });\n\n  // Cancel icon listeners\n  document.querySelectorAll('.cancel-icon').forEach(icon => {\n    icon.addEventListener('click', function () {\n      const rowIndex = this.getAttribute('data-row-index');\n      cancelEdit(rowIndex);\n    });\n  });\n\n  // Delete icon listeners\n  document.querySelectorAll('.delete-icon').forEach(icon => {\n    icon.addEventListener('click', async function () {\n      const deviceId = this.getAttribute('data-device-id');\n      await deleteDevice(deviceId, this);\n    });\n  });\n\n  // Cell click listeners for inline editing\n  document.querySelectorAll('.editable-cell').forEach(cell => {\n    cell.addEventListener('click', function () {\n      const rowIndex = this.getAttribute('data-row-index');\n      if (editingCell === null) {\n        enterEditMode(rowIndex);\n        makeInlineEditable(this);\n      }\n    });\n  });\n\n  // Add keyboard navigation support\n  document.addEventListener('keydown', handleGlobalKeyboard);\n}\n\n// Global keyboard handler for navigation and shortcuts\nfunction handleGlobalKeyboard(e) {\n  // Only handle keyboard events during editing\n  if (editingCell === null) return;\n\n  if (e.key === 'Escape') {\n    e.preventDefault();\n    cancelEdit(editingCell);\n  }\n\n  if (e.ctrlKey && e.key === 's') {\n    e.preventDefault();\n    const row = document.querySelector(`tr[data-row-index=\"${editingCell}\"]`);\n    const saveIcon = row.querySelector('.save-icon');\n    if (saveIcon && saveIcon.style.display !== 'none') {\n      saveIcon.click();\n    }\n  }\n}\n\nfunction enterEditMode(rowIndex) {\n  if (editingCell !== null) {\n    context.grafana.notifyError('Please save or cancel current edit before editing another row');\n    return;\n  }\n\n  const row = document.querySelector(`tr[data-row-index=\"${rowIndex}\"]`);\n  const editIcon = row.querySelector('.edit-icon');\n  const saveIcon = row.querySelector('.save-icon');\n  const cancelIcon = row.querySelector('.cancel-icon');\n\n  // Toggle icon visibility\n  editIcon.style.display = 'none';\n  saveIcon.style.display = 'inline-block';\n  cancelIcon.style.display = 'inline-block';\n\n  // Store current values before making cells editable\n  const cells = row.querySelectorAll('.editable-cell');\n  cells.forEach(cell => {\n    const cellId = cell.getAttribute('data-cell-id');\n    const currentValue = cell.textContent.trim();\n    originalValues[cellId] = currentValue;\n  });\n\n  // Make cells editable\n  cells.forEach(cell => {\n    makeInlineEditable(cell);\n  });\n\n  editingCell = rowIndex;\n}\n\nfunction makeInlineEditable(cell) {\n  const theme = context.grafana.theme;\n  const cellId = cell.getAttribute('data-cell-id');\n  const fieldName = cell.getAttribute('data-field');\n  const rowIndex = cell.getAttribute('data-row-index');\n  \n  // Get original value\n  let originalValue = cell.getAttribute('data-original-value');\n  if (!originalValue) {\n    originalValue = originalValues[cellId];\n  }\n  if (originalValue === undefined || originalValue === null) {\n    originalValue = cell.textContent.trim();\n  }\n  if (!originalValue) {\n    originalValue = '';\n  }\n\n  // Skip ID field editing\n  if (fieldName === tableStructure.idField) {\n    context.grafana.notifyError('ID field cannot be edited');\n    return;\n  }\n\n  // Create input with better styling\n  const input = document.createElement('input');\n  input.type = 'text';\n  input.value = originalValue;\n  input.setAttribute('data-original-value', originalValue);\n  input.setAttribute('data-field', fieldName);\n\n  // Add placeholder for timestamp fields\n  const timestampPatterns = [\n    /.*_at$/i, /.*_time$/i, /.*timestamp$/i, /^date$/i, /^time$/i\n  ];\n  const isTimestampField = timestampPatterns.some(pattern => pattern.test(fieldName));\n\n  if (isTimestampField) {\n    input.placeholder = 'e.g., 1671234567 (Unix) or 2023-12-17T10:30:00Z (ISO)';\n    input.title = 'Enter Unix timestamp or ISO date format';\n  }\n\n  input.style.cssText = `\n    width: 100%;\n    background: ${theme.colors.background.canvas};\n    border: 1px solid ${theme.colors.border.medium};\n    color: ${theme.colors.text.primary};\n    font-family: ${theme.typography.fontFamily};\n    font-size: ${theme.typography.fontSize};\n    padding: ${theme.spacing(1)};\n    border-radius: ${theme.shape.radius.default}px;\n    box-sizing: border-box;\n    outline: none;\n    transition: all 0.2s ease;\n    box-shadow: none;\n  `;\n\n  // Enhanced keyboard navigation and input handling\n  input.addEventListener('keydown', function (e) {\n    handleInputKeyboard(e, cell, input);\n  });\n\n  input.addEventListener('focus', function () {\n    currentFocusedInput = input;\n    this.style.borderColor = theme.colors.primary.border;\n    this.style.boxShadow = `0 0 0 2px ${theme.colors.primary.transparent}`;\n  });\n\n  input.addEventListener('blur', function () {\n    this.style.borderColor = theme.colors.border.medium;\n    this.style.boxShadow = 'none';\n  });\n\n  // Validate input on change\n  input.addEventListener('input', function () {\n    validateInput(input, theme);\n  });\n\n  cell.innerHTML = '';\n  cell.appendChild(input);\n  input.focus();\n  input.select();\n  currentFocusedInput = input;\n}\n\n// Enhanced keyboard handling for input fields\nfunction handleInputKeyboard(e, cell, input) {\n  switch (e.key) {\n    case 'Enter':\n      e.preventDefault();\n      const nextCell = getNextEditableCell(cell);\n      if (nextCell) {\n        makeInlineEditable(nextCell);\n      } else {\n        // If last cell, save the row\n        const row = cell.closest('tr');\n        const saveIcon = row.querySelector('.save-icon');\n        if (saveIcon && saveIcon.style.display !== 'none') {\n          saveIcon.click();\n        }\n      }\n      break;\n\n    case 'Tab':\n      e.preventDefault();\n      if (e.shiftKey) {\n        // Shift+Tab: Move to previous editable cell\n        const prevCell = getPreviousEditableCell(cell);\n        if (prevCell) {\n          makeInlineEditable(prevCell);\n        }\n      } else {\n        // Tab: Move to next editable cell\n        const nextCell = getNextEditableCell(cell);\n        if (nextCell) {\n          makeInlineEditable(nextCell);\n        }\n      }\n      break;\n\n    case 'Escape':\n      e.preventDefault();\n      cancelEdit(editingCell);\n      break;\n  }\n}\n\n// Input validation with visual feedback\nfunction validateInput(input, theme) {\n  const value = input.value.trim();\n  const fieldName = input.getAttribute('data-field');\n\n  // Basic validation\n  if (value === '') {\n    input.style.borderColor = theme.colors.warning.border;\n    input.style.boxShadow = `0 0 0 2px ${theme.colors.warning.transparent}`;\n    input.title = 'Field cannot be empty';\n    return false;\n  }\n\n  // Validate timestamp fields\n  const timestampPatterns = [\n    /.*_at$/i, /.*_time$/i, /.*timestamp$/i, /^date$/i, /^time$/i\n  ];\n\n  const isTimestampField = timestampPatterns.some(pattern => pattern.test(fieldName));\n\n  if (isTimestampField) {\n    // Check if it's a valid Unix timestamp\n    if (/^\\d{10,13}$/.test(value)) {\n      const timestamp = parseInt(value);\n      const date = new Date(timestamp > 9999999999 ? timestamp : timestamp * 1000);\n\n      if (isNaN(date.getTime())) {\n        input.style.borderColor = theme.colors.error.border;\n        input.style.boxShadow = `0 0 0 2px ${theme.colors.error.transparent}`;\n        input.title = 'Invalid timestamp format';\n        return false;\n      }\n\n      // Show parsed date in tooltip for validation\n      input.title = `Will be saved as: ${date.toISOString()}`;\n      input.style.borderColor = theme.colors.success.border;\n      input.style.boxShadow = `0 0 0 2px ${theme.colors.success.transparent}`;\n      return true;\n    }\n\n    // Check if it's a valid date string\n    const parsedDate = new Date(value);\n    if (!isNaN(parsedDate.getTime())) {\n      input.title = `Will be saved as: ${parsedDate.toISOString()}`;\n      input.style.borderColor = theme.colors.success.border;\n      input.style.boxShadow = `0 0 0 2px ${theme.colors.success.transparent}`;\n      return true;\n    }\n\n    // Invalid timestamp format\n    input.style.borderColor = theme.colors.error.border;\n    input.style.boxShadow = `0 0 0 2px ${theme.colors.error.transparent}`;\n    input.title = 'Invalid date/time format. Use Unix timestamp (e.g., 1671234567) or ISO date (e.g., 2023-12-17T10:30:00Z)';\n    return false;\n  }\n\n  // Default validation passed\n  input.style.borderColor = theme.colors.border.medium;\n  input.style.boxShadow = 'none';\n  input.title = '';\n  return true;\n}\n\nfunction getNextEditableCell(currentCell) {\n  const row = currentCell.closest('tr');\n  const allCells = Array.from(row.querySelectorAll('.editable-cell'));\n  const currentIndex = allCells.indexOf(currentCell);\n\n  // Return next editable cell in same row, or first cell of next row\n  if (currentIndex < allCells.length - 1) {\n    return allCells[currentIndex + 1];\n  } else {\n    // Move to next row if available\n    const nextRow = row.nextElementSibling;\n    if (nextRow) {\n      const firstEditableCell = nextRow.querySelector('.editable-cell');\n      if (firstEditableCell) {\n        // Switch to next row edit mode\n        const nextRowIndex = nextRow.getAttribute('data-row-index');\n        if (nextRowIndex !== editingCell) {\n          cancelEdit(editingCell);\n          enterEditMode(nextRowIndex);\n          return firstEditableCell;\n        }\n      }\n    }\n  }\n  return null;\n}\n\nfunction getPreviousEditableCell(currentCell) {\n  const row = currentCell.closest('tr');\n  const allCells = Array.from(row.querySelectorAll('.editable-cell'));\n  const currentIndex = allCells.indexOf(currentCell);\n\n  // Return previous editable cell in same row, or last cell of previous row\n  if (currentIndex > 0) {\n    return allCells[currentIndex - 1];\n  } else {\n    // Move to previous row if available\n    const prevRow = row.previousElementSibling;\n    if (prevRow) {\n      const lastEditableCell = prevRow.querySelector('.editable-cell:last-of-type');\n      if (lastEditableCell) {\n        // Switch to previous row edit mode\n        const prevRowIndex = prevRow.getAttribute('data-row-index');\n        if (prevRowIndex !== editingCell) {\n          cancelEdit(editingCell);\n          enterEditMode(prevRowIndex);\n          return lastEditableCell;\n        }\n      }\n    }\n  }\n  return null;\n}\n\nasync function saveRow(deviceId, rowIndex) {\n  const theme = context.grafana.theme;\n  const row = document.querySelector(`tr[data-row-index=\"${rowIndex}\"]`);\n  const cells = row.querySelectorAll('.editable-cell');\n\n  // Collect updated data from editable fields only\n  const updatedData = {};\n  const changes = {};\n  let hasChanges = false;\n  let hasValidationErrors = false;\n\n  cells.forEach(cell => {\n    const fieldName = cell.getAttribute('data-field');\n    const cellId = cell.getAttribute('data-cell-id');\n    let newValue;\n\n    const input = cell.querySelector('input');\n    if (input) {\n      newValue = input.value.trim();\n\n      // Use the enhanced validation function\n      if (!validateInput(input, theme)) {\n        hasValidationErrors = true;\n        return;\n      }\n    } else {\n      newValue = cell.textContent.trim();\n    }\n\n    updatedData[fieldName] = newValue;\n\n    // Check for changes\n    if (originalValues[cellId] !== newValue) {\n      hasChanges = true;\n      changes[fieldName] = {\n        old: originalValues[cellId],\n        new: newValue\n      };\n    }\n  });\n\n  if (hasValidationErrors) {\n    context.grafana.notifyError('Please fix validation errors before saving');\n    return;\n  }\n\n  if (!hasChanges) {\n    exitEditMode(rowIndex);\n    context.grafana.notifySuccess('No changes to save');\n    return;\n  }\n\n  try {\n    // Build UPDATE SQL query for SQLite with only changed fields\n    const updates = Object.keys(changes)\n      .map(key => {\n        const newValue = changes[key].new;\n        return `${key} = ${formatSqlValue(key, newValue)}`;\n      })\n      .join(', ');\n\n    const updateSql = `UPDATE devices SET ${updates} WHERE ${tableStructure.idField} = ${deviceId}`;\n\n    // Show saving state\n    const saveIcon = row.querySelector('.save-icon');\n    const originalSaveClass = saveIcon.className;\n    saveIcon.className = 'save-icon fas fa-spinner fa-spin';\n    saveIcon.style.pointerEvents = 'none';\n    saveIcon.title = 'Saving to SQLite...';\n\n    console.log('Executing SQLite UPDATE:', updateSql);\n\n    // Execute UPDATE SQL using SQLite\n    await context.dataSource.sql(currentDataSourceUid, updateSql);\n\n    // Update UI with new values and reset original values\n    cells.forEach(cell => {\n      const fieldName = cell.getAttribute('data-field');\n      const cellId = cell.getAttribute('data-cell-id');\n      const newValue = updatedData[fieldName];\n\n      cell.innerHTML = formatDisplayValue(fieldName, newValue);\n      originalValues[cellId] = newValue; // Update stored original values\n    });\n\n    // Restore save icon before exiting edit mode\n    saveIcon.className = originalSaveClass;\n    saveIcon.style.pointerEvents = 'auto';\n    saveIcon.title = 'Save changes';\n\n    exitEditMode(rowIndex);\n\n    // Show detailed success message\n    const changedFields = Object.keys(changes).join(', ');\n    context.grafana.notifySuccess(`SQLite device updated successfully. Changed: ${changedFields}`);\n\n  } catch (error) {\n    // Re-enable save icon on error\n    const saveIcon = row.querySelector('.save-icon');\n    saveIcon.className = originalSaveClass;\n    saveIcon.style.pointerEvents = 'auto';\n    saveIcon.title = 'Save changes';\n\n    // Enhanced error handling for SQLite\n    let errorMessage = 'Unknown error occurred';\n    let errorDetails = '';\n\n    if (error && error.message) {\n      errorMessage = error.message;\n    }\n\n    // Check if it's a database query error with additional details\n    if (error && error.data && error.data.results) {\n      const results = error.data.results;\n      Object.keys(results).forEach(key => {\n        const result = results[key];\n        if (result.error) {\n          errorMessage = result.error;\n          if (result.errorSource) {\n            errorDetails += `Source: ${result.errorSource}. `;\n          }\n\n          // Extract the executed query for debugging\n          if (result.frames && result.frames[0] && result.frames[0].schema && result.frames[0].schema.meta) {\n            const executedQuery = result.frames[0].schema.meta.executedQueryString;\n            if (executedQuery) {\n              console.error('Failed SQLite Query:', executedQuery);\n              errorDetails += `Query: ${executedQuery.substring(0, 100)}${executedQuery.length > 100 ? '...' : ''}`;\n            }\n          }\n        }\n      });\n    }\n\n    // Show comprehensive error notification\n    const fullErrorMessage = `SQLite update failed: ${errorMessage}${errorDetails ? ' | ' + errorDetails : ''}`;\n    context.grafana.notifyError(fullErrorMessage);\n\n    console.error('SQLite save operation failed:', {\n      originalError: error,\n      processedMessage: fullErrorMessage,\n      deviceId: deviceId,\n      changes: changes\n    });\n  }\n}\n\nfunction cancelEdit(rowIndex) {\n  const row = document.querySelector(`tr[data-row-index=\"${rowIndex}\"]`);\n  const cells = row.querySelectorAll('.editable-cell');\n\n  // Restore original values\n  cells.forEach(cell => {\n    const cellId = cell.getAttribute('data-cell-id');\n    const fieldName = cell.getAttribute('data-field');\n    const originalValue = originalValues[cellId];\n    cell.innerHTML = formatDisplayValue(fieldName, originalValue);\n  });\n\n  exitEditMode(rowIndex);\n  context.grafana.notifySuccess('Edit cancelled');\n}\n\nfunction exitEditMode(rowIndex) {\n  const row = document.querySelector(`tr[data-row-index=\"${rowIndex}\"]`);\n  const editIcon = row.querySelector('.edit-icon');\n  const saveIcon = row.querySelector('.save-icon');\n  const cancelIcon = row.querySelector('.cancel-icon');\n\n  // Toggle icon visibility\n  editIcon.style.display = 'inline-block';\n  saveIcon.style.display = 'none';\n  cancelIcon.style.display = 'none';\n\n  // Reset save icon to original state (in case it was a spinner)\n  saveIcon.className = 'save-icon fas fa-check';\n  saveIcon.style.pointerEvents = 'auto';\n  saveIcon.title = 'Save changes';\n\n  // Clean up state\n  editingCell = null;\n  currentFocusedInput = null;\n}\n\nasync function deleteDevice(deviceId, iconElement) {\n  if (!confirm(`Are you sure you want to delete device ${deviceId}?`)) {\n    return;\n  }\n\n  const theme = context.grafana.theme;\n\n  // Disable icon during delete\n  const originalClass = iconElement.className;\n  iconElement.className = 'delete-icon fas fa-spinner fa-spin';\n  iconElement.style.pointerEvents = 'none';\n  iconElement.title = 'Deleting from SQLite...';\n\n  try {\n    // Execute DELETE SQL using SQLite syntax\n    const deleteSql = `DELETE FROM devices WHERE ${tableStructure.idField} = ${deviceId}`;\n    console.log('Executing SQLite DELETE:', deleteSql);\n    \n    await context.dataSource.sql(currentDataSourceUid, deleteSql);\n\n    // Remove the row from table with smooth animation\n    const row = iconElement.closest('tr');\n    row.style.transition = 'opacity 0.3s ease';\n    row.style.opacity = '0';\n    setTimeout(() => row.remove(), 300);\n\n    // Show success notification\n    context.grafana.notifySuccess(`SQLite device ${deviceId} deleted successfully`);\n\n  } catch (error) {\n    // Re-enable icon on error\n    iconElement.className = originalClass;\n    iconElement.style.pointerEvents = 'auto';\n    iconElement.title = 'Delete row';\n\n    // Enhanced error handling for delete operation\n    let errorMessage = 'Unknown error occurred';\n\n    if (error && error.message) {\n      errorMessage = error.message;\n    }\n\n    // Check for database-specific errors\n    if (error && error.data && error.data.results) {\n      const results = error.data.results;\n      Object.keys(results).forEach(key => {\n        const result = results[key];\n        if (result.error) {\n          errorMessage = result.error;\n        }\n      });\n    }\n\n    // Show comprehensive error notification\n    const fullErrorMessage = `SQLite delete failed: ${errorMessage}`;\n    context.grafana.notifyError(fullErrorMessage);\n\n    console.error('SQLite delete operation failed:', {\n      originalError: error,\n      processedMessage: fullErrorMessage,\n      deviceId: deviceId\n    });\n  }\n}\n\n// Clean up function to remove global event listeners\nfunction cleanup() {\n  document.removeEventListener('keydown', handleGlobalKeyboard);\n  editingCell = null;\n  currentFocusedInput = null;\n}\n\n// Initialize: Load devices when the script runs\ndocument.getElementById('load-devices').addEventListener('click', loadDevices);",
        "externalStyles": [],
        "stylesRemoteUrl": "",
        "styles": "",
        "dataSource": {
          "enableDataSourceQueries": true,
          "queryTimeout": 30000,
          "showQueryErrors": true
        }
      }
    }
  ],
  "refresh": "",
  "schemaVersion": 39,
  "tags": [
    "sqlite",
    "device-management",
    "crud"
  ],
  "templating": {
    "list": []
  },
  "time": {
    "from": "now-6h",
    "to": "now"
  },
  "timepicker": {},
  "timezone": "",
  "title": "SQLite based device management",
  "uid": "sqlite_device_management",
  "version": 1
}