{
  "name": "TWBS",
  "nodes": [
    {
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst source = input.body && typeof input.body === 'object' && !Array.isArray(input.body)\n  ? { ...input, ...input.body }\n  : input;\n\nconst pagePresets = {\n  overview: ['protocols', 'users', 'devices', 'syncMetrics', 'notifications', 'announcements'],\n  companyDetail: ['sites', 'powerplants', 'protocols', 'users', 'devices', 'syncMetrics', 'notifications', 'announcements'],\n  technicalHealth: ['syncMetrics', 'devices', 'users', 'notifications'],\n  protocolExplorer: ['protocols', 'sites', 'powerplants']\n};\n\nconst defaultResources = ['sites', 'powerplants', 'protocols', 'users', 'devices', 'syncMetrics', 'notifications', 'announcements'];\nconst defaultFilters = {\n  lang: 'en',\n  status: 'all',\n  sort: 'latest',\n  limit: 100,\n  offset: 0,\n  includeProtocolTopics: false,\n  includeProtocolData: false,\n  dateFrom: null,\n  dateTo: null,\n  siteId: null,\n  powerplantId: null,\n  templateName: null\n};\n\nconst defaultCustomer = {\n  \"key\": \"twbs\",\n  \"label\": \"TWBS\",\n  \"baseUrl\": \"http://host.docker.internal:1345/api/v1\",\n  \"token\": \"\",\n  \"environment\": \"\"\n};\n\nconst resources = Array.isArray(source.resources) && source.resources.length > 0\n  ? source.resources\n  : (source.pagePreset && pagePresets[source.pagePreset]) || defaultResources;\n\nconst customer = {\n  key: source.customerKey || source.customer || defaultCustomer.key,\n  label: source.customerLabel || source.customer || defaultCustomer.label,\n  baseUrl: String(source.baseUrl || defaultCustomer.baseUrl).replace(/\\/+$/, ''),\n  token: source.token || defaultCustomer.token || '',\n  environment: source.environment || defaultCustomer.environment || ''\n};\n\nconst authFromWebhook =\n  input.headers?.authorization ||\n  input.headers?.Authorization ||\n  source.authorization ||\n  '';\n\nreturn [{\n  json: {\n    requestedAt: new Date().toISOString(),\n    customer,\n    pagePreset: source.pagePreset || 'companyDetail',\n    resources: [...new Set(resources)],\n    filters: { ...defaultFilters, ...(source.filters || {}) },\n    resourceFilters: source.resourceFilters || {},\n    includeDebug: !!source.includeDebug,\n    authFromWebhook\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1872,
        -576
      ],
      "id": "e90270b5-b8fb-48b0-b9b1-b220b4e69325",
      "name": "Normalize Request"
    },
    {
      "parameters": {
        "jsCode": "const request = $input.first().json;\n\nconst registry = {\n  health: {\n    path: 'health',\n    auth: false,\n    buildQuery: () => ({})\n  },\n  sites: {\n    path: 'sites',\n    auth: true,\n    buildQuery: (filters) => ({\n      limit: filters.limit,\n      offset: filters.offset,\n      lang: filters.lang\n    })\n  },\n  powerplants: {\n    path: 'powerplants',\n    auth: true,\n    buildQuery: (filters) => ({\n      limit: filters.limit,\n      offset: filters.offset,\n      siteId: filters.siteId || undefined\n    })\n  },\n  protocols: {\n    path: 'protocols',\n    auth: true,\n    buildQuery: (filters) => ({\n      limit: filters.limit,\n      offset: filters.offset,\n      status: filters.status || 'all',\n      sort: filters.sort || 'latest',\n      dateFrom: filters.dateFrom || undefined,\n      dateTo: filters.dateTo || undefined,\n      lang: filters.lang || undefined,\n      powerplantId: filters.powerplantId || undefined,\n      templateName: filters.templateName || undefined,\n      expand: filters.includeProtocolTopics ? 'topics' : undefined,\n      includeData: filters.includeProtocolData ? '1' : undefined\n    })\n  },\n  users: {\n    path: 'users',\n    auth: true,\n    buildQuery: (filters) => ({\n      limit: filters.limit,\n      offset: filters.offset\n    })\n  },\n  devices: {\n    path: 'devices',\n    auth: true,\n    buildQuery: (filters) => ({\n      limit: filters.limit,\n      offset: filters.offset\n    })\n  },\n  syncMetrics: {\n    path: 'sync-metrics',\n    auth: true,\n    buildQuery: (filters) => ({\n      dateFrom: filters.dateFrom || undefined,\n      dateTo: filters.dateTo || undefined\n    })\n  },\n  notifications: {\n    path: 'notifications',\n    auth: true,\n    buildQuery: (filters) => ({\n      limit: filters.limit,\n      offset: filters.offset\n    })\n  },\n  announcements: {\n    path: 'announcements',\n    auth: true,\n    buildQuery: (filters) => ({\n      limit: filters.limit,\n      offset: filters.offset\n    })\n  }\n};\n\nfunction compactObject(value) {\n  const output = {};\n  for (const [key, item] of Object.entries(value || {})) {\n    if (item === undefined || item === null || item === '') continue;\n    output[key] = item;\n  }\n  return output;\n}\n\nfunction toQueryString(query) {\n  return Object.entries(compactObject(query))\n    .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)\n    .join('&');\n}\n\nfunction buildAuthHeader(needsAuth) {\n  if (!needsAuth) return '';\n  const rawToken = String(request.customer.token || request.authFromWebhook || '').trim();\n  if (!rawToken) {\n    throw new Error('Missing token for customer ' + request.customer.key + '.');\n  }\n  return /^Bearer\\s+/i.test(rawToken) ? rawToken : ('Bearer ' + rawToken);\n}\n\nconst items = [];\nfor (const resourceName of request.resources) {\n  const definition = registry[resourceName];\n  if (!definition) {\n    throw new Error('Unknown resource: ' + resourceName);\n  }\n\n  const filters = {\n    ...request.filters,\n    ...(request.resourceFilters?.[resourceName] || {})\n  };\n  const query = definition.buildQuery(filters);\n  const queryString = toQueryString(query);\n  const url = request.customer.baseUrl + '/' + definition.path + (queryString ? '?' + queryString : '');\n\n  items.push({\n    json: {\n      resourceName,\n      customer: request.customer,\n      requestedAt: request.requestedAt,\n      pagePreset: request.pagePreset,\n      filters,\n      query,\n      url,\n      authHeader: buildAuthHeader(definition.auth),\n      includeDebug: request.includeDebug\n    }\n  });\n}\n\nreturn items;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2096,
        -576
      ],
      "id": "d5776358-c398-4a41-9679-69da7ec75151",
      "name": "Build Requests"
    },
    {
      "parameters": {
        "url": "={{ $json.url }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "={{ $json.authHeader }}"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        2320,
        -576
      ],
      "id": "75402c29-fe60-45fb-a3ba-21da563b383a",
      "name": "API Request"
    },
    {
      "parameters": {
        "jsCode": "const request = $('Normalize Request').first().json;\nconst builtItems = $('Build Requests').all().map((item) => item.json);\nconst responseItems = $input.all();\n\nfunction countBy(items, selector) {\n  return (items || []).reduce((accumulator, item) => {\n    const key = selector(item) || 'unknown';\n    accumulator[key] = (accumulator[key] || 0) + 1;\n    return accumulator;\n  }, {});\n}\n\nfunction templateNameValue(item) {\n  const value = item?.templateName;\n  if (typeof value === 'string') return value;\n  if (value && typeof value === 'object') {\n    return value.default || Object.values(value)[0] || 'unknown';\n  }\n  return 'unknown';\n}\n\nfunction deriveProtocols(resource) {\n  const items = Array.isArray(resource?.items) ? resource.items : [];\n  return {\n    totalCount: resource?.totalCount || items.length,\n    byStatus: countBy(items, (item) => item.status),\n    byTemplate: countBy(items, (item) => templateNameValue(item)),\n    byPowerplant: countBy(items, (item) => item.powerplantId)\n  };\n}\n\nfunction deriveUsers(resource) {\n  const items = Array.isArray(resource?.items) ? resource.items : [];\n  const now = Date.parse(request.requestedAt);\n  const withinDays = (value, days) => {\n    if (!value) return false;\n    const timestamp = Date.parse(value);\n    if (Number.isNaN(timestamp)) return false;\n    return now - timestamp <= days * 24 * 60 * 60 * 1000;\n  };\n\n  return {\n    totalCount: resource?.totalCount || items.length,\n    seenLast1d: items.filter((item) => withinDays(item.lastSeenAt, 1)).length,\n    seenLast7d: items.filter((item) => withinDays(item.lastSeenAt, 7)).length,\n    seenLast30d: items.filter((item) => withinDays(item.lastSeenAt, 30)).length\n  };\n}\n\nfunction deriveDevices(resource) {\n  const items = Array.isArray(resource?.items) ? resource.items : [];\n  return {\n    totalCount: resource?.totalCount || items.length,\n    byAppVersion: countBy(items, (item) => item.appVersion),\n    byOs: countBy(items, (item) => item.os),\n    active: items.filter((item) => item.active).length,\n    inactive: items.filter((item) => !item.active).length\n  };\n}\n\nfunction deriveNotifications(resource) {\n  const items = Array.isArray(resource?.items) ? resource.items : [];\n  return {\n    totalCount: resource?.totalCount || items.length,\n    byStatus: countBy(items, (item) => item.status),\n    byType: countBy(items, (item) => item.type)\n  };\n}\n\nfunction deriveSyncMetrics(resource) {\n  const byCustomer = Array.isArray(resource?.failures?.byCustomer) ? resource.failures.byCustomer : [];\n  const byDevice = Array.isArray(resource?.failures?.byDevice) ? resource.failures.byDevice : [];\n  return {\n    environment: resource?.environment || 'unknown',\n    totalFailures: resource?.failures?.total || 0,\n    byCustomer,\n    topFailingDevices: [...byDevice].sort((left, right) => right.count - left.count).slice(0, 10)\n  };\n}\n\nconst data = {};\nconst errors = [];\n\nfor (let index = 0; index < responseItems.length; index += 1) {\n  const meta = builtItems[index];\n  const payload = responseItems[index]?.json;\n\n  if (!meta) continue;\n\n  if (meta.includeDebug && payload && typeof payload === 'object') {\n    payload._meta = {\n      url: meta.url,\n      query: meta.query,\n      limitApplied: meta.filters.limit,\n      truncated: typeof payload.totalCount === 'number' && Array.isArray(payload.items) ? payload.totalCount > payload.items.length : false\n    };\n  }\n\n  data[meta.resourceName] = payload;\n}\n\nconst derived = {};\nif (data.protocols) derived.protocols = deriveProtocols(data.protocols);\nif (data.users) derived.users = deriveUsers(data.users);\nif (data.devices) derived.devices = deriveDevices(data.devices);\nif (data.notifications) derived.notifications = deriveNotifications(data.notifications);\nif (data.syncMetrics) derived.syncMetrics = deriveSyncMetrics(data.syncMetrics);\n\nreturn [{\n  json: {\n    requestedAt: request.requestedAt,\n    customer: request.customer,\n    pagePreset: request.pagePreset,\n    resources: request.resources,\n    filters: request.filters,\n    data,\n    derived,\n    errors,\n    notes: [\n      'This HTTP Request node template fetches one page per list endpoint using the requested limit.',\n      'Set limit up to 100. If totalCount exceeds items.length, the response is truncated and should be paginated in a future workflow revision.'\n    ]\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2544,
        -576
      ],
      "id": "79453fca-af82-4d4b-ab4f-bac94c31d2d0",
      "name": "Aggregate Results"
    },
    {
      "parameters": {
        "content": "## Mock Data for now\nBecause we don't have access to their endpoints yet"
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        2112,
        256
      ],
      "id": "b14412d3-b4c5-4fd6-a8af-da12660ca6a1",
      "name": "Sticky Note"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "dashboarddatav2-twbs",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        1712,
        64
      ],
      "id": "d00f67ad-4b9a-4c6b-91d7-e6ecb0ff5bd0",
      "name": "Webhook"
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "={{ $json.prompt }}",
        "batching": {}
      },
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.9,
      "position": [
        2608,
        192
      ],
      "id": "4e7bd807-9776-49f0-b941-53ab1b321775",
      "name": "Basic LLM Chain1"
    },
    {
      "parameters": {
        "model": "mpn-openai-sweden-gpt-5-mini",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.lmChatAzureOpenAi",
      "typeVersion": 1,
      "position": [
        2688,
        416
      ],
      "id": "e924d0bf-8062-4631-abf6-c53270c5f7f3",
      "name": "Azure OpenAI Chat Model1",
      "credentials": {
        "azureOpenAiApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "raw",
        "jsonOutput": "{\n  \"currentState\": \"={{ $json.currentState }}\",\n  \"prompt\": \"={{ `You are generating realistic mutation actions for an energy operations dashboard mock API.\\n\\nReturn STRICT JSON only.\\nDo not include markdown.\\nDo not include explanations.\\nDo not return the full dashboard.\\n\\nReturn this exact shape:\\n{\\n  \\\"actions\\\": []\\n}\\n\\nAllowed action types: add_user, create_protocol, transition_protocol, add_notification, add_announcement\\nAllowed protocol statuses: open, closed, exported\\nMake only 1 to 3 small realistic changes.\\n\\nCurrent dashboard state:\\n${JSON.stringify($json.currentState)}` }}\"\n}",
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2384,
        192
      ],
      "id": "665b0c4c-b21d-4ad9-96a5-89522963b119",
      "name": "Edit Fields1"
    },
    {
      "parameters": {
        "jsCode": "const store = $getWorkflowStaticData('global');\nconst input = $input.first().json;\nconst source = input.body && typeof input.body === 'object' && !Array.isArray(input.body)\n  ? { ...input, ...input.body }\n  : input;\nconst cacheTtlMs = Math.max(1000, Number(source.cacheTtlMs) || 30000);\n\nif (!store.dashboardStateUpdatedAtMs && store.dashboardState?.requestedAt) {\n  const parsedRequestedAt = Date.parse(store.dashboardState.requestedAt);\n  store.dashboardStateUpdatedAtMs = Number.isFinite(parsedRequestedAt) ? parsedRequestedAt : Date.now();\n}\nstore.dashboardStateRefreshingUntilMs = Number.isFinite(store.dashboardStateRefreshingUntilMs)\n  ? store.dashboardStateRefreshingUntilMs\n  : 0;\n\nif (!store.dashboardState) {\n  store.dashboardState = {\n    requestedAt: \"2026-03-12T11:45:00.000Z\",\n    customer: {\n      key: \"twbs\",\n      label: \"TWBS Energy\",\n      baseUrl: \"http://host.docker.internal:1370/api/v1\",\n      token: \"\",\n      environment: \"production\"\n    },\n    pagePreset: \"companyDetail\",\n    resources: [\n      \"sites\",\n      \"powerplants\",\n      \"protocols\",\n      \"protocolTemplates\",\n      \"groups\",\n      \"users\",\n      \"devices\",\n      \"events\",\n      \"syncMetrics\",\n      \"notifications\",\n      \"announcements\"\n    ],\n    filters: {\n      lang: \"en\",\n      status: \"all\",\n      sort: \"latest\",\n      limit: 100,\n      offset: 0,\n      includeProtocolTopics: true,\n      includeProtocolData: true,\n      dateFrom: \"2026-01-01T00:00:00Z\",\n      dateTo: \"2026-03-12T23:59:59Z\",\n      siteId: null,\n      powerplantId: null,\n      templateName: null,\n      eventName: null,\n      category: null,\n      payloadType: null\n    },\n    data: {\n      sites: {\n        items: [\n          {\n            siteId: \"TWBS_PROD_SITE_a1Kr5N_910001001_1_BY_A1\",\n            name: \"Bavaria Wind Hub\",\n            customerType: \"utility\",\n            hyperLinkString: \"\",\n            primaryLanguage: \"german\",\n            abbreviationName: \"BWH\",\n            address: {\n              id: \"\",\n              name: \"\",\n              street: \"Windparkstrasse 12\",\n              city: \"Munich\",\n              zip: \"80331\",\n              country: \"Germany\",\n              latitude: 48.1351,\n              longitude: 11.5820,\n              geocodingExactMatch: 1\n            },\n            image: \"\"\n          },\n          {\n            siteId: \"TWBS_PROD_SITE_b2Ls6O_910001002_2_HH_B2\",\n            name: \"Hamburg Solar Campus\",\n            customerType: \"commercial\",\n            hyperLinkString: \"\",\n            primaryLanguage: \"german\",\n            abbreviationName: \"HSC\",\n            address: {\n              id: \"\",\n              name: \"\",\n              street: \"Sonnenweg 44\",\n              city: \"Hamburg\",\n              zip: \"20095\",\n              country: \"Germany\",\n              latitude: 53.5511,\n              longitude: 9.9937,\n              geocodingExactMatch: 1\n            },\n            image: \"\"\n          },\n          {\n            siteId: \"TWBS_PROD_SITE_c3Mt7P_910001003_3_NW_C3\",\n            name: \"Rhine Grid Park\",\n            customerType: \"utility\",\n            hyperLinkString: \"\",\n            primaryLanguage: \"english\",\n            abbreviationName: \"RGP\",\n            address: {\n              id: \"\",\n              name: \"\",\n              street: \"Energieallee 7\",\n              city: \"Cologne\",\n              zip: \"50667\",\n              country: \"Germany\",\n              latitude: 50.9375,\n              longitude: 6.9603,\n              geocodingExactMatch: 1\n            },\n            image: \"\"\n          }\n        ],\n        totalCount: 3,\n        limit: 100,\n        offset: 0\n      },\n      powerplants: {\n        items: [\n          {\n            powerplantId: \"TWBS_PROD_PP_a1Kr5N_910002101_1_WT_D4\",\n            siteId: \"TWBS_PROD_SITE_a1Kr5N_910001001_1_BY_A1\",\n            name: \"Bavaria Turbine Cluster A\",\n            code: \"BWH-A\",\n            hyperLinkString: \"\",\n            customerType: \"utility\",\n            thumbnailImage: \"\",\n            image: \"\",\n            latitude: 48.1360,\n            longitude: 11.5830,\n            powerplantTypeIds: [\"wind\", \"onshore\"]\n          },\n          {\n            powerplantId: \"TWBS_PROD_PP_a1Kr5N_910002102_2_WT_E5\",\n            siteId: \"TWBS_PROD_SITE_a1Kr5N_910001001_1_BY_A1\",\n            name: \"Bavaria Turbine Cluster B\",\n            code: \"BWH-B\",\n            hyperLinkString: \"\",\n            customerType: \"utility\",\n            thumbnailImage: \"\",\n            image: \"\",\n            latitude: 48.1340,\n            longitude: 11.5810,\n            powerplantTypeIds: [\"wind\", \"onshore\"]\n          },\n          {\n            powerplantId: \"TWBS_PROD_PP_b2Ls6O_910002103_3_SL_F6\",\n            siteId: \"TWBS_PROD_SITE_b2Ls6O_910001002_2_HH_B2\",\n            name: \"Hamburg Solar Field 1\",\n            code: \"HSC-F1\",\n            hyperLinkString: \"\",\n            customerType: \"commercial\",\n            thumbnailImage: \"\",\n            image: \"\",\n            latitude: 53.5520,\n            longitude: 9.9945,\n            powerplantTypeIds: [\"solar\"]\n          },\n          {\n            powerplantId: \"TWBS_PROD_PP_c3Mt7P_910002104_4_GR_G7\",\n            siteId: \"TWBS_PROD_SITE_c3Mt7P_910001003_3_NW_C3\",\n            name: \"Rhine Storage Unit East\",\n            code: \"RGP-E\",\n            hyperLinkString: \"\",\n            customerType: \"utility\",\n            thumbnailImage: \"\",\n            image: \"\",\n            latitude: 50.9383,\n            longitude: 6.9612,\n            powerplantTypeIds: [\"battery\", \"grid\"]\n          }\n        ],\n        totalCount: 4,\n        limit: 100,\n        offset: 0\n      },\n      protocols: {\n        items: [\n          {\n            protocolId: \"TWBS_PROD_PRT_qwerTy_910003401_1_AA_H1\",\n            powerplantId: \"TWBS_PROD_PP_a1Kr5N_910002101_1_WT_D4\",\n            protocolBriefcaseId: \"\",\n            templateName: \"Turbine Inspection (v2)\",\n            name: \"Turbine Inspection (v2)\",\n            date: { unixOffset: 1772150000 },\n            time: \"07:00\",\n            status: \"open\",\n            reportId: \"\",\n            reports: [],\n            owner: \"Field Engineer Schneider\"\n          },\n          {\n            protocolId: \"TWBS_PROD_PRT_qwerTy_910003402_2_AB_H2\",\n            powerplantId: \"TWBS_PROD_PP_a1Kr5N_910002101_1_WT_D4\",\n            protocolBriefcaseId: \"\",\n            templateName: \"Turbine Inspection (v2)\",\n            name: \"Turbine Inspection (v2)\",\n            date: { unixOffset: 1772080000 },\n            time: \"09:30\",\n            status: \"closed\",\n            reportId: \"RPT-TWBS-2026-042\",\n            reports: [],\n            owner: \"Field Engineer Schneider\"\n          },\n          {\n            protocolId: \"TWBS_PROD_PRT_qwerTy_910003403_3_AC_H3\",\n            powerplantId: \"TWBS_PROD_PP_b2Ls6O_910002103_3_SL_F6\",\n            protocolBriefcaseId: \"\",\n            templateName: \"Solar Array Audit (v1)\",\n            name: \"Solar Array Audit (v1)\",\n            date: { unixOffset: 1772000000 },\n            time: \"11:15\",\n            status: \"closed\",\n            reportId: \"\",\n            reports: [],\n            owner: \"Solar Lead Wagner\"\n          },\n          {\n            protocolId: \"TWBS_PROD_PRT_qwerTy_910003404_4_AD_H4\",\n            powerplantId: \"TWBS_PROD_PP_c3Mt7P_910002104_4_GR_G7\",\n            protocolBriefcaseId: \"\",\n            templateName: \"Storage Safety Check (v1)\",\n            name: \"Storage Safety Check (v1)\",\n            date: { unixOffset: 1771920000 },\n            time: \"14:10\",\n            status: \"exported\",\n            reportId: \"\",\n            reports: [],\n            owner: \"Grid Ops Muller\"\n          }\n        ],\n        totalCount: 4,\n        limit: 100,\n        offset: 0\n      },\n      users: {\n        items: [\n          {\n            username: \"field-engineer-schneider\",\n            firstName: \"Lena\",\n            lastName: \"Schneider\",\n            publicParticipantId: \"PUtwbs01\",\n            lastSeenAt: \"2026-03-12T10:20:00.000Z\",\n            deviceCount: 1,\n            devices: [\n              {\n                lastConnectionDate: \"2026-03-12T10:20:00.000Z\",\n                application: \"engineeroffice\",\n                appVersion: \"121.10.0\"\n              }\n            ]\n          },\n          {\n            username: \"solar-lead-wagner\",\n            firstName: \"Tobias\",\n            lastName: \"Wagner\",\n            publicParticipantId: \"PUtwbs02\",\n            lastSeenAt: \"2026-03-12T09:00:00.000Z\",\n            deviceCount: 2,\n            devices: [\n              {\n                lastConnectionDate: \"2026-03-12T09:00:00.000Z\",\n                application: \"administration\",\n                appVersion: \"120.35.1\"\n              },\n              {\n                lastConnectionDate: \"2026-03-11T17:30:00.000Z\",\n                application: \"engineeroffice\",\n                appVersion: \"121.10.0\"\n              }\n            ]\n          }\n        ],\n        totalCount: 2,\n        limit: 100,\n        offset: 0\n      },\n      devices: {\n        items: [\n          {\n            lastConnectionDate: \"2026-03-12T10:20:00.000Z\",\n            appVersion: \"121.10.0\",\n            application: \"engineeroffice\",\n            active: true\n          },\n          {\n            lastConnectionDate: \"2026-03-12T09:00:00.000Z\",\n            appVersion: \"120.35.1\",\n            application: \"administration\",\n            active: true\n          },\n          {\n            lastConnectionDate: \"2026-03-11T17:30:00.000Z\",\n            appVersion: \"121.10.0\",\n            application: \"engineeronsite\",\n            active: true\n          }\n        ],\n        totalCount: 3,\n        limit: 100,\n        offset: 0\n      },\n      syncMetrics: {\n        environment: \"production\",\n        attemptsAndSuccessesAvailable: true,\n        failures: {\n          total: 2,\n          byCustomer: [{ customerKey: \"twbs\", count: 2 }],\n          byDevice: [\n            { deviceId: \"dev-twbs-007\", count: 1 },\n            { deviceId: \"dev-twbs-006\", count: 1 }\n          ]\n        }\n      },\n      notifications: {\n        items: [\n          {\n            id: \"notif-twbs-001\",\n            type: \"maintenance\",\n            status: \"unread\",\n            message: \"Scheduled maintenance Bavaria Wind Hub \u2013 March 18\"\n          }\n        ],\n        totalCount: 1,\n        limit: 100,\n        offset: 0\n      },\n      announcements: {\n        items: [\n          {\n            id: \"ann-twbs-001\",\n            title: \"Q1 2026 reporting deadline\",\n            publishedAt: \"2026-03-01T08:00:00.000Z\"\n          }\n        ],\n        totalCount: 1,\n        limit: 100,\n        offset: 0\n      }\n    },\n    derived: {\n      protocols: {\n        totalCount: 4,\n        byStatus: {\n          open: 1,\n          closed: 2,\n          exported: 1\n        },\n        byTemplate: {\n          \"Turbine Inspection (v2)\": 2,\n          \"Solar Array Audit (v1)\": 1,\n          \"Storage Safety Check (v1)\": 1\n        },\n        byPowerplant: {\n          \"TWBS_PROD_PP_a1Kr5N_910002101_1_WT_D4\": 2,\n          \"TWBS_PROD_PP_b2Ls6O_910002103_3_SL_F6\": 1,\n          \"TWBS_PROD_PP_c3Mt7P_910002104_4_GR_G7\": 1\n        }\n      },\n      users: {\n        totalCount: 2,\n        seenLast1d: 2,\n        seenLast7d: 2,\n        seenLast30d: 2\n      },\n      devices: {\n        totalCount: 3,\n        byAppVersion: {\n          \"121.10.0\": 2,\n          \"120.35.1\": 1\n        },\n        byOs: {\n          android: 2,\n          ios: 1,\n          unknown: 0\n        },\n        active: 3,\n        inactive: 0\n      },\n      notifications: {\n        totalCount: 1,\n        byStatus: {\n          unread: 1\n        },\n        byType: {\n          maintenance: 1\n        }\n      },\n      syncMetrics: {\n        environment: \"production\",\n        totalFailures: 2,\n        byCustomer: [{ customerKey: \"twbs\", count: 2 }],\n        topFailingDevices: [\n          { deviceId: \"dev-twbs-007\", count: 1 },\n          { deviceId: \"dev-twbs-006\", count: 1 }\n        ]\n      }\n    },\n    errors: [],\n    notes: [\n      \"TWBS mock: 3 sites, 4 powerplants, 4 protocols, 2 users, 3 devices, 1 notification, 1 announcement. Germany locale, production.\",\n      \"Mock data evolves over time through AI-generated mutation actions.\"\n    ]\n  };\n}\n\nif (!Number.isFinite(store.dashboardStateUpdatedAtMs)) {\n  store.dashboardStateUpdatedAtMs = Date.now();\n}\n\nconst now = Date.now();\nconst hasState = !!store.dashboardState;\nconst cacheFresh = hasState && Number.isFinite(store.dashboardStateUpdatedAtMs)\n  ? (now - store.dashboardStateUpdatedAtMs) <= cacheTtlMs\n  : false;\nconst refreshInProgress = hasState && store.dashboardStateRefreshingUntilMs > now;\nconst serveCachedResponse = hasState && (cacheFresh || refreshInProgress);\n\nif (!serveCachedResponse) {\n  store.dashboardStateRefreshingUntilMs = now + cacheTtlMs;\n}\n\nreturn [\n  {\n    json: {\n      currentState: store.dashboardState,\n      cacheTtlMs,\n      serveCachedResponse\n    }\n  }\n];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1936,
        64
      ],
      "id": "0583d0c4-24ae-423d-bf90-59d31d658fa1",
      "name": "Code in JavaScript2"
    },
    {
      "parameters": {
        "jsCode": "const store = $getWorkflowStaticData('global');\nconst state = store.dashboardState;\n\nfunction nowIso() {\n  return new Date().toISOString();\n}\n\nfunction unixNow() {\n  return Math.floor(Date.now() / 1000);\n}\n\nfunction makeId(prefix) {\n  return `${prefix}_${Date.now()}_${Math.floor(Math.random() * 10000)}`;\n}\n\nfunction pickOpenOrClosedProtocol(fromStatus) {\n  const matches = state.data.protocols.items.filter(p => p.status === fromStatus);\n  if (!matches.length) return null;\n  return matches[Math.floor(Math.random() * matches.length)];\n}\n\nfunction safeJsonParse(value) {\n  if (typeof value === 'object' && value !== null) return value;\n  if (typeof value !== 'string') return null;\n  try {\n    return JSON.parse(value);\n  } catch {\n    return null;\n  }\n}\n\nfunction ensureArray(value) {\n  return Array.isArray(value) ? value : [];\n}\n\nfunction ensureCollection(value) {\n  const collection = value && typeof value === 'object' ? value : {};\n  const items = ensureArray(collection.items);\n\n  return {\n    items,\n    totalCount: Number.isFinite(collection.totalCount) ? collection.totalCount : items.length,\n    limit: Number.isFinite(collection.limit) ? collection.limit : (state.filters?.limit ?? 100),\n    offset: Number.isFinite(collection.offset) ? collection.offset : (state.filters?.offset ?? 0)\n  };\n}\n\nfunction ensureGroupCollection(value) {\n  const collection = value && typeof value === 'object' ? value : {};\n  const items = ensureArray(collection.items);\n\n  return {\n    items,\n    totalCount: Number.isFinite(collection.totalCount) ? collection.totalCount : items.length\n  };\n}\n\nfunction ensureSyncMetrics(value) {\n  const metrics = value && typeof value === 'object' ? value : {};\n  const failures = metrics.failures && typeof metrics.failures === 'object' ? metrics.failures : {};\n  const items = ensureArray(metrics.items);\n\n  return {\n    environment: typeof metrics.environment === 'string' && metrics.environment\n      ? metrics.environment\n      : (state.customer?.environment || 'unknown'),\n    attemptsAndSuccessesAvailable: Boolean(metrics.attemptsAndSuccessesAvailable),\n    items,\n    totalCount: Number.isFinite(metrics.totalCount) ? metrics.totalCount : items.length,\n    limit: Number.isFinite(metrics.limit) ? metrics.limit : (state.filters?.limit ?? 100),\n    offset: Number.isFinite(metrics.offset) ? metrics.offset : (state.filters?.offset ?? 0),\n    dateFrom: metrics.dateFrom ?? state.filters?.dateFrom ?? null,\n    dateTo: metrics.dateTo ?? state.filters?.dateTo ?? null,\n    capped: typeof metrics.capped === 'boolean' ? metrics.capped : false,\n    maxDocsRead: Number.isFinite(metrics.maxDocsRead) ? metrics.maxDocsRead : 5000,\n    failures: {\n      total: Number.isFinite(failures.total) ? failures.total : 0,\n      byCustomer: ensureArray(failures.byCustomer),\n      byDevice: ensureArray(failures.byDevice)\n    }\n  };\n}\n\nfunction ensureDashboardShape() {\n  state.resources = Array.from(new Set([\n    ...(Array.isArray(state.resources) ? state.resources : []),\n    'sites',\n    'powerplants',\n    'protocols',\n    'protocolTemplates',\n    'groups',\n    'users',\n    'devices',\n    'events',\n    'syncMetrics',\n    'notifications',\n    'announcements'\n  ]));\n\n  const filters = state.filters && typeof state.filters === 'object' ? state.filters : {};\n  state.filters = {\n    lang: filters.lang || 'en',\n    status: filters.status || 'all',\n    sort: filters.sort || 'latest',\n    limit: Number.isFinite(filters.limit) ? filters.limit : 100,\n    offset: Number.isFinite(filters.offset) ? filters.offset : 0,\n    includeProtocolTopics: Boolean(filters.includeProtocolTopics),\n    includeProtocolData: Boolean(filters.includeProtocolData),\n    dateFrom: filters.dateFrom ?? null,\n    dateTo: filters.dateTo ?? null,\n    siteId: filters.siteId ?? null,\n    powerplantId: filters.powerplantId ?? null,\n    templateName: filters.templateName ?? null,\n    eventName: filters.eventName ?? null,\n    category: filters.category ?? null,\n    payloadType: filters.payloadType ?? null\n  };\n\n  state.data = state.data && typeof state.data === 'object' ? state.data : {};\n  state.data.protocolTemplates = ensureCollection(state.data.protocolTemplates);\n  state.data.groups = ensureGroupCollection(state.data.groups);\n  state.data.events = ensureCollection(state.data.events);\n  state.data.syncMetrics = ensureSyncMetrics(state.data.syncMetrics);\n  state.data.notifications = ensureCollection(state.data.notifications);\n  state.data.announcements = ensureCollection(state.data.announcements);\n}\n\nensureDashboardShape();\nfunction applyFallbackMutation() {\n  const message = `Auto-update: status check at ${new Date().toISOString().slice(11, 16)} UTC`;\n  state.data.notifications.items.unshift({\n    id: makeId('notif-enx'),\n    type: 'announcement',\n    status: 'unread',\n    message\n  });\n\n  if (state.data.users.items[0]) {\n    state.data.users.items[0].lastSeenAt = nowIso();\n  }\n\n  state.data.syncMetrics.failures.total += 1;\n}\n\n// LLM output can vary by node config; try a few common locations\nconst raw =\n  $json.text ??\n  $json.output ??\n  $json.response ??\n  $json.completion ??\n  $json.data ??\n  $json;\n\n// parse\nconst parsed = safeJsonParse(raw);\nlet actions = [];\n\nif (!parsed || !Array.isArray(parsed.actions)) {\n  // keep previous good state if AI output is bad, but still mutate a little\n  state.notes.unshift(`Invalid AI output ignored at ${nowIso()}`);\n} else {\n  actions = parsed.actions.slice(0, 3);\n}\n\nif (!actions.length) {\n  applyFallbackMutation();\n}\n\nfor (const action of actions) {\n  if (!action || typeof action !== 'object') continue;\n\n  if (action.type === 'add_user') {\n    const firstName = action.firstName || 'Alex';\n    const lastName = action.lastName || 'Meyer';\n    const username = `${firstName}-${lastName}`.toLowerCase().replace(/\\s+/g, '-');\n    const app = ['field', 'administration'].includes(action.application) ? action.application : 'field';\n    const version = action.appVersion || '121.10.0';\n\n    const exists = state.data.users.items.some(u => u.username === username);\n    if (!exists) {\n      const device = {\n        lastConnectionDate: nowIso(),\n        application: app,\n        appVersion: version\n      };\n\n      state.data.users.items.push({\n        username,\n        firstName,\n        lastName,\n        publicParticipantId: makeId('PUenx'),\n        lastSeenAt: nowIso(),\n        deviceCount: 1,\n        devices: [device]\n      });\n\n      state.data.devices.items.push({\n        lastConnectionDate: device.lastConnectionDate,\n        appVersion: device.appVersion,\n        application: device.application,\n        active: true\n      });\n    }\n  }\n\n  if (action.type === 'create_protocol') {\n    const powerplantExists = state.data.powerplants.items.some(\n      p => p.powerplantId === action.powerplantId\n    );\n\n    if (powerplantExists) {\n      const allowedTemplates = [\n        'Offshore Inspection (v3)',\n        'Safety Checklist (v2)',\n        'Solar Panel Audit (v1)',\n        'Emergency Repair (v1)'\n      ];\n\n      const templateName = allowedTemplates.includes(action.templateName)\n        ? action.templateName\n        : 'Offshore Inspection (v3)';\n\n      state.data.protocols.items.unshift({\n        protocolId: makeId('ENX_PROD_PRT'),\n        powerplantId: action.powerplantId,\n        protocolBriefcaseId: '',\n        templateName,\n        name: templateName,\n        date: { unixOffset: unixNow() },\n        time: new Date().toISOString().slice(11, 16),\n        status: 'open',\n        reportId: '',\n        reports: [],\n        owner: action.owner || 'Operations Team'\n      });\n    }\n  }\n\n  if (action.type === 'transition_protocol') {\n    const fromStatus = action.fromStatus;\n    const toStatus = action.toStatus;\n    const count = Math.max(1, Math.min(Number(action.count) || 1, 3));\n\n    const valid =\n      (fromStatus === 'open' && toStatus === 'closed') ||\n      (fromStatus === 'closed' && toStatus === 'exported');\n\n    if (valid) {\n      for (let i = 0; i < count; i++) {\n        const protocol = pickOpenOrClosedProtocol(fromStatus);\n        if (!protocol) break;\n\n        protocol.status = toStatus;\n        if (toStatus === 'closed' && !protocol.reportId) {\n          protocol.reportId = `RPT-ENX-${new Date().getFullYear()}-${Math.floor(Math.random() * 1000)\n            .toString()\n            .padStart(3, '0')}`;\n        }\n      }\n    }\n  }\n\n  if (action.type === 'add_notification') {\n    const notificationType = ['maintenance', 'alert', 'announcement'].includes(action.notificationType)\n      ? action.notificationType\n      : 'announcement';\n\n    const status = ['read', 'unread'].includes(action.status) ? action.status : 'unread';\n    const message = action.message || 'Operational update available';\n\n    state.data.notifications.items.unshift({\n      id: makeId('notif-enx'),\n      type: notificationType,\n      status,\n      message\n    });\n  }\n\n  if (action.type === 'add_announcement') {\n    state.data.announcements.items.unshift({\n      id: makeId('ann-enx'),\n      title: action.title || 'Operational update',\n      publishedAt: nowIso()\n    });\n  }\n}\n\n// light natural activity even without explicit AI action\nfor (const user of state.data.users.items) {\n  if (Math.random() < 0.25) {\n    user.lastSeenAt = nowIso();\n    if (Array.isArray(user.devices) && user.devices[0]) {\n      user.devices[0].lastConnectionDate = nowIso();\n    }\n  }\n}\n\nstate.requestedAt = nowIso();\n\n// totals\nstate.data.sites.totalCount = state.data.sites.items.length;\nstate.data.powerplants.totalCount = state.data.powerplants.items.length;\nstate.data.protocols.totalCount = state.data.protocols.items.length;\nstate.data.protocolTemplates.totalCount = state.data.protocolTemplates.items.length;\nstate.data.groups.totalCount = state.data.groups.items.length;\nstate.data.users.totalCount = state.data.users.items.length;\nstate.data.devices.totalCount = state.data.devices.items.length;\nstate.data.events.totalCount = state.data.events.items.length;\nstate.data.syncMetrics.totalCount = state.data.syncMetrics.items.length;\nstate.data.syncMetrics.limit = state.filters.limit;\nstate.data.syncMetrics.offset = state.filters.offset;\nstate.data.syncMetrics.dateFrom = state.data.syncMetrics.dateFrom ?? state.filters.dateFrom ?? null;\nstate.data.syncMetrics.dateTo = state.data.syncMetrics.dateTo ?? state.filters.dateTo ?? null;\nstate.data.notifications.totalCount = state.data.notifications.items.length;\nstate.data.announcements.totalCount = state.data.announcements.items.length;\n\n// derived.protocols\nconst protocolStatus = { open: 0, closed: 0, exported: 0 };\nconst protocolTemplate = {};\nconst protocolPowerplant = {};\n\nfor (const p of state.data.protocols.items) {\n  protocolStatus[p.status] = (protocolStatus[p.status] || 0) + 1;\n  protocolTemplate[p.templateName] = (protocolTemplate[p.templateName] || 0) + 1;\n  protocolPowerplant[p.powerplantId] = (protocolPowerplant[p.powerplantId] || 0) + 1;\n}\n\n// derived.users\nconst now = Date.now();\nlet seenLast1d = 0;\nlet seenLast7d = 0;\nlet seenLast30d = 0;\n\nfor (const u of state.data.users.items) {\n  const t = new Date(u.lastSeenAt).getTime();\n  const diff = now - t;\n  if (diff <= 1 * 24 * 60 * 60 * 1000) seenLast1d++;\n  if (diff <= 7 * 24 * 60 * 60 * 1000) seenLast7d++;\n  if (diff <= 30 * 24 * 60 * 60 * 1000) seenLast30d++;\n}\n\n// derived.devices\nconst byAppVersion = {};\nlet active = 0;\nlet inactive = 0;\n\nfor (const d of state.data.devices.items) {\n  byAppVersion[d.appVersion] = (byAppVersion[d.appVersion] || 0) + 1;\n  if (d.active) active++;\n  else inactive++;\n}\n\n// derived.notifications\nconst notifByStatus = {};\nconst notifByType = {};\nfor (const n of state.data.notifications.items) {\n  notifByStatus[n.status] = (notifByStatus[n.status] || 0) + 1;\n  notifByType[n.type] = (notifByType[n.type] || 0) + 1;\n}\n\nstate.derived = {\n  protocols: {\n    totalCount: state.data.protocols.items.length,\n    byStatus: protocolStatus,\n    byTemplate: protocolTemplate,\n    byPowerplant: protocolPowerplant\n  },\n  users: {\n    totalCount: state.data.users.items.length,\n    seenLast1d,\n    seenLast7d,\n    seenLast30d\n  },\n  devices: {\n    totalCount: state.data.devices.items.length,\n    byAppVersion,\n    byOs: {\n      android: Math.floor(state.data.devices.items.length / 2),\n      ios: Math.floor(state.data.devices.items.length / 2),\n      unknown: state.data.devices.items.length % 2\n    },\n    active,\n    inactive\n  },\n  notifications: {\n    totalCount: state.data.notifications.items.length,\n    byStatus: notifByStatus,\n    byType: notifByType\n  },\n  syncMetrics: {\n    environment: state.data.syncMetrics.environment,\n    totalFailures: state.data.syncMetrics.failures.total,\n    byCustomer: state.data.syncMetrics.failures.byCustomer,\n    topFailingDevices: state.data.syncMetrics.failures.byDevice\n  }\n};\n\nstate.errors = [];\nstore.dashboardState = state;\nstore.dashboardStateUpdatedAtMs = Date.now();\nstore.dashboardStateRefreshingUntilMs = 0;\n\nreturn [{ json: state }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2960,
        192
      ],
      "id": "766be527-4c05-4fc2-b9f4-a6ce23ec3347",
      "name": "Code in JavaScript3"
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.5,
      "position": [
        3184,
        64
      ],
      "id": "adc41473-ccd0-45f5-9a44-664d84bc3981",
      "name": "Respond to Webhook"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "twbs-cache-check",
              "leftValue": "={{ $json.serveCachedResponse ? \"1\" : \"\" }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "notEmpty"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        2160,
        64
      ],
      "id": "dbfd7862-553a-460f-937f-0299f94f0556",
      "name": "Use Cached Response?"
    },
    {
      "parameters": {
        "jsCode": "const response = $input.first().json.cachedResponse || $input.first().json.currentState || {};\nreturn [{ json: response }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2960,
        -64
      ],
      "id": "e791c549-8a97-4174-959d-be49cfd35220",
      "name": "Return Cached Response"
    }
  ],
  "connections": {
    "Normalize Request": {
      "main": [
        [
          {
            "node": "Build Requests",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Requests": {
      "main": [
        [
          {
            "node": "API Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "API Request": {
      "main": [
        [
          {
            "node": "Aggregate Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook": {
      "main": [
        [
          {
            "node": "Code in JavaScript2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Basic LLM Chain1": {
      "main": [
        [
          {
            "node": "Code in JavaScript3",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Azure OpenAI Chat Model1": {
      "ai_languageModel": [
        [
          {
            "node": "Basic LLM Chain1",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields1": {
      "main": [
        [
          {
            "node": "Basic LLM Chain1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript2": {
      "main": [
        [
          {
            "node": "Use Cached Response?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript3": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Use Cached Response?": {
      "main": [
        [
          {
            "node": "Return Cached Response",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Edit Fields1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Return Cached Response": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "availableInMCP": false
  },
  "versionId": "c84ca849-a0b1-4a59-95e9-276572cd1a94",
  "id": "SCKJj4jJrGbDWSqq",
  "tags": []
}