@continuous-excellence/ze-great-dashboard-aws 0.13.7 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ export type ComputeMode = 'lambda' | 'ecs';
2
+ export type ComputeModeInputs = {
3
+ /** Mode from the checked-in bootstrap manifest. */
4
+ persisted?: string;
5
+ /** Mode from an existing/generated CloudFormation parameter file. */
6
+ artifact?: string;
7
+ /** Optional CLI assertion used for inspection and repair. */
8
+ explicit?: string;
9
+ };
10
+ /** Resolves every deployment mode decision and rejects contradictory selectors. */
11
+ export declare function resolveComputeMode(input?: ComputeModeInputs): ComputeMode;
12
+ export declare function computeMode(config: {
13
+ mode?: string;
14
+ }): ComputeMode;
package/dist/guided.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type BootstrapConfig } from './bootstrap.js';
1
+ import { type BootstrapConfig, type ComputeMode } from './bootstrap.js';
2
2
  import { type BootstrapProvider, bootstrapHandoff, type CommandRunner } from './handoff.js';
3
3
  export type BootstrapCheckStatus = 'ready' | 'missing' | 'mismatch' | 'unverified';
4
4
  export type BootstrapPreflightCheck = {
@@ -11,6 +11,7 @@ export type BootstrapPreflight = {
11
11
  checks: BootstrapPreflightCheck[];
12
12
  };
13
13
  export type BootstrapInitInput = {
14
+ mode?: ComputeMode;
14
15
  slug: string;
15
16
  repository: string;
16
17
  environment: string;
package/dist/index.d.ts CHANGED
@@ -1,8 +1,10 @@
1
- export { type BootstrapConfig, type BootstrapConsistency, type BootstrapKind, type BootstrapPlan, type BootstrapTemplateInspection, bootstrapConsistency, bootstrapContractVersion, bootstrapPlan, bootstrapTemplate, bootstrapTemplatePath, bootstrapTemplateRevision, type CloudFormationParameterValue, coreBootstrapOutputs, type DeployedBootstrapStack, deployedBootstrapStack, mergeBootstrapParameters, requiredBootstrapParameters, } from './bootstrap.js';
1
+ import type { ComputeMode } from './bootstrap.js';
2
+ export { type BootstrapConfig, type BootstrapConsistency, type BootstrapKind, type BootstrapPlan, type BootstrapTemplateInspection, bootstrapConsistency, bootstrapContractVersion, bootstrapPlan, bootstrapTemplate, bootstrapTemplatePath, bootstrapTemplateRevision, type CloudFormationParameterValue, type ComputeMode, computeMode, coreBootstrapOutputs, type DeployedBootstrapStack, deployedBootstrapStack, mergeBootstrapParameters, requireComputeMode, requiredBootstrapParameters, resolveComputeMode, } from './bootstrap.js';
2
3
  export { type BootstrapCheck, type BootstrapCheckDependencies, type BootstrapResourceDifference, type BootstrapResourceDrift, type BootstrapResourceDriftResult, type BootstrapStackCheck, checkBootstrap, formatBootstrapCheckText, } from './bootstrap-check.js';
3
4
  export { type BootstrapCheckStatus, type BootstrapInitInput, type BootstrapPreflight, type BootstrapPreflightCheck, bootstrapGuide, bootstrapPreflight, scaffoldBootstrapManifest, } from './guided.js';
4
5
  export { type BootstrapHandoff, type BootstrapPhase, type BootstrapProvider, type BootstrapVerification, bootstrapHandoff, type CommandRunner, githubOidcProvider, verifyBootstrap, } from './handoff.js';
5
6
  export type ReleaseMetadata = {
7
+ computeMode: ComputeMode;
6
8
  dashboardVersion: string;
7
9
  clientAssetUrl: string;
8
10
  serverRuntimeVersion: string;
@@ -14,6 +16,7 @@ export type ReleaseMetadata = {
14
16
  };
15
17
  export type PackagedRelease = ReleaseMetadata & {
16
18
  artifactKey: string;
19
+ image?: string;
17
20
  };
18
21
  export type LambdaPackageOptions = {
19
22
  boardConfigPath: string;
@@ -23,7 +26,13 @@ export type LambdaPackageOptions = {
23
26
  /** ARN of the consumer-owned JSON credential-map secret, never a secret value. */
24
27
  secretReference?: string;
25
28
  };
29
+ export type EcsPackageOptions = Omit<LambdaPackageOptions, 'secretReference'> & {
30
+ secretReference?: string;
31
+ imageReference?: string;
32
+ };
26
33
  export declare function packageLambda(options: LambdaPackageOptions): Promise<PackagedRelease>;
34
+ /** Packages the published container reference and ECS CloudFormation handoff. */
35
+ export declare function packageEcs(options: EcsPackageOptions): Promise<PackagedRelease>;
27
36
  export type PublishClientAssetsOptions = {
28
37
  assetsDir: string;
29
38
  assetsBucket: string;
@@ -32,4 +41,4 @@ export type PublishClientAssetsOptions = {
32
41
  };
33
42
  /** Publishes the versioned client half of a provider-managed release. */
34
43
  export declare function publishClientAssets(options: PublishClientAssetsOptions): Promise<string>;
35
- export declare function cloudFormationTemplate(): Promise<string>;
44
+ export declare function cloudFormationTemplate(mode?: ComputeMode): Promise<string>;
package/dist/index.js CHANGED
@@ -721,6 +721,9 @@ function zipSync(data, opts) {
721
721
  return out;
722
722
  }
723
723
 
724
+ // packages/aws/src/index.ts
725
+ import { parseDocument, Scalar } from "yaml";
726
+
724
727
  // packages/aws/src/release.ts
725
728
  import { createHash } from "node:crypto";
726
729
  import { mkdir, readFile, writeFile } from "node:fs/promises";
@@ -843,16 +846,52 @@ function sha256(value2) {
843
846
  // packages/aws/src/bootstrap.ts
844
847
  import { readFile as readFile2 } from "node:fs/promises";
845
848
  import { fileURLToPath } from "node:url";
849
+
850
+ // packages/aws/src/compute-mode.ts
851
+ function valid(value2, label) {
852
+ if (value2 !== "lambda" && value2 !== "ecs")
853
+ throw new Error(`${label} must be lambda or ecs (received ${value2})`);
854
+ return value2;
855
+ }
856
+ function resolveComputeMode(input = {}) {
857
+ const persisted = input.persisted === void 0 ? void 0 : valid(input.persisted, "persisted mode");
858
+ const artifact = input.artifact === void 0 ? void 0 : valid(input.artifact, "ComputeMode");
859
+ const explicit = input.explicit === void 0 ? void 0 : valid(input.explicit, "mode");
860
+ const selected = persisted ?? explicit ?? artifact ?? "lambda";
861
+ if (artifact !== void 0 && artifact !== selected)
862
+ throw new Error(
863
+ `ComputeMode ${artifact} disagrees with persisted mode ${selected}; regenerate the bootstrap and parameter artifacts to change deployment mode`
864
+ );
865
+ if (explicit !== void 0 && explicit !== selected)
866
+ throw new Error(
867
+ `explicit mode ${explicit} disagrees with persisted mode ${selected}; regenerate the bootstrap and parameter artifacts to change deployment mode`
868
+ );
869
+ return selected;
870
+ }
871
+ function computeMode(config) {
872
+ return resolveComputeMode({ persisted: config.mode });
873
+ }
874
+
875
+ // packages/aws/src/bootstrap.ts
846
876
  var templates = {
847
- core: "../bootstrap/core-v1.yml",
848
- "github-oidc": "../bootstrap/github-oidc-v2.yml"
877
+ core: {
878
+ lambda: "../bootstrap/core-v1.yml",
879
+ ecs: "../bootstrap/core-ecs-v1.yml"
880
+ },
881
+ "github-oidc": {
882
+ lambda: "../bootstrap/github-oidc-v2.yml",
883
+ ecs: "../bootstrap/github-oidc-ecs-v1.yml"
884
+ }
849
885
  };
850
- async function bootstrapTemplatePath(kind) {
886
+ function requireComputeMode(persisted, explicit) {
887
+ return resolveComputeMode({ persisted: persisted.mode, explicit });
888
+ }
889
+ async function bootstrapTemplatePath(kind, mode = "lambda") {
851
890
  if (!Object.hasOwn(templates, kind)) throw new Error(`Unknown bootstrap template: ${kind}`);
852
- return fileURLToPath(new URL(templates[kind], import.meta.url));
891
+ return fileURLToPath(new URL(templates[kind][mode], import.meta.url));
853
892
  }
854
- async function bootstrapTemplate(kind) {
855
- return readFile2(await bootstrapTemplatePath(kind), "utf8");
893
+ async function bootstrapTemplate(kind, mode = "lambda") {
894
+ return readFile2(await bootstrapTemplatePath(kind, mode), "utf8");
856
895
  }
857
896
  function bootstrapContractVersion(template) {
858
897
  const version = template.match(/BootstrapContractVersion:\s*\{\s*Value:\s*'([^']+)'\s*}/)?.[1];
@@ -889,9 +928,10 @@ async function bootstrapPlan(config) {
889
928
  );
890
929
  const packageTemplates = await Promise.all(
891
930
  ["core", "github-oidc"].map(async (kind) => {
931
+ const mode = computeMode(config);
892
932
  const [path, template] = await Promise.all([
893
- bootstrapTemplatePath(kind),
894
- bootstrapTemplate(kind)
933
+ bootstrapTemplatePath(kind, mode),
934
+ bootstrapTemplate(kind, mode)
895
935
  ]);
896
936
  return inspectTemplate(kind, path, template);
897
937
  })
@@ -964,6 +1004,7 @@ function bootstrapConsistency(kind, config, stackInput, expectedContract, expect
964
1004
  ArtifactBucketName: config.core?.artifactBucketName,
965
1005
  ApplicationStackName: config.core?.applicationStackName,
966
1006
  DashboardFunctionName: config.core?.dashboardFunctionName,
1007
+ ComputeMode: config.mode,
967
1008
  RuntimeSecretArn: config.core?.runtimeSecretArn ?? "",
968
1009
  ArtifactKmsKeyArn: config.core?.artifactKmsKeyArn ?? ""
969
1010
  } : {
@@ -975,6 +1016,7 @@ function bootstrapConsistency(kind, config, stackInput, expectedContract, expect
975
1016
  CoreBootstrapStackName: config.core?.stackName,
976
1017
  ApplicationStackName: config.core?.applicationStackName,
977
1018
  ArtifactBucketName: config.core?.artifactBucketName,
1019
+ ComputeMode: config.mode,
978
1020
  CloudFormationExecutionRoleArn: coreOutputs.CloudFormationExecutionRoleArn,
979
1021
  ConsumerGatewayStackName: config.githubOidc?.consumerGatewayStackName ?? ""
980
1022
  };
@@ -1037,7 +1079,7 @@ function requiredBootstrapParameters(kind, values) {
1037
1079
  ];
1038
1080
  const missing = keys.filter((key) => !values[key]);
1039
1081
  if (missing.length) throw new Error(`Missing required bootstrap values: ${missing.join(", ")}`);
1040
- const optional2 = kind === "core" ? ["RuntimeSecretArn", "ArtifactKmsKeyArn"] : ["ConsumerGatewayStackName"];
1082
+ const optional2 = kind === "core" ? ["RuntimeSecretArn", "ArtifactKmsKeyArn", "ComputeMode"] : ["ConsumerGatewayStackName", "ComputeMode"];
1041
1083
  return [...keys, ...optional2].filter((key) => values[key] !== void 0).map((ParameterKey) => ({ ParameterKey, ParameterValue: values[ParameterKey] ?? "" }));
1042
1084
  }
1043
1085
 
@@ -1460,14 +1502,16 @@ var githubOidcProvider = {
1460
1502
  }
1461
1503
  }
1462
1504
  };
1463
- async function contracts() {
1505
+ async function contracts(config) {
1464
1506
  return {
1465
- core: bootstrapContractVersion(await bootstrapTemplate("core")),
1466
- githubOidc: bootstrapContractVersion(await bootstrapTemplate("github-oidc"))
1507
+ core: bootstrapContractVersion(await bootstrapTemplate("core", computeMode(config))),
1508
+ githubOidc: bootstrapContractVersion(
1509
+ await bootstrapTemplate("github-oidc", computeMode(config))
1510
+ )
1467
1511
  };
1468
1512
  }
1469
1513
  async function bootstrapHandoff(input) {
1470
- const expectedContracts = await contracts();
1514
+ const expectedContracts = await contracts(input.config);
1471
1515
  const coreCapturePath = input.coreStackPath ?? workFile(input.workDir, "core-deployed-stack.json");
1472
1516
  const core = input.coreStack === void 0 ? void 0 : deployedBootstrapStack(input.coreStack, expectedContracts.core);
1473
1517
  const oidc = input.githubOidcStack === void 0 ? void 0 : deployedBootstrapStack(input.githubOidcStack, expectedContracts.githubOidc);
@@ -1477,7 +1521,7 @@ async function bootstrapHandoff(input) {
1477
1521
  ApplicationStackName: input.config.core?.applicationStackName,
1478
1522
  DashboardFunctionName: input.config.core?.dashboardFunctionName
1479
1523
  });
1480
- const templatePath = await bootstrapTemplatePath("core");
1524
+ const templatePath = await bootstrapTemplatePath("core", computeMode(input.config));
1481
1525
  return {
1482
1526
  phase: "core",
1483
1527
  expectedContracts,
@@ -1517,7 +1561,7 @@ async function bootstrapHandoff(input) {
1517
1561
  ConsumerGatewayStackName: input.config.githubOidc?.consumerGatewayStackName,
1518
1562
  ...coreBootstrapOutputs(core)
1519
1563
  });
1520
- const templatePath = await bootstrapTemplatePath("github-oidc");
1564
+ const templatePath = await bootstrapTemplatePath("github-oidc", computeMode(input.config));
1521
1565
  return {
1522
1566
  phase: "github-oidc",
1523
1567
  expectedContracts,
@@ -1584,7 +1628,7 @@ function assertEqual(actual, expected, label) {
1584
1628
  );
1585
1629
  }
1586
1630
  async function verifyBootstrap(input) {
1587
- const expected = await contracts();
1631
+ const expected = await contracts(input.config);
1588
1632
  const core = deployedBootstrapStack(input.coreStack, expected.core);
1589
1633
  const oidc = deployedBootstrapStack(input.githubOidcStack, expected.githubOidc);
1590
1634
  for (const [kind, capture] of [
@@ -1732,6 +1776,7 @@ async function discovered(input) {
1732
1776
  const repo = await optional(input.runner, "gh", ["api", `repos/${input.repository}`]);
1733
1777
  const github = parseOrUnavailable(repo) ?? {};
1734
1778
  return {
1779
+ ...input.mode ? { mode: input.mode } : {},
1735
1780
  accountId,
1736
1781
  region: region2?.trim() || void 0,
1737
1782
  ownerId: typeof github.owner?.id === "number" || typeof github.owner?.id === "string" ? String(github.owner.id) : void 0,
@@ -1747,6 +1792,8 @@ async function scaffoldBootstrapManifest(input) {
1747
1792
  const environment = value(input.environment, "environment");
1748
1793
  const providerArn = value(input.providerArn, "github OIDC provider ARN");
1749
1794
  if (!accountFromArn(providerArn)) throw new Error("github OIDC provider ARN is invalid");
1795
+ if (input.mode && input.mode !== "lambda" && input.mode !== "ecs")
1796
+ throw new Error("mode must be lambda or ecs");
1750
1797
  const found = await discovered(input);
1751
1798
  const accountId = input.accountId ?? found.accountId;
1752
1799
  const region2 = input.region ?? found.region;
@@ -1970,27 +2017,45 @@ async function bootstrapGuide(input) {
1970
2017
 
1971
2018
  // packages/aws/src/index.ts
1972
2019
  var run = promisify(execFile);
2020
+ function requireImageDigest(image) {
2021
+ if (!/^[^\s@]+@sha256:[a-f0-9]{64}$/.test(image))
2022
+ throw new Error(
2023
+ "ECS imageReference must be an immutable registry digest (image@sha256:<64 hex>)"
2024
+ );
2025
+ return image;
2026
+ }
1973
2027
  function deploymentTemplate(template, values) {
1974
- const listed = template.match(/^ {2}PackageManagedParameters: \[([^\]]*)\]\s*$/m)?.[1];
1975
- if (listed === void 0)
2028
+ const document = parseDocument(template);
2029
+ if (document.errors.length)
2030
+ throw new Error(`Invalid CloudFormation template: ${document.errors[0]}`);
2031
+ const managedValue = document.toJS().Metadata?.PackageManagedParameters;
2032
+ if (!Array.isArray(managedValue))
1976
2033
  throw new Error("CloudFormation template has no PackageManagedParameters metadata");
1977
- const managed = listed.split(",").map((key) => key.trim()).filter(Boolean);
2034
+ const managed = managedValue.filter((key) => typeof key === "string");
2035
+ if (managed.length !== managedValue.length)
2036
+ throw new Error("CloudFormation template has invalid PackageManagedParameters metadata");
1978
2037
  const missing = managed.filter((key) => !Object.hasOwn(values, key));
1979
2038
  const unknown = Object.keys(values).filter((key) => !managed.includes(key));
1980
2039
  if (missing.length || unknown.length)
1981
2040
  throw new Error(
1982
2041
  `Package-managed CloudFormation parameters are out of sync (missing: ${missing.join(", ") || "none"}; unknown: ${unknown.join(", ") || "none"})`
1983
2042
  );
1984
- return managed.reduce((rendered, key) => {
1985
- const pattern = new RegExp(`^ ${key}: \\{[^\\n]+\\}\\s*$`, "m");
1986
- const declaration = rendered.match(pattern)?.[0];
1987
- if (!declaration || /\bDefault:/.test(declaration))
1988
- throw new Error(`Unable to set the CloudFormation default for ${key}`);
1989
- return rendered.replace(
1990
- pattern,
1991
- declaration.replace(/\s*}\s*$/, `, Default: ${JSON.stringify(values[key])} }`)
1992
- );
1993
- }, template);
2043
+ for (const key of managed) {
2044
+ const path = ["Parameters", key];
2045
+ const parameter = document.getIn(path);
2046
+ if (!parameter || typeof parameter !== "object")
2047
+ throw new Error(`CloudFormation template has no declaration for ${key}`);
2048
+ const current = document.getIn([...path, "Default"], true);
2049
+ if (current !== void 0) {
2050
+ if (String(current) !== values[key])
2051
+ throw new Error(`Package-managed CloudFormation parameter ${key} has a conflicting default`);
2052
+ } else {
2053
+ const scalar = new Scalar(values[key]);
2054
+ scalar.type = "QUOTE_DOUBLE";
2055
+ document.setIn([...path, "Default"], scalar);
2056
+ }
2057
+ }
2058
+ return document.toString({ lineWidth: 0 });
1994
2059
  }
1995
2060
  async function packageLambda(options) {
1996
2061
  const outputDir = resolve2(options.outputDir);
@@ -2009,6 +2074,7 @@ async function packageLambda(options) {
2009
2074
  });
2010
2075
  const runtimeMetadata = {
2011
2076
  ...release.metadata,
2077
+ computeMode: "lambda",
2012
2078
  artifactChecksums: {
2013
2079
  ...release.metadata.artifactChecksums,
2014
2080
  "index.mjs": sha256(await readFile3(join2(runtimeDir, "index.mjs")))
@@ -2045,13 +2111,49 @@ async function packageLambda(options) {
2045
2111
  `);
2046
2112
  await writeFile2(
2047
2113
  join2(outputDir, "template.yml"),
2048
- deploymentTemplate(await cloudFormationTemplate(), {
2114
+ deploymentTemplate(await cloudFormationTemplate("lambda"), {
2115
+ ComputeMode: "lambda",
2049
2116
  LambdaArtifactKey: packagedRelease.artifactKey,
2050
2117
  DashboardVersion: packagedRelease.dashboardVersion
2051
2118
  })
2052
2119
  );
2053
2120
  return packagedRelease;
2054
2121
  }
2122
+ async function packageEcs(options) {
2123
+ const outputDir = resolve2(options.outputDir);
2124
+ await mkdir2(outputDir, { recursive: true });
2125
+ const imageReference = requireImageDigest(options.imageReference ?? "");
2126
+ const release = await assembleRelease({
2127
+ boardConfigPath: options.boardConfigPath,
2128
+ outputDir,
2129
+ version: options.version,
2130
+ providers: ["aws-ecs-fargate"],
2131
+ assetDomain: options.assetDomain,
2132
+ secretReference: options.secretReference
2133
+ });
2134
+ const packagedRelease = {
2135
+ ...release.metadata,
2136
+ computeMode: "ecs",
2137
+ image: imageReference,
2138
+ artifactKey: `ecs/${sha256(JSON.stringify({ ...release.metadata, image: imageReference }))}`
2139
+ };
2140
+ await writeFile2(join2(outputDir, "release.json"), `${JSON.stringify(packagedRelease, null, 2)}
2141
+ `);
2142
+ await writeFile2(
2143
+ join2(outputDir, "template.yml"),
2144
+ deploymentTemplate(await cloudFormationTemplate("ecs"), {
2145
+ ComputeMode: "ecs",
2146
+ Image: imageReference,
2147
+ DashboardVersion: packagedRelease.dashboardVersion
2148
+ })
2149
+ );
2150
+ await writeFile2(
2151
+ join2(outputDir, "SHA256SUMS"),
2152
+ `${sha256(await readFile3(join2(outputDir, "release.json")))} release.json
2153
+ `
2154
+ );
2155
+ return packagedRelease;
2156
+ }
2055
2157
  async function publishClientAssets(options) {
2056
2158
  const assetsDir = resolve2(options.assetsDir);
2057
2159
  await readFile3(join2(assetsDir, "index.html"));
@@ -2076,8 +2178,13 @@ async function publishClientAssets(options) {
2076
2178
  ]);
2077
2179
  return assetPath;
2078
2180
  }
2079
- async function cloudFormationTemplate() {
2080
- return readFile3(fileURLToPath2(new URL("../template.yml", import.meta.url)), "utf8");
2181
+ async function cloudFormationTemplate(mode = "lambda") {
2182
+ return readFile3(
2183
+ fileURLToPath2(
2184
+ new URL(mode === "ecs" ? "../template-ecs.yml" : "../template.yml", import.meta.url)
2185
+ ),
2186
+ "utf8"
2187
+ );
2081
2188
  }
2082
2189
  export {
2083
2190
  bootstrapConsistency,
@@ -2091,14 +2198,18 @@ export {
2091
2198
  bootstrapTemplateRevision,
2092
2199
  checkBootstrap,
2093
2200
  cloudFormationTemplate,
2201
+ computeMode,
2094
2202
  coreBootstrapOutputs,
2095
2203
  deployedBootstrapStack,
2096
2204
  formatBootstrapCheckText,
2097
2205
  githubOidcProvider,
2098
2206
  mergeBootstrapParameters,
2207
+ packageEcs,
2099
2208
  packageLambda,
2100
2209
  publishClientAssets,
2210
+ requireComputeMode,
2101
2211
  requiredBootstrapParameters,
2212
+ resolveComputeMode,
2102
2213
  scaffoldBootstrapManifest,
2103
2214
  verifyBootstrap
2104
2215
  };
package/dist/lambda.mjs CHANGED
@@ -63988,6 +63988,12 @@ var panelSchema = external_exports.looseObject({
63988
63988
  /** Advisory in v1 — a panel without a position renders in config order rather than not at all. */
63989
63989
  position: positionSchema.optional(),
63990
63990
  refresh: durationSchema.optional(),
63991
+ /** Active pipeline runs are checked more often than normal panels. */
63992
+ running_refresh: durationSchema.optional(),
63993
+ /** Polling cadence around the estimated completion boundary. */
63994
+ running_completion_refresh: durationSchema.optional(),
63995
+ /** Maximum length of the tighter completion polling burst. */
63996
+ running_completion_window: durationSchema.optional(),
63991
63997
  /** Optional active-run treatment. Omitted selects a visible treatment at random. */
63992
63998
  running_animation: runningAnimationSchema.optional(),
63993
63999
  /** Local animation-demo cycle duration; ignored by other panel types. */
@@ -64008,6 +64014,9 @@ var sourceSchema = external_exports.looseObject({
64008
64014
  });
64009
64015
  var boardSchema = external_exports.object({
64010
64016
  refresh: durationSchema.optional(),
64017
+ running_refresh: durationSchema.optional(),
64018
+ running_completion_refresh: durationSchema.optional(),
64019
+ running_completion_window: durationSchema.optional(),
64011
64020
  panels: external_exports.array(panelSchema).min(1).superRefine((panels, ctx) => {
64012
64021
  const seen = /* @__PURE__ */ new Map();
64013
64022
  panels.forEach((panel, index) => {
@@ -67150,8 +67159,10 @@ function isLocalHost(host) {
67150
67159
  async function startup(options = {}) {
67151
67160
  const config2 = loadConfig2();
67152
67161
  const fetcher = options.fetcher ?? globalThis.fetch;
67153
- await waitForTemplate(config2, fetcher);
67154
- const boardConfig = await loadBoardConfig(config2.boardConfigUrl, fetcher);
67162
+ const [, boardConfig] = await Promise.all([
67163
+ waitForTemplate(config2, fetcher),
67164
+ loadBoardConfig(config2.boardConfigUrl, fetcher)
67165
+ ]);
67155
67166
  const board = selectBoard(config2.board, boardConfig);
67156
67167
  const resolvedConfig = { ...config2, board };
67157
67168
  const credentialNames = Object.values(boardConfig.sources).map((source) => source.token_env).filter((name) => Boolean(name));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@continuous-excellence/ze-great-dashboard-aws",
3
- "version": "0.13.7",
3
+ "version": "0.14.0",
4
4
  "type": "module",
5
5
  "description": "AWS Lambda and CloudFormation adapter for Ze Great Dashboard.",
6
6
  "keywords": [
@@ -31,6 +31,7 @@
31
31
  "client",
32
32
  "bootstrap",
33
33
  "template.yml",
34
+ "template-ecs.yml",
34
35
  "LICENSE"
35
36
  ],
36
37
  "dependencies": {
@@ -0,0 +1,101 @@
1
+ AWSTemplateFormatVersion: '2010-09-09'
2
+ Description: Ze Great Dashboard ECS/Fargate application (consumer-owned network and ingress)
3
+
4
+ Metadata:
5
+ PackageManagedParameters: [ComputeMode, Image, DashboardVersion]
6
+
7
+ Parameters:
8
+ ComputeMode: { Type: String, Default: ecs, AllowedValues: [ecs] }
9
+ Image: { Type: String }
10
+ DashboardVersion: { Type: String }
11
+ Name: { Type: String, Default: dashboard }
12
+ AssetBaseUrl: { Type: String, Default: https://public-assets.zegreatrob.com }
13
+ BoardConfigPath: { Type: String, Default: /app/boards/example.yaml }
14
+ Subnets: { Type: List<AWS::EC2::Subnet::Id> }
15
+ SecurityGroups: { Type: List<AWS::EC2::SecurityGroup::Id> }
16
+ DesiredCount: { Type: Number, Default: 1, MinValue: 1 }
17
+ Cpu: { Type: String, Default: '256' }
18
+ Memory: { Type: String, Default: '512' }
19
+ ContainerPort: { Type: Number, Default: 3000 }
20
+ LogRetentionInDays: { Type: Number, Default: 14, MinValue: 1 }
21
+ SecretReference: { Type: String, Default: '' }
22
+
23
+ Conditions:
24
+ HasSecretReference: !Not [!Equals [!Ref SecretReference, '']]
25
+
26
+ Resources:
27
+ Cluster:
28
+ Type: AWS::ECS::Cluster
29
+ Properties: { ClusterName: !Ref Name }
30
+ LogGroup:
31
+ Type: AWS::Logs::LogGroup
32
+ Properties: { LogGroupName: !Sub '/aws/ecs/${Name}', RetentionInDays: !Ref LogRetentionInDays }
33
+ ExecutionRole:
34
+ Type: AWS::IAM::Role
35
+ Properties:
36
+ RoleName: !Sub '${Name}-execution'
37
+ AssumeRolePolicyDocument: { Version: '2012-10-17', Statement: [{ Effect: Allow, Principal: { Service: ecs-tasks.amazonaws.com }, Action: sts:AssumeRole }] }
38
+ ManagedPolicyArns: [arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy]
39
+ TaskRole:
40
+ Type: AWS::IAM::Role
41
+ Properties:
42
+ RoleName: !Sub '${Name}-task'
43
+ AssumeRolePolicyDocument: { Version: '2012-10-17', Statement: [{ Effect: Allow, Principal: { Service: ecs-tasks.amazonaws.com }, Action: sts:AssumeRole }] }
44
+ Policies:
45
+ - PolicyName: read-consumer-credentials
46
+ PolicyDocument:
47
+ Version: '2012-10-17'
48
+ Statement:
49
+ - Effect: Allow
50
+ Action: [secretsmanager:DescribeSecret, secretsmanager:GetSecretValue]
51
+ Resource: !If [HasSecretReference, !Ref SecretReference, !Sub 'arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:ze-great-dashboard-no-secret-configured']
52
+ - Effect: Allow
53
+ Action: ssm:GetParameter
54
+ Resource: !If [HasSecretReference, !Ref SecretReference, !Sub 'arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/ze-great-dashboard-no-credential-configured']
55
+ - Effect: Allow
56
+ Action: kms:Decrypt
57
+ Resource: '*'
58
+ Condition:
59
+ StringEquals:
60
+ 'kms:ViaService': !Sub 'ssm.${AWS::Region}.amazonaws.com'
61
+ 'kms:EncryptionContext:PARAMETER_ARN': !Ref SecretReference
62
+ TaskDefinition:
63
+ Type: AWS::ECS::TaskDefinition
64
+ Properties:
65
+ Family: !Ref Name
66
+ Cpu: !Ref Cpu
67
+ Memory: !Ref Memory
68
+ NetworkMode: awsvpc
69
+ RequiresCompatibilities: [FARGATE]
70
+ ExecutionRoleArn: !GetAtt ExecutionRole.Arn
71
+ TaskRoleArn: !GetAtt TaskRole.Arn
72
+ ContainerDefinitions:
73
+ - Name: dashboard
74
+ Image: !Ref Image
75
+ Essential: true
76
+ PortMappings: [{ ContainerPort: !Ref ContainerPort, Protocol: tcp }]
77
+ Environment:
78
+ - { Name: ASSET_PATH, Value: !Sub '${AssetBaseUrl}/dashboard/${DashboardVersion}' }
79
+ - { Name: BOARD_CONFIG_URL, Value: !Ref BoardConfigPath }
80
+ - { Name: HOST, Value: 0.0.0.0 }
81
+ - { Name: PORT, Value: '3000' }
82
+ - { Name: SECRET_REFERENCE, Value: !If [HasSecretReference, !Ref SecretReference, ''] }
83
+ LogConfiguration: { LogDriver: awslogs, Options: { awslogs-group: !Ref LogGroup, awslogs-region: !Ref AWS::Region, awslogs-stream-prefix: dashboard } }
84
+ HealthCheck: { Command: [CMD-SHELL, 'node /app/docker-healthcheck.mjs'], Interval: 30, Timeout: 5, Retries: 3, StartPeriod: 20 }
85
+ Service:
86
+ Type: AWS::ECS::Service
87
+ DependsOn: LogGroup
88
+ Properties:
89
+ Cluster: !Ref Cluster
90
+ ServiceName: !Ref Name
91
+ DesiredCount: !Ref DesiredCount
92
+ LaunchType: FARGATE
93
+ TaskDefinition: !Ref TaskDefinition
94
+ NetworkConfiguration: { AwsvpcConfiguration: { AssignPublicIp: DISABLED, Subnets: !Ref Subnets, SecurityGroups: !Ref SecurityGroups } }
95
+
96
+ Outputs:
97
+ ComputeMode: { Value: !Ref ComputeMode }
98
+ ServiceName: { Value: !Ref Service }
99
+ ClusterName: { Value: !Ref Cluster }
100
+ Image: { Value: !Ref Image }
101
+ AssetPath: { Value: !Sub '${AssetBaseUrl}/dashboard/${DashboardVersion}' }
package/template.yml CHANGED
@@ -2,9 +2,10 @@ AWSTemplateFormatVersion: '2010-09-09'
2
2
  Description: Ze Great Dashboard private Lambda application (integrate through a customer-managed gateway)
3
3
 
4
4
  Metadata:
5
- PackageManagedParameters: [LambdaArtifactKey, DashboardVersion]
5
+ PackageManagedParameters: [ComputeMode, LambdaArtifactKey, DashboardVersion]
6
6
 
7
7
  Parameters:
8
+ ComputeMode: { Type: String, Default: lambda, AllowedValues: [lambda] }
8
9
  Name: { Type: String, Default: dashboard }
9
10
  LambdaArtifactBucket: { Type: String }
10
11
  LambdaArtifactKey: { Type: String }