diff --git a/BUILD-NOTES.md b/BUILD-NOTES.md index 0b517cd..b36da68 100644 --- a/BUILD-NOTES.md +++ b/BUILD-NOTES.md @@ -757,3 +757,53 @@ when the production privacy salt correctly rotates. This one test now fixes tomorrow/rotation coverage and all production analytics code remain unchanged. The unchanged-base failure and corrected fourteen-test pass are recorded in the review; no runtime clock, customer data, analytics behavior or financial change. + +## Accepted technical templates — Astra integration + +Adopts the parent's accepted Batch04 compositions: Changelog / Release Ledger, Migration guide / +Conversion Workbench, and Runbook / Operator's Bench. gpt-6-astra is the sole integrator. The +immutable acceptance bundle is +`aa-template-revamp-1788686289312/batch-04-technical/bundle.json` (SHA-256 +`bd219cc78d9f71c984ed4c49a1744a8c55d25d0213c5a934ecf4e85227b7c886`). +Integration evidence is published under +`https://anacreon.ai/downloads/aa-template-revamp-1788686289312/integrated-batch04/`. + +Changelog and Migration guide are exact accepted files, including CSS and reader-facing titles. +Runbook has precisely one authorized copy correction: “Only once the check below has been clean +for five minutes.” becomes “Only once the verification check has been clean for five minutes.” +The complete Verify block stays between the four-step ordered list and the list starting at5. +Every other byte is unchanged. No sample commands are executed; no production instructions, +credentials, customer records or financial behavior are modified by these inert examples. + +Canonical SHA-256: +- Changelog: `77ff080efe080fc40813e626c7e8d217bd9d5e31d25b7fdeec8e539ef9bda124` +- Migration guide: `f903fe929606e0538cd66a5ca3f316055a5b17edd808f3b6d2c66bc0431d8606` +- Runbook: `64f9d4322e821044456be0a4ebd798339a9eb52d1ef16982759eba6995c965c7` + (accepted `bc6270a80eb633b43f5e48afb65800563f76b3c6e19364cde3906d50547633cc` + plus the one sentence above). + +All262 original text nodes,147 numeric occurrences and49 keyed record groups are compared with +only that explicit expected sentence change. The11 original pre blocks and every command/output +descendant remain exact; no SVG or meter exists in this batch. The existing Changelog patch-count +mismatch and Migration guide documentation link are preserved, not “corrected” by inventing facts. +Historical preview comments remain as acceptance provenance, not an indication that canonical files +are unapproved. There is no document-height cap. + +The unchanged thumbnail generator uses1280px for Changelog and Migration guide, showing the version +slab/breaking actions and actual code conversion pairs. Runbook uses1920px so its whole precondition +rail, Stop panel and opening command/output records enter the crop; the explicit trade-off is smaller +text and wider gutters at gallery scale. All output remains1000×625. Descriptions match these actual +compositions. Other template sources, PNGs and catalog fields remain untouched. + +Native sections name locally scrolling code, with no explicit role/tabindex or lint suppression. +Chromium keyboard tests exercise native region focus, ArrowRight panning, Enter disclosures, and +footer reachability inside the real app sandbox. Cross-engine auto-focus and screen-reader +navigation are not certified. Unit tests protect the Runbook order/copy and original pre descendants, +including negative controls; e2e coverage runs through the existing seven viewport projects. + +Lint has zero errors.40warnings comprise30 unchanged outside this batch and10 in the accepted +Runbook CSS, replacing7 legacy Runbook warnings: seven descending-specificity diagnostics plus +three new important-style diagnostics. The three local note-margin overrides preserve the selected +base/tablet/mobile caution spacing, including the existing inline Verify note margin. They are +inventoried rather than suppressed or “fixed” by changing approved CSS. No global configuration, +runtime, schema, billing, dependencies, sandbox or launch settings change. diff --git a/public/assets/template-thumbs/changelog.png b/public/assets/template-thumbs/changelog.png index 56cf303..72e016a 100644 Binary files a/public/assets/template-thumbs/changelog.png and b/public/assets/template-thumbs/changelog.png differ diff --git a/public/assets/template-thumbs/migration-guide.png b/public/assets/template-thumbs/migration-guide.png index 7cf83bb..0e9d44f 100644 Binary files a/public/assets/template-thumbs/migration-guide.png and b/public/assets/template-thumbs/migration-guide.png differ diff --git a/public/assets/template-thumbs/runbook.png b/public/assets/template-thumbs/runbook.png index aff8e64..44c5177 100644 Binary files a/public/assets/template-thumbs/runbook.png and b/public/assets/template-thumbs/runbook.png differ diff --git a/templates/changelog.html b/templates/changelog.html index d75479d..cda86a8 100644 --- a/templates/changelog.html +++ b/templates/changelog.html @@ -1,380 +1,14 @@ - - + + Kestrel Platform — Changelog - - +
-

