<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Paminga for Dynamics 365 - Configuration</title>
<!--
  Web resource: paminga_/config/index.html
  Set as the solution's Configuration Page (Solutions > Paminga for Dynamics 365 > ... > Settings >
  Configuration page). Opens from the solution Overview.

  Constraints honoured here:
   - No external scripts or stylesheets except ClientGlobalContext.js.aspx (inline CSS/JS only).
   - ClientGlobalContext.js.aspx is referenced RELATIVELY. This web resource is named
     "paminga_/config/index.html" (two simulated folders deep), so the root is "../../".
     Microsoft warns against "/WebResources/ClientGlobalContext.js.aspx" (loses org context):
     https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/clientapi/reference/getglobalcontext-clientglobalcontext.js.aspx
   - No Xrm.Page. Xrm is reached only through parent.Xrm (works only when the page is loaded in a
     form container, per the page above) with a same-origin fallback described in the script.
-->
<script src="../../ClientGlobalContext.js.aspx" type="text/javascript"></script>
<style>
  :root {
    --paminga-blue: #0298FF;
    --paminga-blue-dark: #0277c8;
    --ink: #1f2933;
    --muted: #5f6b7a;
    --line: #e3e8ee;
    --bg: #f7f9fb;
    --card: #ffffff;
    --ok: #1a7f37;
    --ok-bg: #e6f4ea;
    --warn: #8a5a00;
    --warn-bg: #fff4d6;
    --err: #b3261e;
    --err-bg: #fde8e7;
  }
  * { box-sizing: border-box; }
  html, body { margin: 0; }
  body {
    font: 14px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    color: var(--ink);
    background: var(--bg);
    padding: 20px 16px 40px;
  }
  .wrap { max-width: 860px; margin: 0 auto; }
  header {
    display: flex; align-items: center; gap: 12px;
    padding-bottom: 14px; margin-bottom: 20px;
    border-bottom: 3px solid var(--paminga-blue);
  }
  header h1 { font-size: 20px; font-weight: 600; margin: 0; }
  header .sub { color: var(--muted); font-size: 13px; margin: 2px 0 0; }
  section.card {
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: 8px;
    padding: 18px 22px;
    margin-bottom: 16px;
  }
  h2 {
    font-size: 15px; font-weight: 600; margin: 0 0 10px;
    display: flex; align-items: center; gap: 10px;
  }
  .step {
    display: inline-flex; align-items: center; justify-content: center;
    width: 24px; height: 24px; border-radius: 50%;
    background: var(--paminga-blue); color: #fff; font-size: 13px; font-weight: 700;
  }
  p { margin: 0 0 10px; }
  p.muted, li.muted, span.muted { color: var(--muted); }
  ol, ul { margin: 0 0 10px; padding-left: 22px; }
  li { margin-bottom: 5px; }
  code, kbd {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 12.5px; background: #eef4fa; border-radius: 4px; padding: 1px 5px;
  }
  table { border-collapse: collapse; width: 100%; margin: 8px 0 12px; font-size: 13px; }
  th, td { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--line); vertical-align: top; }
  th { color: var(--muted); font-weight: 600; white-space: nowrap; }
  a { color: var(--paminga-blue); text-decoration: none; }
  a:hover { text-decoration: underline; }
  .todo { background: var(--warn-bg); color: var(--warn); border-radius: 4px; padding: 1px 6px; font-size: 12px; font-weight: 600; }
  .note { border-left: 3px solid var(--paminga-blue); background: #f0f7ff; padding: 8px 12px; border-radius: 0 6px 6px 0; margin: 8px 0 12px; }
  label { display: block; font-size: 12px; font-weight: 600; color: var(--muted); margin-bottom: 4px; }
  input[type=text], input[type=password] {
    width: 100%; padding: 8px 10px; font: inherit; border: 1px solid #c9d2dc; border-radius: 6px; background: #fff;
  }
  input:focus { outline: 2px solid var(--paminga-blue); outline-offset: 1px; border-color: var(--paminga-blue); }
  .field { margin-bottom: 12px; }
  .row { display: flex; gap: 12px; flex-wrap: wrap; }
  .row .field { flex: 1 1 260px; }
  button {
    font: inherit; font-weight: 600; color: #fff; background: var(--paminga-blue);
    border: 0; border-radius: 6px; padding: 9px 16px; cursor: pointer;
  }
  button:hover { background: var(--paminga-blue-dark); }
  button:disabled { opacity: .6; cursor: default; }
  button.secondary { background: #fff; color: var(--paminga-blue); border: 1px solid var(--paminga-blue); }
  .status { border-radius: 6px; padding: 8px 12px; margin-top: 10px; font-size: 13px; white-space: pre-wrap; word-break: break-word; }
  .status.ok { background: var(--ok-bg); color: var(--ok); }
  .status.warn { background: var(--warn-bg); color: var(--warn); }
  .status.err { background: var(--err-bg); color: var(--err); }
  .status.info { background: #eef4fa; color: var(--ink); }
  .kv { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 13px; margin: 8px 0 12px; }
  .kv dt { color: var(--muted); }
  .kv dd { margin: 0; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12.5px; }
  [hidden] { display: none !important; }
</style>
</head>
<body>
<div class="wrap">

  <header>
    <svg width="36" height="36" viewBox="0 0 28 28" aria-hidden="true">
      <circle cx="14" cy="14" r="14" fill="#0298FF"/>
      <text x="14" y="19.5" text-anchor="middle" font-family="-apple-system, Segoe UI, Roboto, Arial, sans-serif" font-size="16" font-weight="700" fill="#ffffff">P</text>
    </svg>
    <div>
      <h1>Paminga for Dynamics 365</h1>
      <p class="sub">Shows the Paminga CRM panel on Account, Contact and Lead forms. Version 1.0.0.0</p>
    </div>
  </header>

  <section class="card">
    <h2>Before you start</h2>
    <ul>
      <li>You need a Paminga <strong>personal access token</strong> for the Paminga user the panel should act as. Create one in Paminga under your user profile. One token serves the whole Dynamics 365 organization.</li>
      <li>You need the <strong>System Administrator</strong> or <strong>System Customizer</strong> role in this environment to edit forms and environment variables.</li>
      <li>Full guide: <a id="docsLink" href="https://www.paminga.com/docs/integrations/crm-integrations/dynamics/dynamics-installation-package" target="_blank" rel="noopener noreferrer">Paminga docs: Dynamics 365 solution package</a></li>
    </ul>
    <div class="note">
      <strong>One token, one identity.</strong> The panel signs in to Paminga with that token, so:
      <ul>
        <li>Everything anyone does in the panel is attributed to the token's Paminga user, not to the Dynamics 365 user viewing the form.</li>
        <li>Every user who holds the <strong>Paminga User</strong> role can read the token (the role grants read access to it, and it is part of the panel URL in the form). Treat it as a shared credential: create it for a dedicated, least-privilege Paminga user rather than an administrator's account.</li>
        <li>Rotating or revoking the token disconnects the panel for every user at once until <code>paminga_ApiToken</code> is updated.</li>
      </ul>
    </div>
  </section>

  <section class="card">
    <h2><span class="step">1</span> Set the environment variables</h2>
    <p>In Power Apps open <strong>Solutions &rsaquo; Paminga for Dynamics 365 &rsaquo; Environment variables</strong> and set the <em>current value</em> of each:</p>
    <table>
      <thead><tr><th>Schema name</th><th>Type</th><th>Value</th></tr></thead>
      <tbody>
        <tr>
          <td><code>paminga_ApiToken</code></td>
          <td>Text</td>
          <td>Your Paminga personal access token. No default; the panel shows "Paminga is not connected yet" until this is set.</td>
        </tr>
        <tr>
          <td><code>paminga_PanelBaseUrl</code></td>
          <td>Text</td>
          <td>Default <code>https://crm.paminga.com/crm/panel/</code>. Paminga EU customers set <code>https://crm.eu.paminga.com/crm/panel/</code>. Keep the trailing slash.</td>
        </tr>
      </tbody>
    </table>
    <p class="muted">Environment variable values are read by the panel script as the signed-in user, so every user who opens these forms needs read access to them: see step 4.</p>
  </section>

  <section class="card">
    <h2><span class="step">2</span> Add the panel IFRAME to each form</h2>
    <p>Repeat for the <strong>main form</strong> of <strong>Account</strong>, <strong>Contact</strong> and <strong>Lead</strong> (the solution ships no forms of its own, so your existing forms are edited in place):</p>
    <ol>
      <li>Open the form in the form designer, select the section where the panel should appear (a full-width section or its own tab works best) and, under <strong>Components &rsaquo; Display</strong>, add <strong>External website</strong>. (In the classic form designer this is <strong>Insert &rsaquo; IFRAME</strong>; the classic property names are shown in brackets.)</li>
      <li>Set the control's properties exactly as below and save.</li>
    </ol>
    <table>
      <thead><tr><th>Tab</th><th>Property</th><th>Value</th></tr></thead>
      <tbody>
        <tr><td>Display options</td><td>Name</td><td><code>IFRAME_paminga_panel</code> <span class="muted">(the script finds the control by this name; it must match exactly)</span></td></tr>
        <tr><td>Display options</td><td>Label</td><td><code>Paminga</code> (tick <em>Hide label</em> if you prefer)</td></tr>
        <tr><td>Display options</td><td>URL <span class="muted">(Site URL)</span></td><td><code>about:blank</code> <span class="muted">(the script sets the real URL at load time)</span></td></tr>
        <tr><td>Display options</td><td>Hide <span class="muted">(classic: Visible by default)</span></td><td>Not hidden <span class="muted">(classic: Visible by default checked)</span></td></tr>
        <tr><td>Advanced</td><td>Pass row object-type code and unique identifiers as parameters</td><td>Unchecked</td></tr>
        <tr><td>Advanced</td><td>Restrict cross-frame scripting, where supported</td><td><strong>Unchecked. Required.</strong> <span class="muted">When it is checked the platform renders the frame with <code>sandbox=""</code>, which blocks all scripts, and the panel cannot load at all.</span></td></tr>
        <tr><td>Formatting</td><td>Column width / Component height</td><td>Full width; enough rows to give the panel about 600px, or tick <em>Use all available vertical space</em> <span class="muted">(classic: Automatically expand to use available space)</span></td></tr>
      </tbody>
    </table>
  </section>

  <section class="card">
    <h2><span class="step">3</span> Add the form library and event handlers</h2>
    <p>Still in the same form:</p>
    <ol>
      <li>Under <strong>Form libraries</strong> add <code>paminga_/scripts/panel.js</code>.</li>
      <li>Register the handler below on the three events. Every registration uses the same function and must have <strong>Pass execution context as first parameter</strong> checked and <strong>Enabled</strong> on.</li>
    </ol>
    <table>
      <thead><tr><th>Where</th><th>Event</th><th>Library</th><th>Function</th></tr></thead>
      <tbody>
        <tr><td>Form</td><td><code>OnLoad</code></td><td><code>paminga_/scripts/panel.js</code></td><td><code>Paminga.Panel.load</code></td></tr>
        <tr><td>Control <code>IFRAME_paminga_panel</code></td><td><code>OnReadyStateComplete</code></td><td><code>paminga_/scripts/panel.js</code></td><td><code>Paminga.Panel.load</code></td></tr>
        <tr><td>Contact and Lead: column <code>emailaddress1</code> (Email)<br>Account: column <code>name</code> (Account Name)</td><td><code>OnChange</code></td><td><code>paminga_/scripts/panel.js</code></td><td><code>Paminga.Panel.load</code></td></tr>
      </tbody>
    </table>
    <div class="note">
      The handler is safe to fire more than once: it checks the IFRAME's current URL and only sets it when it would change, so navigating between records in the same session keeps the panel current without reloading it needlessly.
    </div>
    <ol start="3">
      <li><strong>Save</strong> and <strong>Publish</strong> the form.</li>
    </ol>
  </section>

  <section class="card">
    <h2><span class="step">4</span> Assign security roles</h2>
    <ul>
      <li><strong>Paminga User</strong>: assign to every user who opens Account, Contact or Lead forms. It grants organization-level <em>Read</em> on Environment Variable Definitions and Environment Variable Values only, which the panel script needs to read the token and panel URL. Anyone holding this role can therefore read the token itself (see <em>Before you start</em>).</li>
      <li><strong>Paminga Sync</strong>: assign to the Dynamics 365 user account Paminga uses to synchronize records (the same privilege set as the sync role in the earlier Dynamics 365 solution).</li>
    </ul>
  </section>

  <section class="card">
    <h2>Test token</h2>
    <p>Reads the two environment variables through the Dataverse Web API, then asks the Paminga API who the token belongs to. Nothing is written.</p>

    <div id="ctxStatus" class="status info">Checking how this page is hosted...</div>

    <dl class="kv">
      <dt>Panel base URL</dt><dd id="kvBase">-</dd>
      <dt>API endpoint</dt><dd id="kvApi">-</dd>
      <dt>Token</dt><dd id="kvToken">-</dd>
    </dl>

    <div class="row">
      <div class="field">
        <label for="baseInput">Panel base URL (override)</label>
        <input type="text" id="baseInput" placeholder="https://crm.paminga.com/crm/panel/" autocomplete="off" spellcheck="false">
      </div>
      <div class="field">
        <label for="tokenInput">Token (paste here if it could not be read from the environment variable)</label>
        <input type="password" id="tokenInput" placeholder="Paminga personal access token" autocomplete="off">
      </div>
    </div>

    <button id="btnReload" class="secondary" type="button">Re-read environment variables</button>
    <button id="btnTest" type="button">Test token</button>

    <div id="testStatus" class="status info" hidden></div>
  </section>

  <p class="muted">Questions? <a href="mailto:support@paminga.com">support@paminga.com</a> &middot; <a href="https://www.paminga.com" target="_blank" rel="noopener noreferrer">www.paminga.com</a></p>
</div>

<script>
(function () {
  'use strict';

  var DEFAULT_BASE = 'https://crm.paminga.com/crm/panel/';
  var VAR_TOKEN = 'paminga_ApiToken';
  var VAR_BASE = 'paminga_PanelBaseUrl';

  var els = {
    ctx: document.getElementById('ctxStatus'),
    kvBase: document.getElementById('kvBase'),
    kvApi: document.getElementById('kvApi'),
    kvToken: document.getElementById('kvToken'),
    baseInput: document.getElementById('baseInput'),
    tokenInput: document.getElementById('tokenInput'),
    btnReload: document.getElementById('btnReload'),
    btnTest: document.getElementById('btnTest'),
    test: document.getElementById('testStatus')
  };

  var state = { token: '', base: '', tokenSource: '', baseSource: '' };

  function setStatus(el, kind, text) {
    el.className = 'status ' + kind;
    el.textContent = text;
    el.hidden = false;
  }

  function mask(token) {
    if (!token) { return '(not set)'; }
    var tail = token.length > 4 ? token.slice(-4) : '';
    return '••••••••' + tail + '  (' + token.length + ' chars)';
  }

  // --- Where is Xrm? -------------------------------------------------------------------------
  // Microsoft: "parent.Xrm.* will work if the HTML web resource is loaded in a form container.
  // However, for other places ... parent.Xrm.* also won't work."
  // https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/clientapi/reference/getglobalcontext-clientglobalcontext.js.aspx
  // A configuration page is not a form container, and the modern maker portal (make.powerapps.com)
  // is a different origin from the org, so touching window.parent can throw. Every access is guarded.
  function getXrm() {
    try {
      if (window.parent && window.parent !== window && window.parent.Xrm && window.parent.Xrm.WebApi) {
        return window.parent.Xrm;
      }
    } catch (e) { /* cross-origin parent: fall through */ }
    try {
      if (window.Xrm && window.Xrm.WebApi) { return window.Xrm; }
    } catch (e) { /* ignore */ }
    return null;
  }

  // Client (org) URL, from Xrm.Utility.getGlobalContext() when Xrm is reachable, else from the
  // GetGlobalContext() function that ClientGlobalContext.js.aspx defines for HTML web resources.
  function getClientUrl(xrm) {
    try {
      if (xrm && xrm.Utility && typeof xrm.Utility.getGlobalContext === 'function') {
        return xrm.Utility.getGlobalContext().getClientUrl();
      }
    } catch (e) { /* ignore */ }
    try {
      if (typeof window.GetGlobalContext === 'function') {
        return window.GetGlobalContext().getClientUrl();
      }
    } catch (e) { /* ignore */ }
    return '';
  }

  // --- Environment variable readers ---------------------------------------------------------
  // Primary: RetrieveEnvironmentVariableValue function (DefinitionSchemaName: Edm.String).
  // https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/reference/retrieveenvironmentvariablevalue
  // Executed through Xrm.WebApi.online.execute (operationType 1 = Function, structuralProperty 1 = PrimitiveType).
  // https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/clientapi/reference/xrm-webapi/online/execute
  function readViaExecute(xrm, schemaName) {
    return new Promise(function (resolve, reject) {
      var Req = function (name) { this.DefinitionSchemaName = name; };
      Req.prototype.getMetadata = function () {
        return {
          boundParameter: null,
          parameterTypes: {
            DefinitionSchemaName: { typeName: 'Edm.String', structuralProperty: 1 }
          },
          operationType: 1,
          operationName: 'RetrieveEnvironmentVariableValue'
        };
      };
      try {
        xrm.WebApi.online.execute(new Req(schemaName)).then(function (response) {
          if (!response.ok) { reject(new Error('HTTP ' + response.status + ' ' + response.statusText)); return; }
          return response.json();
        }).then(function (body) {
          if (body === undefined) { return; }
          resolve({ value: body && typeof body.Value === 'string' ? body.Value : '', source: 'RetrieveEnvironmentVariableValue' });
        }).catch(function (err) { reject(err instanceof Error ? err : new Error(err && err.message ? err.message : String(err))); });
      } catch (e) { reject(e); }
    });
  }

  // Fallback: read the definition and its (0..1) value row; prefer the current value over the default.
  // Entity set "environmentvariabledefinitions", navigation property
  // "environmentvariabledefinition_environmentvariablevalue":
  // https://learn.microsoft.com/en-us/power-apps/developer/data-platform/reference/entities/environmentvariabledefinition
  var DEF_QUERY = "?$select=schemaname,defaultvalue&$filter=schemaname eq '%s'&$expand=environmentvariabledefinition_environmentvariablevalue($select=value)";

  function pickFromDefinition(result) {
    var rows = (result && result.entities) || (result && result.value) || [];
    if (!rows.length) { return { value: '', source: 'definition not found' }; }
    var def = rows[0];
    var vals = def.environmentvariabledefinition_environmentvariablevalue || [];
    if (vals.length && typeof vals[0].value === 'string' && vals[0].value !== '') {
      return { value: vals[0].value, source: 'current value' };
    }
    return { value: typeof def.defaultvalue === 'string' ? def.defaultvalue : '', source: 'default value' };
  }

  function readViaRetrieveMultiple(xrm, schemaName) {
    return xrm.WebApi.retrieveMultipleRecords('environmentvariabledefinition', DEF_QUERY.replace('%s', schemaName))
      .then(pickFromDefinition);
  }

  // Last resort when no Xrm object is reachable but the org URL is known: call the Web API directly
  // from this (same-origin) web resource. The browser sends the org session cookies with a same-origin
  // fetch. Microsoft: "A web resource can contain code that the browser processes to execute web
  // service calls to interact with Dataverse data."
  // https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/web-resources
  function odataFetch(clientUrl, path) {
    return fetch(clientUrl.replace(/\/+$/, '') + '/api/data/v9.2/' + path, {
      method: 'GET',
      credentials: 'same-origin',
      headers: {
        'Accept': 'application/json',
        'OData-MaxVersion': '4.0',
        'OData-Version': '4.0'
      }
    }).then(function (r) {
      if (!r.ok) { throw new Error('Dataverse HTTP ' + r.status); }
      return r.json();
    });
  }

  function readViaDirectFetch(clientUrl, schemaName) {
    return odataFetch(clientUrl, "RetrieveEnvironmentVariableValue(DefinitionSchemaName='" + schemaName + "')")
      .then(function (body) {
        return { value: body && typeof body.Value === 'string' ? body.Value : '', source: 'RetrieveEnvironmentVariableValue (direct)' };
      })
      .catch(function () {
        return odataFetch(clientUrl, 'environmentvariabledefinitions' + DEF_QUERY.replace('%s', schemaName)).then(pickFromDefinition);
      });
  }

  function readEnvVar(schemaName) {
    var xrm = getXrm();
    if (xrm) {
      return readViaExecute(xrm, schemaName).catch(function () { return readViaRetrieveMultiple(xrm, schemaName); });
    }
    var clientUrl = getClientUrl(null);
    if (clientUrl) {
      return readViaDirectFetch(clientUrl, schemaName);
    }
    return Promise.reject(new Error('No Dataverse context available'));
  }

  // --- Paminga API endpoint -------------------------------------------------------------------
  // The panel hosts (crm.paminga.com, crm.eu.paminga.com) serve only the single-page app; the GraphQL
  // API lives on the matching api.* host (api.paminga.com, api.eu.paminga.com). Swap the first label.
  // The token is sent as a bearer header, so the target is limited to Paminga-operated hosts: a
  // mistyped or hostile base URL must not receive the organization's token.
  var ALLOWED_HOST = /(^|\.)paminga\.com$/i;

  function apiUrlFor(base) {
    var u;
    try { u = new URL(base); } catch (e) { return ''; }
    var host = u.hostname;
    if (u.protocol !== 'https:' || !ALLOWED_HOST.test(host)) { return ''; }
    if (/^crm\./i.test(host)) { host = host.replace(/^crm\./i, 'api.'); }
    return u.protocol + '//' + host + '/graphql';
  }

  function currentBase() {
    var typed = (els.baseInput.value || '').trim();
    return typed || state.base || DEFAULT_BASE;
  }

  function currentToken() {
    var typed = (els.tokenInput.value || '').trim();
    return typed || state.token || '';
  }

  function renderKv() {
    var base = currentBase();
    els.kvBase.textContent = base + (state.baseSource ? '  [' + state.baseSource + ']' : '');
    els.kvApi.textContent = apiUrlFor(base) || '(invalid base URL)';
    var typed = (els.tokenInput.value || '').trim();
    els.kvToken.textContent = typed ? mask(typed) + '  [pasted]' : mask(state.token) + (state.tokenSource ? '  [' + state.tokenSource + ']' : '');
  }

  // --- Load ------------------------------------------------------------------------------------
  function loadEnv() {
    var xrm = getXrm();
    var clientUrl = getClientUrl(xrm);
    if (xrm) {
      setStatus(els.ctx, 'info', 'Dataverse context: Xrm.WebApi available' + (clientUrl ? ' (' + clientUrl + ')' : '') + '. Reading environment variables...');
    } else if (clientUrl) {
      setStatus(els.ctx, 'info', 'Dataverse context: Xrm is not reachable from this page (expected outside a form), using the Web API directly at ' + clientUrl + '. Reading environment variables...');
    } else {
      setStatus(els.ctx, 'warn', 'Dataverse context is not available: this page is not running inside Dynamics 365, so the environment variables cannot be read here. Paste the token below to test it, and set the variables in Solutions > Paminga for Dynamics 365 > Environment variables.');
      state.token = ''; state.base = ''; state.tokenSource = ''; state.baseSource = '';
      renderKv();
      return;
    }

    els.btnReload.disabled = true;
    var problems = [];
    Promise.all([
      readEnvVar(VAR_BASE).then(function (r) { state.base = (r.value || '').trim(); state.baseSource = r.source; })
        .catch(function (e) { problems.push(VAR_BASE + ': ' + e.message); state.base = ''; state.baseSource = ''; }),
      readEnvVar(VAR_TOKEN).then(function (r) { state.token = (r.value || '').trim(); state.tokenSource = r.source; })
        .catch(function (e) { problems.push(VAR_TOKEN + ': ' + e.message); state.token = ''; state.tokenSource = ''; })
    ]).then(function () {
      els.btnReload.disabled = false;
      renderKv();
      if (problems.length) {
        setStatus(els.ctx, 'warn', 'Could not read: ' + problems.join('; ') + '. If you lack the Paminga User (or a customizer) role this is expected; otherwise check the variable names.');
      } else if (!state.token) {
        setStatus(els.ctx, 'warn', VAR_TOKEN + ' has no current value yet. Set it under Solutions > Paminga for Dynamics 365 > Environment variables, then Re-read.');
      } else {
        setStatus(els.ctx, 'ok', 'Environment variables read. ' + VAR_TOKEN + ' is set (' + state.tokenSource + '); ' + VAR_BASE + ' = ' + (state.base || DEFAULT_BASE + ' (default)') + '.');
      }
    });
  }

  // --- Test ------------------------------------------------------------------------------------
  function testToken() {
    var token = currentToken();
    var base = currentBase();
    var api = apiUrlFor(base);
    renderKv();
    if (!token) { setStatus(els.test, 'err', 'No token to test. Set ' + VAR_TOKEN + ' or paste a token above.'); return; }
    if (!api) { setStatus(els.test, 'err', 'The panel base URL must be an https URL on a paminga.com host (for example https://crm.paminga.com/crm/panel/). Not testing against: ' + base); return; }

    els.btnTest.disabled = true;
    setStatus(els.test, 'info', 'Calling ' + api + ' ...');
    fetch(api, {
      method: 'POST',
      mode: 'cors',
      credentials: 'omit',
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'Authorization': 'Bearer ' + token
      },
      body: JSON.stringify({ query: '{ me { email } }' })
    }).then(function (r) {
      return r.text().then(function (text) {
        var body = null;
        try { body = JSON.parse(text); } catch (e) { /* not JSON */ }
        return { status: r.status, body: body, text: text };
      });
    }).then(function (res) {
      var email = res.body && res.body.data && res.body.data.me && res.body.data.me.email;
      if (email) {
        setStatus(els.test, 'ok', 'Token is valid. The panel will act as ' + email + '.');
        return;
      }
      var msg = '';
      var errors = (res.body && res.body.errors && res.body.errors.length) ? res.body.errors : [];
      if (errors.length) {
        msg = errors.map(function (e) { return e.message; }).join('; ');
      } else if (res.body && res.body.message) {
        msg = res.body.message;
      } else {
        msg = (res.text || '').slice(0, 300);
      }
      // A bad, revoked or expired token is answered with HTTP 200: `me` is null and (because the
      // query type is guarded) an error with extensions.code UNAUTHENTICATED. Treat any of these
      // as a rejected token; the 401/403 test stays as a defence if that ever changes.
      var unauthCode = errors.some(function (e) { return e && e.extensions && e.extensions.code === 'UNAUTHENTICATED'; });
      var meNull = !!(res.body && res.body.data && res.body.data.me === null);
      if (res.status === 401 || res.status === 403 || unauthCode || meNull || /unauthenticated|unauthorized/i.test(msg)) {
        setStatus(els.test, 'err', 'Paminga rejected the token (HTTP ' + res.status + '). ' + msg + '\nGenerate a new personal access token in Paminga and update ' + VAR_TOKEN + '.');
      } else {
        setStatus(els.test, 'err', 'Unexpected response (HTTP ' + res.status + '): ' + (msg || '(empty)'));
      }
    }).catch(function (err) {
      setStatus(els.test, 'err', 'Could not reach ' + api + ': ' + err.message + '\nThis is usually a network or firewall problem, or the panel base URL points at the wrong region.');
    }).then(function () { els.btnTest.disabled = false; });
  }

  els.btnReload.addEventListener('click', loadEnv);
  els.btnTest.addEventListener('click', testToken);
  els.baseInput.addEventListener('input', renderKv);
  els.tokenInput.addEventListener('input', renderKv);

  loadEnv();
})();
</script>
</body>
</html>
