/* eslint-disable */
// install-guide.jsx
// The long-form install walkthrough: "Installing MikeOSS on your Azure tenant".
// Loaded as its own script tag so app.jsx stays readable. The component is
// defined at top-level scope so app.jsx can reference it directly.

function InstallGuide() {
  const { useState, useEffect } = React;

  /* ----- design helpers, mirrored from PrivacyPolicy ----- */
  const H2 = ({ n, id, children }) => (
    <h2 id={id} style={{ fontFamily: 'var(--serif)', fontWeight: 400, fontSize: 34, margin: '64px 0 16px', letterSpacing: '-0.01em', scrollMarginTop: 80, color: 'var(--ink)', display: 'flex', alignItems: 'baseline', gap: 14 }}>
      <span style={{ fontFamily: 'var(--mono)', fontSize: 14, color: 'var(--ink-3)' }}>Step {n}.</span>
      <span>{children}</span>
    </h2>
  );
  const H3 = ({ children, id }) => (
    <h3 id={id} style={{ fontFamily: 'var(--ui)', fontWeight: 500, fontSize: 17, margin: '32px 0 10px', color: 'var(--ink)', scrollMarginTop: 80 }}>{children}</h3>
  );
  const H4 = ({ children }) => (
    <h4 style={{ fontFamily: 'var(--ui)', fontWeight: 500, fontSize: 14, textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--ink-3)', margin: '24px 0 10px' }}>{children}</h4>
  );
  const P = ({ children, narrow }) => (
    <p style={{ fontSize: 15, lineHeight: 1.7, color: 'var(--ink-2)', margin: '0 0 14px', maxWidth: narrow ? 720 : 'none' }}>{children}</p>
  );
  const Ul = ({ children }) => (
    <ul style={{ fontSize: 15, lineHeight: 1.7, color: 'var(--ink-2)', margin: '0 0 14px', paddingLeft: 22 }}>{children}</ul>
  );
  const Code = ({ children }) => (
    <code style={{ fontFamily: 'var(--mono)', fontSize: '0.92em', background: 'var(--paper-2)', border: '1px solid var(--line)', padding: '1px 6px', borderRadius: 4, color: 'var(--ink)' }}>{children}</code>
  );
  const Pre = ({ children }) => (
    <pre style={{ fontFamily: 'var(--mono)', fontSize: 12.5, lineHeight: 1.55, background: 'var(--paper-2)', border: '1px solid var(--line)', padding: '14px 16px', borderRadius: 8, color: 'var(--ink)', margin: '14px 0 18px', overflowX: 'auto', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>{children}</pre>
  );
  const Note = ({ tone = 'azure', children }) => {
    const palette = {
      azure: { border: 'var(--azure)', bg: 'var(--azure-soft)' },
      warn:  { border: '#c7942b', bg: 'rgba(255,210,80,0.18)' },
      ink:   { border: 'var(--ink-3)', bg: 'var(--paper-2)' },
    }[tone] || { border: 'var(--azure)', bg: 'var(--azure-soft)' };
    return (
      <div style={{ padding: '14px 18px', borderLeft: `3px solid ${palette.border}`, background: palette.bg, fontSize: 14, lineHeight: 1.65, color: 'var(--ink)', margin: '18px 0', borderRadius: '0 6px 6px 0' }}>{children}</div>
    );
  };
  const Verify = ({ children }) => (
    <div style={{ marginTop: 28, padding: '18px 22px', border: '1px dashed var(--line)', borderRadius: 10, background: 'var(--paper-2)' }}>
      <div style={{ fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.12em', textTransform: 'uppercase', color: 'var(--ink-3)', marginBottom: 8 }}>Verification</div>
      <div style={{ fontSize: 14.5, lineHeight: 1.65, color: 'var(--ink-2)' }}>{children}</div>
    </div>
  );

  /* ----- image map: which steps have multiple paired screenshots ----- */
  const PAIR_COUNT = {
    '01.4': 2, '02.5': 3, '03.4': 2, '03.4a': 2, '03.5': 3,
    '04.6': 2, '04.11': 2, '05.5': 2, '06.2': 2, '06.4': 2,
    '06.5': 4, '06.9': 2, '07.3': 2, '08.G.2': 2, '09.5': 2,
    '11.B.3': 2, '12.B.9a': 2, '13.D.1': 2, '13.D.6': 2, '14.2': 2,
  };

  /* ----- lightbox: click any screenshot to view full size ----- */
  const [lightbox, setLightbox] = useState(null); // string filename or null
  useEffect(() => {
    if (!lightbox) return;
    const onKey = (e) => { if (e.key === 'Escape') setLightbox(null); };
    document.addEventListener('keydown', onKey);
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    return () => {
      document.removeEventListener('keydown', onKey);
      document.body.style.overflow = prev;
    };
  }, [lightbox]);

  /* ----- single image element with click-to-zoom ----- */
  const oneShot = (file, alt) => (
    <button
      key={file}
      onClick={() => setLightbox(file)}
      style={{
        appearance: 'none', border: '1px solid var(--line)', background: 'var(--paper)',
        padding: 6, borderRadius: 8, cursor: 'zoom-in', display: 'block', width: '100%',
        boxShadow: '0 1px 0 rgba(0,0,0,0.02)'
      }}
      aria-label={`Open screenshot ${alt} at full size`}
    >
      <img
        src={`/install-guide-images/${file}`}
        alt={alt}
        loading="lazy"
        style={{ display: 'block', width: '100%', height: 'auto', borderRadius: 4 }}
      />
    </button>
  );

  /* ----- Shot: render all screenshots for a given step number ----- */
  const Shot = ({ step, alt }) => {
    const count = PAIR_COUNT[step] || 1;
    const files = Array.from({ length: count }, (_, i) => `step-${step}-${i + 1}.png`);
    return (
      <figure style={{ margin: '20px 0 26px', padding: 0 }}>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          {files.map(f => oneShot(f, alt))}
        </div>
        <figcaption style={{ fontFamily: 'var(--mono)', fontSize: 11, color: 'var(--ink-3)', marginTop: 8, lineHeight: 1.5 }}>
          Screenshot {step}{count > 1 ? ` (${count} panels)` : ''}. {alt}
        </figcaption>
      </figure>
    );
  };

  /* ----- table of contents ----- */
  const sections = [
    ['1', 'Create the resource group', 's1'],
    ['2', 'Create the Log Analytics workspace', 's2'],
    ['3', 'Create the Postgres Flexible Server', 's3'],
    ['4', 'Create the storage account and the documents container', 's4'],
    ['5', 'Create the container registry', 's5'],
    ['6', 'Build the application image', 's6'],
    ['7', 'Run schema migrations from your laptop', 's7'],
    ['8', 'Deploy the Container App and its environment', 's8'],
    ['9', 'Add the PostgREST sidecar', 's9'],
    ['10', 'Wire the public URL back into the application', 's10'],
    ['11', 'Verify with /diag and the local-auth smoke test', 's11'],
    ['12', 'Create the two Entra app registrations', 's12'],
    ['13', 'Switch the backend to AUTH_PROVIDER=entra', 's13'],
    ['14', 'Verify Microsoft sign-in', 's14'],
  ];

  return (
    <section style={{ padding: '72px 32px 120px', background: 'var(--paper)' }}>
      <div style={{ maxWidth: 880, margin: '0 auto' }}>
        <div style={{ fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.18em', textTransform: 'uppercase', color: 'var(--ink-3)', marginBottom: 16 }}>Install guide</div>
        <h1 style={{ fontFamily: 'var(--serif)', fontWeight: 400, fontSize: 'clamp(40px, 5vw, 64px)', lineHeight: 1.05, letterSpacing: '-0.02em', margin: '0 0 12px', color: 'var(--ink)' }}>
          Installing MikeOSS on your Azure tenant
        </h1>
        <div style={{ fontFamily: 'var(--mono)', fontSize: 12, color: 'var(--ink-3)', marginBottom: 32 }}>
          The minimal, hand-rolled deployment. Around an hour, end to end.
        </div>

        <p style={{ fontSize: 17, lineHeight: 1.6, color: 'var(--ink-2)', margin: '0 0 22px' }}>
          This guide walks an Azure professional through the minimal hand-rolled install of MikeOSS into their own tenant. You will need access to the Azure portal and a Postgres-capable region. For a supported, one-click install please see the <a href="https://github.com/Altien/mikeOssAzure" target="_blank" rel="noopener" style={{ borderBottom: '1px dotted currentColor' }}>Azure Marketplace listing</a>; this guide is for people who want to understand every resource.
        </p>

        <Note tone="ink">
          <b>Goal of step 1:</b> one container that holds every other Azure resource we are about to create. Deleting that resource group at the end deletes everything we deployed in a single click. That is handy for cleanup.
        </Note>

        <H3>Contents</H3>
        <ol style={{ paddingLeft: 22, margin: '0 0 8px', fontSize: 14, lineHeight: 1.85, color: 'var(--ink-2)' }}>
          {sections.map(([n, t, id]) => (
            <li key={id} value={Number(n)}>
              <a href={`#${id}`} style={{ color: 'var(--ink)', borderBottom: '1px dotted var(--ink-3)' }}>{t}</a>
            </li>
          ))}
        </ol>
        <P>Click any screenshot to view it at full size. Where the screen was too tall for a single capture, you will see two or three panels stacked together; they are all part of the same view.</P>

        {/* ============================ STEP 1 ============================ */}
        <H2 n="1" id="s1">Create the resource group</H2>
        <P>The resource group is where you decide the residency for your data. That is essential if you are a UK or EU organisation, so pick the location that suits you.</P>

        <H3>1.1 Sign in to the portal</H3>
        <P>Open <a href="https://portal.azure.com" target="_blank" rel="noopener" style={{ borderBottom: '1px dotted currentColor' }}>portal.azure.com</a> and sign in.</P>

        <H3>1.2 Find Resource groups</H3>
        <P>In the top search bar, type <Code>resource groups</Code> and click the <b>Resource groups</b> service result (the one with the cube-stack icon, under Services).</P>

        <H3>1.3 Click Create</H3>
        <P>On the Resource groups list page, click <b>+ Create</b> in the top toolbar.</P>

        <H3>1.4 Fill in the Basics tab</H3>
        <Ul>
          <li><b>Subscription:</b> pick the subscription you want to bill this to.</li>
          <li><b>Resource group:</b> <Code>rg-mike-mini</Code> (substitute your own suffix if you prefer).</li>
          <li><b>Region:</b> (Europe) UK South.</li>
        </Ul>
        <Note tone="warn">
          The region you pick needs to support Postgres Flexible Server, Container Apps and Azure OpenAI if you ever extend the deployment. UK South does. So do most major regions (North Europe, East US, and so on). Pick one and stick with it for every resource in this guide.
        </Note>
        <P>Skip the Tags tab, click <b>Review + create</b>, then <b>Create</b> when validation passes.</P>
        <Shot step="01.4" alt="Basics tab fully filled in, then validation passing on Review + create." />

        <H3>1.5 Wait for deployment</H3>
        <P>Wait for the &ldquo;Your deployment is complete&rdquo; banner (usually under 10 seconds) and click <b>Go to resource group</b>. You will land on the empty overview page for <Code>rg-mike-mini</Code>.</P>
        <Shot step="01.5" alt="Empty resource group overview page right after creation." />

        <Verify>
          You should be looking at the overview page for <Code>rg-mike-mini</Code>, in your chosen region, with zero resources inside it.
        </Verify>

        {/* ============================ STEP 2 ============================ */}
        <H2 n="2" id="s2">Create the Log Analytics workspace</H2>
        <P>Container Apps need somewhere to send their logs. The cheapest and simplest option is a basic Log Analytics workspace, which we will create first. We will then create the Container Apps Environment (the runtime that hosts the actual container) and point it at this workspace. Both resources are free at the volumes a single small app produces; you only start paying once you cross the free ingestion tier (5 GB per month at the time of writing).</P>

        <H3>2.1 Find Log Analytics workspaces</H3>
        <P>In the top search bar, type <Code>log analytics</Code> and click the <b>Log Analytics workspaces</b> result.</P>
        <Shot step="02.1" alt="Search bar showing the Log Analytics workspaces match." />

        <H3>2.2 Click Create</H3>
        <P>Click <b>Create</b> in the top toolbar.</P>
        <Shot step="02.2" alt="Log Analytics workspaces list with the Create button." />

        <H3>2.3 Fill in the Basics tab</H3>
        <Ul>
          <li><b>Subscription:</b> same as before.</li>
          <li><b>Resource group:</b> <Code>rg-mike-mini</Code>.</li>
          <li><b>Name:</b> <Code>law-mike-mini</Code>.</li>
          <li><b>Region:</b> UK South (use the same region as your resource group).</li>
        </Ul>
        <Shot step="02.3" alt="Create blade, Basics tab, fully filled in." />

        <H3>2.4 Skip the optional tabs</H3>
        <P>Skip the Pricing tier tab. The default Pay-as-you-go tier with the free 5 GB included is fine for a minimal deployment. Skip Tags. Click <b>Review and create</b>, then <b>Create</b>.</P>

        <H3>2.5 Wait, then open the workspace</H3>
        <P>Wait for the deployment to complete (around 20 seconds) and click <b>Go to resource</b>. You will land on the workspace overview page.</P>
        <Shot step="02.5" alt="Workspace overview page after creation, showing the customer ID and resource ID at the top right." />

        <Verify>
          Inside <Code>rg-mike-mini</Code> you should now see one resource: <Code>law-mike-mini</Code>.
        </Verify>

        {/* ============================ STEP 3 ============================ */}
        <H2 n="3" id="s3">Create the Postgres Flexible Server</H2>
        <P>Mike stores its application data in Postgres. We will use a Flexible Server on the Burstable tier, which is the cheapest configuration that runs a real workload.</P>

        <H3>3.1 Open the Marketplace</H3>
        <P>From the <Code>rg-mike-mini</Code> overview page, click <b>Create</b> in the top toolbar. This opens the Marketplace.</P>
        <Shot step="03.1" alt="Resource group toolbar with the Create button highlighted." />

        <H3>3.2 Find the right tile</H3>
        <P>In the Marketplace search box, type <Code>postgresql flexible</Code> and click the <b>Azure Database for PostgreSQL Flexible Server</b> tile. The publisher is Microsoft.</P>
        <Note tone="warn">
          There is also a tile for &ldquo;Azure Cosmos DB for PostgreSQL&rdquo;, which is a different product. Do not pick that one.
        </Note>
        <Shot step="03.2" alt="Marketplace search showing the Azure Database for PostgreSQL Flexible Server tile." />

        <H3>3.3 Pick the Flexible server deployment option</H3>
        <P>On the tile, click <b>Create</b>. The next page asks you to pick a deployment option. Click <b>Create</b> on the &ldquo;Flexible server&rdquo; card. There are usually three cards: Flexible server, Single server (deprecated), and Cosmos DB for PostgreSQL. We want Flexible server.</P>

        <H3>3.4 Fill in the Basics tab</H3>
        <Ul>
          <li><b>Subscription:</b> same as before.</li>
          <li><b>Resource group:</b> <Code>rg-mike-mini</Code>.</li>
          <li><b>Server name:</b> <Code>pg-mike-mini</Code>.</li>
          <li><b>Region:</b> UK South.</li>
          <li><b>PostgreSQL version:</b> 16.</li>
          <li><b>Workload type:</b> Development.</li>
        </Ul>
        <P>Pick Development even if you plan to take the deployment to production later. It sets sane defaults; the Production preset overprovisions and costs a lot more.</P>
        <P><b>Compute and storage:</b> click <b>Configure server</b>. On the Configure blade, pick the <b>Burstable</b> tier, <b>B1ms</b> compute size, <b>32 GiB</b> storage, <b>7 days</b> backup retention. Click <b>Save</b> to return to the Basics tab. You should see the monthly price drop from around £400 to roughly £13.</P>
        <Ul>
          <li><b>Availability zone:</b> No preference.</li>
          <li><b>High availability:</b> leave unchecked. Burstable does not support zone-redundant HA anyway, and we are deliberately not paying for it in a minimal deploy.</li>
          <li><b>Authentication method:</b> PostgreSQL authentication only. Do <b>not</b> pick &ldquo;Microsoft Entra authentication only&rdquo; or &ldquo;PostgreSQL and Microsoft Entra authentication&rdquo;. The minimal deployment uses password auth; switching to Entra auth is a separate exercise covered in the production-hardening notes.</li>
          <li><b>Admin username:</b> <Code>mikeadmin</Code>.</li>
          <li><b>Password:</b> generate a strong one and save it now. You will need it three times: for the schema migration in step 7, for the application connection string in step 8, and any time you debug Postgres directly.</li>
        </Ul>
        <Note tone="warn">
          Use a password manager. There is no Key Vault holding it for you in this deployment.
        </Note>
        <Shot step="03.4" alt="Basics tab, top half: subscription, resource group, server name, region, version, workload type." />
        <Shot step="03.4a" alt="Configure server side blade showing Burstable, B1ms and 32 GiB." />

        <H3>3.5 Networking tab</H3>
        <P>Click <b>Next</b> to go to the Networking tab. This is where the firewall rules go.</P>
        <Ul>
          <li><b>Connectivity method:</b> Public access (allowed IP addresses). Do not pick Private access (VNet integration); that is the production hardening path.</li>
          <li>Tick <b>Allow public access to this resource through the internet using a public IP address</b>.</li>
          <li>Tick <b>Allow public access from any Azure service within Azure to this server</b>. This is what lets the Container App reach Postgres later.</li>
          <li>Under Firewall rules, click <b>Add current client IP address</b>. This adds a single rule with your laptop&rsquo;s public IP, which is what we will use to run schema migrations from local in step 7. The portal shows you the IP it detected.</li>
        </Ul>
        <Shot step="03.5" alt="Networking tab with both public-access checkboxes ticked and your client IP added as a firewall rule." />

        <H3>3.6 Skip Security and Tags</H3>
        <P>Click <b>Next</b> twice to skip Security and Tags (defaults are fine). On Review and create, wait for validation to pass and click <b>Create</b>.</P>
        <Shot step="03.6" alt="Review and create page with validation passed." />

        <H3>3.7 Wait for provisioning</H3>
        <P>This takes around 5 to 8 minutes. While you wait, you can read ahead to step 4 (storage account), but do not start clicking yet. We have one more thing to do on Postgres before moving on.</P>
        <Shot step="03.7" alt="Deployment-in-progress page. Useful as the 'Postgres takes a while' image." />

        <H3>3.8 Capture the FQDN</H3>
        <P>When the deployment finishes, click <b>Go to resource</b>. You will land on the server overview page. Capture the value of <b>Server name</b> at the top right of the Essentials panel. It will be in the form <Code>pg-mike-mini.postgres.database.azure.com</Code>. That is the FQDN we need in steps 7 and 8.</P>
        <Shot step="03.8" alt="Server overview page after creation, with the FQDN visible in the Essentials panel." />

        <H3>3.9 Allow-list the required Postgres extensions</H3>
        <P>In the left navigation, scroll down to the <b>Settings</b> section and click <b>Server parameters</b>. The page lists hundreds of parameters; in the search box at the top type <Code>azure.extensions</Code>.</P>
        <P>Click into the row, change the value to include both <Code>PGCRYPTO</Code> and <Code>VECTOR</Code> (you can multi-select from the dropdown), and click <b>Save</b> at the top of the page. The portal will warn that the change requires a restart and offer to restart the server now. Click <b>Save and Restart</b>.</P>
        <Note tone="warn">
          This step matters. Without it, the schema migrations in step 7 fail when they hit <Code>CREATE EXTENSION pgcrypto</Code>.
        </Note>
        <Shot step="03.9" alt="Server parameters page filtered to azure.extensions, with PGCRYPTO and VECTOR selected." />
        <Shot step="03.9a" alt="Save and Restart confirmation dialog." />

        <H3>3.10 Wait for the restart</H3>
        <P>The restart takes 1 to 2 minutes. The server overview moves from a yellow &ldquo;Updating&rdquo; status to a green &ldquo;Available&rdquo; status when it is ready.</P>
        <Shot step="03.10" alt="Server overview showing the Available status after the restart." />

        <Verify>
          <P>In your notes outside Azure:</P>
          <Ul>
            <li>The admin password, somewhere safe (and admin name if you used a different one).</li>
            <li>The FQDN <Code>pg-mike-mini.postgres.database.azure.com</Code>.</li>
          </Ul>
          <P>In the portal, <Code>rg-mike-mini</Code> should now contain <Code>law-mike-mini</Code>, <Code>pg-mike-mini</Code>, and the Postgres server&rsquo;s deployment artefacts. The Postgres server&rsquo;s status should be Available.</P>
        </Verify>

        {/* ============================ STEP 4 ============================ */}
        <H2 n="4" id="s4">Create the storage account and the documents container</H2>
        <P>This is the simplest of the resource-creation steps. Two clicky bits: the storage account itself, then a single blob container inside it. Plus capturing the connection string at the end.</P>

        <H3>4.1 Open the Marketplace</H3>
        <P>From the <Code>rg-mike-mini</Code> overview page, click <b>Create</b> in the top toolbar. In the Marketplace search, type <Code>storage account</Code> and click the <b>Storage account</b> tile. The publisher is Microsoft. Click <b>Create</b> on the tile.</P>
        <Shot step="04.1" alt="Marketplace search showing the Storage account tile." />

        <H3>4.2 Fill in the Basics tab</H3>
        <Ul>
          <li><b>Subscription:</b> same as before.</li>
          <li><b>Resource group:</b> <Code>rg-mike-mini</Code>.</li>
          <li><b>Storage account name:</b> <Code>stmikemini</Code>.</li>
          <li><b>Region:</b> UK South.</li>
          <li><b>Primary service:</b> leave on the default &ldquo;Azure Blob Storage or Azure Data Lake Storage Gen 2&rdquo;.</li>
          <li><b>Performance:</b> Standard.</li>
          <li><b>Redundancy:</b> Locally-redundant storage (LRS). The other redundancy options are nice-to-haves we do not need for a minimal deployment and they cost more.</li>
        </Ul>
        <Shot step="04.2" alt="Storage account Basics tab fully filled in." />

        <H3>4.3 Advanced and Networking tabs</H3>
        <P>Click <b>Next</b> through Advanced (defaults are fine) and Networking (default: Enable public access from all networks). We will lock the network path down later as part of production hardening.</P>

        <H3>4.4 Data protection, Encryption, Tags</H3>
        <P>Click <b>Next</b> through Data protection, Encryption, and Tags. Defaults are fine throughout.</P>

        <H3>4.5 Security tab: two things to verify</H3>
        <Ul>
          <li><b>Require secure transfer for REST API operations:</b> ticked. This forces HTTPS.</li>
          <li><b>Allow enabling anonymous access on individual containers:</b> unticked. We want all blobs auth-gated.</li>
          <li><b>Minimum TLS version:</b> Version 1.2. This is the default on new accounts but check the dropdown.</li>
        </Ul>
        <P>Leave the rest of the tab on defaults: hierarchical namespace off, SFTP off, NFS v3 off, large file shares disabled.</P>
        <Shot step="04.6" alt="Security tab with the toggles visible. Top and bottom of the tab in two panels." />

        <H3>4.6 Review and create</H3>
        <P>On Review and create, wait for validation to pass and click <b>Create</b>. Provisioning takes 30 to 60 seconds.</P>

        <H3>4.7 Open the resource</H3>
        <P>When deployment finishes, click <b>Go to resource</b>. You will land on the storage account overview page.</P>
        <Shot step="04.7" alt="Storage account overview page." />

        <H3>4.8 Create the documents container</H3>
        <P>In the left navigation, expand <b>Data storage</b> and click <b>Containers</b>. The Containers page has a <b>+ Container</b> button at the top. Click it.</P>
        <Shot step="04.8" alt="Containers page with the + Container button visible." />

        <H3>4.9 Name it and set access</H3>
        <P>In the side blade, set <b>Name</b> to <Code>documents</Code> (lowercase, no spaces). Set <b>Anonymous access level</b> to Private (no anonymous access). Click <b>Create</b>.</P>
        <Shot step="04.9" alt="New container side blade with Name 'documents' and Private access selected." />

        <H3>4.10 Confirm the container exists</H3>
        <Shot step="04.10" alt="Containers list showing the documents container." />

        <H3>4.11 Capture the connection string</H3>
        <P>We need this for step 8 to wire the application up to blob storage. In the left navigation under <b>Security + networking</b>, click <b>Access keys</b>. Click the <b>Show</b> button next to <b>key1</b>. Copy the entire <b>Connection string</b> value (the one that starts with <Code>DefaultEndpointsProtocol=https;AccountName=...</Code>). Save it somewhere safe; you will paste it into the Container App secrets in step 8.</P>
        <Shot step="04.11" alt="Access keys page with key1 expanded and the connection string visible. Blur or crop the actual value before publishing." />

        <Verify>
          <Code>rg-mike-mini</Code> should now contain <Code>law-mike-mini</Code>, <Code>pg-mike-mini</Code>, <Code>stmikemini</Code>, plus any Postgres deployment artefacts. In your notes you should have the Postgres admin password and FQDN from step 3, and the storage connection string from step 4.
        </Verify>

        {/* ============================ STEP 5 ============================ */}
        <H2 n="5" id="s5">Create the container registry</H2>
        <P>The container registry (ACR) is where the application image lives. The Container App pulls from here on every cold start. We use the Basic SKU with the admin user enabled, which is the cheapest and simplest option for a manual deployment. The production-grade alternative is to attach a Managed Identity with the AcrPull role, but that is part of the production-hardening track.</P>

        <H3>5.1 Open the Marketplace tile</H3>
        <P>From the <Code>rg-mike-mini</Code> overview page, click <b>Create</b> in the top toolbar. In the Marketplace search, type <Code>container registry</Code> and click the <b>Container Registry</b> tile. The publisher is Microsoft. Click <b>Create</b> on the tile.</P>
        <Shot step="05.1" alt="Marketplace tile for Container Registry." />

        <H3>5.2 Fill in the Basics tab</H3>
        <Ul>
          <li><b>Subscription:</b> same as before.</li>
          <li><b>Resource group:</b> <Code>rg-mike-mini</Code>.</li>
          <li><b>Registry name:</b> <Code>acrmikemini</Code>. This name must be globally unique across all of Azure, so if validation later fails with &ldquo;name not available&rdquo;, append two or three random characters and use that everywhere from this step onwards.</li>
          <li><b>Location:</b> UK South.</li>
          <li><b>Pricing plan:</b> Basic.</li>
        </Ul>
        <Shot step="05.2" alt="ACR Basics tab fully filled in." />

        <H3>5.3 Skip Networking, Encryption, Identity, Tags</H3>
        <P>Click <b>Next</b> through Networking (default: Public access enabled), Encryption, Identity and Tags. Defaults are fine.</P>
        <Shot step="05.3" alt="ACR Networking tab on its defaults." />

        <H3>5.4 Review and create</H3>
        <P>Wait for validation to pass and click <b>Create</b>. Provisioning takes around 30 seconds.</P>
        <Shot step="05.5" alt="ACR Review and create then deployment progress." />

        <H3>5.5 Capture the Login server</H3>
        <P>When deployment finishes, click <b>Go to resource</b>. Capture the <b>Login server</b> value from the Essentials panel. It will be <Code>acrmikemini.azurecr.io</Code>. We will use that in steps 6 and 8.</P>
        <Shot step="05.6" alt="Registry overview page with the Login server value visible." />

        <H3>5.6 Enable the admin user</H3>
        <P>In the left navigation, scroll to the <b>Settings</b> section and click <b>Access keys</b>. The page shows the <b>Admin user</b> toggle, set to Disabled by default. Click it to <b>Enabled</b>. The page refreshes and shows the username and two passwords (password, password2).</P>
        <Shot step="05.7" alt="Access keys page before the toggle is flipped (Admin user disabled)." />
        <Shot step="05.7a" alt="Access keys page after enabling, showing the username and the masked passwords." />

        <H3>5.7 Capture the credentials</H3>
        <P>Username is the registry name (<Code>acrmikemini</Code>). Click the show button next to <b>password</b> and copy the value. Save the username and password somewhere safe; we paste them into the Container App pull-credentials field in step 8. There is a <b>password2</b> too; that is the standby key for rotation. Ignore it for now.</P>
        <Shot step="05.8" alt="Access keys page with password revealed. Blur or crop the actual value before publishing." />

        <Verify>
          <Code>rg-mike-mini</Code> should now contain <Code>law-mike-mini</Code>, <Code>pg-mike-mini</Code>, <Code>stmikemini</Code>, <Code>acrmikemini</Code> and assorted deployment artefacts. In your notes: Postgres admin password and FQDN, storage connection string, ACR login server (<Code>acrmikemini.azurecr.io</Code>), ACR admin username (<Code>acrmikemini</Code>) and password.
        </Verify>

        {/* ============================ STEP 6 ============================ */}
        <H2 n="6" id="s6">Build the application image</H2>
        <P>This is the first of two steps that cannot be done from the portal blades alone. We use Azure Cloud Shell, the bash terminal embedded in the portal. No local Docker or az CLI install is needed, and Cloud Shell is already authenticated as your portal user.</P>
        <P>The build itself runs on Azure&rsquo;s side via ACR Tasks. Cloud Shell is just where we type the command from. The first build of this image takes around 5 to 10 minutes because the multistage Dockerfile installs LibreOffice (used for DOC and DOCX to PDF conversion at runtime) on top of the application bits.</P>

        <H3>6.1 Open Cloud Shell</H3>
        <P>In the top toolbar of the portal, click the icon that looks like <Code>{'>_'}</Code>. It sits between the Copilot button and the bell icon. A terminal pane slides up from the bottom.</P>
        <Shot step="06.1" alt="Portal top toolbar with the Cloud Shell icon highlighted." />

        <H3>6.2 First-time setup</H3>
        <P>If this is your first time using Cloud Shell, a Welcome dialog appears. Pick <b>Bash</b> (not PowerShell). When it asks about storage, pick <b>No storage account required</b> if available, or <b>Mount storage account</b> with the defaults if not. The minimal-cost path is no storage; the session is ephemeral but that is fine for a one-off image build.</P>
        <Shot step="06.2" alt="Cloud Shell first-run dialog." />

        <H3>6.3 Wait for the prompt</H3>
        <P>Cloud Shell takes around 20 seconds on a cold start to provision a session. You will end up at a bash prompt that looks like <Code>user@Azure:~$</Code>.</P>
        <Shot step="06.3" alt="Cloud Shell with the bash prompt ready." />

        <H3>6.4 Clone the Mike repository</H3>
        <Pre>{`git clone https://github.com/Altien/mikeOssAzure.git`}</Pre>
        <P>If your fork lives at a different URL, use that instead. Wait for the clone (around 5 seconds).</P>
        <Shot step="06.4" alt="Cloud Shell after the clone, showing the cloned directory." />

        <H3>6.5 Start the image build</H3>
        <P>Change into the repo and start the build:</P>
        <Pre>{`cd mikeOssAzure/
az acr build --registry acrmikemini --image backend:latest --file Dockerfile --build-arg NEXT_PUBLIC_API_BASE_URL='' .`}</Pre>
        <P>Note the trailing dot. That is the build context (the current directory). Cloud Shell uploads the source as a tarball to the ACR build agent, which streams build logs back into your Cloud Shell session in real time. The <Code>.dockerignore</Code> in the repo keeps the upload small (<Code>node_modules</Code> and build outputs are excluded).</P>
        <Shot step="06.5" alt="Cloud Shell mid-build then after completion, with the Run ID success message at the end." />

        <H3>6.6 Wait for the build</H3>
        <P>The first build takes 5 to 10 minutes; subsequent rebuilds (when you push application changes) are faster because layers cache. You can leave Cloud Shell open and read ahead.</P>

        <H3>6.7 Import the PostgREST image</H3>
        <P>Once the backend build is complete, type one more command into Cloud Shell:</P>
        <Pre>{`az acr import --name acrmikemini --source docker.io/postgrest/postgrest:v12.2.3 --image postgrest:v12.2.3`}</Pre>
        <P>This pulls the official PostgREST image from Docker Hub into your private registry. PostgREST will run as a sidecar in the same Container App as the backend (step 9). Mirroring it locally means the Container App pulls everything from a single registry and avoids Docker Hub rate limits.</P>
        <Shot step="06.7" alt="Cloud Shell after the import command completes." />

        <H3>6.8 Confirm both images are present</H3>
        <P>Switch tabs to the portal, navigate to <Code>acrmikemini</Code>, and click <b>Repositories</b> in the left navigation under <b>Services</b>. You should see two entries: <Code>backend</Code> and <Code>postgrest</Code>.</P>
        <Shot step="06.8" alt="ACR Repositories page showing backend and postgrest." />

        <H3>6.9 Optional: verify the tags</H3>
        <P>Click into each repository and confirm the tag: <Code>backend</Code> should show tag <Code>latest</Code>; <Code>postgrest</Code> should show tag <Code>v12.2.3</Code>. The Last updated timestamp should match when you ran the commands.</P>
        <Shot step="06.9" alt="Repository detail page showing the tag." />

        <Verify>
          You should now have all the values from previous steps plus working <Code>backend:latest</Code> and <Code>postgrest:v12.2.3</Code> images in the registry. No new values are produced by this step.
        </Verify>

        {/* ============================ STEP 7 ============================ */}
        <H2 n="7" id="s7">Run schema migrations from your laptop</H2>
        <P>This is the second and last non-portal step. Migrations run from your local clone of the repo against the public Postgres FQDN we created in step 3. The firewall rule for your laptop&rsquo;s IP from step 3.5 is what allows this connection through.</P>

        <H3>7.1 Change to the backend folder</H3>
        <P>Open PowerShell on your laptop and change to the backend folder of your clone:</P>
        <Pre>{`cd ~\\MikeOSSAzure\\backend`}</Pre>

        <H3>7.2 Install backend dependencies</H3>
        <P>Install backend dependencies if you have not already in this checkout:</P>
        <Pre>{`npm install`}</Pre>
        <P>This takes 1 to 2 minutes the first time. Skip if you already have a <Code>node_modules</Code> directory from earlier development on the repo.</P>
        <Shot step="07.2" alt="Backend folder structure after npm install." />

        <H3>7.3 Set the database URL and run migrations</H3>
        <Pre>{`$env:DATABASE_URL = "postgres://mikeadmin:YOUR_PASSWORD@pg-mike-mini.postgres.database.azure.com:5432/postgres?sslmode=require"
npm run migrate:dev`}</Pre>
        <P>Replace <Code>YOUR_PASSWORD</Code> with the Postgres admin password from step 3. Two details that matter:</P>
        <Ul>
          <li>The <Code>sslmode=require</Code> parameter at the end. Azure Postgres rejects non-SSL connections by default; without this you get a &ldquo;SSL connection is required&rdquo; or &ldquo;no pg_hba.conf entry&rdquo; error.</li>
          <li>We use <Code>migrate:dev</Code> rather than <Code>migrate</Code>. The <Code>migrate</Code> script runs the compiled JavaScript from <Code>dist</Code>; <Code>migrate:dev</Code> runs the TypeScript source directly via <Code>tsx</Code> and avoids needing to build the backend first. Either works once you have built the backend, but <Code>migrate:dev</Code> is simpler for a clean checkout.</li>
        </Ul>
        <Shot step="07.3" alt="Terminal window during and after the migration run, showing the streaming output then the success line." />

        <H3>7.4 Check the result and common errors</H3>
        <P>The output streams as each migration runs. Successful output ends with a &ldquo;Migrations complete&rdquo; line. <Code>node-pg-migrate</Code> is idempotent: it records every applied migration in a <Code>pgmigrations</Code> table and skips anything already there, so re-running the command is safe.</P>
        <Ul>
          <li>If migrations fail with &ldquo;extension pgcrypto is not allow-listed&rdquo;, you missed step 3.9 (allow-listing the extensions). Apply step 3.9 and rerun this step.</li>
          <li>If migrations fail with &ldquo;could not connect to server&rdquo;, your laptop&rsquo;s public IP has changed since you set the firewall rule in step 3.5. Find your current IP at <a href="https://api.ipify.org" target="_blank" rel="noopener" style={{ borderBottom: '1px dotted currentColor' }}>api.ipify.org</a> and add a new firewall rule via the Postgres server&rsquo;s Networking blade.</li>
        </Ul>
        <Shot step="07.4" alt="Terminal window after migrations complete, showing the success line." />

        <Verify>
          <P>The <Code>pgmigrations</Code> table now exists with one row per applied migration. The application schema is in place. If you have <Code>psql</Code> installed locally, you can confirm with:</P>
          <Pre>{`$env:PGPASSWORD = "YOUR_PASSWORD"
psql -h pg-mike-mini.postgres.database.azure.com -U mikeadmin -d postgres -c "SELECT count(*) FROM pgmigrations;"`}</Pre>
          <P>The count should be the number of migration files in <Code>backend/migrations</Code>.</P>
        </Verify>

        {/* ============================ STEP 8 ============================ */}
        <H2 n="8" id="s8">Deploy the Container App and its environment</H2>
        <P>What you produce in this step: a running Container App that hosts the backend container, sitting in a newly-created Container Apps Environment that uses your step-2 Log Analytics workspace. The PostgREST sidecar comes in step 9. The <Code>FRONTEND_URL</Code> and <Code>BACKEND_PUBLIC_URL</Code> env vars come in step 10, once we know the public FQDN.</P>

        <H4>Part A. Generate the three application secrets</H4>
        <P>The application needs three random secrets that you generate yourself: a JWT signing secret, a download URL signing secret, an auth state secret, and a one-time bootstrap token used to sign in to the install configurator. Use the Cloud Shell session you already have open from step 6 (it has <Code>openssl</Code> and <Code>uuidgen</Code> ready):</P>
        <Pre>{`JWT_SECRET=$(openssl rand -base64 48)
DOWNLOAD_SECRET=$(openssl rand -base64 32)
AUTH_STATE_SECRET=$(openssl rand -base64 32)
BOOTSTRAP_TOKEN=$(uuidgen)
echo "JWT_SECRET=$JWT_SECRET"
echo "DOWNLOAD_SECRET=$DOWNLOAD_SECRET"
echo "AUTH_STATE_SECRET=$AUTH_STATE_SECRET"
echo "BOOTSTRAP_TOKEN=$BOOTSTRAP_TOKEN"`}</Pre>
        <P>Copy all four values into your password manager or a temporary text file. You will paste them into portal fields in Part D. <Code>JWT_SECRET</Code> in particular is critical: if you lose it, every signed-in user is signed out and there is no recovery.</P>
        <Shot step="08.A" alt="Cloud Shell with the four echoed values visible. Blur or crop the actual values before publishing." />

        <H4>Part B. Construct the Postgres connection string</H4>
        <P>You will need this in Part D. Open a notepad and write out, on one line:</P>
        <Pre>{`postgres://mikeadmin:YOUR_POSTGRES_PASSWORD@pg-mike-mini.postgres.database.azure.com:5432/postgres?sslmode=require`}</Pre>
        <P>Replace <Code>YOUR_POSTGRES_PASSWORD</Code> with the admin password from step 3. The trailing <Code>sslmode=require</Code> is the same SSL flag we needed in step 7; Postgres rejects the connection without it. Save this string; you will paste it into a secret called <Code>pg-uri</Code> in Part D.</P>

        <H4>Part C. Open the Container App create flow</H4>
        <H3>8.C.1 Open the tile</H3>
        <P>From the <Code>rg-mike-mini</Code> overview page, click <b>Create</b> in the top toolbar. In the Marketplace search, type <Code>container app</Code> and click the <b>Container App</b> tile. Publisher Microsoft, type Azure Service.</P>
        <Note tone="warn">
          Container App Job and Container App Session Pool are different products. Click <b>Create</b> on the plain Container App tile.
        </Note>
        <Shot step="08.C.1" alt="Marketplace tile for Container App." />

        <P>The Create blade has six tabs across the top: Basics, Container, Bindings, Ingress, Tags, Review and create. We will fill in Basics, Container and Ingress. Skip the rest.</P>

        <H4>Part D. Basics tab and inline Environment creation</H4>
        <H3>8.D.1 Top of the Basics tab</H3>
        <Ul>
          <li><b>Subscription:</b> same as before.</li>
          <li><b>Resource group:</b> <Code>rg-mike-mini</Code>.</li>
          <li><b>Container app name:</b> <Code>backend</Code>.</li>
          <li><b>Workload profile:</b> Consumption (the default; do not pick Dedicated for this minimal deploy).</li>
          <li><b>Region:</b> UK South.</li>
        </Ul>
        <Shot step="08.D.1" alt="Basics tab top section with the four values filled in." />

        <H3>8.D.2 Create the Container Apps Environment inline</H3>
        <P>Click the <b>Create new</b> link below the empty <b>Container Apps Environment</b> dropdown. A side blade opens.</P>

        <H3>8.D.3 Fill in the environment side blade</H3>
        <Ul>
          <li><b>Environment name:</b> <Code>cae-mike-mini</Code>.</li>
          <li><b>Region:</b> UK South.</li>
          <li><b>Environment type:</b> Consumption only.</li>
        </Ul>
        <P>Click the <b>Monitoring</b> tab inside the side blade. Set <b>Logs destination</b> to <b>Azure Log Analytics</b> and pick <Code>law-mike-mini</Code> from the workspace dropdown.</P>
        <P>Click the <b>Networking</b> tab inside the side blade and confirm <b>Use your own virtual network</b> is set to <b>No</b>. Click <b>Create</b> at the bottom of the side blade.</P>
        <Shot step="08.D.3" alt="Side blade Basics tab filled in." />
        <Shot step="08.D.3a" alt="Side blade Monitoring tab with law-mike-mini selected." />
        <Shot step="08.D.3b" alt="Side blade Networking tab confirming No VNet." />

        <H3>8.D.4 Back on the main create blade</H3>
        <P>The side blade closes and the Container Apps Environment dropdown now shows <Code>cae-mike-mini</Code> selected.</P>
        <Shot step="08.D.4" alt="Basics tab fully filled in with cae-mike-mini in the Environment dropdown." />

        <H4>Part E. Container tab</H4>
        <H3>8.E.1 Move to the Container tab</H3>
        <P>Click <b>Next: Container</b>.</P>

        <H3>8.E.2 Disable the quickstart image</H3>
        <P>Untick the <b>Use quickstart image</b> checkbox at the top. We are pulling our own image from ACR.</P>

        <H3>8.E.3 Pick the image</H3>
        <Ul>
          <li><b>Image source:</b> Azure Container Registry.</li>
          <li><b>Subscription:</b> same as before.</li>
          <li><b>Registry:</b> pick <Code>acrmikemini</Code> from the dropdown.</li>
          <li><b>Image:</b> pick <Code>backend</Code>.</li>
          <li><b>Image tag:</b> pick <Code>latest</Code>.</li>
        </Ul>
        <P>The form will read the registry credentials from the ACR resource you set up with admin user enabled in step 5.</P>
        <Shot step="08.E.3" alt="Container tab top section with image source, registry, image, and tag selected." />

        <H3>8.E.4 Container resource allocation</H3>
        <P>Set CPU cores to <b>0.5</b> and Memory to <b>1 Gi</b>. The portal often defaults to 0.25 cores and 0.5 Gi which is too small for the backend with LibreOffice and a Node runtime in the same container.</P>
        <Shot step="08.E.4" alt="CPU and memory selectors set to 0.5 and 1 Gi." />

        <H3>8.E.5 Environment variables and secrets</H3>
        <P>Scroll down within the Container tab to find the <b>Environment variables</b> section. Add the rows below. Where <b>Source</b> is &ldquo;Reference a secret&rdquo;, the dropdown lets you pick the secret name you added. For &ldquo;Manual entry&rdquo; rows, you type the value directly.</P>

        <Ul>
          <li><Code>PG_URI</Code>: the Postgres connection string from Part B.</li>
          <li><Code>AZURE_STORAGE_CONNECTION_STRING</Code>: the storage account connection string from step 4.11.</li>
          <li><Code>AZURE_STORAGE_CONTAINER_NAME</Code>: manual entry, value <Code>documents</Code>.</li>
          <li><Code>ANTHROPIC_API_KEY</Code>: your Anthropic API key, or leave blank if not used.</li>
          <li><Code>OPENAI_API_KEY</Code>: your OpenAI API key, or leave blank if not used.</li>
          <li><Code>GEMINI_API_KEY</Code>: your Gemini API key, or leave blank if not used.</li>
          <li><Code>azure-openai-key</Code>: leave blank for now; populate later if you add Azure OpenAI.</li>
          <li><Code>NODE_ENV</Code>: manual entry, value <Code>production</Code>.</li>
          <li><Code>PORT</Code>: manual entry, value <Code>8080</Code>.</li>
          <li><Code>AUTH_PROVIDER</Code>: manual entry, value <Code>local</Code>.</li>
          <li><Code>SUPABASE_URL</Code>: manual entry, value <Code>http://localhost:3000</Code>.</li>
          <li><Code>JWT_SECRET</Code>: the JWT secret generated in Part A.</li>
          <li><Code>SUPABASE_SECRET_KEY</Code>: same value as <Code>JWT_SECRET</Code>.</li>
          <li><Code>DOWNLOAD_SIGNING_SECRET</Code>: the download secret from Part A.</li>
          <li><Code>INSTALL_BOOTSTRAP_TOKEN</Code>: the bootstrap token from Part A.</li>
          <li><Code>AUTH_STATE_SECRET</Code>: the auth state secret from Part A.</li>
        </Ul>
        <Note>
          You need at least one model key set. The other rows can be empty strings; the application falls back across providers based on per-user preference.
        </Note>
        <Note tone="ink">
          The <Code>pg-uri</Code> secret intentionally has no env var pointing at it on the backend in this step. The PostgREST sidecar in step 9 is the one that consumes it.
        </Note>
        <Shot step="08.E.7" alt="Environment variables section showing the manual-entry rows." />

        <H4>Part F. Ingress tab</H4>
        <H3>8.F.1 Move to Ingress</H3>
        <P>Click <b>Next: Bindings</b>, then <b>Next: Ingress</b> (skip Bindings, defaults are fine).</P>

        <H3>8.F.2 Enable and configure ingress</H3>
        <Ul>
          <li>Tick <b>Enabled</b> to enable ingress.</li>
          <li><b>Ingress traffic:</b> Accepting traffic from anywhere.</li>
          <li><b>Ingress type:</b> HTTP.</li>
          <li><b>Transport:</b> Auto.</li>
          <li><b>Insecure connections:</b> leave unticked.</li>
          <li><b>Client certificate mode:</b> Ignore (default).</li>
          <li><b>Target port:</b> <Code>8080</Code>. This must match the <Code>PORT</Code> env var we set in Part E and the <Code>EXPOSE 8080</Code> in the Dockerfile.</li>
        </Ul>
        <Shot step="08.F.2" alt="Ingress tab fully configured." />

        <H4>Part G. Review and create</H4>
        <H3>8.G.1 Validate and create</H3>
        <P>Click <b>Review and create</b>. Wait for validation. Click <b>Create</b>. Provisioning takes 1 to 3 minutes.</P>
        <Shot step="08.G.1" alt="Validation passed page just before clicking Create." />

        <H3>8.G.2 Open the resource and save the URL</H3>
        <P>When deployment finishes, click <b>Go to resource</b>. You will land on the Container App overview page. The <b>Application Url</b> field on the top right is the FQDN we will use in steps 10 and 11. It will look like <Code>backend.kindword-12345678.uksouth.azurecontainerapps.io</Code>. Save this value.</P>
        <Shot step="08.G.2" alt="Container App overview page with Application Url visible." />

        <H4>Part H. Verify the backend booted</H4>
        <H3>8.H.1 Watch the log stream</H3>
        <P>Click <b>Log stream</b> in the left navigation under <b>Monitoring</b>. You should see Express startup logs from the backend container. If you see error messages and the container is restarting in a loop, the most common cause is a missing or wrong env var; capture the first error in the stack.</P>
        <Shot step="08.H.1" alt="Log stream showing the 'Backend listening on port 8080' startup line." />
        <Note tone="warn">
          If you see a completely black panel, the app has scaled to zero. Hit the health URL below to bring it back up.
        </Note>

        <H3>8.H.2 Health check from your laptop</H3>
        <Pre>{`curl https://YOUR_BACKEND_FQDN/health`}</Pre>
        <P>You should get back <Code>{'{"ok":true}'}</Code> or similar. If the request hangs or returns a 502, check the Log stream and the Revisions and replicas blade.</P>
        <Shot step="08.H.2" alt="Terminal output showing the health-check JSON response." />

        <Verify>
          <P>The Container App backend is provisioned and the backend container is running. The PostgREST sidecar is not yet present, so the application UI will not work end to end until step 9 (any request that hits the database via PostgREST will fail with a connection error). The <Code>/health</Code> endpoint, which does not touch PostgREST, should respond cleanly.</P>
          <P>In your notes: the Container App&rsquo;s Application Url, plus the three application secrets from Part A.</P>
        </Verify>

        {/* ============================ STEP 9 ============================ */}
        <H2 n="9" id="s9">Add the PostgREST sidecar</H2>
        <P>The backend on its own cannot reach the database. It expects to talk to PostgREST at <Code>http://localhost:3000</Code> (the <Code>SUPABASE_URL</Code> we set in step 8). PostgREST is the second container we now add to the same Container App. Containers in the same app share localhost networking, which is what makes the sidecar pattern work without any service discovery.</P>

        <H3>9.1 Open the backend Container App</H3>
        <P>From <Code>rg-mike-mini</Code>, click the <Code>backend</Code> Container App.</P>

        <H3>9.2 Open the Containers blade</H3>
        <P>In the left navigation, expand <b>Application</b> and click <b>Containers</b>.</P>
        <Shot step="09.2" alt="Containers blade showing one container, the backend, with image acrmikemini.azurecr.io/backend:latest." />

        <H3>9.3 Start adding a new container</H3>
        <P>Click the <b>Create new container</b> link.</P>
        <Shot step="09.4" alt="Form for the new container after clicking Create new container." />

        <H3>9.4 Fill in the Properties tab</H3>
        <Ul>
          <li><b>Name:</b> <Code>postgrest</Code>.</li>
          <li><b>Image source:</b> Azure Container Registry.</li>
          <li><b>Authentication:</b> Secrets (not Managed identity). Pick Secrets so the sidecar uses the same admin credentials we configured at app creation. If you pick Managed identity it will fail to pull because we have not granted AcrPull to any MI.</li>
          <li><b>Subscription:</b> same as the backend.</li>
          <li><b>Registry:</b> <Code>acrmikemini.azurecr.io</Code>.</li>
          <li><b>Image:</b> <Code>postgrest</Code>.</li>
          <li><b>Image tag:</b> <Code>v12.2.3</Code>.</li>
          <li><b>Command override:</b> leave empty.</li>
          <li><b>Arguments override:</b> leave empty.</li>
          <li><b>CPU:</b> <Code>0.25</Code>. <b>Memory:</b> <Code>0.5 Gi</Code>.</li>
        </Ul>
        <Shot step="09.5" alt="Properties tab for the new postgrest container with Authentication set to Secrets, and the CPU/memory selectors." />

        <H3>9.5 Environment variables tab</H3>
        <P>Click the <b>Environment variables</b> tab. Add five rows, all manual, no secret references.</P>
        <Ul>
          <li><Code>PGRST_DB_URI</Code>: paste the Postgres connection string you used as <Code>PG_URI</Code> on the backend (<Code>postgres://mikeadmin:...sslmode=require</Code>).</li>
          <li><Code>PGRST_DB_SCHEMA</Code>: <Code>public</Code>.</li>
          <li><Code>PGRST_DB_ANON_ROLE</Code>: <Code>web_anon</Code>.</li>
          <li><Code>PGRST_SERVER_PORT</Code>: <Code>3000</Code>.</li>
          <li><Code>PGRST_JWT_SECRET</Code>: paste the same JWT secret you used as <Code>JWT_SECRET</Code> on the backend.</li>
        </Ul>
        <Shot step="09.6" alt="Environment variables tab showing the five rows. Blur or crop secret values before publishing." />

        <H3>9.6 Save changes as a new revision</H3>
        <P>Look for a Save or Create button at the top or bottom of the blade. The exact location varies by portal version: sometimes it sits at the very top above Refresh, sometimes at the bottom of the form, and sometimes the workflow is &ldquo;click Save and a new revision is triggered automatically&rdquo;.</P>
        <Shot step="09.7" alt="Save action and the resulting 'Revision created' notification." />

        <H3>9.7 Watch the rollout</H3>
        <P>Watch <b>Revisions and replicas</b> in the left nav. There should now be a new revision with two containers (backend and postgrest), provisioning then Running, while the previous single-container revision drains.</P>

        <H3>9.8 Verify the sidecar is reachable</H3>
        <P>From your laptop, hit the application URL again:</P>
        <Pre>{`curl https://YOUR_BACKEND_FQDN/health`}</Pre>
        <P>It should still return ok. The <Code>/health</Code> endpoint does not actually exercise PostgREST, but it confirms the new revision is taking traffic. To confirm PostgREST itself is wired up, hit a route that does touch it. The simplest is <Code>/config</Code>:</P>
        <Pre>{`https://YOUR_BACKEND_FQDN/config`}</Pre>
        <P>This returns a JSON object with <Code>authProvider</Code>, <Code>entra</Code>, and similar fields. If it returns 500 with a database-related error, PostgREST is unreachable from the backend. The most common cause is a typo in <Code>PGRST_DB_URI</Code> or a missing <Code>sslmode=require</Code>.</P>
        <Shot step="09.10" alt="Terminal output showing /config returning JSON." />

        <Verify>
          Two containers in the same Container App, both reporting healthy. <Code>/config</Code> returns valid JSON. <Code>/health</Code> still returns ok. The Log stream blade now offers two containers in the Container dropdown (backend and postgrest); switching to the postgrest container shows PostgREST&rsquo;s startup line &ldquo;Listening on port 3000&rdquo;.
        </Verify>

        {/* ============================ STEP 10 ============================ */}
        <H2 n="10" id="s10">Wire the public URL back into the application</H2>
        <P>Two env vars on the backend container need to know the Container App&rsquo;s public FQDN: <Code>FRONTEND_URL</Code> (used for CORS allow-lists and login-redirect URL construction) and <Code>BACKEND_PUBLIC_URL</Code> (used to build absolute URLs in OpenID flows).</P>
        <P>We could not set them in step 8 because the FQDN does not exist until the Container App is created. Now that the app exists and you have its Application Url, we add them.</P>
        <P>In our deploy the frontend is bundled inside the backend image, served by the same Express process. Both env vars get the same value: the public URL of the Container App.</P>

        <H3>10.1 Open the backend Container App</H3>
        <P>From <Code>rg-mike-mini</Code>, click <Code>backend</Code>.</P>

        <H3>10.2 Confirm the FQDN</H3>
        <P>The Application Url field on the right of the Essentials panel shows something like <Code>https://backend.kindword-12345678.uksouth.azurecontainerapps.io</Code>. Copy that to a notepad; you will paste it twice in 10.4.</P>

        <H3>10.3 Open the Containers blade</H3>
        <P>In the left navigation, expand <b>Application</b> and click <b>Containers</b>.</P>

        <H3>10.4 Add the two new env vars</H3>
        <P>The Container dropdown should default to <Code>backend</Code>. If not, select it. Click the <b>Environment variables</b> tab. At the empty row at the bottom, add two new entries:</P>
        <Ul>
          <li><b>Name</b> <Code>FRONTEND_URL</Code>, <b>Value</b>: paste the full Application Url including the <Code>https://</Code> prefix and no trailing slash.</li>
          <li><b>Name</b> <Code>BACKEND_PUBLIC_URL</Code>, <b>Value</b>: paste exactly the same Application Url.</li>
        </Ul>
        <Shot step="10.4" alt="Environment variables tab showing the two new rows alongside the existing 14." />

        <H3>10.5 Save and wait</H3>
        <P>As with step 9, click Save. The save creates a new revision (your third) and rolls it out. Watch <b>Revisions and replicas</b>; the new revision shows Provisioning, then Running, in 1 to 2 minutes.</P>

        <H3>10.6 Verify</H3>
        <P>From your laptop:</P>
        <Pre>{`https://YOUR_BACKEND_FQDN/config`}</Pre>
        <P>The response should be JSON with at minimum an <Code>authProvider</Code> field. The exact shape varies by config, but it should not 500.</P>

        <Verify>
          The backend container has 16 env vars (14 from step 8 plus the 2 we just added). The newest revision is Running with both containers healthy. Hitting <Code>/config</Code> returns JSON.
        </Verify>

        {/* ============================ STEP 11 ============================ */}
        <H2 n="11" id="s11">Verify with /diag and the local-auth smoke test</H2>
        <P>This is the milestone we have been aiming at: a deployed application you can sign in to. We do two things here. First we open <Code>/diag</Code>, the deploy verifier, to confirm the deploy is healthy from the backend&rsquo;s own perspective. Then we open the application proper and sign in via &ldquo;Continue locally&rdquo; to confirm the full vertical works: backend booted, PostgREST sidecar reachable on localhost, schema in place, blob container ready, JWT signing works.</P>

        <H4>Part A. Verify the deploy with /diag</H4>
        <P>The MikeOSS fork ships a <Code>/diag</Code> route specifically to make verification of an Azure deployment fast. It is the right first stop after step 10. No bootstrap token, no Microsoft sign-in, no <Code>/api</Code> gating: anyone who can reach the backend&rsquo;s URL can load <Code>/diag</Code> and see the configuration status.</P>
        <P>Open <Code>https://YOUR_BACKEND_FQDN/diag</Code> in any browser. After cold start the page renders in well under a second.</P>
        <Shot step="11.A" alt="/diag landing page in its post-step-10 state. The amber banner at the top says 'You are not signed in', which is expected here." />

        <H3>11.A.1 Walk the env-var checklist</H3>
        <P>The page groups env vars by category. Every required row should have a green &ldquo;set&rdquo; badge. Every secret displays as a partial reveal (first 6 chars, ellipsis, last 4) so you can match against your password manager. Every non-secret displays in full so you can spot a typo at a glance.</P>
        <P>What you should see at this stage:</P>
        <Ul>
          <li><b>Auth core:</b> <Code>AUTH_PROVIDER</Code> shows <Code>local</Code> in full. <Code>JWT_SECRET</Code>, <Code>SUPABASE_SECRET_KEY</Code> and <Code>AUTH_STATE_SECRET</Code> each show partial-reveal masks. All four green.</li>
          <li><b>Entra:</b> section absent. <Code>/diag</Code> hides this category entirely when <Code>AUTH_PROVIDER</Code> is not <Code>entra</Code>; it will appear in step 13.</li>
          <li><b>Database:</b> <Code>PG_URI</Code> partial-reveal green. <Code>SUPABASE_URL</Code> showing <Code>http://localhost:3000</Code> in full. <Code>DATABASE_URL</Code> &ldquo;(not set)&rdquo; in grey, because that one is only used by the migration job that runs from your laptop, never on the Container App.</li>
          <li><b>Storage:</b> <Code>AZURE_STORAGE_CONNECTION_STRING</Code> masked green. <Code>AZURE_STORAGE_CONTAINER_NAME</Code> shows <Code>documents</Code> in full.</li>
          <li><b>Networking:</b> <Code>FRONTEND_URL</Code> and <Code>BACKEND_PUBLIC_URL</Code> each show the full <Code>https://YOUR_BACKEND_FQDN</Code>. <Code>NODE_ENV</Code> shows <Code>production</Code>. <Code>PORT</Code> shows <Code>8080</Code>.</li>
          <li><b>Model providers:</b> a green roll-up banner at the top of the section saying &ldquo;At least one key is set&rdquo;. Each individual key (Anthropic, OpenAI, Gemini, Azure OpenAI) green or grey depending on which you configured.</li>
          <li><b>Operator:</b> <Code>DOWNLOAD_SIGNING_SECRET</Code> and <Code>INSTALL_BOOTSTRAP_TOKEN</Code> masked green.</li>
          <li><b>Optional:</b> <Code>KEY_VAULT_NAME</Code> and <Code>AZURE_CLIENT_ID</Code> grey &ldquo;(not set)&rdquo;. Both are production-hardening features we are deliberately skipping.</li>
        </Ul>

        <H3>11.A.2 If anything is red</H3>
        <P>Red on a row means the env var is required but unset. The most likely culprits at this stage:</P>
        <Ul>
          <li><Code>PG_URI</Code> or <Code>AZURE_STORAGE_CONNECTION_STRING</Code> red: a typo in step 8 Part E. Re-paste from your notes. <Code>PG_URI</Code> must end with <Code>?sslmode=require</Code>.</li>
          <li>Model banner red: no provider key configured. Add at least one from your provider in the env vars.</li>
        </Ul>
        <P>Fix anything red, save (which creates a new revision), wait for the rollout, reload <Code>/diag</Code>, repeat until everything is green. When the checklist is green from top to bottom, move on to Part B.</P>

        <H4>Part B. Smoke-test the running app</H4>
        <H3>11.B.1 Open the app</H3>
        <Pre>{`https://YOUR_BACKEND_FQDN/`}</Pre>
        <P>You should see the login page. In local-auth mode the login page shows a &ldquo;Continue locally&rdquo; button (or similar copy) which creates an authentication token for any email you type. This is expected; <Code>AUTH_PROVIDER=local</Code> is meant for first-deploy validation and dev, not for any deployment with real users.</P>
        <Shot step="11.B.2" alt="Login page rendered." />

        <H3>11.B.2 Continue locally</H3>
        <P>Click <b>Continue locally</b> and type any email address. You can use your own. You should land on the application home page, signed in.</P>
        <Shot step="11.B.3" alt="Application home page after sign-in, top and bottom." />

        <H3>11.B.3 Quick functional check</H3>
        <P>Click around the app: open the Projects page, the Assistant, Workflows. You do not need to upload a real document; the goal here is to confirm pages render without 500 errors. Any 500 indicates a backend problem; capture the URL and the network-tab response.</P>
        <Shot step="11.B.4" alt="Workflows page rendered after sign-in." />

        <Verify>
          <P>Three things confirm the minimal deploy is healthy:</P>
          <Ul>
            <li><Code>/diag</Code> renders the checklist with everything green.</li>
            <li><Code>/health</Code> and <Code>/config</Code> return valid responses.</li>
            <li>The login page accepts &ldquo;Continue locally&rdquo; and lands you in the application.</li>
          </Ul>
          <P>If all three are green, the entire vertical is wired up. This is the end of the minimal-deploy track.</P>
        </Verify>

        {/* ============================ STEP 12 ============================ */}
        <H2 n="12" id="s12">Create the two Entra app registrations</H2>
        <P>Two app registrations are needed. The first represents the backend API itself, the protected resource that Entra-issued tokens grant access to. The second represents the web client that runs the OpenID code flow against the user&rsquo;s browser. They sit in your Microsoft Entra ID tenant, not in any resource group; tearing down the resource group does not delete them.</P>
        <P>Before you start, capture three values you will need throughout this step. From the Container App backend&rsquo;s Overview page, copy the Application Url (your <Code>BACKEND_FQDN</Code>). From the portal&rsquo;s top right corner, click your account name and note the Directory tenant ID listed in the dropdown; that is your <Code>TENANT_ID</Code>. The third value, the Backend app&rsquo;s client ID, you will get after creating the first app reg.</P>

        <H4>Part A. Create the Backend API app registration</H4>
        <H3>12.A.1 Open Microsoft Entra ID</H3>
        <Shot step="12.A.1" alt="Search bar showing the Microsoft Entra ID match." />

        <H3>12.A.2 Go to App registrations</H3>
        <P>In the left navigation, expand <b>Manage</b> and click <b>App registrations</b>.</P>
        <Shot step="12.A.2" alt="App registrations list page." />

        <H3>12.A.3 New registration</H3>
        <P>Click <b>+ New registration</b> in the top toolbar.</P>

        <H3>12.A.4 Fill in the form</H3>
        <Ul>
          <li><b>Name:</b> Mike Backend API.</li>
          <li><b>Supported account types:</b> &ldquo;Accounts in this organisational directory only (Single tenant)&rdquo;.</li>
          <li><b>Redirect URI:</b> leave both fields blank. The backend API does not handle redirects.</li>
        </Ul>
        <P>Click <b>Register</b>.</P>
        <Shot step="12.A.4" alt="New registration form filled in." />

        <H3>12.A.5 Capture the client ID</H3>
        <P>You land on the Mike Backend API overview. Copy the Application (client) ID at the top, save it as <Code>BACKEND_APP_ID</Code>. You will paste it twice in step 13.</P>
        <Shot step="12.A.5" alt="Mike Backend API overview page with the Application (client) ID highlighted." />

        <H3>12.A.6 Set the Application ID URI</H3>
        <P>In the left navigation of this app reg, click <b>Expose an API</b>. Click the <b>Add</b> link next to <b>Application ID URI</b> near the top. The portal proposes <Code>api://YOUR_BACKEND_APP_ID</Code>; accept the default by clicking <b>Save</b>.</P>
        <Shot step="12.A.6" alt="Expose an API page with the Application ID URI now showing api://..." />

        <H3>12.A.7 Add the access_as_user scope</H3>
        <P>Still on the Expose an API page, click <b>+ Add a scope</b>. Fill in:</P>
        <Ul>
          <li><b>Scope name:</b> <Code>access_as_user</Code>.</li>
          <li><b>Who can consent:</b> Admins and users.</li>
          <li><b>Admin consent display name:</b> Access Mike Backend API.</li>
          <li><b>Admin consent description:</b> Allows the app to access the Mike backend API as the signed-in user.</li>
          <li><b>User consent display name:</b> Access Mike Backend API.</li>
          <li><b>User consent description:</b> Allows this app to access the Mike backend API.</li>
          <li><b>State:</b> Enabled.</li>
        </Ul>
        <P>Click <b>Add scope</b>.</P>
        <Shot step="12.A.7" alt="Add a scope side blade filled in." />
        <Shot step="12.A.7a" alt="Expose an API page after the scope is added, showing the new access_as_user row." />

        <H4>Part B. Create the Web Login client app registration</H4>
        <H3>12.B.1 Back to App registrations</H3>
        <P>Use the breadcrumb at the top or the search bar.</P>

        <H3>12.B.2 New registration</H3>
        <P>Click <b>+ New registration</b>.</P>

        <H3>12.B.3 Fill in the form</H3>
        <Ul>
          <li><b>Name:</b> Mike Web Login.</li>
          <li><b>Supported account types:</b> &ldquo;Accounts in this organisational directory only (Single tenant)&rdquo;.</li>
          <li><b>Redirect URI:</b> pick <b>Web</b> from the platform dropdown, then in the URI field paste <Code>https://YOUR_BACKEND_FQDN/api/auth/openid-callback/microsoft</Code>. Replace <Code>YOUR_BACKEND_FQDN</Code> with the Container App&rsquo;s Application Url, no trailing slash before <Code>/api</Code>.</li>
        </Ul>
        <P>Click <b>Register</b>.</P>
        <Shot step="12.B.3" alt="New registration form for Mike Web Login." />

        <H3>12.B.4 Capture the client ID</H3>
        <P>Copy the Application (client) ID from the overview page and save it as <Code>WEB_APP_ID</Code>.</P>
        <Shot step="12.B.4" alt="Mike Web Login overview page with the Application (client) ID highlighted." />

        <H3>12.B.5 Add a client secret</H3>
        <P>In the left navigation, click <b>Certificates &amp; secrets</b>. On the Client secrets tab, click <b>+ New client secret</b>.</P>
        <Ul>
          <li><b>Description:</b> <Code>deploy-time</Code>.</li>
          <li><b>Expires:</b> pick 6 months or 12 months. The longer option simplifies operations; you will need to rotate before expiry.</li>
        </Ul>
        <P>Click <b>Add</b>.</P>
        <Shot step="12.B.5" alt="New client secret side blade." />
        <Shot step="12.B.5a" alt="Client secrets table immediately after creation, showing the Value column populated for the new row." />

        <H3>12.B.6 Capture the secret value</H3>
        <P>Copy the <b>Value</b> column for the new secret (<b>not</b> the Secret ID). This is the only chance to see the value; once you leave the page it is masked forever. Save it as <Code>WEB_SECRET</Code>. Treat it like a password.</P>

        <H3>12.B.7 Grant the Web Login app delegated access</H3>
        <P>In the left navigation, click <b>API permissions</b>. Click <b>+ Add a permission</b>. In the side blade, click the <b>APIs my organisation uses</b> tab and filter by Mike. Click <b>Mike Backend API</b> in the list.</P>
        <Shot step="12.B.7" alt="API permissions page mid-flow with My org APIs tab selected." />

        <H3>12.B.8 Pick the scope</H3>
        <P>Pick permission type <b>Delegated permissions</b>. Tick the <b>access_as_user</b> scope (the one we created in Part A.7). Click <b>Add permissions</b> at the bottom.</P>
        <Shot step="12.B.8" alt="Delegated permissions panel with access_as_user ticked." />

        <H3>12.B.9 Grant admin consent</H3>
        <P>Back on the API permissions page, the <Code>access_as_user</Code> row now shows &ldquo;Not granted&rdquo; with a yellow icon. Click <b>Grant admin consent for {'<tenant>'}</b> at the top of the page. Confirm in the dialog. The status flips to a green tick &ldquo;Granted for {'<tenant>'}&rdquo;.</P>
        <Shot step="12.B.9" alt="API permissions page with the Grant admin consent button highlighted." />
        <Shot step="12.B.9a" alt="After grant, showing the green tick status." />
        <Note tone="warn">
          If you do not have permission to grant admin consent, the button is greyed out. Ask a Directory admin to do it, or skip and rely on individual user consent at first sign-in (which works in single-tenant mode for users who can self-consent).
        </Note>

        <Verify>
          <P>You should have, in your notes outside Azure, four new values:</P>
          <Ul>
            <li><Code>TENANT_ID</Code> (your Entra directory tenant ID).</li>
            <li><Code>BACKEND_APP_ID</Code> (Mike Backend API&rsquo;s Application (client) ID).</li>
            <li><Code>WEB_APP_ID</Code> (Mike Web Login&rsquo;s Application (client) ID).</li>
            <li><Code>WEB_SECRET</Code> (Mike Web Login&rsquo;s client secret value).</li>
          </Ul>
        </Verify>

        {/* ============================ STEP 13 ============================ */}
        <H2 n="13" id="s13">Switch the backend to AUTH_PROVIDER=entra</H2>
        <P>This step changes the backend container&rsquo;s env vars to point at the two app registrations you just created and flips <Code>AUTH_PROVIDER</Code> from <Code>local</Code> to <Code>entra</Code>. It also adjusts the PostgREST sidecar&rsquo;s role and JWT-validation behaviour, because in Entra mode the trust boundary changes: the backend no longer mints JWTs that PostgREST validates; instead the sidecar runs as <Code>service_role</Code> for every request and trusts that only the backend can reach it on localhost.</P>
        <P>You need eight values in front of you before you start. From step 12 you have <Code>BACKEND_APP_ID</Code>, <Code>WEB_APP_ID</Code>, <Code>WEB_SECRET</Code> and <Code>TENANT_ID</Code>. From step 8 you have <Code>BACKEND_FQDN</Code> (without protocol or trailing slash; we will prefix <Code>https://</Code> where needed). The two group OID values (<Code>ENTRA_ADMIN_GROUP_IDS</Code> and <Code>ENTRA_MEMBER_GROUP_IDS</Code>) are optional; you can skip them now and add them later, in which case role assignment falls back to manual via the database.</P>

        <H4>Part A. Update the backend container&rsquo;s env vars</H4>
        <H3>13.A.1 Open the env vars table</H3>
        <P>Container App <Code>backend</Code>: left navigation <b>Application</b> then <b>Containers</b>, container dropdown <b>backend</b>, tab <b>Environment variables</b>.</P>
        <Shot step="13.A.1" alt="Backend env vars table as it stands today, 17 rows." />

        <H3>13.A.2 Flip AUTH_PROVIDER</H3>
        <P>Find the existing <Code>AUTH_PROVIDER</Code> row. Click into the Value field and change <Code>local</Code> to <Code>entra</Code>.</P>
        <Shot step="13.A.2" alt="Edited AUTH_PROVIDER row showing value entra." />

        <H3>13.A.3 Add the eight new Entra rows</H3>
        <P>Use the Enter name / Enter value placeholders at the bottom of the table. All are manual entries.</P>
        <Ul>
          <li><Code>ENTRA_TENANT_ID</Code>: paste your <Code>TENANT_ID</Code>.</li>
          <li><Code>ENTRA_BACKEND_CLIENT_ID</Code>: paste <Code>BACKEND_APP_ID</Code>.</li>
          <li><Code>ENTRA_BACKEND_SCOPE</Code>: <Code>api://YOUR_BACKEND_APP_ID/access_as_user</Code> (paste the Backend client ID into the placeholder).</li>
          <li><Code>ENTRA_CLIENT_ID</Code>: paste <Code>WEB_APP_ID</Code>.</li>
          <li><Code>ENTRA_CLIENT_SECRET</Code>: paste <Code>WEB_SECRET</Code> (treat this like a password; do not screenshot the unblurred value).</li>
          <li><Code>ENTRA_REDIRECT_URI</Code>: <Code>https://YOUR_BACKEND_FQDN/api/auth/openid-callback/microsoft</Code>.</li>
          <li><Code>TENANT_ONBOARDING_MODE</Code>: <Code>auto</Code>. Automatically create users on logon.</li>
          <li><Code>ENTRA_ADMIN_GROUP_IDS</Code>: leave empty for now, or comma-separated Entra group OIDs of admin users.</li>
          <li><Code>ENTRA_MEMBER_GROUP_IDS</Code>: leave empty for now, or comma-separated Entra group OIDs of regular users.</li>
        </Ul>
        <P>If you miss any row, the backend will throw at startup; the System log stream tells you which env var is unset.</P>
        <Shot step="13.A.3" alt="Backend env vars table after additions, all rows visible." />

        <H4>Part B. Update the PostgREST sidecar</H4>
        <H3>13.B.1 Switch to the postgrest container</H3>
        <P>Still on the Containers blade. Container dropdown: switch to <Code>postgrest</Code>. Tab: <b>Environment variables</b>.</P>

        <H3>13.B.2 Change PGRST_DB_ANON_ROLE</H3>
        <P>Edit the <Code>PGRST_DB_ANON_ROLE</Code> row. Change its value from <Code>web_anon</Code> to <Code>service_role</Code>.</P>
        <Shot step="13.B.2" alt="PGRST_DB_ANON_ROLE row showing the updated value." />

        <H3>13.B.3 Clear PGRST_JWT_SECRET</H3>
        <P>Edit the <Code>PGRST_JWT_SECRET</Code> row. Clear the value, leaving it as an empty string. This disables JWT validation in PostgREST. Do not delete the row entirely; we want the env var to exist with an empty value, which the sidecar treats as &ldquo;do not validate JWTs&rdquo;.</P>
        <Note tone="warn">
          Some portal versions consider an empty value to be invalid and refuse to save. If you hit that, delete the row entirely instead. The PostgREST default behaviour in absence of <Code>PGRST_JWT_SECRET</Code> is the same as setting it empty: no JWT validation.
        </Note>
        <Shot step="13.B.3" alt="PGRST_JWT_SECRET row with the cleared value, or deleted entirely." />

        <H4>Part C. Save and verify rollout</H4>
        <H3>13.C.1 Save</H3>
        <P>Click Save at the top or bottom of the blade. Saving creates a new revision (your fourth), with both containers updated, and rolls it out.</P>

        <H3>13.C.2 Watch the rollout</H3>
        <P>Left navigation: <b>Application</b> then <b>Revisions and replicas</b>. The new revision should provision in 1 to 2 minutes and report Running. The previous revision drains out.</P>

        <H3>13.C.3 Verify /config reflects entra mode</H3>
        <P>From your laptop:</P>
        <Pre>{`https://YOUR_BACKEND_FQDN/config`}</Pre>
        <P>The response should now include <Code>{'"authProvider":"entra"'}</Code> plus an <Code>entra</Code> object populated with your <Code>tenantId</Code> and <Code>clientId</Code> values. If you see <Code>{'"authProvider":"local"'}</Code> still, something did not save; revisit the env vars table.</P>
        <Shot step="13.C.3" alt="Terminal output showing /config with entra mode." />

        <H4>Part D. Get and set the admin and member group Object IDs</H4>
        <P>Microsoft Entra distinguishes between users by group membership. Mike maps two groups to two application roles: admin and member. Group membership flows through Entra-issued tokens as the <Code>groups</Code> claim, and the backend matches the OIDs in the claim against the comma-separated lists in <Code>ENTRA_ADMIN_GROUP_IDS</Code> and <Code>ENTRA_MEMBER_GROUP_IDS</Code>. To make this work you need three things: the groups claim emission turned on for the Backend API app, the OIDs of the two groups you want to use, and those OIDs in the two env vars.</P>

        <H3>13.D.1 Turn on the groups claim</H3>
        <P>From the portal search, open Microsoft Entra ID. Go to App registrations, click <b>Mike Backend API</b> in the list. Left navigation: <b>Token configuration</b>. Click <b>+ Add groups claim</b>. In the side blade, tick <b>Security groups</b>. Below, under <b>Customise token properties by type</b>, expand both Access and ID and tick <b>Group ID</b> in each. Click <b>Add</b>.</P>
        <Shot step="13.D.1" alt="Token configuration page after the groups claim is added, two panels showing setup and result." />

        <H3>13.D.2 Find the admin group</H3>
        <P>From the Microsoft Entra ID home, click <b>Groups</b> in the left navigation. Use the search box to find your admin group. If it does not exist yet, create it now: click <b>+ New group</b>, set Group type <b>Security</b>, give it a meaningful name like Mike Admins, add yourself and any other admins as members, and click Create. The group needs to be a Security group, not a Microsoft 365 group.</P>
        <Shot step="13.D.2" alt="Groups list page filtered to your admin group." />

        <H3>13.D.3 Capture the admin group OID</H3>
        <P>Click into the admin group. The Overview page shows an Object ID near the top of the Essentials panel; it is a UUID. Copy it. Save it as <Code>ADMIN_GROUP_OID</Code>.</P>
        <Shot step="13.D.3" alt="Admin group overview page with the Object ID highlighted." />

        <H3>13.D.4 Repeat for the member group</H3>
        <P>Find or create a Security group like Mike Members. Copy its Object ID. Save it as <Code>MEMBER_GROUP_OID</Code>.</P>

        <H3>13.D.5 Verify the group OIDs are actually in your JWT</H3>
        <P>Before you paste the OIDs into the env vars and live with whatever they map to, take 30 seconds to confirm they are reachable. Up to this point we have copied OIDs from the Microsoft Entra portal; we have not confirmed that those OIDs appear in the JWT Microsoft will issue for your sign-in. Several Entra group-type quirks (M365 groups, mail-enabled security groups, on-prem AD groups synced via Entra Connect, group-claim overage) will silently strip a group from the token even though the OID looks correct in the portal. <Code>/diag</Code> is built specifically for this check.</P>
        <P>You need to be signed in for <Code>/diag</Code> to show groups, so sign in first. Open <Code>https://YOUR_BACKEND_FQDN/</Code> in a fresh browser tab and complete the Microsoft sign-in flow. You will land on the application home with 403 errors on every API call. That is expected: <Code>ENTRA_ADMIN_GROUP_IDS</Code> and <Code>ENTRA_MEMBER_GROUP_IDS</Code> are still empty, so the backend&rsquo;s role resolver returns no roles and the tenantAccess middleware denies every <Code>/api/</Code> request. The 403s look noisy in the browser console; ignore them for now.</P>
        <P>Open <Code>/diag</Code> in another tab. Because you are now signed in, the page renders three new sections below the env-var checklist: a &ldquo;Groups in your JWT&rdquo; table, an &ldquo;Env vars currently in effect&rdquo; card, and a Status banner.</P>
        <Shot step="13.D.5" alt="/diag with the Groups-in-your-JWT table populated." />

        <P>Look at the <b>Groups in your JWT</b> table. Both the admin OID from 13.D.3 and the member OID from 13.D.4 should be in this list, tagged &ldquo;unmapped&rdquo; in the role column until we set the env vars. If both are present, proceed to 13.D.6.</P>

        <P><b>If either OID is missing.</b> The most common cause is that the Backend API app reg&rsquo;s manifest is set to emit <Code>SecurityGroup</Code> only, and your group is not classified as a cloud-native Security group. Three group types are silently filtered out: Microsoft 365 groups, mail-enabled security groups, and on-premises AD groups synced via Entra Connect that did not propagate the security identifier.</P>

        <H4>Three ways to fix it</H4>
        <Ul>
          <li><b>Path A. Use a clean cloud-native security group.</b> In Microsoft Entra ID, Groups, click <b>+ New group</b>, set type Security, name it something deliberate like Mike Admins, add yourself as a member, click Create. Copy the new group&rsquo;s Object ID from its Overview page. Use that OID as the value for <Code>ENTRA_ADMIN_GROUP_IDS</Code> in the next step. Sign out and back in. <Code>/diag</Code> will show the new OID in your JWT groups list. This is the cleaner long-term answer because the group&rsquo;s purpose is explicit.</li>
          <li><b>Path B. Reuse a cloud-native group you are already in.</b> Walk down the &ldquo;Groups in your JWT&rdquo; table on <Code>/diag</Code>. For each row, click the <b>Open in Entra</b> button next to the OID; it deep-links to the group&rsquo;s Overview page. Look at the <b>Group type</b> field. Anything labelled Security is a cloud-native security group, which is what we want. Skim each one&rsquo;s display name and member list, find one whose members are an appropriate set for Mike admins (typically a small IT admins or platform group), copy its Object ID, and use that as <Code>ENTRA_ADMIN_GROUP_IDS</Code>. The trade-off versus Path A is that you tie Mike&rsquo;s admin role to a group whose membership was decided elsewhere for some other purpose.</li>
          <li><b>Path C. Broaden the filter.</b> Microsoft Entra ID, App registrations, Mike Backend API, left navigation <b>Manifest</b>. Search the JSON for <Code>groupMembershipClaims</Code>. The current value is <Code>SecurityGroup</Code>. Change it to <Code>All</Code>. Save. Sign out of Mike (clear cookies for the Container App URL or use a fresh incognito window for the next sign-in). Sign back in. Reload <Code>/diag</Code>. The missing OIDs should now appear. Trade-off: your JWT now also contains distribution lists and any M365 group memberships, which adds noise to <Code>/diag</Code>&rsquo;s groups table but does not change role mapping.</li>
        </Ul>
        <P>Pick whichever fix is appropriate and verify in <Code>/diag</Code> before moving on. Once the OID you intend to use is present in the &ldquo;Groups in your JWT&rdquo; table, proceed to 13.D.6.</P>

        <H3>13.D.6 Set the two env vars on the backend container</H3>
        <P>Container App <Code>backend</Code>, left navigation <b>Application</b> then <b>Containers</b>, container dropdown <b>backend</b>, tab <b>Environment variables</b>. If you put placeholder empty values in 13.A.3, click into the Value field of each row and paste the OID. If those rows do not exist yet, click the empty placeholder row at the bottom and add them.</P>
        <Ul>
          <li><Code>ENTRA_ADMIN_GROUP_IDS</Code>: paste <Code>ADMIN_GROUP_OID</Code>. If you have multiple admin groups, comma-separate the OIDs with no spaces.</li>
          <li><Code>ENTRA_MEMBER_GROUP_IDS</Code>: paste <Code>MEMBER_GROUP_OID</Code>. Same comma-separated format if multiple.</li>
        </Ul>
        <Shot step="13.D.6" alt="Env vars table showing the two group rows populated." />

        <H3>13.D.7 Save and verify</H3>
        <P>The save creates a new revision and rolls out in 1 to 2 minutes. Reload <Code>/diag</Code>. The &ldquo;Env vars currently in effect&rdquo; card now shows <Code>ENTRA_ADMIN_GROUP_IDS</Code> with a green &ldquo;in your token&rdquo; badge against the OID. The Status banner flips to green saying &ldquo;Your JWT maps to TenantAdmin, Member. Group setup is correct.&rdquo; That is the canonical &ldquo;Entra is fully wired&rdquo; verification screenshot.</P>

        <Verify>
          Backend container has 25 env vars. PostgREST container has 4 or 5 env vars depending on whether you cleared or deleted <Code>PGRST_JWT_SECRET</Code>. The newest revision is Running. <Code>/config</Code> returns JSON with <Code>authProvider entra</Code> and your tenant and client IDs visible in the <Code>entra</Code> object. In Entra, App registrations shows two new entries: <b>Mike Backend API</b> and <b>Mike Web Login</b>.
        </Verify>

        {/* ============================ STEP 14 ============================ */}
        <H2 n="14" id="s14">Verify Microsoft sign-in</H2>
        <P>Now that the env vars are wired up and the group OID is set correctly, we sign in for real and confirm the application works for an authenticated user. This is the closing milestone of the Entra phase.</P>

        <H3>14.1 Sign in via Microsoft</H3>
        <P>Open <Code>https://YOUR_BACKEND_FQDN/</Code> in a fresh browser window (use incognito or clear cookies for the Container App URL so any stale token from the local-auth phase is dropped). Click the <b>Sign in with Microsoft</b> button on the login page. The browser redirects you to <Code>login.microsoftonline.com</Code>, which prompts for your work account, asks you to consent if this is the first time the app is hitting your tenant, and redirects back to the application.</P>
        <P>You should land on the application home, signed in. The header should show your name and email. The application&rsquo;s UI panes (Projects, Assistant, Workflows) should render without 403 errors in the browser console.</P>
        <Shot step="14.1a" alt="Microsoft sign-in screen during the redirect." />

        <H3>14.2 Verify with /diag</H3>
        <P>Open <Code>https://YOUR_BACKEND_FQDN/diag</Code> in another tab. The page should now show all of:</P>
        <Ul>
          <li>The env-var checklist all-green, including the Entra section that was hidden in step 11 (when <Code>AUTH_PROVIDER</Code> was local).</li>
          <li>A &ldquo;Sign-in&rdquo; card showing <Code>provider=entra</Code>, your tenant ID, your user ID, your email, your display name, and &ldquo;Resolved roles: TenantAdmin, Member&rdquo; (or just Member if you used the member group OID).</li>
          <li>A &ldquo;Groups in your JWT&rdquo; table showing every group OID from your token. The OID you put in <Code>ENTRA_ADMIN_GROUP_IDS</Code> is tagged &ldquo;admin&rdquo; in the role column; everything else is &ldquo;unmapped&rdquo;.</li>
          <li>An &ldquo;Env vars currently in effect&rdquo; section showing <Code>ENTRA_ADMIN_GROUP_IDS</Code> and <Code>ENTRA_MEMBER_GROUP_IDS</Code>. The OID(s) you set should have a green &ldquo;in your token&rdquo; badge.</li>
          <li>A green status banner saying &ldquo;Your JWT maps to TenantAdmin, Member. Group setup is correct; this user has access.&rdquo;</li>
        </Ul>
        <P>That combination of green ticks is the canonical &ldquo;Entra is fully wired&rdquo; state.</P>
        <Shot step="14.2" alt="/diag with everything green after Entra sign-in, top and bottom of the page." />

        <H3>14.3 If something is wrong</H3>
        <P>If sign-in failed or <Code>/diag</Code> shows red badges or unmapped groups, <Code>/diag</Code> is the place to start. The most common failure modes:</P>
        <Ul>
          <li><b>AADSTS50011 redirect URI mismatch</b> on the Microsoft consent screen. The application sent a redirect URI that does not match what you registered. Check step 12 Part B.3 (the Web Login app reg&rsquo;s redirect URI) and the <Code>ENTRA_REDIRECT_URI</Code> env var in 13.A.3 are both exactly <Code>https://YOUR_BACKEND_FQDN/api/auth/openid-callback/microsoft</Code>, with the <Code>/api</Code> prefix.</li>
          <li><b>Sign-in succeeds but 403 with detail <Code>TENANT_UNKNOWN</Code>.</b> <Code>/diag</Code>&rsquo;s env-var checklist will show <Code>TENANT_ONBOARDING_MODE</Code> in the Entra section. If it is <Code>manual</Code> or unset, change it to <Code>auto</Code> and save.</li>
          <li><b>Sign-in succeeds but 403 <Code>GROUP_NOT_WHITELISTED</Code>.</b> <Code>/diag</Code> will show two things: the OIDs in your JWT, and the OIDs in the env vars, with red badges where they do not match. Fix one of them. If your intended admin group&rsquo;s OID is not in the JWT at all, that is the manifest-filter problem from step 13.D.5; pick Path A, B, or C from there.</li>
          <li><b>Token rejected, &ldquo;invalid audience&rdquo; or &ldquo;invalid issuer&rdquo;.</b> Cross-reference <Code>ENTRA_BACKEND_CLIENT_ID</Code> and <Code>ENTRA_TENANT_ID</Code> against the actual Backend API app reg&rsquo;s Application (client) ID and your directory tenant ID.</li>
          <li><b>Token rejected, &ldquo;Token expired&rdquo;.</b> You signed in then walked away for an hour. Sign out and back in.</li>
        </Ul>
        <P>Whichever fix you apply: save the env var or app-reg change, wait for the new revision to roll out (1 to 2 minutes for env var, 0 for app-reg changes), reload <Code>/diag</Code>, retry. Most fixes converge in a single iteration once <Code>/diag</Code> tells you what is wrong.</P>

        <Verify>
          <Ul>
            <li>Sign-in works end to end against your real Entra tenant.</li>
            <li><Code>/diag</Code> shows everything green.</li>
            <li>The application&rsquo;s API endpoints return 200s in the browser network tab, not 403s.</li>
          </Ul>
          <P>When all three are true, the deploy is complete. You could give the URL to a colleague in the same admin group and they would be able to sign in and use it.</P>
        </Verify>

        {/* ----- closing CTA ----- */}
        <div style={{ marginTop: 64, padding: '28px 28px', border: '1px solid var(--azure)', borderRadius: 12, background: 'linear-gradient(180deg, var(--azure-soft) 0%, var(--paper) 100%)' }}>
          <div style={{ fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.12em', textTransform: 'uppercase', color: 'var(--ink-3)', marginBottom: 8 }}>You made it</div>
          <h3 style={{ fontFamily: 'var(--serif)', fontWeight: 400, fontSize: 28, margin: '0 0 10px' }}>
            That is the whole minimal install.
          </h3>
          <p style={{ fontSize: 15, color: 'var(--ink-2)', lineHeight: 1.65, margin: '0 0 16px' }}>
            If you would rather not click through fifteen blades by hand, the same deployment is available as a one-click install from Azure Marketplace, and we are happy to do the hardening pass with you for a fixed fee.
          </p>
          <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
            <a href="https://github.com/Altien/mikeOssAzure" target="_blank" rel="noopener" style={{ padding: '12px 18px', background: 'var(--azure)', color: '#fff', borderRadius: 8, fontSize: 13, display: 'inline-flex', gap: 8, alignItems: 'center' }}>
              Source on GitHub
            </a>
            <a href="https://calendly.com/altien-allenmorgan" target="_blank" rel="noopener" style={{ padding: '12px 18px', border: '1px solid var(--line)', borderRadius: 8, fontSize: 13, color: 'var(--ink-2)', background: 'var(--paper)' }}>
              Book a 30-min scoping call
            </a>
          </div>
        </div>

        {/* ----- lightbox modal ----- */}
        {lightbox && (
          <div
            onClick={() => setLightbox(null)}
            style={{ position: 'fixed', inset: 0, zIndex: 2000, background: 'rgba(15,15,15,0.88)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24, cursor: 'zoom-out' }}
          >
            <img
              src={`/install-guide-images/${lightbox}`}
              alt=""
              onClick={(e) => e.stopPropagation()}
              style={{ maxWidth: '95vw', maxHeight: '95vh', height: 'auto', width: 'auto', objectFit: 'contain', borderRadius: 6, boxShadow: '0 30px 80px rgba(0,0,0,0.5)', background: '#fff', cursor: 'default' }}
            />
            <button
              onClick={() => setLightbox(null)}
              aria-label="Close"
              style={{ position: 'fixed', top: 20, right: 24, appearance: 'none', border: 'none', background: 'rgba(255,255,255,0.12)', color: '#fff', width: 38, height: 38, borderRadius: 99, fontSize: 22, lineHeight: 1, cursor: 'pointer' }}
            >
              ✕
            </button>
          </div>
        )}
      </div>
    </section>
  );
}
