diff --git a/docusaurus.config.js b/docusaurus.config.js index e849c18ed90..4eb577e4701 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -1,7 +1,15 @@ // @ts-check // Note: type annotations allow type checking and IDEs autocompletion -const { themes: prismThemes } = require('prism-react-renderer'); +const identityLinks = require('./static/identity/links.json'); +const codeTheme = { + plain: {color: '#E8EDEA', backgroundColor: '#0F1513'}, + styles: [ + {types: ['comment', 'prolog', 'doctype', 'cdata'], style: {color: '#9AA6A1'}}, + {types: ['keyword', 'selector', 'string', 'attr-value'], style: {color: '#5FE0A6'}}, + {types: ['function', 'class-name', 'number', 'boolean'], style: {color: '#B7C2BD'}}, + ], +}; const { artifactVersionRemarkPlugin } = require('./scripts/public-artifact-versions'); const cloudflareWebAnalytics = Object.freeze({ @@ -179,12 +187,14 @@ const config = { /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ ({ colorMode: { - defaultMode: 'dark', + defaultMode: 'light', disableSwitch: false, respectPrefersColorScheme: false, }, announcementBar: { id: 'server-3-0-launch', + backgroundColor: 'var(--dw-surface)', + textColor: 'var(--dw-ink)', content: 'Durable Workflow Server 3.0 runs as one native process plus a database. Read the launch post.', isCloseable: true, @@ -196,27 +206,29 @@ const config = { src: 'img/logo.svg', }, items: [ + {type: 'doc', docId: 'introduction', position: 'left', label: 'Docs'}, { - type: 'doc', - docId: 'introduction', - position: 'left', - label: 'Docs', + type: 'custom-sdkSwitcher', position: 'left', label: 'SDKs', + items: [ + {label: 'Python', href: identityLinks.python}, + {label: 'Rust', href: identityLinks.rust}, + {label: 'PHP', href: identityLinks.php}, + ], }, + {href: identityLinks.cloud, label: 'Cloud', position: 'left'}, + {href: identityLinks.pricing, label: 'Pricing', position: 'left'}, + {href: identityLinks.status, label: 'Status', position: 'left'}, + {href: identityLinks.github, label: 'GitHub', position: 'right'}, { - type: 'docsVersionDropdown', - position: 'left', - dropdownActiveClassDisabled: true, + to: '/docs/introduction/', label: 'Get started', position: 'right', + className: 'button button--primary dw-nav-cta', }, - {to: '/blog', label: 'Blog', position: 'left'}, - {type: 'localeDropdown', position: 'right'}, { - type: 'custom-githubStar', - href: 'https://github.com/durable-workflow/workflow', - repo: 'durable-workflow/workflow', - label: 'Star on GitHub', - position: 'right', - 'aria-label': 'Star Durable Workflow on GitHub', + type: 'docsVersionDropdown', position: 'left', + dropdownActiveClassDisabled: true, className: 'dw-context-item', }, + {to: '/blog', label: 'Blog', position: 'left', className: 'dw-context-item'}, + {type: 'localeDropdown', position: 'right', className: 'dw-context-item'}, ], }, footer: { @@ -265,8 +277,8 @@ const config = { copyright: `Copyright © ${new Date().getFullYear()} Durable Workflow.`, }, prism: { - theme: prismThemes.github, - darkTheme: prismThemes.dracula, + theme: codeTheme, + darkTheme: codeTheme, }, image: 'img/durable-workflow-2.0.png', }), diff --git a/i18n/de/code.json b/i18n/de/code.json index 975644d5966..da62a3f0f4d 100644 --- a/i18n/de/code.json +++ b/i18n/de/code.json @@ -301,5 +301,80 @@ }, "simulator.signalWaitReceived": { "message": "await(): Signal empfangen" + }, + "identity.documentation": { + "message": "Dokumentation" + }, + "identity.durableExecution": { + "message": "Dauerhafte Ausführung" + }, + "identity.heroTitle": { + "message": "Arbeit, die weiterläuft." + }, + "identity.startEyebrow": { + "message": "Ihr nächster Schritt" + }, + "identity.startTitle": { + "message": "Starten Sie dort, wo Sie entwickeln." + }, + "identity.embedded": { + "message": "In Ihre Anwendung eingebettet" + }, + "identity.sdkGuide": { + "message": "SDK-Anleitung und API-Referenz" + }, + "identity.runtimeNote": { + "message": "Betreiben Sie den Server selbst oder nutzen Sie die verwaltete Cloud." + }, + "identity.footerDescription": { + "message": "Betreiben Sie den Server selbst oder nutzen Sie die Cloud." + }, + "identity.run": { + "message": "Betrieb" + }, + "identity.selfHosted": { + "message": "Selbst gehosteter Server" + }, + "identity.pricing": { + "message": "Preise" + }, + "identity.status": { + "message": "Status" + }, + "identity.project": { + "message": "Projekt" + }, + "identity.compatibility": { + "message": "Versionskompatibilität" + }, + "identity.terms": { + "message": "Nutzungsbedingungen" + }, + "identity.privacy": { + "message": "Datenschutzerklärung" + }, + "identity.statusUnknown": { + "message": "Status unbekannt" + }, + "identity.statusOperational": { + "message": "Alle Systeme betriebsbereit" + }, + "identity.statusDegraded": { + "message": "Dienst beeinträchtigt" + }, + "identity.statusIncident": { + "message": "Dienststörung" + }, + "identity.statusMaintenance": { + "message": "Wartung" + }, + "identity.statusStale": { + "message": "Status veraltet" + }, + "identity.statusUnverified": { + "message": "Status nicht verifiziert" + }, + "identity.statusUnavailable": { + "message": "Status nicht verfügbar" } } diff --git a/i18n/de/docusaurus-theme-classic/navbar.json b/i18n/de/docusaurus-theme-classic/navbar.json index e08e3434684..be701fc7b49 100644 --- a/i18n/de/docusaurus-theme-classic/navbar.json +++ b/i18n/de/docusaurus-theme-classic/navbar.json @@ -10,5 +10,14 @@ }, "item.label.Star on GitHub": { "message": "Stern auf GitHub geben" + }, + "item.label.Pricing": { + "message": "Preise" + }, + "item.label.Status": { + "message": "Status" + }, + "item.label.Get started": { + "message": "Erste Schritte" } } diff --git a/i18n/es/code.json b/i18n/es/code.json index 6045ec93a17..e43abd1ff07 100644 --- a/i18n/es/code.json +++ b/i18n/es/code.json @@ -98,5 +98,30 @@ "simulator.totalTime": {"message": "Tiempo total:"}, "simulator.heartbeatElapsed": {"message": "Tiempo desde el último latido: {elapsed} / plazo de 5s"}, "simulator.noHeartbeat": {"message": "❌ ¡Plazo vencido por falta de latidos!"}, - "simulator.heartbeatActive": {"message": "💓 Latido activo"} + "simulator.heartbeatActive": {"message": "💓 Latido activo"}, + "identity.documentation": {"message": "Documentación"}, + "identity.durableExecution": {"message": "Ejecución duradera"}, + "identity.heroTitle": {"message": "Trabajo que sigue adelante."}, + "identity.startEyebrow": {"message": "Tu próximo paso"}, + "identity.startTitle": {"message": "Empieza donde desarrollas."}, + "identity.embedded": {"message": "Integrado en tu aplicación"}, + "identity.sdkGuide": {"message": "Guía del SDK y referencia de la API"}, + "identity.runtimeNote": {"message": "Ejecuta tu propio Server o conéctate a Cloud administrado."}, + "identity.footerDescription": {"message": "Aloja Server o utiliza Cloud."}, + "identity.run": {"message": "Ejecución"}, + "identity.selfHosted": {"message": "Server autogestionado"}, + "identity.pricing": {"message": "Precios"}, + "identity.status": {"message": "Estado"}, + "identity.project": {"message": "Proyecto"}, + "identity.compatibility": {"message": "Compatibilidad de versiones"}, + "identity.terms": {"message": "Términos del servicio"}, + "identity.privacy": {"message": "Política de privacidad"}, + "identity.statusUnknown": {"message": "Estado desconocido"}, + "identity.statusOperational": {"message": "Todos los sistemas operativos"}, + "identity.statusDegraded": {"message": "Servicio degradado"}, + "identity.statusIncident": {"message": "Incidente del servicio"}, + "identity.statusMaintenance": {"message": "Mantenimiento"}, + "identity.statusStale": {"message": "Estado desactualizado"}, + "identity.statusUnverified": {"message": "Estado sin verificar"}, + "identity.statusUnavailable": {"message": "Estado no disponible"} } diff --git a/i18n/es/docusaurus-theme-classic/navbar.json b/i18n/es/docusaurus-theme-classic/navbar.json index e0f74322f4c..578261ff00a 100644 --- a/i18n/es/docusaurus-theme-classic/navbar.json +++ b/i18n/es/docusaurus-theme-classic/navbar.json @@ -2,5 +2,8 @@ "logo.alt": {"message": "Logotipo de Durable Workflow"}, "item.label.Docs": {"message": "Documentación"}, "item.label.Blog": {"message": "Blog (en inglés)"}, - "item.label.Star on GitHub": {"message": "Apoya el proyecto en GitHub"} + "item.label.Star on GitHub": {"message": "Apoya el proyecto en GitHub"}, + "item.label.Pricing": {"message": "Precios"}, + "item.label.Status": {"message": "Estado"}, + "item.label.Get started": {"message": "Empieza aquí"} } diff --git a/i18n/fr/code.json b/i18n/fr/code.json index 81fd8e82dee..2144d6b479f 100644 --- a/i18n/fr/code.json +++ b/i18n/fr/code.json @@ -301,5 +301,80 @@ }, "simulator.signalWaitReceived": { "message": "await() a reçu le signal" + }, + "identity.documentation": { + "message": "Documentation" + }, + "identity.durableExecution": { + "message": "Exécution durable" + }, + "identity.heroTitle": { + "message": "Un travail qui continue." + }, + "identity.startEyebrow": { + "message": "Votre prochaine étape" + }, + "identity.startTitle": { + "message": "Commencez là où vous développez." + }, + "identity.embedded": { + "message": "Intégré à votre application" + }, + "identity.sdkGuide": { + "message": "Guide du SDK et référence API" + }, + "identity.runtimeNote": { + "message": "Hébergez Server vous-même ou connectez-vous à Cloud géré." + }, + "identity.footerDescription": { + "message": "Hébergez Server ou utilisez Cloud." + }, + "identity.run": { + "message": "Exécution" + }, + "identity.selfHosted": { + "message": "Server auto-hébergé" + }, + "identity.pricing": { + "message": "Tarifs" + }, + "identity.status": { + "message": "État" + }, + "identity.project": { + "message": "Projet" + }, + "identity.compatibility": { + "message": "Compatibilité des versions" + }, + "identity.terms": { + "message": "Conditions d’utilisation" + }, + "identity.privacy": { + "message": "Politique de confidentialité" + }, + "identity.statusUnknown": { + "message": "État inconnu" + }, + "identity.statusOperational": { + "message": "Tous les systèmes sont opérationnels" + }, + "identity.statusDegraded": { + "message": "Service dégradé" + }, + "identity.statusIncident": { + "message": "Incident de service" + }, + "identity.statusMaintenance": { + "message": "Maintenance" + }, + "identity.statusStale": { + "message": "État obsolète" + }, + "identity.statusUnverified": { + "message": "État non vérifié" + }, + "identity.statusUnavailable": { + "message": "État indisponible" } } diff --git a/i18n/fr/docusaurus-theme-classic/navbar.json b/i18n/fr/docusaurus-theme-classic/navbar.json index 2c21824dde3..3fa1698286a 100644 --- a/i18n/fr/docusaurus-theme-classic/navbar.json +++ b/i18n/fr/docusaurus-theme-classic/navbar.json @@ -10,5 +10,14 @@ }, "item.label.Star on GitHub": { "message": "Ajouter une étoile sur GitHub" + }, + "item.label.Pricing": { + "message": "Tarifs" + }, + "item.label.Status": { + "message": "État" + }, + "item.label.Get started": { + "message": "Commencer" } } diff --git a/i18n/ja/code.json b/i18n/ja/code.json index 338d3166b50..7bbe582f283 100644 --- a/i18n/ja/code.json +++ b/i18n/ja/code.json @@ -99,5 +99,30 @@ "workerAuthority.historicalConformance": {"message": "保持されている過去の適合性の対応関係"}, "workerAuthority.historicalBindings": {"message": "プロトコル {protocols} について {marker} と記された対応関係"}, "workerAuthority.historicalRecords": {"message": "不変のリゾルバーとダイジェストの記録:"}, - "workerAuthority.manifest": {"message": "適合性マニフェスト"} + "workerAuthority.manifest": {"message": "適合性マニフェスト"}, + "identity.documentation": {"message": "ドキュメント"}, + "identity.durableExecution": {"message": "耐久性のある実行"}, + "identity.heroTitle": {"message": "止まらずに進むワークフロー。"}, + "identity.startEyebrow": {"message": "次のステップ"}, + "identity.startTitle": {"message": "使い慣れた環境から始める。"}, + "identity.embedded": {"message": "アプリケーションに組み込み"}, + "identity.sdkGuide": {"message": "SDK ガイドと API リファレンス"}, + "identity.runtimeNote": {"message": "Server をセルフホストするか、マネージド Cloud に接続します。"}, + "identity.footerDescription": {"message": "Server のセルフホスト、または Cloud での実行。"}, + "identity.run": {"message": "実行"}, + "identity.selfHosted": {"message": "セルフホストの Server"}, + "identity.pricing": {"message": "料金"}, + "identity.status": {"message": "ステータス"}, + "identity.project": {"message": "プロジェクト"}, + "identity.compatibility": {"message": "バージョン互換性"}, + "identity.terms": {"message": "利用規約"}, + "identity.privacy": {"message": "プライバシーポリシー"}, + "identity.statusUnknown": {"message": "ステータス不明"}, + "identity.statusOperational": {"message": "すべてのシステムが正常稼働中"}, + "identity.statusDegraded": {"message": "サービスの一部が低下"}, + "identity.statusIncident": {"message": "サービス障害"}, + "identity.statusMaintenance": {"message": "メンテナンス"}, + "identity.statusStale": {"message": "ステータス情報が古くなっています"}, + "identity.statusUnverified": {"message": "ステータス未確認"}, + "identity.statusUnavailable": {"message": "ステータスを取得できません"} } diff --git a/i18n/ja/docusaurus-theme-classic/navbar.json b/i18n/ja/docusaurus-theme-classic/navbar.json index 3d41db48356..654179de70c 100644 --- a/i18n/ja/docusaurus-theme-classic/navbar.json +++ b/i18n/ja/docusaurus-theme-classic/navbar.json @@ -2,5 +2,8 @@ "logo.alt": {"message": "Durable Workflow のロゴ"}, "item.label.Docs": {"message": "ドキュメント"}, "item.label.Blog": {"message": "ブログ(英語)"}, - "item.label.Star on GitHub": {"message": "GitHub でスターを付ける"} + "item.label.Star on GitHub": {"message": "GitHub でスターを付ける"}, + "item.label.Pricing": {"message": "料金"}, + "item.label.Status": {"message": "ステータス"}, + "item.label.Get started": {"message": "はじめる"} } diff --git a/i18n/pt-BR/code.json b/i18n/pt-BR/code.json index 81f4cb0e9cf..208f33e198f 100644 --- a/i18n/pt-BR/code.json +++ b/i18n/pt-BR/code.json @@ -98,5 +98,30 @@ "workerAuthority.manifest": {"message": "manifesto de conformidade"}, "simulator.queryAction": {"message": "🔍 Consultar:"}, "simulator.signalAccepted": {"message": "Sinal {signal} aceito"}, - "simulator.signalWaitReceived": {"message": "sinal de await() recebido"} + "simulator.signalWaitReceived": {"message": "sinal de await() recebido"}, + "identity.documentation": {"message": "Documentação"}, + "identity.durableExecution": {"message": "Execução durável"}, + "identity.heroTitle": {"message": "Trabalho que continua."}, + "identity.startEyebrow": {"message": "Seu próximo passo"}, + "identity.startTitle": {"message": "Comece onde você desenvolve."}, + "identity.embedded": {"message": "Integrado ao seu aplicativo"}, + "identity.sdkGuide": {"message": "Guia do SDK e referência da API"}, + "identity.runtimeNote": {"message": "Hospede o Server ou conecte-se ao Cloud gerenciado."}, + "identity.footerDescription": {"message": "Hospede o Server ou use o Cloud."}, + "identity.run": {"message": "Execução"}, + "identity.selfHosted": {"message": "Server auto-hospedado"}, + "identity.pricing": {"message": "Preços"}, + "identity.status": {"message": "Status"}, + "identity.project": {"message": "Projeto"}, + "identity.compatibility": {"message": "Compatibilidade de versões"}, + "identity.terms": {"message": "Termos de Serviço"}, + "identity.privacy": {"message": "Política de Privacidade"}, + "identity.statusUnknown": {"message": "Status desconhecido"}, + "identity.statusOperational": {"message": "Todos os sistemas operacionais"}, + "identity.statusDegraded": {"message": "Serviço degradado"}, + "identity.statusIncident": {"message": "Incidente no serviço"}, + "identity.statusMaintenance": {"message": "Manutenção"}, + "identity.statusStale": {"message": "Status desatualizado"}, + "identity.statusUnverified": {"message": "Status não verificado"}, + "identity.statusUnavailable": {"message": "Status indisponível"} } diff --git a/i18n/pt-BR/docusaurus-theme-classic/navbar.json b/i18n/pt-BR/docusaurus-theme-classic/navbar.json index d28ce818671..3b231428a5e 100644 --- a/i18n/pt-BR/docusaurus-theme-classic/navbar.json +++ b/i18n/pt-BR/docusaurus-theme-classic/navbar.json @@ -2,5 +2,8 @@ "logo.alt": {"message": "Logotipo do Durable Workflow"}, "item.label.Docs": {"message": "Documentação"}, "item.label.Blog": {"message": "Blog (em inglês)"}, - "item.label.Star on GitHub": {"message": "Apoie o projeto no GitHub"} + "item.label.Star on GitHub": {"message": "Apoie o projeto no GitHub"}, + "item.label.Pricing": {"message": "Preços"}, + "item.label.Status": {"message": "Status"}, + "item.label.Get started": {"message": "Comece aqui"} } diff --git a/i18n/uk/code.json b/i18n/uk/code.json index f0cfb105c87..c945c8f2a11 100644 --- a/i18n/uk/code.json +++ b/i18n/uk/code.json @@ -630,5 +630,80 @@ }, "workerAuthority.manifest": { "message": "маніфесті відповідності" + }, + "identity.documentation": { + "message": "Документація" + }, + "identity.durableExecution": { + "message": "Надійне виконання" + }, + "identity.heroTitle": { + "message": "Робота, що триває." + }, + "identity.startEyebrow": { + "message": "Ваш наступний крок" + }, + "identity.startTitle": { + "message": "Починайте там, де розробляєте." + }, + "identity.embedded": { + "message": "Вбудовано у ваш застосунок" + }, + "identity.sdkGuide": { + "message": "Посібник SDK та довідник API" + }, + "identity.runtimeNote": { + "message": "Запускайте власний Server або підключайтеся до керованого Cloud." + }, + "identity.footerDescription": { + "message": "Розгортайте власний Server або використовуйте Cloud." + }, + "identity.run": { + "message": "Запуск" + }, + "identity.selfHosted": { + "message": "Власний Server" + }, + "identity.pricing": { + "message": "Ціни" + }, + "identity.status": { + "message": "Стан" + }, + "identity.project": { + "message": "Проєкт" + }, + "identity.compatibility": { + "message": "Сумісність версій" + }, + "identity.terms": { + "message": "Умови використання" + }, + "identity.privacy": { + "message": "Політика конфіденційності" + }, + "identity.statusUnknown": { + "message": "Стан невідомий" + }, + "identity.statusOperational": { + "message": "Усі системи працюють" + }, + "identity.statusDegraded": { + "message": "Сервіс працює з обмеженнями" + }, + "identity.statusIncident": { + "message": "Інцидент сервісу" + }, + "identity.statusMaintenance": { + "message": "Технічне обслуговування" + }, + "identity.statusStale": { + "message": "Дані про стан застаріли" + }, + "identity.statusUnverified": { + "message": "Стан не підтверджено" + }, + "identity.statusUnavailable": { + "message": "Стан недоступний" } } diff --git a/i18n/uk/docusaurus-theme-classic/navbar.json b/i18n/uk/docusaurus-theme-classic/navbar.json index 4c472be6191..72e7881fd97 100644 --- a/i18n/uk/docusaurus-theme-classic/navbar.json +++ b/i18n/uk/docusaurus-theme-classic/navbar.json @@ -18,5 +18,14 @@ "item.label.Star on GitHub": { "message": "Підтримати зіркою на GitHub", "description": "Navbar item with label Star on GitHub" + }, + "item.label.Pricing": { + "message": "Ціни" + }, + "item.label.Status": { + "message": "Стан" + }, + "item.label.Get started": { + "message": "Почати" } } diff --git a/i18n/zh-Hans/code.json b/i18n/zh-Hans/code.json index 6285d185ced..63b4efe8a82 100644 --- a/i18n/zh-Hans/code.json +++ b/i18n/zh-Hans/code.json @@ -99,5 +99,30 @@ "cliInstall.verification": {"message": "安装程序在写入二进制文件前验证发布版本的 SHA256SUMS 清单。"}, "cliInstall.directDownload": {"message": "或直接下载二进制文件"}, "cliInstall.fromRelease": {"message": "来自"}, - "cliInstall.stableRelease": {"message": "稳定发布版本"} + "cliInstall.stableRelease": {"message": "稳定发布版本"}, + "identity.documentation": {"message": "文档"}, + "identity.durableExecution": {"message": "持久化执行"}, + "identity.heroTitle": {"message": "持续推进的工作流。"}, + "identity.startEyebrow": {"message": "下一步"}, + "identity.startTitle": {"message": "从熟悉的环境开始。"}, + "identity.embedded": {"message": "嵌入您的应用"}, + "identity.sdkGuide": {"message": "SDK 指南与 API 参考"}, + "identity.runtimeNote": {"message": "自行托管 Server,或连接托管式 Cloud。"}, + "identity.footerDescription": {"message": "自行托管 Server,或在 Cloud 上运行。"}, + "identity.run": {"message": "运行"}, + "identity.selfHosted": {"message": "自托管 Server"}, + "identity.pricing": {"message": "价格"}, + "identity.status": {"message": "状态"}, + "identity.project": {"message": "项目"}, + "identity.compatibility": {"message": "版本兼容性"}, + "identity.terms": {"message": "服务条款"}, + "identity.privacy": {"message": "隐私政策"}, + "identity.statusUnknown": {"message": "状态未知"}, + "identity.statusOperational": {"message": "所有系统运行正常"}, + "identity.statusDegraded": {"message": "服务降级"}, + "identity.statusIncident": {"message": "服务故障"}, + "identity.statusMaintenance": {"message": "维护中"}, + "identity.statusStale": {"message": "状态信息已过期"}, + "identity.statusUnverified": {"message": "状态未经验证"}, + "identity.statusUnavailable": {"message": "无法获取状态"} } diff --git a/i18n/zh-Hans/docusaurus-theme-classic/navbar.json b/i18n/zh-Hans/docusaurus-theme-classic/navbar.json index fdb8d206388..17449592425 100644 --- a/i18n/zh-Hans/docusaurus-theme-classic/navbar.json +++ b/i18n/zh-Hans/docusaurus-theme-classic/navbar.json @@ -2,5 +2,8 @@ "logo.alt": {"message": "Durable Workflow 标志"}, "item.label.Docs": {"message": "文档"}, "item.label.Blog": {"message": "博客(英文)"}, - "item.label.Star on GitHub": {"message": "在 GitHub 上加星"} + "item.label.Star on GitHub": {"message": "在 GitHub 上加星"}, + "item.label.Pricing": {"message": "价格"}, + "item.label.Status": {"message": "状态"}, + "item.label.Get started": {"message": "开始使用"} } diff --git a/package.json b/package.json index d8f8511e8b2..7923571ed28 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "postinstall": "patch-package", "prestart": "node scripts/check-dependency-security.js", "start": "docusaurus start", - "build": "npm run check:dependency-security && npm run check:node-toolchain && node scripts/check-helm-chart-pairing.test.js && node scripts/check-install-sdk.test.js && node scripts/check-installer-source.test.js && node scripts/check-server-config-reference.test.js && node scripts/check-server-config-reference.js && node scripts/check-local-search.test.js && node scripts/check-doc-translation-updates.test.js && node scripts/check-doc-translations.js && node scripts/check-localized-links.test.js && npm run test:browser-security-headers && docusaurus build && node scripts/generate-llms.js && node scripts/generate-llms-full.js", + "build": "npm run check:dependency-security && npm run check:node-toolchain && npm run test:identity && node scripts/check-helm-chart-pairing.test.js && node scripts/check-install-sdk.test.js && node scripts/check-installer-source.test.js && node scripts/check-server-config-reference.test.js && node scripts/check-server-config-reference.js && node scripts/check-local-search.test.js && node scripts/check-doc-translation-updates.test.js && node scripts/check-doc-translations.js && node scripts/check-localized-links.test.js && npm run test:browser-security-headers && docusaurus build && node scripts/generate-llms.js && node scripts/generate-llms-full.js", "check:node-toolchain": "node scripts/check-node-toolchain.js && node scripts/check-node-toolchain.test.js", "check:dependency-security": "node scripts/check-dependency-security.js && node scripts/check-dependency-security.test.js && node scripts/check-open-browser.test.js", "check:platform-protocol-specs": "node scripts/check-platform-protocol-specs.js", @@ -29,7 +29,8 @@ "serve": "docusaurus serve", "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", - "test:browser-security-headers": "node --test --test-concurrency=2 scripts/browser-security-headers.test.js security/browser-headers/rust-browser-source.test.cjs" + "test:browser-security-headers": "node --test --test-concurrency=2 scripts/browser-security-headers.test.js security/browser-headers/rust-browser-source.test.cjs", + "test:identity": "node --test --test-concurrency=2 scripts/identity-status.test.mjs" }, "dependencies": { "@docusaurus/core": "3.10.2", diff --git a/scripts/identity-status.test.mjs b/scripts/identity-status.test.mjs new file mode 100644 index 00000000000..568f3487efe --- /dev/null +++ b/scripts/identity-status.test.mjs @@ -0,0 +1,128 @@ +import assert from 'node:assert/strict'; +import {test} from 'node:test'; +import {publicStatus, fetchPublicStatus, watchPublicStatus, STATUS_API} from '../static/identity/status.mjs'; + +const now = Date.parse('2026-11-05T12:00:30Z'); +function snapshot() { + return { + schema: 'durable_workflow.status_page_snapshot.v1', product: 'cloud', environment: 'prod', + checked_at: '2026-11-05T12:00:00Z', generated_at: '2026-11-05T12:00:05Z', + status: 'ok', alert_level: 'ok', customer_visible: false, + freshness: {status: 'fresh', age_seconds: 30, stale_after_seconds: 900}, + components: [], customer_impact: {status: 'ok', alert_level: 'ok', affected_families: []}, + }; +} +const state = s => publicStatus(s, now).state; +test('fresh public aggregate is healthy with no customer-visible incident rows', () => { + assert.equal(state(snapshot()), 'operational'); + assert.equal(publicStatus(snapshot(), now).validUntil, Date.parse('2026-11-05T12:15:00Z')); +}); +test('public impact cannot be hidden by healthy top-level fields', () => { + for (const [status, level, expected] of [['failed','critical','incident'],['warning','warning','degraded'],['unknown','unknown','unknown']]) { + const s = snapshot(); s.customer_impact.status = status; s.customer_impact.alert_level = level; + assert.equal(state(s), expected); + } +}); +test('severity tokens and maintenance-only affected family follow producer precedence', () => { + const s = snapshot(); s.status = 'warning'; s.alert_level = 'warning'; + assert.equal(state(s), 'degraded'); + s.customer_impact.affected_families = ['maintenance']; + assert.equal(state(s), 'maintenance'); + s.customer_impact.affected_families.push('managed_runtime'); + assert.equal(state(s), 'degraded'); + s.alert_level = 'critical'; assert.equal(state(s), 'incident'); +}); +test('public failure reasons retain unavailable, unverified and stale distinctions', () => { + for (const [reason, expected] of [['cloud_unreachable','unavailable'],['monitor_stale','unverified'],['probe_unverified','unverified'],['publisher_stale','stale'],['new_reason','unknown']]) { + const s = snapshot(); s.meta = {public_status_reason: reason}; assert.equal(state(s), expected); + } +}); +test('freshness is recomputed from checked time, including exact cutoff', () => { + const s = snapshot(); + assert.equal(publicStatus(s, Date.parse('2026-11-05T12:14:59Z')).state, 'operational'); + assert.equal(publicStatus(s, Date.parse('2026-11-05T12:15:00Z')).state, 'stale'); + s.freshness.age_seconds = 900; assert.equal(state(s), 'stale'); + s.freshness.age_seconds = 30; s.freshness.status = 'stale'; assert.equal(state(s), 'stale'); +}); +test('future, malformed, invalid and out-of-order evidence cannot show healthy', () => { + const mutations = [ + s => s.checked_at = '2026-11-05T12:01:00Z', + s => s.generated_at = '2026-11-05T12:01:00Z', + s => s.generated_at = '2026-11-05T11:59:00Z', + s => s.checked_at = '2026-99-99T12:00:00Z', + s => s.checked_at = '2026-11-05', + s => s.freshness.age_seconds = -1, + s => s.freshness.stale_after_seconds = 86400, + s => s.freshness.status = 'unrecognized', + s => s.schema = 'unknown', s => s.product = 'server', s => s.environment = 'test', + s => s.customer_impact = undefined, s => s.status = 'healthy', s => s.alert_level = 'healthy', + ]; + for (const mutate of mutations) {const s = snapshot(); mutate(s); assert.equal(state(s), 'unknown');} +}); +test('private/internal rows are ignored and never returned', () => { + const s = snapshot(); s.components = [{status: 'failed', message: 'private'}]; s.operator_alerts = ['private']; + const result = publicStatus(s, now); + assert.deepEqual(Object.keys(result).sort(), ['state','validUntil']); + assert.equal(result.state, 'operational'); +}); +test('anonymous feed uses exact public GET contract', async () => { + let request; + const result = await fetchPublicStatus({now: () => now, fetchImpl: async (url, options) => { + request = {url, options}; return {ok: true, text: async () => JSON.stringify(snapshot())}; + }}); + assert.equal(result.state, 'operational'); assert.equal(request.url, STATUS_API); + for (const [k,v] of Object.entries({method:'GET',mode:'cors',credentials:'omit',cache:'no-store',redirect:'error',referrerPolicy:'no-referrer'})) assert.equal(request.options[k], v); +}); +test('HTTP, oversized and malformed responses refuse', async () => { + for (const response of [{ok:false},{ok:true,text:async()=> 'x'.repeat(65537)},{ok:true,text:async()=> '{'}]) { + await assert.rejects(fetchPublicStatus({fetchImpl: async () => response})); + } +}); +test('network failure publishes unknown and unmount aborts pending work', async () => { + const values = []; let signal; + const stop = watchPublicStatus(value => values.push(value), {fetchImpl: async (_, options) => {signal = options.signal; throw new Error('offline');}}); + await new Promise(resolve => setImmediate(resolve)); + assert.deepEqual(values, ['unknown']); stop(); assert.equal(signal.aborted, true); + const pending = []; + const stopPending = watchPublicStatus(value => pending.push(value), {fetchImpl: (_, options) => new Promise((_, reject) => options.signal.addEventListener('abort', () => reject(new Error('aborted'))))}); + stopPending(); await new Promise(resolve => setImmediate(resolve)); + assert.deepEqual(pending, []); +}); +test('a successful result expires while a later request is still pending', async t => { + t.mock.timers.enable({apis: ['setTimeout', 'Date'], now}); + const values = []; let requests = 0; + const s = snapshot(); s.freshness.stale_after_seconds = 90; + const stop = watchPublicStatus(value => values.push(value), {fetchImpl: async (_, options) => { + requests++; + if (requests === 1) return {ok:true, text:async()=>JSON.stringify(s)}; + return new Promise((_, reject) => options.signal.addEventListener('abort', () => reject(new Error('timeout')))); + }}); + await new Promise(resolve => setImmediate(resolve)); + assert.deepEqual(values, ['operational']); + t.mock.timers.tick(60000); + assert.equal(values.at(-1), 'stale'); + assert.equal(requests, 2); + t.mock.timers.tick(5000); + await new Promise(resolve => setImmediate(resolve)); + assert.equal(values.at(-1), 'unknown'); + stop(); +}); +test('calendar overflow and 24-hour normalization cannot create fresh evidence', () => { + for (const [bad, normalized] of [['2026-02-30T12:00:00Z','2026-03-02T12:00:00Z'],['2026-11-04T24:00:00Z','2026-11-05T00:00:00Z']]) { + const s = snapshot(); const normalizedMs = Date.parse(normalized); + s.checked_at = bad; s.generated_at = new Date(normalizedMs + 5000).toISOString(); + assert.equal(publicStatus(s, normalizedMs + 30000).state, 'unknown'); + } +}); +test('valid offsets and subsecond timestamps preserve the same checked instant', () => { + const s = snapshot(); s.checked_at = '2026-11-05T14:00:00.000000+02:00'; s.generated_at = '2026-11-05T07:00:05-05:00'; + assert.equal(state(s), 'operational'); + assert.equal(publicStatus(s, now).validUntil, Date.parse('2026-11-05T12:15:00Z')); +}); +test('Z, positive-zero and unknown-local-offset forms identify the same UTC instant', () => { + for (const offset of ['Z','+00:00','-00:00']) { + const s = snapshot(); s.checked_at = '2026-11-05T12:00:00'+offset; + assert.equal(state(s), 'operational'); + assert.equal(publicStatus(s, now).validUntil, Date.parse('2026-11-05T12:15:00Z')); + } +}); diff --git a/src/clientModules/mobileNavigationReachability.js b/src/clientModules/mobileNavigationReachability.js index 0ec7678a072..38355e2161f 100644 --- a/src/clientModules/mobileNavigationReachability.js +++ b/src/clientModules/mobileNavigationReachability.js @@ -4,7 +4,7 @@ let observer; function syncNavigationIsolation() { const drawerOpen = Boolean(document.querySelector('.navbar-sidebar--show .navbar-sidebar')); const backgroundSurfaces = document.querySelectorAll( - '.navbar__inner, main, footer', + '.navbar__inner, .dw-docs-toolbar, .theme-announcement-bar, a[class^="skipToContent"], main, footer', ); for (const surface of backgroundSurfaces) { @@ -20,12 +20,22 @@ function syncNavigationIsolation() { } } +function closeNavigationOnEscape(event) { + if (event.key !== 'Escape' || event.defaultPrevented) return; + const closeButton = document.querySelector('.navbar-sidebar--show .navbar-sidebar__close'); + if (!closeButton) return; + event.preventDefault(); + closeButton.click(); + requestAnimationFrame(() => document.querySelector('.navbar__toggle')?.focus()); +} + function observeNavigationDrawer() { if (observer || !document.body) { return; } syncNavigationIsolation(); + document.addEventListener('keydown', closeNavigationOnEscape); observer = new MutationObserver(syncNavigationIsolation); observer.observe(document.body, { attributes: true, diff --git a/src/components/HomepageFeatures/index.js b/src/components/HomepageFeatures/index.js index 122c779dda9..f37b20444fd 100644 --- a/src/components/HomepageFeatures/index.js +++ b/src/components/HomepageFeatures/index.js @@ -1,12 +1,11 @@ import React from 'react'; -import clsx from 'clsx'; import Translate from '@docusaurus/Translate'; import styles from './styles.module.css'; const FeatureList = [ { + id: 'languages', title: Build in PHP, Python, or Rust, - Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, description: ( Author clients, workflows, and activities with first-party SDKs that @@ -15,8 +14,8 @@ const FeatureList = [ ), }, { + id: 'runtime', title: Choose who operates the runtime, - Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default, description: ( Use Durable Workflow Cloud, run the standalone Server, or keep the @@ -25,8 +24,8 @@ const FeatureList = [ ), }, { + id: 'recovery', title: Recover through failures, - Svg: require('@site/static/img/undraw_docusaurus_react.svg').default, description: ( Durable history, deterministic replay, timers, retries, signals, @@ -36,17 +35,13 @@ const FeatureList = [ }, ]; -function Feature({Svg, title, description}) { +function Feature({title, description, index}) { return ( -
-
- -
-
-