Changelog

Kestrel Platform

@@ -382,15 +16,14 @@

Kestrel Platform

Every user-visible change to the Kestrel API, CLI and dashboard. Dates are the day a release reached the stable channel.

-
+
+ +
Latest
4.3.0
Released
4 Sep 2026
Channel
stable
API version
2026-06-01
- - -

Breaking Two changes need action before you upgrade @@ -409,8 +42,7 @@

- -
+

4.3.0

4 Sep 2026

@@ -424,55 +56,54 @@

4.3.0

  • Added -

    Backfill for scheduled runs

    +

    Backfill for scheduled runs

    kestrel schedule backfill <id> --from 2026-08-01 replays a schedule over a past window, at most 50 runs at a time, with the same concurrency limit as live execution.

    #2841 · docs/schedules#backfill

    -
  • +
  • Added -

    Run log streaming over SSE

    +

    Run log streaming over SSE

    GET /v1/runs/:id/logs?stream=true emits log lines as they are written. Logs are retained for 30 days on Team plans, 7 on Free.

    #2803

    -
  • +
  • Changed -

    Run logs keep the last 200k lines, not 10k

    +

    Run logs keep the last 200k lines, not 10k

    Long-running jobs were silently losing their tail, which is precisely the part anyone reads. Older lines are now dropped from the head instead.

    #2790 · reported by 14 workspaces

    -
  • +
  • Fixed -

    Retries no longer double-count against the rate limit

    +

    Retries no longer double-count against the rate limit

    An automatic retry consumed two units of quota instead of one. Affected workspaces have had the difference credited back to the current billing period.

    #2822 · severity: high

    -
  • +
  • Removed -

    The legacy /v0 endpoints are gone

    +

    The legacy /v0 endpoints are gone

    Deprecated since March, at 0.02% of traffic for the last eight weeks. Every caller was contacted directly in August.

    #2755 · announced 12 Mar 2026

    -
  • +
    -

    4.2.2

    @@ -484,24 +115,23 @@

    4.2.2

    • Fixed -

      Webhook signatures verified against the wrong secret after rotation

      +

      Webhook signatures verified against the wrong secret after rotation

      For roughly six hours after a secret was rotated, deliveries were signed with the retired key. Rotation now writes both keys and accepts either for 24 hours.

      #2731 · postmortem published

      -
    • +
  • Changed -

    Timeouts are reported in seconds everywhere

    +

    Timeouts are reported in seconds everywhere

    The API returned milliseconds, the CLI printed seconds, and the dashboard mixed both.

    #2724

    -
  • +
    -
    Six earlier releases, 4.0.0 – 4.2.1
    diff --git a/templates/runbook.html b/templates/runbook.html index 41b93cc..02dd7e3 100644 --- a/templates/runbook.html +++ b/templates/runbook.html @@ -1,271 +1,25 @@ - - + + Runbook — rotate the production database credentials - +/* Native named scroll regions; only genuinely overflowing regions enter Chromium tab order. */ +section.code-region{margin:0;padding:0;border:0;min-width:0;overflow:auto}section.code-region>pre,section.code-region>.cmd,section.code-region>code{width:max-content;min-width:100%;max-width:none;overflow:visible;overflow-wrap:normal} +.pre li>.code-region{margin-top:10px}.pre .code-region>code{margin:0}.body>.code-region{margin-top:16px}.code-region>pre{margin:0}.verify>.code-region,.rollback>.code-region{margin-top:16px}@media(min-width:681px) and (max-width:1040px){.body>.code-region{margin:0}} + - -
    -

    Runbook

    +
    +

    Runbook

    Rotate the production database credentials

    Quarterly rotation of api_rw on prod-pg-1. Zero downtime — but only if step 4 finishes before you touch step 5.

    -
    +
    When
    Quarterly
    Takes
    ~15 min
    Downtime
    None
    @@ -273,28 +27,27 @@

    Rotate the production database credentials

    - -
    -

    Before you start

    +
    +

    Before you start

    All four. They take a minute together.

    • - You are on the VPN and reach the primary. - psql -h prod-pg-1 -c '\conninfo' -
    • + You are on the VPN and reach the primary. +
      psql -h prod-pg-1 -c '\conninfo' +
    • - The deploy freeze is off. - ./bin/freeze status # => open -
    • + The deploy freeze is off. +
      ./bin/freeze status # => open +
    • - No migration is running. - ./bin/migrations running # => 0 -
    • + No migration is running. +
      ./bin/migrations running # => 0 +
    • - You can write to the secret store. - vault kv get -field=version \ + You can write to the secret store. +
      vault kv get -field=version \ secret/prod/db -
    • +

    Stop if any is false @@ -302,115 +55,111 @@

    Before you start

    - -
    +

    The procedure

    Two roles exist at once for a few minutes. That overlap is the design — nothing restarts, nothing has to be timed.

      -
    1. - 1 -

      Announce it

      +
    2. + +

      Announce it

      A rotation nobody expected costs more than the one you announce.

      -
      ./bin/announce "rotating api_rw,
      +            
      ./bin/announce "rotating api_rw,
         ~15 min, no downtime expected"posted to #eng-oncall
      -
      +
    -
  • - 2 -

    Generate the new secret

    +
  • + +

    Generate the new secret

    Written to a staging slot the application does not read yet.

    -
    export NEW=$(openssl rand -hex 24)vault kv put secret/prod/db/next \
    +            
    export NEW=$(openssl rand -hex 24)vault kv put secret/prod/db/next \
       password="$NEW"Key       Value
     version   8
    -
    +
  • -
  • - 3 -

    Create the successor role

    +
  • + +

    Create the successor role

    It inherits from the api_rw group role, so you never re-grant table by table.

    -
    psql -h prod-pg-1 \
    +            
    psql -h prod-pg-1 \
       -f sql/rotate.sql \
       -v pw="$NEW" -v role=api_rw_q3CREATE ROLE
     GRANT
    -
    +
  • -
  • - 4 -

    Point the application at it

    +
  • + +

    Point the application at it

    This is the step that matters. The sidecar reloads on its own, no restart. Until it returns, the old role is still serving traffic.

    -
    vault kv put secret/prod/db \
    +            
    vault kv put secret/prod/db \
       username=api_rw_q3 \
       password="$NEW"./bin/wait-for-rollout \
       --key secret/prod/db \
       --timeout 120sall 14 pods on version 9 (47s)
    -
    +
  • -
  • - 5 -

    Retire the old role

    -
    -

    Only once the check below has been clean for five minutes.

    -
    psql -h prod-pg-1 \
    -  -c "drop role api_rw_q2"DROP ROLE
    -

    - If it says objects depend on it: something still owns tables as that role. - Stop and escalate — never REASSIGN OWNED on production alone. -

    -
    -
  • - - - - -
    -

    Verify — between steps 4 and 5

    + +
    +

    Verify — between steps 4 and 5

    Zero on the old role for five straight minutes. That is the bar.

    -
    psql -h prod-pg-1 -c "select usename,
    +      
    psql -h prod-pg-1 -c "select usename,
       count(*) from pg_stat_activity
       where datname='app' group by 1" api_rw_q3 | 42
      api_rw_q2 |  0
    -

    +

    If the old role still has connections: a pod missed the reload. Restart it.

    - -
    -

    If it goes wrong

    +
    1. + +

      Retire the old role

      +
      +

      Only once the verification check has been clean for five minutes.

      +
      psql -h prod-pg-1 \
      +  -c "drop role api_rw_q2"DROP ROLE
      +

      + If it says objects depend on it: something still owns tables as that role. + Stop and escalate — never REASSIGN OWNED on production alone. +

      +
      +
    2. +
    + +
    +

    If it goes wrong

    -

    Rollback — safe at any point before step 5

    +

    Rollback — safe at any point before step 5

    The old secret version and the old role both still exist, so this is a sixty-second undo — not a deploy.

    -
    vault kv rollback -version=8 \
    +        
    vault kv rollback -version=8 \
       secret/prod/db./bin/wait-for-rollout \
       --key secret/prod/dball 14 pods back on version 8 (39s)
    -
    +
    - -
    -

    When to escalate

    +
    +

    When to escalate

    • Connections have not drained after 10 minutes. @@ -423,10 +172,10 @@

      When to escalate

    -
    +

    rotate-db-credentials · owner data-platform · reviewed 28 Aug 2026 · last run 12 Jun 2026

    - +
    diff --git a/tests/e2e/template-technical-semantics.spec.ts b/tests/e2e/template-technical-semantics.spec.ts new file mode 100644 index 0000000..9c84192 --- /dev/null +++ b/tests/e2e/template-technical-semantics.spec.ts @@ -0,0 +1,95 @@ +import { expect, test } from '@playwright/test'; + +const cloudOrigin = process.env.E2E_CLOUD_BASE_URL; +const sandboxOrigin = process.env.E2E_CLOUD_SANDBOX_ORIGIN; + +for (const [slug, regionCount, summaryCount] of [ + ['changelog', 0, 1], + ['migration-guide', 7, 2], + ['runbook', 11, 0], +] as const) { + test(`${slug}: native reading order, disclosures and long code survive the real sandbox`, async ({ + page, + }) => { + await page.goto(`${cloudOrigin}/templates/${slug}`); + await expect(page.locator('iframe')).toHaveAttribute('sandbox', 'allow-scripts'); + await expect(page.locator('iframe')).toHaveAttribute( + 'src', + `${sandboxOrigin}/templates/${slug}/frame` + ); + const frame = page.frameLocator('iframe'); + await expect(frame.locator('h1')).toHaveCount(1); + await expect(frame.locator('[role], [tabindex]')).toHaveCount(0); + await expect(frame.locator('section.code-region')).toHaveCount(regionCount); + for (const region of await frame.locator('section.code-region').all()) { + const name = await region.evaluate((element) => + (element.getAttribute('aria-labelledby') ?? '') + .split(/\s+/) + .map((id) => document.getElementById(id)?.textContent?.trim()) + ); + expect(name.every((label) => Boolean(label))).toBe(true); + // Generated Before/After signs and step counters also contribute to the accessible name. + // Require every referenced label's complete text, in order, without discarding those signs. + const pattern = name + .map((label) => (label ?? '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) + .join('.*'); + await expect(region).toHaveAccessibleName(new RegExp(`^.*${pattern}.*$`)); + } + if (slug === 'runbook') { + await expect(frame.locator('ol.steps').first().locator(':scope > li')).toHaveCount(4); + await expect(frame.locator('ol.final-step')).toHaveAttribute('start', '5'); + expect( + await frame.locator('.verify').evaluate((element) => ({ + previous: element.previousElementSibling?.tagName, + previousItems: element.previousElementSibling?.children.length, + next: element.nextElementSibling?.getAttribute('start'), + })) + ).toEqual({ previous: 'OL', previousItems: 4, next: '5' }); + await expect(frame.locator('[data-step="5"] p').first()).toHaveText( + 'Only once the verification check has been clean for five minutes.' + ); + expect(await frame.locator('.procedure').ariaSnapshot()).toContain('listitem'); + } + await expect(frame.locator('summary')).toHaveCount(summaryCount); + for (const summary of await frame.locator('summary').all()) { + await summary.focus(); + await page.keyboard.press('Enter'); + await expect(summary.locator('xpath=..')).toHaveAttribute('open', ''); + } + if (regionCount) { + const region = frame.locator('section.code-region').first(); + // Inert DOM-only stress: lengthen a sample line, never evaluate or execute its content. + await region.evaluate((element) => { + const code = element.querySelector('code, pre'); + if (!code) throw new Error('Missing code sample'); + code.append(document.createTextNode(' LONG_SAMPLE_SEGMENT'.repeat(40))); + }); + await frame.locator('body').click({ position: { x: 5, y: 5 } }); + await page.keyboard.press('Control+Home'); + let reached = false; + for (let attempt = 0; attempt < 24; attempt += 1) { + await page.keyboard.press('Tab'); + if (await region.evaluate((element) => document.activeElement === element)) { + reached = true; + break; + } + } + expect(reached).toBe(true); + expect(await region.evaluate((element) => element.matches(':focus-visible'))).toBe(true); + await page.keyboard.press('ArrowRight'); + await expect.poll(() => region.evaluate((element) => element.scrollLeft)).toBeGreaterThan(0); + expect( + await region.evaluate((element) => + [...element.querySelectorAll('pre, code')].every( + (code) => Number.parseFloat(getComputedStyle(code).fontSize) >= 14 + ) + ) + ).toBe(true); + } + expect( + await frame.locator('html').evaluate((element) => element.scrollWidth <= innerWidth + 1) + ).toBe(true); + await frame.locator('footer').scrollIntoViewIfNeeded(); + await expect(frame.locator('footer')).toBeInViewport(); + }); +} diff --git a/tests/unit/template-technical-semantics.test.ts b/tests/unit/template-technical-semantics.test.ts new file mode 100644 index 0000000..e6be07a --- /dev/null +++ b/tests/unit/template-technical-semantics.test.ts @@ -0,0 +1,49 @@ +import { createHash } from 'node:crypto'; +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; + +const source = (slug: string) => + readFileSync(new URL(`../../templates/${slug}.html`, import.meta.url), 'utf8'); + +const expectRunbookOrder = (html: string) => { + expect(html).toContain('Only once the verification check has been clean for five minutes.'); + expect(html).not.toContain('Only once the check below has been clean for five minutes.'); + expect(html).toMatch( + /
      [\s\S]*?data-step="4"[\s\S]*?<\/ol>\s*
      [\s\S]*?
      1. / + ); + expect(html.match(/data-step="[1-5]"/g)).toEqual( + [1, 2, 3, 4, 5].map((step) => `data-step="${step}"`) + ); +}; + +describe('technical template native structure and inert sample content', () => { + it('keeps the five-minute check explicitly between steps four and five', () => { + const html = source('runbook'); + expectRunbookOrder(html); + // Red controls exercise the same assertions without touching a file or running a command. + expect(() => + expectRunbookOrder(html.replace('the verification check', 'the check below')) + ).toThrow(); + expect(() => expectRunbookOrder(html.replace('start="5"', 'start="1"'))).toThrow(); + }); + + for (const [slug, regions, preCount, digest] of [ + ['changelog', 0, 0, '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945'], + ['migration-guide', 7, 4, '9cc5c6c6da16ccdf1536321d467aa984620f23349fce4d254b864cb818d7126a'], + ['runbook', 11, 7, '39e4d3307250b0bcffaa58064f7c7382215a24c527c9bdd691411b8a018dd7ec'], + ] as const) { + it(`${slug} preserves named native regions and complete sample pre descendants`, () => { + const html = source(slug); + expect(html).not.toMatch(/<[a-z][^>]*\s(?:role|tabindex)=|/g) ?? [] + ).toHaveLength(regions); + const blocks = [...html.matchAll(/]*>([\s\S]*?)<\/pre>/g)].map( + (match) => match[1] ?? '' + ); + expect(blocks).toHaveLength(preCount); + // Order can follow the approved reading flow; descendants and sample code bytes cannot drift. + expect(createHash('sha256').update(JSON.stringify(blocks.sort())).digest('hex')).toBe(digest); + }); + } +});