{title}

-

{description}

-
-
+
+

0{index + 1}

+

{title}

+

{description}

+
); } @@ -56,9 +51,9 @@ export default function HomepageFeatures() { className={styles.features} data-homepage-release="stable-2.0">
-
+
{FeatureList.map((props, idx) => ( - + ))}
diff --git a/src/components/HomepageFeatures/styles.module.css b/src/components/HomepageFeatures/styles.module.css index b248eb2e5de..25b9bb45a2c 100644 --- a/src/components/HomepageFeatures/styles.module.css +++ b/src/components/HomepageFeatures/styles.module.css @@ -1,11 +1,6 @@ -.features { - display: flex; - align-items: center; - padding: 2rem 0; - width: 100%; -} - -.featureSvg { - height: 200px; - width: 200px; -} +.features { padding: 2rem 0; width: 100%; } +.grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.5rem; } +.feature { background: var(--dw-surface); padding: 2rem; border: 1px solid var(--dw-line); border-radius: 8px; } +.feature h3 { font-size: 1.25rem; line-height: 1.3; margin: 1rem 0; } +.feature p:last-child { color: var(--dw-ink-2); margin-bottom: 0; } +@media (max-width: 800px) { .grid { grid-template-columns: 1fr; } .feature { padding: 1.5rem; } } diff --git a/src/components/ProductPromotion/styles.module.css b/src/components/ProductPromotion/styles.module.css index 3fb24ae0ceb..6199e49b9d6 100644 --- a/src/components/ProductPromotion/styles.module.css +++ b/src/components/ProductPromotion/styles.module.css @@ -7,10 +7,8 @@ margin: 2rem auto; padding: 1.25rem 1.5rem; border: 1px solid var(--ifm-color-emphasis-300); - border-left: 3px solid var(--ifm-color-primary); border-radius: 0.5rem; - background: var(--ifm-background-surface-color); - box-shadow: 0 0.35rem 1.25rem rgb(0 0 0 / 8%); + background: var(--dw-surface); } .copy { @@ -40,8 +38,9 @@ margin: 0; color: var(--ifm-color-primary); font-size: 0.75rem; - font-weight: 700; - letter-spacing: 0; + font-family: var(--dw-font-mono); + font-weight: 500; + letter-spacing: 0.08em; text-transform: uppercase; } diff --git a/src/components/StatusPill/index.js b/src/components/StatusPill/index.js new file mode 100644 index 00000000000..1dbf8e61de3 --- /dev/null +++ b/src/components/StatusPill/index.js @@ -0,0 +1,20 @@ +import React, {useEffect, useState} from 'react'; +import {translate} from '@docusaurus/Translate'; +import {watchPublicStatus} from '@site/static/identity/status.mjs'; +import links from '@site/static/identity/links.json'; + +export default function StatusPill() { + const [state, setState] = useState('unknown'); + useEffect(() => watchPublicStatus(setState), []); + const labels = { + operational: translate({id: 'identity.statusOperational', message: 'All systems operational'}), + degraded: translate({id: 'identity.statusDegraded', message: 'Service degraded'}), + incident: translate({id: 'identity.statusIncident', message: 'Service incident'}), + maintenance: translate({id: 'identity.statusMaintenance', message: 'Maintenance'}), + stale: translate({id: 'identity.statusStale', message: 'Status stale'}), + unverified: translate({id: 'identity.statusUnverified', message: 'Status unverified'}), + unavailable: translate({id: 'identity.statusUnavailable', message: 'Status unavailable'}), + unknown: translate({id: 'identity.statusUnknown', message: 'Status unknown'}), + }; + return ; +} diff --git a/src/css/custom.css b/src/css/custom.css index ba14a24ac92..3b816710ad2 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -1,3 +1,6 @@ +@import url('../../static/identity/tokens.css'); +@import url('../../static/identity/fonts.css'); + /** * Any CSS included here will be global. The classic template * bundles Infima by default. Infima is a CSS framework designed to @@ -717,3 +720,146 @@ a[class^='skipToContent']:focus { margin-right: 2.75rem; } } + +/* One Identity: bridge shared role tokens into the existing Docusaurus shell. */ +:root, html[data-theme='light'], html[data-theme='dark'] { + --ifm-color-primary: var(--dw-action); + --ifm-color-primary-dark: var(--dw-action); + --ifm-color-primary-darker: var(--dw-action); + --ifm-color-primary-darkest: var(--dw-action); + --ifm-color-primary-light: var(--dw-action); + --ifm-color-primary-lighter: var(--dw-action); + --ifm-color-primary-lightest: var(--dw-action); + --ifm-background-color: var(--dw-paper); + --ifm-background-surface-color: var(--dw-surface); + --ifm-font-color-base: var(--dw-ink-2); + --ifm-color-content: var(--dw-ink); + --ifm-color-content-secondary: var(--dw-muted); + --ifm-heading-color: var(--dw-ink); + --ifm-color-emphasis-200: var(--dw-line); + --ifm-color-emphasis-300: var(--dw-line); + --ifm-color-emphasis-600: var(--dw-muted); + --ifm-color-emphasis-700: var(--dw-ink-2); + --ifm-font-family-base: var(--dw-font-body); + --ifm-font-family-monospace: var(--dw-font-mono); + --ifm-heading-font-family: var(--dw-font-display); + --ifm-heading-font-weight: 600; + --ifm-font-weight-semibold: 600; + --ifm-line-height-base: 1.5; + --ifm-navbar-height: 7.75rem; + --ifm-navbar-background-color: var(--dw-surface); + --ifm-navbar-link-color: var(--dw-ink-2); + --ifm-navbar-shadow: none; + --ifm-navbar-search-input-background-color: var(--dw-paper); + --ifm-navbar-search-input-color: var(--dw-ink); + --ifm-navbar-search-input-placeholder-color: var(--dw-muted); + --ifm-menu-color: var(--dw-ink-2); + --ifm-menu-color-active: var(--dw-action); + --ifm-menu-color-background-active: var(--dw-tint); + --ifm-menu-color-background-hover: var(--dw-tint); + --ifm-code-background: var(--dw-tint); + --ifm-code-font-size: .875rem; + --ifm-global-radius: 8px; + --ifm-button-border-radius: 6px; + --ifm-table-border-color: var(--dw-line); + --docusaurus-highlighted-code-line-bg: #123b2a; +} +html, body { background: var(--dw-paper); } +h1, h2 { letter-spacing: -.03em; } +h3 { font-size: 1.25rem; } +.markdown h1 { font-size: clamp(2.25rem, 4vw, 3.5rem); line-height: 1.12; } +.markdown h2 { font-size: clamp(1.75rem, 3vw, 2.25rem); } +.markdown p, .markdown li { line-height: 1.65; } +.markdown pre, div[class*='codeBlockContainer'] { background: var(--dw-code-background); color: var(--dw-code-foreground); border-radius: 8px; } +pre code { color: var(--dw-code-foreground); background: transparent; font-size: .8125rem; } +div[class*='codeBlockTitle'] { border-color: #2e3934; color: var(--dw-code-muted); } +div[class*='codeBlockContainer'] button { color: #e8edea; background: #172019; border-color: #5e6964; } +.button { min-height: 44px; display: inline-flex; justify-content: center; align-items: center; gap: .4rem; font-size: .875rem; font-weight: 600; padding: .65rem 1.1rem; } +.button--primary, .button--primary:hover { color: var(--dw-on-action); background: var(--dw-action); border-color: var(--dw-action); } +.button.button--secondary, .button.button--secondary:hover { color: var(--dw-ink); background: var(--dw-surface); border-color: var(--dw-line); } +.button:hover { filter: brightness(.94); } +:where(a, button, input, select, summary):focus-visible { outline: 2px solid var(--dw-action); outline-offset: 4px; } +.dw-eyebrow { font: 500 .75rem/1.5 var(--dw-font-mono); letter-spacing: .08em; text-transform: uppercase; color: var(--dw-action); margin: 0; } +.navbar { display: block; padding: 0; border-bottom: 1px solid var(--dw-line); } +.dw-global-navigation { max-width: 1440px; height: 76px; margin: 0 auto; padding: 0 2.5rem; flex-wrap: nowrap; } +.navbar__brand { flex-shrink: 0; margin-right: 1.5rem; gap: .625rem; } +.dw-wordmark { font: 600 1.125rem/1.2 var(--dw-font-display); color: var(--dw-ink); } +.dw-surface-label { font: 500 .8125rem/1.5 var(--dw-font-mono); color: var(--dw-action); white-space: nowrap; } +.dw-surface-label span { color: var(--dw-muted); margin-right: .25rem; } +.dw-logo-chip { flex: 0 0 auto; display: inline-flex; padding: 4px; border-radius: 8px; } +[data-theme='dark'] .dw-logo-chip { background: var(--dw-logo-paper); } +.dw-logo-chip img { display: block; } +.navbar__link { font-size: .875rem; font-weight: 400; padding: .5rem .65rem; } +.navbar__link--active { color: var(--dw-ink); font-weight: 600; } +.dw-nav-cta { margin: 0 .75rem; } +.dw-docs-toolbar { height: 48px; border-top: 1px solid var(--dw-line); padding: 0 2.5rem; display: flex; align-items: center; gap: 1rem; background: var(--dw-paper); } +.dw-docs-toolbar-label { font: 500 .75rem var(--dw-font-mono); color: var(--dw-muted); } +.dw-docs-controls { display: flex; align-items: center; gap: .5rem; } +.dw-docs-controls .navbar__item { display: inline-flex; } +.dw-docs-search { margin-left: auto; min-width: 0; } +.dw-docs-search .navbar__search { margin-left: 0; } +.navbar__search-input { border: 1px solid var(--dw-line); border-radius: 6px; font-family: var(--dw-font-body); } +.dropdown__menu { background: var(--dw-surface); border: 1px solid var(--dw-line); box-shadow: none; } +.dw-sdk-switcher { position: relative; } +.dw-sdk-switcher > button { background: transparent; border: 0; font-family: inherit; cursor: pointer; color: var(--dw-ink-2); } +.dw-sdk-links { list-style: none; margin: 0; padding: .5rem; position: absolute; top: 100%; left: 0; min-width: 160px; background: var(--dw-surface); border: 1px solid var(--dw-line); border-radius: 8px; z-index: 10; } +.dw-sdk-links[hidden] { display: none; } +.dw-sdk-links a { display: flex; align-items: center; justify-content: space-between; min-height: 44px; padding: .5rem .75rem; color: var(--dw-ink); border-radius: 6px; } +.dw-sdk-links a:hover { background: var(--dw-tint); text-decoration: none; } +.dw-sdk-switcher-mobile .dw-sdk-links { position: static; margin: .5rem 0; } +.dw-sdk-switcher-mobile > button { width: 100%; } +.dw-footer { background: var(--dw-surface); border-top: 1px solid var(--dw-line); padding: 4rem 0 2rem; font-size: .875rem; color: var(--dw-ink-2); } +.dw-footer-grid { display: grid; grid-template-columns: 1.5fr 1fr 1fr 1fr; gap: 3rem; } +.dw-footer-brand { display: inline-flex; gap: .625rem; align-items: center; color: var(--dw-ink); font-family: var(--dw-font-display); font-weight: 600; margin-bottom: 1rem; } +.dw-footer-brand + p { max-width: 18rem; } +.dw-footer h2 { font: 500 .75rem var(--dw-font-mono); text-transform: uppercase; letter-spacing: .08em; color: var(--dw-muted); margin-bottom: 1rem; } +.dw-footer ul { padding: 0; list-style: none; } +.dw-footer li { margin: .4rem 0; } +.dw-footer li a { color: var(--dw-ink-2); display: inline-block; padding: .15rem 0; } +.dw-footer a:hover { color: var(--dw-action); } +.dw-footer-bottom { border-top: 1px solid var(--dw-line); padding-top: 1.5rem; margin-top: 2rem; display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 1rem; color: var(--dw-muted); font-size: .8125rem; } +.dw-status-pill { display: inline-flex; align-items: center; gap: .5rem; min-height: 36px; border-radius: 999px; padding: .4rem .85rem; color: var(--dw-muted); background: var(--dw-paper); font: 500 .75rem/1.5 var(--dw-font-mono); } +.dw-status-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; } +@media (max-width: 1200px) and (min-width: 997px) { + .dw-global-navigation { padding: 0 1rem; } + .navbar__brand { margin-right: .5rem; } + .dw-wordmark { font-size: 1rem; } + .navbar__link { padding: .5rem; } +} +@media (max-width: 996px) { + :root, html[data-theme='dark'], html[data-theme='light'] { --ifm-navbar-height: 7.25rem; } + .dw-global-navigation { height: 64px; padding: 0 1rem; } + .dw-global-navigation .navbar__items--right { flex: 0 0 auto; } + .dw-docs-toolbar { height: 52px; padding: 0 1rem; gap: .5rem; } + .dw-docs-toolbar-label { display: none; } + .dw-docs-controls a[href*='/blog'] { display: none; } + .dw-docs-controls .navbar__link { font-size: .8125rem; padding: .5rem; } + .navbar-sidebar__brand { height: 64px; padding: .5rem; } + .navbar-sidebar__items { height: calc(100% - 65px); } + .dw-footer-grid { gap: 2rem; grid-template-columns: repeat(2, minmax(0, 1fr)); } +} +@media (max-width: 520px) { + .dw-wordmark { max-width: 9rem; font-size: 1rem; } + .dw-surface-label { font-size: .75rem; } + .navbar__brand { gap: .35rem; margin-right: .25rem; } + .dw-docs-search .navbar__search-input { width: 8rem; padding-right: .5rem; } + .dw-docs-search kbd, .dw-docs-search [class*='searchHintContainer'] { display: none; } + .dw-docs-search .navbar__search { position: relative; right: auto; } + .dw-footer { padding: 2.5rem 0 2rem; } + .dw-footer-grid { gap: 1.5rem; } + .dw-footer-grid > :first-child { grid-column: 1 / -1; } + .dw-footer .dw-footer-brand + p { max-width: 100%; } +} +.dw-status-pill[data-status='operational'] { color: var(--dw-action); background: var(--dw-tint); } +.dw-status-pill[data-status='degraded'] { color: #7a4405; background: #fcefd9; } +.dw-status-pill[data-status='incident'], .dw-status-pill[data-status='unavailable'] { color: #8e1b12; background: #fbe4e1; } +.dw-status-pill[data-status='maintenance'] { color: #2f3e78; background: #e6eaf5; } +[data-theme='dark'] .dw-status-pill[data-status='degraded'] { color: #f5c575; background: #352b19; } +[data-theme='dark'] .dw-status-pill[data-status='incident'], [data-theme='dark'] .dw-status-pill[data-status='unavailable'] { color: #ffb4ab; background: #3b2423; } +[data-theme='dark'] .dw-status-pill[data-status='maintenance'] { color: #becbfa; background: #232c43; } +@media (max-width: 360px) { + .dw-global-navigation, .dw-docs-toolbar { padding-left: .75rem; padding-right: .75rem; } + .dw-docs-search .navbar__search-input { width: 7rem; } +} +/* Global product navigation uses the same plain labels as the SDK shells. */ +.dw-global-navigation .navbar__link svg[class*='iconExternalLink'] { display: none; } diff --git a/src/pages/index.js b/src/pages/index.js index c39fd22875e..278441afd74 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -1,42 +1,43 @@ import React from 'react'; -import clsx from 'clsx'; import Head from '@docusaurus/Head'; import Link from '@docusaurus/Link'; import Translate, {translate} from '@docusaurus/Translate'; -import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import Layout from '@theme/Layout'; import HomepageFeatures from '@site/src/components/HomepageFeatures'; import ProductPromotion from '@site/src/components/ProductPromotion'; - +import identityLinks from '@site/static/identity/links.json'; import styles from './index.module.css'; -const homepageTitle = 'Durable Workflow 2.0'; function HomepageHeader() { - const {siteConfig} = useDocusaurusContext(); return ( -
-
-

{siteConfig.title}

-

- Durable execution for PHP, Python, and Rust. -

-
- - Get started - - - Choose a deployment mode +
+
+
+

Durable execution

+

Work that keeps going.

+

Durable execution for PHP, Python, and Rust.

+

A language-neutral durable execution platform for PHP, Python, and Rust, available as managed Cloud, self-hosted Server, or an embedded Laravel runtime.

+
+ + Get started + + + Choose a deployment mode + +
+
+
+

Your next step

+

Start where you build.

+ + LaravelEmbedded in your application + {['python', 'rust', 'php'].map(language => ( + + {language === 'php' ? 'PHP' : language[0].toUpperCase() + language.slice(1)}SDK guide and API reference + + ))} +

Run the Server yourself or connect to managed Cloud.

@@ -44,52 +45,19 @@ function HomepageHeader() { } export default function Home() { - const homepageDescription = translate({ - id: 'homepage.description', - message: 'A language-neutral durable execution platform for PHP, Python, and Rust, available as managed Cloud, self-hosted Server, or an embedded Laravel runtime.', - }); + const description = translate({id: 'homepage.description', message: 'A language-neutral durable execution platform for PHP, Python, and Rust, available as managed Cloud, self-hosted Server, or an embedded Laravel runtime.'}); return ( - - - {homepageTitle} - - - - - - - + +
-
+ + +
-

- - Run it managed: Cloud Dev, $0.075/hour, capped at $50/month, no per-workflow charges. - -

-

- - USD per runtime, metered by the minute while provisioned, with a $2 minimum per active month and 5 GB included storage. Dev has no uptime SLA. - -

+

Run it managed: Cloud Dev, $0.075/hour, capped at $50/month, no per-workflow charges.

+

USD per runtime, metered by the minute while provisioned, with a $2 minimum per active month and 5 GB included storage. Dev has no uptime SLA.

-
); diff --git a/src/pages/index.module.css b/src/pages/index.module.css index 2d6373a13a1..05efec09ff4 100644 --- a/src/pages/index.module.css +++ b/src/pages/index.module.css @@ -1,32 +1,25 @@ -/** - * CSS files with the .module.css suffix will be treated as CSS modules - * and scoped locally. - */ - -.heroBanner { - padding: 4rem 0; - text-align: center; - position: relative; - overflow: hidden; +.heroBanner { padding: 6rem 0 5rem; } +.heroGrid { display: grid; grid-template-columns: 1.3fr 1fr; gap: 5rem; align-items: center; } +.heroBanner h1 { max-width: 12ch; font-size: clamp(3.5rem, 5.2vw, 4.5rem); line-height: 1.05; letter-spacing: -.03em; margin: 1.25rem 0; } +.lede { font-size: 1.25rem; color: var(--dw-ink); margin-bottom: 1rem; } +.description { max-width: 36rem; color: var(--dw-ink-2); } +.buttons { display: flex; flex-wrap: wrap; gap: .75rem; margin-top: 2rem; } +.startCard { min-width: 0; padding: 2rem; background: var(--dw-surface); border: 1px solid var(--dw-line); border-radius: 8px; } +.startCard h2 { font-size: 1.75rem; margin: .75rem 0 1.5rem; } +.startLink { display: flex; justify-content: space-between; align-items: center; gap: 1rem; min-height: 76px; padding: 1rem 0; border-top: 1px solid var(--dw-line); color: var(--dw-ink); } +.startLink:hover { text-decoration: none; color: var(--dw-action); } +.startLink strong { font-weight: 600; } +.startLink small { display: block; color: var(--dw-muted); margin-top: .25rem; font-size: .8125rem; } +.cardNote { margin: 1rem 0 0; color: var(--dw-muted); font-size: .8125rem; } +.cloudSection { padding-top: 2rem; padding-bottom: 5rem; } +@media (max-width: 996px) { + .heroBanner { padding: 3.5rem 0; } + .heroGrid { gap: 2rem; grid-template-columns: 1.1fr 1fr; } } - -@media screen and (max-width: 996px) { - .heroBanner { - padding: 2rem; - } -} - -.buttons { - display: flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; - gap: 0.75rem; -} - -.supportingLine { - margin: 1rem auto 0; - max-width: 52rem; - font-size: 1.1rem; - line-height: 1.6; +@media (max-width: 700px) { + .heroGrid { grid-template-columns: 1fr; } + .heroBanner h1 { font-size: clamp(2.75rem, 11vw, 3.5rem); } + .startCard { padding: 1.5rem; } + .buttons { flex-direction: column; align-items: stretch; } + .cloudSection { padding-bottom: 3rem; } } diff --git a/src/theme/Footer/index.js b/src/theme/Footer/index.js index f3b25d524b4..f70f3a5e31c 100644 --- a/src/theme/Footer/index.js +++ b/src/theme/Footer/index.js @@ -1,34 +1,53 @@ -import React, { useEffect, useRef } from 'react'; -import Footer from '@theme-original/Footer'; -import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; +import React from 'react'; +import Link from '@docusaurus/Link'; +import Translate from '@docusaurus/Translate'; +import useBaseUrl from '@docusaurus/useBaseUrl'; +import {useLocation} from '@docusaurus/router'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import StatusPill from '@site/src/components/StatusPill'; +import links from '@site/static/identity/links.json'; -export default function FooterWrapper(props) { - const footerRef = useRef(null); +export default function Footer() { const {i18n: {currentLocale, defaultLocale}} = useDocusaurusContext(); - - useEffect(() => { - if (!ExecutionEnvironment.canUseDOM || !footerRef.current) { - return; - } - - // Canonical /llms-full.txt tracks the stable unversioned docs line. - // Only the explicit prerelease docs path should switch to the 2.0 bundle. - const pathname = window.location.pathname; - const docsPrefix = currentLocale === defaultLocale - ? '/docs/' - : `/${currentLocale}/docs/`; - const isV2Docs = pathname.startsWith(docsPrefix); - - const llmDocsLink = footerRef.current.querySelector('a[href*="llms-full.txt"]'); - if (llmDocsLink && isV2Docs) { - llmDocsLink.setAttribute('href', 'https://durable-workflow.com/llms-full-2.0.txt'); - } - }, [currentLocale, defaultLocale]); - + const {pathname} = useLocation(); + const prefix = currentLocale === defaultLocale ? '/docs/' : `/${currentLocale}/docs/`; + const currentDocs = pathname.startsWith(prefix) && !pathname.startsWith(`${prefix}1.x/`); + const llmDocs = currentDocs ? '/llms-full-2.0.txt' : '/llms-full.txt'; return ( -
-
-
+ ); } diff --git a/src/theme/Navbar/Content/index.js b/src/theme/Navbar/Content/index.js new file mode 100644 index 00000000000..65986c3aa01 --- /dev/null +++ b/src/theme/Navbar/Content/index.js @@ -0,0 +1,40 @@ +import React from 'react'; +import {useThemeConfig} from '@docusaurus/theme-common'; +import {useNavbarMobileSidebar} from '@docusaurus/theme-common/internal'; +import NavbarItem from '@theme/NavbarItem'; +import NavbarColorModeToggle from '@theme/Navbar/ColorModeToggle'; +import NavbarMobileSidebarToggle from '@theme/Navbar/MobileSidebar/Toggle'; +import NavbarLogo from '@theme/Navbar/Logo'; +import SearchBar from '@theme/SearchBar'; +import Translate from '@docusaurus/Translate'; + +// Keep Docusaurus' native sidebar, version/locale routing and search behavior. +// Only the composition separates product navigation from local docs controls. +export default function NavbarContent() { + const {navbar: {items}} = useThemeConfig(); + const mobileSidebar = useNavbarMobileSidebar(); + const contextual = item => item.className === 'dw-context-item'; + const global = items.filter(item => !contextual(item)); + return ( + <> +
+
+ {!mobileSidebar.disabled && } + + {global.filter(item => item.position !== 'right').map(item => )} +
+
+ {global.filter(item => item.position === 'right').map(item => )} + +
+
+
+ Documentation +
+ {items.filter(contextual).map(item => )} +
+
+
+ + ); +} diff --git a/src/theme/Navbar/Logo/index.js b/src/theme/Navbar/Logo/index.js new file mode 100644 index 00000000000..c4a231f5a26 --- /dev/null +++ b/src/theme/Navbar/Logo/index.js @@ -0,0 +1,13 @@ +import React from 'react'; +import Link from '@docusaurus/Link'; +import useBaseUrl from '@docusaurus/useBaseUrl'; + +export default function NavbarLogo() { + return ( + + + Durable Workflow + docs + + ); +} diff --git a/src/theme/NavbarItem/ComponentTypes.js b/src/theme/NavbarItem/ComponentTypes.js index d3524e6622f..c10801be41d 100644 --- a/src/theme/NavbarItem/ComponentTypes.js +++ b/src/theme/NavbarItem/ComponentTypes.js @@ -1,9 +1,11 @@ import ComponentTypes from '@theme-original/NavbarItem/ComponentTypes'; import GitHubStarNavbarItem from '@theme/GitHubStarNavbarItem'; +import SdkSwitcherNavbarItem from '@theme/SdkSwitcherNavbarItem'; const customComponentTypes = { ...ComponentTypes, 'custom-githubStar': GitHubStarNavbarItem, + 'custom-sdkSwitcher': SdkSwitcherNavbarItem, }; export default customComponentTypes; diff --git a/src/theme/SdkSwitcherNavbarItem/index.js b/src/theme/SdkSwitcherNavbarItem/index.js new file mode 100644 index 00000000000..716b74b03b0 --- /dev/null +++ b/src/theme/SdkSwitcherNavbarItem/index.js @@ -0,0 +1,44 @@ +import React, {useEffect, useRef, useState} from 'react'; +import Link from '@docusaurus/Link'; + +export default function SdkSwitcherNavbarItem({items, mobile, label = 'SDKs', onClick}) { + const [open, setOpen] = useState(false); + const root = useRef(null); + const trigger = useRef(null); + useEffect(() => { + const closeOutside = event => { + if (!root.current?.contains(event.target)) setOpen(false); + }; + document.addEventListener('pointerdown', closeOutside); + document.addEventListener('focusin', closeOutside); + return () => { + document.removeEventListener('pointerdown', closeOutside); + document.removeEventListener('focusin', closeOutside); + }; + }, []); + const handleKey = event => { + if (event.key === 'Escape' && open) { + event.preventDefault(); + event.stopPropagation(); + setOpen(false); + trigger.current?.focus(); + } + if (event.key === 'ArrowDown' && event.target === trigger.current) { + event.preventDefault(); + setOpen(true); + requestAnimationFrame(() => root.current?.querySelector('a')?.focus()); + } + }; + const content = ( +
+ + +
+ ); + return mobile ?
  • {content}
  • : content; +} diff --git a/static/identity/README.md b/static/identity/README.md new file mode 100644 index 00000000000..f930b34a9dc --- /dev/null +++ b/static/identity/README.md @@ -0,0 +1,111 @@ +# One Durable Workflow + +Canonical shared identity for the main documentation, PHP, Python, Rust, Cloud +and status sites. Implement these values in each site's existing components; +these exports do not require a shared package or a runtime dependency. + +Design authority: One Identity PDF SHA256 +`5a71d08636a1e2714e8dd4b6561ce0a619453c842de4de647cc3e345f7706e50` +and identity artboard SHA256 +`c992bf43b7c7492f181bc1b8045812dc2a6c85a33bbdbe0be64f714234825187`. +The viewer scripts and illustrative product data are not implementation inputs. + +## Assets and roles + +- [Original flowchart SVG](../img/logo.svg), 3,087 bytes, SHA256 + `24e3ef905304b6bbb255ca7855cb47d2e9712d7af82dcfbea15570851ee091f8`. + Preserve its proportions, colors and complete viewBox. No recoloring or filter. +- [Role tokens](tokens.css): Paper, Surface, Ink, Ink2, Muted, Line, Action, Tint. + Light is the default; dark is an explicit user choice. Persist that choice. +- Use Space Grotesk 600 headings, Inter 400/500/600 body and UI, JetBrains Mono + code and labels. Serve fonts locally, retain their licenses, use `font-display: + swap`, and keep system fallbacks for scripts outside the local font subset. +- H1 56–72px, H2 32–40px, H3 20px; display tracking -0.03em, scaled on mobile. + Body 16px/1.5; lede 18–20px; UI 14px; small 13px; code 13–15px. + Eyebrows 12px uppercase with 0.08em tracking. +- Code blocks use fixed Ink `#0F1513` and light text in both themes. Contain + horizontal code scrolling. Cards use Surface, a 1px Line border and 8px radius, + without shadows or accent stripes. Actions have 6px radius and a minimum + 44px target height. Keep one contextual primary action per view. +- Amber, red and blue express status, not ornamental language identities. + +## Chrome and navigation + +Use `Durable Workflow / ` beside the original mark. The surface label +is lowercase mono and Action (Durable Green in light, Mint in dark). Main docs +use `docs`; the other labels are `python`, `rust`, `php`, `cloud`, `status`. +The logo sits on an unchanging Paper chip (`#F6F7F5`, 8px radius, 4px padding) +in dark mode. The main brand link returns to the site's landing page. + +Global navigation order: **Docs / SDKs / Cloud / Pricing / Status**. SDKs opens +Python / Rust / PHP. Indicate the current surface with bold Ink and an accessible +current-page state. Include GitHub and a contextual action (Get started, +Dashboard or Subscribe). Keep keyboard focus visible. The switcher must open +with keyboard and pointer, close with Escape, and retain ordinary link behavior. +Mobile navigation must expose all destinations and close without trapping focus. + +The local documentation toolbar retains version, locale and search controls; +it is separate from the shared navigation row. Keep existing API navigation, +stable and historical routes, anchors and locale routing intact. + +The [public link map](links.json) uses canonical current docs URLs rather than +the historical `/docs/2.0/` alias. Each renderer may localize internal main-site +links using its existing router. External SDK and product links stay canonical. + +Footer columns: **Brand / SDKs / Run / Project**. Brand has the original mark, +wordmark and product description. SDKs: Python / Rust / PHP. Run: self-hosted +Server / Cloud / Pricing / Status. Project retains GitHub, compatibility, terms, +privacy and existing community/documentation links. Keep legal destinations +bound to their owning site's real routes. + +## Status and product authority + +The footer status pill links to the public status site. Without accepted current +health data, display **Status unknown** in neutral colors. A linked static green +"all systems operational" is not evidence. Use the existing anonymous public snapshot at `statusApi` in the link map. +The [shared consumer](status.mjs) reads only the aggregate and sanitized public +customer impact. Require schema `durable_workflow.status_page_snapshot.v1`, +Cloud/prod identity, valid nonfuture checked/generated timestamps, a fresh state, +and a recomputed evidence age below the producer cutoff (at most 900 seconds). +`customer_visible=false` and empty component rows alone do not imply unknown. +Calendar dates must round-trip without normalization (for example, February30 +and24:00 cannot establish evidence). Freshness requires a known UTC instant, +rather than a known producer-local offset. Equivalent `Z`, `+00:00` and `-00:00` +forms use the same absolute-time calculation and evidence cutoff; valid nonzero +offsets remain supported. [RFC3339 section4.3](https://www.rfc-editor.org/rfc/rfc3339#section-4.3) +permits a known UTC instant with an unknown local offset (`-00:00`). + + +Top-level status vocabulary is `ok|warning|failed|unknown`; alert levels are +`ok|warning|critical|unknown`. A fresh `failed` or `critical` aggregate/impact +shows **Service incident**; warning shows **Service degraded** (or **Maintenance** +when maintenance is the sole public affected family). Unknown or unrecognized +values cannot show green. Only fresh `ok/ok` with no contradictory impact/reason +shows **All systems operational**. + +Public reasons override presentation: `cloud_unreachable` → **Status unavailable**; +`monitor_stale|probe_unverified` → **Status unverified**; `publisher_stale` or expired +freshness → **Status stale**. Malformed/future evidence and network/HTTP/JSON failures +show **Status unknown**. Never render internal components, operator alerts, or +artifact-version rows from the snapshot. + +Use anonymous GET, credentials omitted, no referrer, no cache and no redirects; +a five-second request deadline, at most one request at a time and a sixty-second +refresh after completion. Invalidate a previous result at its evidence deadline +while refreshing. Cleanup aborts the request and clears timers on navigation. +Cloud owns the existing public-GET CORS allowlist; each consumer owns its CSP +`connect-src https://status.durable-workflow.com`. This adds no status service, +account or SDK-runtime telemetry. Keep an accessible linked unknown fallback if +the feed cannot be read. + +Versions, commands, support floors, prices, SLA and health values come from the +existing published package/catalog/status contracts. The design's sample values +are illustrative. Styling may ship independently of package releases; it does +not authorize promoting unreleased APIs or changing the selected stable package. + +## Delivery + +Check actual built pages at desktop and narrow mobile widths in both themes. +Verify navigation, SDK switching, search, version/locale selectors, focus and +copy controls. Preserve executable examples and existing publication boundaries. +Each site owns its normal protected build/deployment and actual live acceptance. diff --git a/static/identity/fonts.css b/static/identity/fonts.css new file mode 100644 index 00000000000..fb6e203c8e5 --- /dev/null +++ b/static/identity/fonts.css @@ -0,0 +1,25 @@ +/* Self-hosted OFL fonts. Sources and hashes: fonts/manifest.json. */ +@font-face { + font-family: 'Inter'; + src: url('./fonts/inter-latin.woff2') format('woff2'); + font-style: normal; + font-weight: 400 600; + font-display: swap; + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'JetBrains Mono'; + src: url('./fonts/jetbrains-mono-latin.woff2') format('woff2'); + font-style: normal; + font-weight: 400 600; + font-display: swap; + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Space Grotesk'; + src: url('./fonts/space-grotesk-latin.woff2') format('woff2'); + font-style: normal; + font-weight: 600; + font-display: swap; + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} diff --git a/static/identity/fonts/inter-OFL.txt b/static/identity/fonts/inter-OFL.txt new file mode 100644 index 00000000000..21f6aff9610 --- /dev/null +++ b/static/identity/fonts/inter-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/static/identity/fonts/inter-latin.woff2 b/static/identity/fonts/inter-latin.woff2 new file mode 100644 index 00000000000..d15208de03c Binary files /dev/null and b/static/identity/fonts/inter-latin.woff2 differ diff --git a/static/identity/fonts/jetbrains-mono-latin.woff2 b/static/identity/fonts/jetbrains-mono-latin.woff2 new file mode 100644 index 00000000000..4d09cda4a4c Binary files /dev/null and b/static/identity/fonts/jetbrains-mono-latin.woff2 differ diff --git a/static/identity/fonts/jetbrainsmono-OFL.txt b/static/identity/fonts/jetbrainsmono-OFL.txt new file mode 100644 index 00000000000..821a3dac22a --- /dev/null +++ b/static/identity/fonts/jetbrainsmono-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. + +This license is copied below, and is also available with a FAQ at: https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/static/identity/fonts/manifest.json b/static/identity/fonts/manifest.json new file mode 100644 index 00000000000..7eff1ef33cc --- /dev/null +++ b/static/identity/fonts/manifest.json @@ -0,0 +1,38 @@ +{ + "css_source": "https://fonts.googleapis.com/css2?family=Inter:wght@400..600&family=JetBrains+Mono:wght@400..600&family=Space+Grotesk:wght@600&display=swap", + "fonts": [ + { + "bytes": 48256, + "family": "Inter", + "file": "inter-latin.woff2", + "license_file": "inter-OFL.txt", + "license_sha256": "5b9321a4298cfeb6b34354164a1c3afc3db114569984c502b9b35d988fd58c57", + "license_source": "https://raw.githubusercontent.com/google/fonts/24ddc1f324cac5d9f5aedca126d298f56e3f1640/ofl/inter/OFL.txt", + "sha256": "3100e775e8616cd2611beecfa23a4263d7037586789b43f035236a2e6fbd4c62", + "source": "https://fonts.gstatic.com/s/inter/v20/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2" + }, + { + "bytes": 31432, + "family": "JetBrains Mono", + "file": "jetbrains-mono-latin.woff2", + "license_file": "jetbrainsmono-OFL.txt", + "license_sha256": "b2fe5e8987594e9ffd1d2ca52a2f5d73eb8335243893c5d6254b5ad69269591d", + "license_source": "https://raw.githubusercontent.com/google/fonts/24ddc1f324cac5d9f5aedca126d298f56e3f1640/ofl/jetbrainsmono/OFL.txt", + "sha256": "83c005d49d8a6a50474c73a5a36ac0468076e9c4a29da7bdb14995d80560a5be", + "source": "https://fonts.gstatic.com/s/jetbrainsmono/v24/tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPxDcwg.woff2" + }, + { + "bytes": 13284, + "family": "Space Grotesk", + "file": "space-grotesk-latin.woff2", + "license_file": "spacegrotesk-OFL.txt", + "license_sha256": "564ce565c371c5e5bbf286006565a7c9aa55a9f56e7ca58d56e05d649dd61a72", + "license_source": "https://raw.githubusercontent.com/google/fonts/24ddc1f324cac5d9f5aedca126d298f56e3f1640/ofl/spacegrotesk/OFL.txt", + "sha256": "685bbbf69fa616df1ef81847c85fc76be097ddfb3468ff2257be54511ab3130f", + "source": "https://fonts.gstatic.com/s/spacegrotesk/v22/V8mQoQDjQSkFtoMM3T6r8E7mF71Q-gOoraIAEj42VnskPMA.woff2" + } + ], + "license_repository_commit": "24ddc1f324cac5d9f5aedca126d298f56e3f1640", + "runtime_cdn": false, + "self_hosted": true +} diff --git a/static/identity/fonts/space-grotesk-latin.woff2 b/static/identity/fonts/space-grotesk-latin.woff2 new file mode 100644 index 00000000000..a1db41a0ac0 Binary files /dev/null and b/static/identity/fonts/space-grotesk-latin.woff2 differ diff --git a/static/identity/fonts/spacegrotesk-OFL.txt b/static/identity/fonts/spacegrotesk-OFL.txt new file mode 100644 index 00000000000..cb512b9af44 --- /dev/null +++ b/static/identity/fonts/spacegrotesk-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Space Grotesk Project Authors (https://github.com/floriankarsten/space-grotesk) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/static/identity/links.json b/static/identity/links.json new file mode 100644 index 00000000000..484db1c3761 --- /dev/null +++ b/static/identity/links.json @@ -0,0 +1,17 @@ +{ + "home": "https://durable-workflow.com/", + "docs": "https://durable-workflow.com/docs/introduction/", + "python": "https://python.durable-workflow.com/", + "rust": "https://rust.durable-workflow.com/", + "php": "https://php.durable-workflow.com/", + "server": "https://durable-workflow.com/docs/polyglot/server/", + "cloud": "https://cloud.durable-workflow.com/", + "pricing": "https://cloud.durable-workflow.com/pricing", + "status": "https://status.durable-workflow.com/", + "github": "https://github.com/durable-workflow", + "compatibility": "https://durable-workflow.com/docs/compatibility/", + "catalog": "https://cloud.durable-workflow.com/early-access", + "terms": "https://cloud.durable-workflow.com/terms", + "privacy": "https://cloud.durable-workflow.com/privacy", + "statusApi": "https://status.durable-workflow.com/api/current.json" +} diff --git a/static/identity/status.mjs b/static/identity/status.mjs new file mode 100644 index 00000000000..2659eae6410 --- /dev/null +++ b/static/identity/status.mjs @@ -0,0 +1,109 @@ +// Shared public footer summary. No component rows, credentials or telemetry. +export const STATUS_API = 'https://status.durable-workflow.com/api/current.json'; +const statuses = new Set(['ok', 'warning', 'failed', 'unknown']); +const levels = new Set(['ok', 'warning', 'critical', 'unknown']); +const unknown = () => ({state: 'unknown', validUntil: 0}); +function timestamp(value) { + if (typeof value !== 'string') return Number.NaN; + const match = /^(\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d)(?:\.\d{1,9})?(Z|([+-])(\d\d):(\d\d))$/.exec(value); + if (!match) return Number.NaN; + const parsed = Date.parse(value); + if (!Number.isFinite(parsed)) return Number.NaN; + const offsetMinutes = match[2] === 'Z' ? 0 : Number(match[4]) * 60 + Number(match[5]); + const offsetSign = match[3] === '-' ? -1 : 1; + // Date.parse normalizes impossible days and 24:00. Round-trip the supplied + // calendar fields in their original offset before accepting the evidence. + const suppliedCalendar = new Date(parsed + offsetMinutes * offsetSign * 60000).toISOString().slice(0, 19); + return suppliedCalendar === match[1] ? parsed : Number.NaN; +} + +function evidenceWindow(snapshot, now) { + if (snapshot?.schema !== 'durable_workflow.status_page_snapshot.v1' + || snapshot.product !== 'cloud' || snapshot.environment !== 'prod' || !Number.isFinite(now)) return unknown(); + const checked = timestamp(snapshot.checked_at); + const generated = timestamp(snapshot.generated_at); + const freshness = snapshot.freshness; + if (!Number.isFinite(checked) || !Number.isFinite(generated) || checked > now + || generated > now || generated < checked || !freshness + || !Number.isFinite(freshness.stale_after_seconds) || freshness.stale_after_seconds <= 0 + || freshness.stale_after_seconds > 900 || !Number.isFinite(freshness.age_seconds) + || freshness.age_seconds < 0) return unknown(); + const validUntil = checked + freshness.stale_after_seconds * 1000; + if (freshness.status === 'stale' || now >= validUntil || freshness.age_seconds >= freshness.stale_after_seconds) return {state: 'stale', validUntil: 0}; + if (freshness.status !== 'fresh') return unknown(); + return {validUntil}; +} + +function aggregateState(snapshot, validUntil) { + const impact = snapshot.customer_impact; + if (!statuses.has(snapshot.status) || !levels.has(snapshot.alert_level) + || !impact || !statuses.has(impact.status) || !levels.has(impact.alert_level)) return unknown(); + const reason = snapshot.meta?.public_status_reason; + if (reason === 'cloud_unreachable') return {state: 'unavailable', validUntil}; + if (reason === 'monitor_stale' || reason === 'probe_unverified') return {state: 'unverified', validUntil}; + if (reason === 'publisher_stale') return {state: 'stale', validUntil: 0}; + if (reason !== undefined && reason !== null && reason !== '') return unknown(); + const stateTokens = new Set([snapshot.status, impact.status]); + const levelTokens = new Set([snapshot.alert_level, impact.alert_level]); + if (stateTokens.has('failed') || levelTokens.has('critical')) return {state: 'incident', validUntil}; + if (stateTokens.has('warning') || levelTokens.has('warning')) { + const families = impact.affected_families; + const maintenanceOnly = Array.isArray(families) && families.length === 1 && families[0] === 'maintenance'; + return {state: maintenanceOnly ? 'maintenance' : 'degraded', validUntil}; + } + if (stateTokens.has('unknown') || levelTokens.has('unknown')) return {state: 'unknown', validUntil}; + return {state: 'operational', validUntil}; +} + +export function publicStatus(snapshot, now = Date.now()) { + const evidence = evidenceWindow(snapshot, now); + return evidence.state ? evidence : aggregateState(snapshot, evidence.validUntil); +} + +export async function fetchPublicStatus({fetchImpl = globalThis.fetch, now = Date.now, signal} = {}) { + const response = await fetchImpl(STATUS_API, { + method: 'GET', mode: 'cors', credentials: 'omit', cache: 'no-store', + redirect: 'error', referrerPolicy: 'no-referrer', signal, + }); + if (!response.ok) throw new Error('Public status unavailable'); + const body = await response.text(); + if (body.length > 65536) throw new Error('Public status response too large'); + return publicStatus(JSON.parse(body), now()); +} + +// At most one request at a time, five-second deadline, sixty-second cadence. +// Invalidate a previously successful result at its evidence cutoff, even if the +// next request hangs or the tab has been backgrounded. The caller owns cleanup. +export function watchPublicStatus(onChange, {fetchImpl = globalThis.fetch, now = Date.now} = {}) { + let stopped = false; + let controller; + let timer; + let expiry; + const publish = summary => { + clearTimeout(expiry); + onChange(summary.state); + if (summary.validUntil > now()) { + expiry = setTimeout(() => onChange('stale'), summary.validUntil - now()); + } + }; + const refresh = async () => { + controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + try { + const summary = await fetchPublicStatus({fetchImpl, now, signal: controller.signal}); + if (!stopped) publish(summary); + } catch { + if (!stopped) publish(unknown()); + } finally { + clearTimeout(timeout); + if (!stopped) timer = setTimeout(refresh, 60000); + } + }; + void refresh(); + return () => { + stopped = true; + controller?.abort(); + clearTimeout(timer); + clearTimeout(expiry); + }; +} diff --git a/static/identity/tokens.css b/static/identity/tokens.css new file mode 100644 index 00000000000..ab57ae7a4ea --- /dev/null +++ b/static/identity/tokens.css @@ -0,0 +1,36 @@ +/* One Durable Workflow. Shared role tokens; no framework or remote imports. */ +:root, +[data-theme='light'] { + color-scheme: light; + --dw-paper: #f6f7f5; + --dw-surface: #ffffff; + --dw-ink: #0f1513; + --dw-ink-2: #3b4541; + --dw-muted: #5e6964; + --dw-line: #dde2df; + --dw-action: #0f7a52; + --dw-tint: #e3f4ec; + --dw-on-action: #ffffff; + --dw-font-display: 'Space Grotesk', Inter, system-ui, sans-serif; + --dw-font-body: Inter, system-ui, sans-serif; + --dw-font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Consolas, monospace; + --dw-code-background: #0f1513; + --dw-code-foreground: #e8edea; + --dw-code-muted: #9aa6a1; + --dw-logo-paper: #f6f7f5; + --dw-radius-action: 6px; + --dw-radius-card: 8px; +} + +[data-theme='dark'] { + color-scheme: dark; + --dw-paper: #0f1513; + --dw-surface: #172019; + --dw-ink: #e8edea; + --dw-ink-2: #b7c2bd; + --dw-muted: #9aa6a1; + --dw-line: #2e3934; + --dw-action: #5fe0a6; + --dw-tint: #123b2a; + --dw-on-action: #0f1513; +} diff --git a/static/img/logo.svg b/static/img/logo.svg index 38e3ed76032..02879965073 100644 --- a/static/img/logo.svg +++ b/static/img/logo.svg @@ -1,10 +1,9 @@ - - - - - - - - - - + + + + + + + + + \ No newline at end of file diff --git a/tests/one-identity/README.md b/tests/one-identity/README.md new file mode 100644 index 00000000000..1f0e0d412c3 --- /dev/null +++ b/tests/one-identity/README.md @@ -0,0 +1,47 @@ +# One Identity built-page acceptance + +Build the site with the repository's supported Node 24 toolchain and normal +checks. Run `browser.cjs` against that output using an existing Playwright client +and an owned headless Chromium profile: + +```sh +NODE_PATH= \ +DW_BROWSER_EXECUTABLE= \ +DW_SOURCE_REVISION= \ +node tests/one-identity/browser.cjs +``` + +Set `DW_PREVIEW_MODE=chrome` for the bounded follow-up after a global-header +styling correction: retain the four desktop/mobile theme views, check the real +nav destinations and absence of added header glyphs, and capture updated home +and docs screenshots. It does not repeat the full functional matrix. + +The runner starts an ephemeral loopback HTTP server, uses one browser with at +most two renderer processes, runs checks sequentially, and closes the browser +and server. An absolute guard bounds the run. No signed-in browser profile or +production credential is needed. It does not install tools or alter the site. + +Checks cover light/dark, desktop/390px/320px widths, visible action contrast, +SDK keyboard disclosure and Escape focus restoration, mobile drawer isolation, +contained code blocks, real copy-to-clipboard content, 1.x navigation, search, +light-default/dark-opt-in persistence, and representative localized routes. +Screenshots are home and docs at 1440px/390px in both themes. + +**Status results in these screenshots are synthetic browser fixtures, not live +service-health evidence.** The built checks separately exercise operational, +degraded, incident, stale, unknown and unavailable-response behavior. The +consumer's producer-contract and request-lifecycle controls are in +`scripts/identity-status.test.mjs`. Actual cross-origin public-status acceptance +requires Cloud's real CORS deployment and a separate browser read from the +published site. Do not turn these fixtures into production data. + +The report records the source revision, built index SHA256, browser/client +versions, checks and screenshot names. The screenshot inventory binds the +reviewed PNG bytes. Evidence-only changes do not alter the site implementation. + +The retained `baseline-report.json`/`baseline-inventory.json` bind the50-check +functional matrix and its original PNGs at the recorded immutable evidence +commit. The current `report.json`/`inventory.json` bind the16-check scoped header +follow-up and current PNGs. A later equivalent arithmetic extraction has the +same14 status-control results and changes no layout or labels; its carry is +explicit in the inventory. This does not claim the older matrix was rerun. diff --git a/tests/one-identity/browser.cjs b/tests/one-identity/browser.cjs new file mode 100644 index 00000000000..f121afe9a13 --- /dev/null +++ b/tests/one-identity/browser.cjs @@ -0,0 +1,200 @@ +// Built-site checks. Use an existing Playwright client and an owned browser. +// Usage: NODE_PATH= DW_BROWSER_EXECUTABLE= node browser.cjs +const {chromium} = require('playwright'); +const assert = require('node:assert/strict'); +const fs = require('node:fs/promises'); +const path = require('node:path'); +const http = require('node:http'); +const {createHash} = require('node:crypto'); +const root = path.resolve(process.argv[2]); +const output = path.resolve(process.argv[3]); +const types = {'.html':'text/html','.js':'application/javascript','.mjs':'application/javascript','.css':'text/css','.json':'application/json','.svg':'image/svg+xml','.woff2':'font/woff2','.png':'image/png','.ico':'image/x-icon','.txt':'text/plain'}; +const report = {checks: [], screenshots: [], browser: null, syntheticStatus: true, sourceRevision: process.env.DW_SOURCE_REVISION || null, clientVersion: require('playwright/package.json').version}; +let page; +const chromeOnly = process.env.DW_PREVIEW_MODE === 'chrome'; +report.mode = chromeOnly ? 'chrome' : 'full'; +function fixture(state = 'operational') { + const now = Date.now(); + const result = { + schema:'durable_workflow.status_page_snapshot.v1', product:'cloud', environment:'prod', + checked_at: new Date(now - 10000).toISOString(), generated_at: new Date(now - 5000).toISOString(), + status:'ok', alert_level:'ok', customer_visible:false, components:[], + freshness:{status:'fresh',age_seconds:10,stale_after_seconds:900}, + customer_impact:{status:'ok',alert_level:'ok',affected_families:[]}, + }; + if (state === 'degraded') result.status = result.alert_level = 'warning'; + if (state === 'incident') {result.status = 'failed'; result.alert_level = 'critical';} + if (state === 'stale') {result.checked_at = new Date(now - 901000).toISOString();} + if (state === 'unknown') result.status = result.alert_level = 'unknown'; + return result; +} +async function check(name, fn) { + await fn(); report.checks.push(name); console.log('PASS', name); +} +async function screenshot(name, fullPage = false) { + await page.screenshot({path:path.join(output,name+'.png'),fullPage}); + report.screenshots.push(name+'.png'); +} +async function noOverflow() { + const sizes = await page.evaluate(() => ({width: innerWidth, content: document.documentElement.scrollWidth})); + assert.ok(sizes.content <= sizes.width + 1, JSON.stringify(sizes)); +} +async function run() { + await fs.mkdir(output,{recursive:true}); + report.buildIndexSha256 = createHash('sha256').update(await fs.readFile(path.join(root,'index.html'))).digest('hex'); + const server = http.createServer(async (req,res) => { + try { + let file = path.resolve(root,'.'+decodeURIComponent(new URL(req.url,'http://localhost').pathname)); + if (file !== root && !file.startsWith(root+path.sep)) throw new Error('path'); + if ((await fs.stat(file)).isDirectory()) file = path.join(file,'index.html'); + const body = await fs.readFile(file); + res.writeHead(200,{'Content-Type':types[path.extname(file)] || 'application/octet-stream'}); res.end(body); + } catch {res.writeHead(404);res.end('Not found');} + }); + await new Promise(resolve => server.listen(0,'127.0.0.1',resolve)); + const base = 'http://127.0.0.1:'+server.address().port; + let browser; + try { + browser = await chromium.launch({headless:true,executablePath:process.env.DW_BROWSER_EXECUTABLE,args:['--renderer-process-limit=2','--disable-background-networking']}); + report.browser = browser.version(); + for (const theme of ['light','dark']) { + for (const width of (chromeOnly ? [1440,390] : [1440,390,320])) { + const context = await browser.newContext({viewport:{width,height:900},reducedMotion:'reduce'}); + await context.grantPermissions(['clipboard-read','clipboard-write']); + await context.addInitScript(theme => localStorage.setItem('theme',theme),theme); + let status = 'operational'; + await context.route('https://status.durable-workflow.com/api/current.json', route => status === 'offline' ? route.abort('failed') : route.fulfill({contentType:'application/json',body:JSON.stringify(fixture(status))})); + page = await context.newPage(); + const errors = []; page.on('pageerror',error=>errors.push(error.message)); + await page.goto(base+'/',{waitUntil:'networkidle'}); + await page.evaluate(() => document.fonts.ready); + await page.waitForFunction(() => document.querySelector('.dw-status-pill')?.dataset.status === 'operational'); + await check(`${theme}/${width}: home fits viewport and uses selected theme`,async()=>{ + await noOverflow(); assert.equal(await page.locator('html').getAttribute('data-theme'),theme); + assert.equal(await page.locator('.dw-wordmark').first().innerText(),'Durable Workflow'); + assert.equal(await page.locator('.dw-logo-chip img').first().evaluate(img=>img.naturalWidth),200); + }); + await check(`${theme}/${width}: action text meets 4.5:1 contrast`,async()=>{ + const ratios = await page.locator('.button').evaluateAll(nodes => { + const luminance = color => { + const channels = color.match(/[\d.]+/g).slice(0,3).map(Number).map(v=>v/255).map(v=>v<=.04045?v/12.92:((v+.055)/1.055)**2.4); + return channels[0]*.2126+channels[1]*.7152+channels[2]*.0722; + }; + return nodes.map(node=>{ + const style=getComputedStyle(node); const a=luminance(style.color); const b=luminance(style.backgroundColor); + return {text:node.textContent,ratio:(Math.max(a,b)+.05)/(Math.min(a,b)+.05)}; + }); + }); + assert.ok(ratios.every(item=>item.ratio>=4.5),JSON.stringify(ratios)); + }); + if (width !== 320) await screenshot(`home-${theme}-${width}`,true); + if (chromeOnly) { + await check(`${theme}/${width}: global nav labels are plain and destinations preserved`,async()=>{ + const links = await page.locator('.dw-global-navigation a[href^="https://"]').evaluateAll(nodes=>nodes.filter(node=>['Cloud','Pricing','Status','GitHub'].includes(node.textContent.trim())).map(node=>({label:node.textContent.trim(),href:node.href,icons:[...node.querySelectorAll('svg')].map(icon=>getComputedStyle(icon).display)}))); + assert.deepEqual(links.map(item=>[item.label,item.href]),[['Cloud','https://cloud.durable-workflow.com/'],['Pricing','https://cloud.durable-workflow.com/pricing'],['Status','https://status.durable-workflow.com/'],['GitHub','https://github.com/durable-workflow']]); + assert.ok(links.every(item=>item.icons.every(display=>display==='none'))); + }); + await page.goto(base+'/docs/installation/',{waitUntil:'networkidle'}); + await check(`${theme}/${width}: docs remain contained after scoped nav correction`,noOverflow); + await screenshot(`docs-${theme}-${width}`); + assert.deepEqual(errors,[]); + await context.close(); + continue; + } + await check(`${theme}/${width}: SDK disclosure opens and closes with keyboard`,async()=>{ + if (width < 997) await page.getByRole('button',{name:'Toggle navigation bar'}).click(); + const selector = width < 997 ? '.navbar-sidebar .dw-sdk-switcher' : '.dw-global-navigation .dw-sdk-switcher'; + const trigger = page.locator(selector+' > button'); + await trigger.focus(); await page.keyboard.press('Enter'); + assert.equal(await trigger.getAttribute('aria-expanded'),'true'); + const links = page.locator(selector+' .dw-sdk-links a'); + assert.equal(await links.count(),3); + assert.deepEqual(await links.evaluateAll(nodes=>nodes.map(n=>n.href)),['https://python.durable-workflow.com/','https://rust.durable-workflow.com/','https://php.durable-workflow.com/']); + await page.keyboard.press('Tab'); await page.keyboard.press('Escape'); + assert.equal(await trigger.getAttribute('aria-expanded'),'false'); + assert.equal(await trigger.evaluate(node=>node===document.activeElement),true); + if (width < 997) { + assert.equal(await page.locator('.dw-docs-toolbar').evaluate(node=>node.inert),true); + await page.keyboard.press('Escape'); + await page.waitForFunction(()=>!document.querySelector('.navbar-sidebar--show')); + assert.equal(await page.getByRole('button',{name:'Toggle navigation bar'}).evaluate(node=>node===document.activeElement),true); + assert.equal(await page.locator('.dw-docs-toolbar').evaluate(node=>node.inert),false); + } + }); + await page.goto(base+'/docs/installation/',{waitUntil:'networkidle'}); + await check(`${theme}/${width}: docs and code stay contained`,noOverflow); + assert.equal(await page.locator('pre').first().evaluate(node=>getComputedStyle(node).backgroundColor),'rgb(15, 21, 19)'); + if (width !== 320) await screenshot(`docs-${theme}-${width}`); + await check(`${theme}/${width}: copy control copies actual code`,async()=>{ + const block = page.locator('div[class*="codeBlockContainer"]').first(); + const expected = (await block.locator('pre').innerText()).trim(); + const copy = block.getByRole('button',{name:/Copy code to clipboard/}); + await copy.click(); + const actual = await page.evaluate(()=>navigator.clipboard.readText()); + assert.equal(actual.trim(),expected); + }); + await check(`${theme}/${width}: historical version navigation works`,async()=>{ + const versions = page.locator('.dw-docs-controls .dropdown').first(); + await versions.locator('a[role="button"]').press('Enter'); + await versions.getByRole('link',{name:'1.x',exact:true}).click(); + await page.waitForURL('**/docs/1.x/**'); + await noOverflow(); assert.ok(await page.locator('main h1').innerText()); + }); + assert.deepEqual(errors,[]); + await context.close(); + } + } + if (chromeOnly) return; + const context = await browser.newContext({viewport:{width:768,height:1024},colorScheme:'dark'}); + let status = 'operational'; + await context.route('https://status.durable-workflow.com/api/current.json',route=>status==='offline'?route.abort('failed'):route.fulfill({contentType:'application/json',body:JSON.stringify(fixture(status))})); + page = await context.newPage(); + await page.goto(base+'/',{waitUntil:'networkidle'}); + await check('light default ignores system dark until user opts in',async()=>{ + assert.equal(await page.locator('html').getAttribute('data-theme'),'light'); + await page.getByRole('button',{name:/Switch between dark and light mode/}).click(); + assert.equal(await page.locator('html').getAttribute('data-theme'),'dark'); + await page.reload({waitUntil:'networkidle'}); + assert.equal(await page.locator('html').getAttribute('data-theme'),'dark'); + }); + for (status of ['operational','degraded','incident','stale','unknown','offline']) { + await page.goto(base+'/',{waitUntil:'networkidle'}); + const expected = status === 'offline' ? 'unknown' : status; + await page.waitForFunction(expected=>document.querySelector('.dw-status-pill')?.dataset.status === expected,expected); + await check(`built status ${status} => ${expected}`,async()=>{ + await noOverflow(); assert.equal(await page.locator('.dw-status-pill').getAttribute('href'),'https://status.durable-workflow.com/'); + }); + } + await page.goto(base+'/docs/introduction/',{waitUntil:'networkidle'}); + await check('built search returns navigable documentation results',async()=>{ + const input=page.locator('.navbar__search-input'); + await input.fill('installation'); + const result=page.locator('.navbar__search [class*="suggestion"]').first(); + await result.waitFor({state:'visible'}); + await input.press('Enter'); + await page.waitForURL(url=>url.pathname !== '/docs/introduction/'); + assert.ok(new URL(page.url()).pathname.startsWith('/docs/') || new URL(page.url()).pathname.startsWith('/search')); + }); + for (const locale of ['de','pt-BR','ja']) { + await page.setViewportSize({width:390,height:900}); + await page.goto(base+'/'+locale+'/',{waitUntil:'networkidle'}); + await check(`localized ${locale}: translated home and navigation fit`, async()=>{ + await noOverflow(); assert.equal(await page.locator('html').getAttribute('lang'),locale === 'pt-BR' ? 'pt-BR' : locale); + assert.notEqual(await page.locator('main h1').innerText(),'Work that keeps going.'); + }); + await page.goto(base+'/'+locale+'/docs/installation/',{waitUntil:'networkidle'}); + await check(`localized ${locale}: docs preserve local route and chrome`,noOverflow); + } + await context.close(); + } catch(error) { + report.failure = error.stack; + if(page&&!page.isClosed()) await page.screenshot({path:path.join(output,'failure.png'),fullPage:true}).catch(()=>{}); + throw error; + } finally { + await fs.writeFile(path.join(output,'report.json'),JSON.stringify(report,null,2)+'\n'); + await browser?.close(); + await new Promise(resolve=>server.close(resolve)); + } +} +const guard = setTimeout(()=>{console.error('Absolute preview deadline');process.exit(124);},230000); +run().then(()=>{clearTimeout(guard);console.log(JSON.stringify(report));}).catch(error=>{clearTimeout(guard);console.error(error);process.exitCode=1;}); diff --git a/tests/one-identity/screenshots/baseline-inventory.json b/tests/one-identity/screenshots/baseline-inventory.json new file mode 100644 index 00000000000..8f2fa68430f --- /dev/null +++ b/tests/one-identity/screenshots/baseline-inventory.json @@ -0,0 +1,47 @@ +{ + "sourceRevision": "aee4a1fd1645fc94f60e42cc442ae496576c481f", + "syntheticStatus": true, + "screenshots": [ + { + "file": "docs-dark-1440.png", + "bytes": 198409, + "sha256": "85f180598470280fa4b54da43667b3f0b43174a7e7f578c2e6d9706d8ebbaa29" + }, + { + "file": "docs-dark-390.png", + "bytes": 88736, + "sha256": "71d92976d0a9760af543f763b63b70b98ca5382c1e12cb4e7dbd98847adb308f" + }, + { + "file": "docs-light-1440.png", + "bytes": 198626, + "sha256": "fbe6dc76cf174bb17578c61909a5857a6a695323819f227c9f667e8329d6eb22" + }, + { + "file": "docs-light-390.png", + "bytes": 87744, + "sha256": "1243dcf7b48bb1b495fb7c7366ba671f9e2cd04545ea79049f244425f418dd99" + }, + { + "file": "home-dark-1440.png", + "bytes": 228042, + "sha256": "6caa0032e7a49f7880d26dc7f46da70e9c8ef7e110339845971694dc05a9704b" + }, + { + "file": "home-dark-390.png", + "bytes": 210993, + "sha256": "ffb37d761e511f6d47d1b34fca345295fc5eb16cf2d3284cda6a4ddec63ea7ed" + }, + { + "file": "home-light-1440.png", + "bytes": 224335, + "sha256": "ffe95d74297a9a3d6f9785ec6ce0defbcd130ce39ac96865ca1bb6003e0c4e10" + }, + { + "file": "home-light-390.png", + "bytes": 205496, + "sha256": "4974b701030172105af5ffd4178be47dc0ed59337a626e2717dabfed46278530" + } + ], + "screenshotEvidenceCommit": "f4bbb6a00ccabb6490e3e072c9bfbfb6d45982b4" +} diff --git a/tests/one-identity/screenshots/baseline-report.json b/tests/one-identity/screenshots/baseline-report.json new file mode 100644 index 00000000000..b7d5e22673c --- /dev/null +++ b/tests/one-identity/screenshots/baseline-report.json @@ -0,0 +1,70 @@ +{ + "checks": [ + "light/1440: home fits viewport and uses selected theme", + "light/1440: action text meets 4.5:1 contrast", + "light/1440: SDK disclosure opens and closes with keyboard", + "light/1440: docs and code stay contained", + "light/1440: copy control copies actual code", + "light/1440: historical version navigation works", + "light/390: home fits viewport and uses selected theme", + "light/390: action text meets 4.5:1 contrast", + "light/390: SDK disclosure opens and closes with keyboard", + "light/390: docs and code stay contained", + "light/390: copy control copies actual code", + "light/390: historical version navigation works", + "light/320: home fits viewport and uses selected theme", + "light/320: action text meets 4.5:1 contrast", + "light/320: SDK disclosure opens and closes with keyboard", + "light/320: docs and code stay contained", + "light/320: copy control copies actual code", + "light/320: historical version navigation works", + "dark/1440: home fits viewport and uses selected theme", + "dark/1440: action text meets 4.5:1 contrast", + "dark/1440: SDK disclosure opens and closes with keyboard", + "dark/1440: docs and code stay contained", + "dark/1440: copy control copies actual code", + "dark/1440: historical version navigation works", + "dark/390: home fits viewport and uses selected theme", + "dark/390: action text meets 4.5:1 contrast", + "dark/390: SDK disclosure opens and closes with keyboard", + "dark/390: docs and code stay contained", + "dark/390: copy control copies actual code", + "dark/390: historical version navigation works", + "dark/320: home fits viewport and uses selected theme", + "dark/320: action text meets 4.5:1 contrast", + "dark/320: SDK disclosure opens and closes with keyboard", + "dark/320: docs and code stay contained", + "dark/320: copy control copies actual code", + "dark/320: historical version navigation works", + "light default ignores system dark until user opts in", + "built status operational => operational", + "built status degraded => degraded", + "built status incident => incident", + "built status stale => stale", + "built status unknown => unknown", + "built status offline => unknown", + "built search returns navigable documentation results", + "localized de: translated home and navigation fit", + "localized de: docs preserve local route and chrome", + "localized pt-BR: translated home and navigation fit", + "localized pt-BR: docs preserve local route and chrome", + "localized ja: translated home and navigation fit", + "localized ja: docs preserve local route and chrome" + ], + "screenshots": [ + "home-light-1440.png", + "docs-light-1440.png", + "home-light-390.png", + "docs-light-390.png", + "home-dark-1440.png", + "docs-dark-1440.png", + "home-dark-390.png", + "docs-dark-390.png" + ], + "browser": "156.0.8078.80", + "syntheticStatus": true, + "sourceRevision": "aee4a1fd1645fc94f60e42cc442ae496576c481f", + "clientVersion": "1.62.1", + "buildIndexSha256": "0ac9b6a9b4ff6ca393636242a7b4ab154d54c382b3ae04062730b34905bd9988", + "screenshotEvidenceCommit": "f4bbb6a00ccabb6490e3e072c9bfbfb6d45982b4" +} diff --git a/tests/one-identity/screenshots/docs-dark-1440.png b/tests/one-identity/screenshots/docs-dark-1440.png new file mode 100644 index 00000000000..394d7f84240 Binary files /dev/null and b/tests/one-identity/screenshots/docs-dark-1440.png differ diff --git a/tests/one-identity/screenshots/docs-dark-390.png b/tests/one-identity/screenshots/docs-dark-390.png new file mode 100644 index 00000000000..06f7f4c2c91 Binary files /dev/null and b/tests/one-identity/screenshots/docs-dark-390.png differ diff --git a/tests/one-identity/screenshots/docs-light-1440.png b/tests/one-identity/screenshots/docs-light-1440.png new file mode 100644 index 00000000000..db19b05d188 Binary files /dev/null and b/tests/one-identity/screenshots/docs-light-1440.png differ diff --git a/tests/one-identity/screenshots/docs-light-390.png b/tests/one-identity/screenshots/docs-light-390.png new file mode 100644 index 00000000000..7912d4a39f6 Binary files /dev/null and b/tests/one-identity/screenshots/docs-light-390.png differ diff --git a/tests/one-identity/screenshots/home-dark-1440.png b/tests/one-identity/screenshots/home-dark-1440.png new file mode 100644 index 00000000000..9731c7c379e Binary files /dev/null and b/tests/one-identity/screenshots/home-dark-1440.png differ diff --git a/tests/one-identity/screenshots/home-dark-390.png b/tests/one-identity/screenshots/home-dark-390.png new file mode 100644 index 00000000000..b3e93e1e1df Binary files /dev/null and b/tests/one-identity/screenshots/home-dark-390.png differ diff --git a/tests/one-identity/screenshots/home-light-1440.png b/tests/one-identity/screenshots/home-light-1440.png new file mode 100644 index 00000000000..e96f5158f19 Binary files /dev/null and b/tests/one-identity/screenshots/home-light-1440.png differ diff --git a/tests/one-identity/screenshots/home-light-390.png b/tests/one-identity/screenshots/home-light-390.png new file mode 100644 index 00000000000..4e517049451 Binary files /dev/null and b/tests/one-identity/screenshots/home-light-390.png differ diff --git a/tests/one-identity/screenshots/inventory.json b/tests/one-identity/screenshots/inventory.json new file mode 100644 index 00000000000..d9cf2130778 --- /dev/null +++ b/tests/one-identity/screenshots/inventory.json @@ -0,0 +1,52 @@ +{ + "sourceRevision": "a274429e2c70594dfda10a979ddb6e455c4ef473", + "syntheticStatus": true, + "mode": "chrome", + "baselineReport": "baseline-report.json", + "screenshots": [ + { + "file": "docs-dark-1440.png", + "bytes": 198286, + "sha256": "2a9b80e9a01c515f64bbc51a313d2663f47d5df9f8b1f5852ba829e5f537427d" + }, + { + "file": "docs-dark-390.png", + "bytes": 88741, + "sha256": "8d607549036ebca8615730032905b0347092146d656869a2d2cdc8c54426ce7e" + }, + { + "file": "docs-light-1440.png", + "bytes": 198456, + "sha256": "4bd71386fb02e8bd5892afce0643cdc6c319b82fe0051004cf985e98db40015d" + }, + { + "file": "docs-light-390.png", + "bytes": 87744, + "sha256": "1243dcf7b48bb1b495fb7c7366ba671f9e2cd04545ea79049f244425f418dd99" + }, + { + "file": "home-dark-1440.png", + "bytes": 227927, + "sha256": "3dcf73635dce42f5548167705e250830be918a83334d3ca6b711c5e2f37b3753" + }, + { + "file": "home-dark-390.png", + "bytes": 210993, + "sha256": "ffb37d761e511f6d47d1b34fca345295fc5eb16cf2d3284cda6a4ddec63ea7ed" + }, + { + "file": "home-light-1440.png", + "bytes": 224129, + "sha256": "a52144dbec1ed23f453a91c14fe459d8e2ef20a14df7cff001371ecd4fe3d19f" + }, + { + "file": "home-light-390.png", + "bytes": 205496, + "sha256": "4974b701030172105af5ffd4178be47dc0ed59337a626e2717dabfed46278530" + } + ], + "laterSourceDelta": { + "revision": "f8198fa25998eac2aeb537f3b1655bfe59c841a1", + "scope": "Equivalent offset arithmetic extracted to independent statements; 14 status controls pass; no layout or label change." + } +} diff --git a/tests/one-identity/screenshots/report.json b/tests/one-identity/screenshots/report.json new file mode 100644 index 00000000000..fef57c78f7e --- /dev/null +++ b/tests/one-identity/screenshots/report.json @@ -0,0 +1,36 @@ +{ + "checks": [ + "light/1440: home fits viewport and uses selected theme", + "light/1440: action text meets 4.5:1 contrast", + "light/1440: global nav labels are plain and destinations preserved", + "light/1440: docs remain contained after scoped nav correction", + "light/390: home fits viewport and uses selected theme", + "light/390: action text meets 4.5:1 contrast", + "light/390: global nav labels are plain and destinations preserved", + "light/390: docs remain contained after scoped nav correction", + "dark/1440: home fits viewport and uses selected theme", + "dark/1440: action text meets 4.5:1 contrast", + "dark/1440: global nav labels are plain and destinations preserved", + "dark/1440: docs remain contained after scoped nav correction", + "dark/390: home fits viewport and uses selected theme", + "dark/390: action text meets 4.5:1 contrast", + "dark/390: global nav labels are plain and destinations preserved", + "dark/390: docs remain contained after scoped nav correction" + ], + "screenshots": [ + "home-light-1440.png", + "docs-light-1440.png", + "home-light-390.png", + "docs-light-390.png", + "home-dark-1440.png", + "docs-dark-1440.png", + "home-dark-390.png", + "docs-dark-390.png" + ], + "browser": "156.0.8078.80", + "syntheticStatus": true, + "sourceRevision": "a274429e2c70594dfda10a979ddb6e455c4ef473", + "clientVersion": "1.62.1", + "mode": "chrome", + "buildIndexSha256": "46c0aa4f342ed9350772875f0aab21204f436239e69ff159e5cefac926e5e9ee" +}