@warpgogol/werkstatt-shared 0.13.0 → 0.14.1
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.
- package/README.md +26 -16
- package/README.uk.md +24 -14
- package/package.json +27 -69
- package/src/checks/result-helpers.ts +21 -4
- package/src/checks/tests/remediation-auto-populate.test.ts +83 -0
- package/src/integration/funnel.ts +78 -1
- package/src/integration/index.ts +19 -2
- package/src/integration/lagebild-ingress.ts +169 -0
- package/src/integration/orchestration.ts +7 -13
- package/src/integration/port-barrel.ts +9 -28
- package/src/integration/tests/funnel-bridge.test.ts +7 -7
- package/src/integration/tests/lagebild-ingress.test.ts +118 -0
- package/src/integration/vitest.config.ts +7 -0
- package/src/observability/typed-refs.ts +1 -1
- package/src/ontology/analytics/matomo-fleet.registry.yaml +1 -1
- package/src/ontology/archetypes/index.json +12 -9
- package/src/ontology/archetypes/index.yaml +26 -10
- package/src/ontology/blueprints/website-local.yaml +11 -11
- package/src/ontology/blueprints/website-service.yaml +20 -20
- package/src/ontology/capabilities/lead.submit.yaml +2 -2
- package/src/ontology/enums.ts +2 -2
- package/src/ontology/external-surfaces/jsonld-types.yaml +14 -14
- package/src/ontology/external-surfaces/url-schema.yaml +1 -1
- package/src/ontology/index.ts +2 -0
- package/src/ontology/manifest.ts +19 -0
- package/src/ontology/schemas/index.ts +7 -2
- package/src/ontology/schemas/system/integrations.ts +1 -1
- package/src/ontology/schemas/system/manifest.ts +38 -1
- package/src/ontology/schemas/system/tests/growth.test.ts +22 -0
- package/src/ontology/schemas/system/tests/integrations.test.ts +12 -0
- package/src/ontology/schemas/system/tests/manifest.test.ts +22 -0
- package/src/ontology/schemas/system/tests/page-output.test.ts +13 -0
- package/src/ontology/schemas/system/tests/release.test.ts +22 -0
- package/src/ontology/schemas/system/tests/text.test.ts +18 -0
- package/src/ontology/schemas/system/tests/verification.test.ts +12 -0
- package/src/ontology/schemas/system.ts +7 -2
- package/src/ontology/schemas/tests/biome.test.ts +19 -0
- package/src/ontology/schemas/tests/capability.test.ts +22 -0
- package/src/ontology/schemas/tests/constellation.test.ts +22 -0
- package/src/ontology/schemas/tests/dns-records.test.ts +13 -0
- package/src/ontology/schemas/tests/manifest-resolver.test.ts +13 -0
- package/src/ontology/schemas/tests/page-entry.test.ts +22 -0
- package/src/ontology/schemas/tests/section-archetype.test.ts +12 -0
- package/src/ontology/schemas/tests/site-family.test.ts +18 -0
- package/src/ontology/schemas/tests/system.test.ts +20 -0
- package/src/ontology/shared-section-props/common.ts +11 -0
- package/src/ontology/shared-section-props/visual-header.ts +1 -1
- package/src/share/agent/api-catalog.ts +5 -0
- package/src/share/agent/ard-catalog.ts +14 -0
- package/src/share/agent/index.ts +2 -0
- package/src/share/agent/manifest.ts +25 -2
- package/src/share/agent/mcp-card.ts +4 -0
- package/src/share/agent/openapi.ts +78 -0
- package/src/share/agent/search.test.ts +188 -0
- package/src/share/agent/search.ts +164 -0
- package/src/share/agent/tests/api-catalog.test.ts +8 -0
- package/src/share/agent/tests/ard-catalog.test.ts +8 -0
- package/src/share/agent/tests/capability.test.ts +14 -0
- package/src/share/agent/tests/fleet-catalog.test.ts +15 -0
- package/src/share/agent/tests/knowledge.test.ts +20 -0
- package/src/share/agent/tests/manifest.test.ts +9 -0
- package/src/share/agent/tests/mcp-card.test.ts +8 -0
- package/src/share/agent/tests/omit-empty.test.ts +33 -0
- package/src/share/agent/tests/openapi.test.ts +8 -0
- package/src/share/agent/tests/proof.test.ts +15 -0
- package/src/share/env.d.ts +0 -4
- package/src/share/knowledge/tests/claim.test.ts +11 -0
- package/src/share/knowledge/tests/derivation.test.ts +30 -0
- package/src/share/knowledge/tests/freshness.test.ts +18 -0
- package/src/share/knowledge/tests/ledger.test.ts +35 -0
- package/src/share/knowledge/tests/plan.test.ts +19 -0
- package/src/share/knowledge/tests/source.test.ts +19 -0
- package/src/share/remediation/remediation-catalog.ts +196 -0
- package/src/share/schemas/effects.ts +16 -0
- package/src/share/schemas/media.ts +4 -0
- package/src/share/schemas/section-shell.ts +2 -1
- package/src/share/schemas/tests/claim-records.test.ts +52 -0
- package/src/share/schemas/tests/claims.test.ts +23 -0
- package/src/share/schemas/tests/effects.test.ts +116 -0
- package/src/share/schemas/tests/features.test.ts +61 -0
- package/src/share/schemas/tests/horizontal-align.test.ts +16 -0
- package/src/share/schemas/tests/icon-color.test.ts +16 -0
- package/src/share/schemas/tests/live-photo.test.ts +40 -0
- package/src/share/schemas/tests/material-credit.test.ts +51 -0
- package/src/share/schemas/tests/media.test.ts +26 -0
- package/src/share/schemas/tests/navigation.test.ts +58 -0
- package/src/share/schemas/tests/page-base.test.ts +19 -0
- package/src/share/schemas/tests/participant.test.ts +26 -0
- package/src/share/schemas/tests/person.test.ts +12 -0
- package/src/share/schemas/tests/print.test.ts +30 -0
- package/src/share/schemas/tests/section-background.test.ts +31 -0
- package/src/share/schemas/tests/section-body.test.ts +16 -0
- package/src/share/schemas/tests/section-cards.test.ts +17 -0
- package/src/share/schemas/tests/section-cta.test.ts +41 -0
- package/src/share/schemas/tests/section-header.test.ts +31 -0
- package/src/share/schemas/tests/section-image.test.ts +24 -0
- package/src/share/schemas/tests/section-motion.test.ts +29 -0
- package/src/share/schemas/tests/section-shell.test.ts +42 -0
- package/src/share/schemas/tests/section-stats.test.ts +17 -0
- package/src/share/schemas/tests/site-background.test.ts +33 -0
- package/src/share/schemas/tests/standard-list-item.test.ts +16 -0
- package/src/share/scripts/external-link-qr.ts +9 -1
- package/src/share/scripts/gsap-counter.ts +2 -0
- package/src/share/scripts/inline-number-animation.ts +2 -0
- package/src/share/scripts/live-photos.ts +2 -0
- package/src/share/scripts/tests/external-link-qr.test.ts +8 -0
- package/src/share/scripts/tests/external-links.test.ts +25 -0
- package/src/share/scripts/tests/gsap-counter.test.ts +8 -0
- package/src/share/scripts/tests/gsap-mountain-journey.test.ts +8 -0
- package/src/share/scripts/tests/gsap-parallax.test.ts +8 -0
- package/src/share/scripts/tests/gsap-reveal.test.ts +8 -0
- package/src/share/scripts/tests/gsap-stagger.test.ts +8 -0
- package/src/share/scripts/tests/inline-number-animation.test.ts +8 -0
- package/src/share/scripts/tests/lenis.test.ts +8 -0
- package/src/share/scripts/tests/live-photos.test.ts +8 -0
- package/src/share/scripts/tests/lordicon.test.ts +8 -0
- package/src/share/scripts/tests/orchestrator.test.ts +8 -0
- package/src/share/scripts/tests/scheduler.test.ts +23 -0
- package/src/share/scripts/video-player.test.ts +210 -0
- package/src/share/scripts/video-player.ts +73 -13
- package/src/share/semantic/extract.ts +25 -1
- package/src/share/semantic/jsonld/tests/article.test.ts +89 -0
- package/src/share/semantic/jsonld/tests/breadcrumb.test.ts +62 -0
- package/src/share/semantic/jsonld/tests/collection-list.test.ts +39 -0
- package/src/share/semantic/jsonld/tests/faq.test.ts +60 -0
- package/src/share/semantic/jsonld/tests/initiative.test.ts +56 -0
- package/src/share/semantic/jsonld/tests/organization.test.ts +123 -0
- package/src/share/semantic/jsonld/tests/person.test.ts +89 -0
- package/src/share/semantic/jsonld/tests/service.test.ts +65 -0
- package/src/share/semantic/jsonld/tests/shared.test.ts +41 -0
- package/src/share/semantic/jsonld/tests/video.test.ts +84 -0
- package/src/share/semantic/jsonld/tests/webpage.test.ts +93 -0
- package/src/share/semantic/jsonld/tests/website.test.ts +27 -0
- package/src/share/semantic/jsonld/types.ts +1 -0
- package/src/share/semantic/models.ts +1 -0
- package/src/share/semantic/tests/ai.test.ts +55 -0
- package/src/share/semantic/tests/block-extraction.test.ts +34 -0
- package/src/share/semantic/tests/breadcrumbs.test.ts +127 -0
- package/src/share/semantic/tests/build-page.test.ts +122 -0
- package/src/share/semantic/tests/business-projection.test.ts +188 -0
- package/src/share/semantic/tests/derived-prices-loader.test.ts +46 -0
- package/src/share/semantic/tests/extract.test.ts +126 -0
- package/src/share/semantic/tests/feed.test.ts +113 -0
- package/src/share/semantic/tests/helpers.ts +32 -0
- package/src/share/semantic/tests/ids.test.ts +118 -0
- package/src/share/semantic/tests/image-sitemap.test.ts +109 -0
- package/src/share/semantic/tests/jsonld.test.ts +61 -0
- package/src/share/semantic/tests/llms-policy.test.ts +60 -0
- package/src/share/semantic/tests/llms.test.ts +118 -0
- package/src/share/semantic/tests/markdown-hygiene.test.ts +72 -0
- package/src/share/semantic/tests/markdown-page.test.ts +97 -0
- package/src/share/semantic/tests/markdown-twin-provenance.test.ts +152 -0
- package/src/share/semantic/tests/organization-profile.test.ts +107 -0
- package/src/share/semantic/tests/output-projection.test.ts +77 -0
- package/src/share/semantic/tests/page-markdown.test.ts +98 -0
- package/src/share/semantic/tests/page-utils.test.ts +88 -0
- package/src/share/semantic/tests/price-marker-resolver.test.ts +88 -0
- package/src/share/semantic/tests/robots.test.ts +70 -0
- package/src/share/semantic/tests/split-sentences.test.ts +57 -0
- package/src/share/semantic/tests/update-stamp.test.ts +102 -0
- package/src/share/tests/manifest.test.ts +51 -0
- package/src/share/tests/openapi.test.ts +30 -0
- package/src/share/types/qrcode-browser.d.ts +18 -0
- package/src/share/utility-registry.yaml +16 -2
- package/src/integration/crm-buffer.ts +0 -491
- package/src/integration-adapter-supabase-crm/adapter.ts +0 -437
- package/src/integration-adapter-supabase-crm/client.ts +0 -605
- package/src/integration-adapter-supabase-crm/index.ts +0 -28
- package/src/integration-adapter-supabase-crm/pipedrive-sync-target.ts +0 -482
- package/src/integration-adapter-supabase-crm/supabase-rest.ts +0 -89
- package/src/integration-adapter-supabase-crm/tenant-registry.ts +0 -334
- package/src/integration-adapter-supabase-crm/tests/change-balance.pbt.test.ts +0 -83
- package/src/integration-adapter-supabase-crm/tests/funnel-persistence.test.ts +0 -409
- package/src/integration-adapter-supabase-crm/tests/lifecycle-sync.test.ts +0 -288
- package/src/integration-adapter-supabase-crm/tests/stage-map.test.ts +0 -80
- package/src/integration-adapter-supabase-crm/vitest.config.ts +0 -8
- package/src/integration-adapter-supabase-crm/worker.ts +0 -223
- package/src/ontology/archetypes/components/currency-selector.yaml +0 -19
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Thin HTTP client for Lagebild API POST /v1/ingress. Werkstatt does NOT
|
|
4
|
+
recreate Lagebild semantics locally — it only calls the public WebsiteIngress v1
|
|
5
|
+
contract. The client builds the payload from form data, sends it with a service
|
|
6
|
+
principal API key, and returns the acceptance result. Replay protection is
|
|
7
|
+
built-in via a stable idempotency_key (derived from submission_id + tenant).</purpose>
|
|
8
|
+
<non-goals>
|
|
9
|
+
<item>Do not implement Lagebild domain logic — only the HTTP call.</item>
|
|
10
|
+
<item>Do not persist anything locally — the API owns canonical state.</item>
|
|
11
|
+
<item>Do not retry on non-2xx — the caller decides retry strategy.</item>
|
|
12
|
+
</non-goals>
|
|
13
|
+
</MODULE_CONTRACT>
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const LAGEBILD_INGRESS_CONTRACT_VERSION = 1 as const;
|
|
17
|
+
|
|
18
|
+
export type IngressInteractionKind =
|
|
19
|
+
| "inquiry"
|
|
20
|
+
| "contact_message"
|
|
21
|
+
| "callback_request"
|
|
22
|
+
| "appointment_request"
|
|
23
|
+
| "other";
|
|
24
|
+
|
|
25
|
+
export type IdentityClaimType =
|
|
26
|
+
| "person_name"
|
|
27
|
+
| "organization_name"
|
|
28
|
+
| "email"
|
|
29
|
+
| "phone"
|
|
30
|
+
| "website"
|
|
31
|
+
| "postal_address"
|
|
32
|
+
| "other";
|
|
33
|
+
|
|
34
|
+
export interface IdentityClaim {
|
|
35
|
+
readonly claim_type: IdentityClaimType;
|
|
36
|
+
readonly value: string;
|
|
37
|
+
readonly label?: string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface IngressOrigin {
|
|
41
|
+
readonly page_uri?: string | null;
|
|
42
|
+
readonly referrer_uri?: string | null;
|
|
43
|
+
readonly locale?: string | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type PolicyAssertionKind = "consent" | "objection" | "preference";
|
|
47
|
+
|
|
48
|
+
export interface ExplicitPolicyAssertion {
|
|
49
|
+
readonly kind: PolicyAssertionKind;
|
|
50
|
+
readonly purpose: string;
|
|
51
|
+
readonly channel?: string | null;
|
|
52
|
+
readonly value: boolean;
|
|
53
|
+
readonly statement_ref?: string | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface WebsiteIngressPayload {
|
|
57
|
+
readonly contract_version: typeof LAGEBILD_INGRESS_CONTRACT_VERSION;
|
|
58
|
+
readonly tenant_id: string;
|
|
59
|
+
readonly source_system_id: string;
|
|
60
|
+
readonly submission_id: string;
|
|
61
|
+
readonly idempotency_key: string;
|
|
62
|
+
readonly occurred_at: string;
|
|
63
|
+
readonly interaction_kind: IngressInteractionKind;
|
|
64
|
+
readonly identity_claims: readonly IdentityClaim[];
|
|
65
|
+
readonly message?: string | null;
|
|
66
|
+
readonly origin?: IngressOrigin;
|
|
67
|
+
readonly explicit_policy_assertions?: readonly ExplicitPolicyAssertion[];
|
|
68
|
+
readonly correlation_id?: string | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface IngressResult {
|
|
72
|
+
readonly submission_id: string | null;
|
|
73
|
+
readonly accepted: boolean;
|
|
74
|
+
readonly is_replay: boolean;
|
|
75
|
+
readonly rejection_class?: string | null;
|
|
76
|
+
readonly correlation_id?: string | null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface LagebildIngressConfig {
|
|
80
|
+
readonly apiUrl: string;
|
|
81
|
+
readonly apiKey: string;
|
|
82
|
+
readonly tenantId: string;
|
|
83
|
+
readonly sourceSystemId: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface IngressSubmitInput {
|
|
87
|
+
readonly submissionId: string;
|
|
88
|
+
readonly idempotencyKey: string;
|
|
89
|
+
readonly interactionKind: IngressInteractionKind;
|
|
90
|
+
readonly identityClaims: readonly IdentityClaim[];
|
|
91
|
+
readonly message?: string | null;
|
|
92
|
+
readonly origin?: IngressOrigin;
|
|
93
|
+
readonly explicitPolicyAssertions?: readonly ExplicitPolicyAssertion[];
|
|
94
|
+
readonly correlationId?: string | null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Submit a website ingress event to the Lagebild API.
|
|
99
|
+
* Returns the acceptance result. Does not throw on non-2xx — returns
|
|
100
|
+
* a result with accepted=false so the caller can decide retry strategy.
|
|
101
|
+
*/
|
|
102
|
+
export async function submitIngress(
|
|
103
|
+
config: LagebildIngressConfig,
|
|
104
|
+
input: IngressSubmitInput,
|
|
105
|
+
): Promise<IngressResult> {
|
|
106
|
+
const payload: WebsiteIngressPayload = {
|
|
107
|
+
contract_version: LAGEBILD_INGRESS_CONTRACT_VERSION,
|
|
108
|
+
tenant_id: config.tenantId,
|
|
109
|
+
source_system_id: config.sourceSystemId,
|
|
110
|
+
submission_id: input.submissionId,
|
|
111
|
+
idempotency_key: input.idempotencyKey,
|
|
112
|
+
occurred_at: new Date().toISOString(),
|
|
113
|
+
interaction_kind: input.interactionKind,
|
|
114
|
+
identity_claims: input.identityClaims,
|
|
115
|
+
...(input.message != null ? { message: input.message } : {}),
|
|
116
|
+
...(input.origin != null ? { origin: input.origin } : {}),
|
|
117
|
+
...(input.explicitPolicyAssertions != null
|
|
118
|
+
? { explicit_policy_assertions: input.explicitPolicyAssertions }
|
|
119
|
+
: {}),
|
|
120
|
+
...(input.correlationId != null ? { correlation_id: input.correlationId } : {}),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const url = config.apiUrl.replace(/\/$/, "") + "/v1/ingress";
|
|
124
|
+
|
|
125
|
+
let response: Response;
|
|
126
|
+
try {
|
|
127
|
+
response = await fetch(url, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: {
|
|
130
|
+
"Content-Type": "application/json",
|
|
131
|
+
Authorization: "Bearer " + config.apiKey,
|
|
132
|
+
},
|
|
133
|
+
body: JSON.stringify(payload),
|
|
134
|
+
});
|
|
135
|
+
} catch {
|
|
136
|
+
return {
|
|
137
|
+
submission_id: input.submissionId,
|
|
138
|
+
accepted: false,
|
|
139
|
+
is_replay: false,
|
|
140
|
+
rejection_class: "network_error",
|
|
141
|
+
correlation_id: input.correlationId ?? null,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let body: unknown;
|
|
146
|
+
try {
|
|
147
|
+
body = await response.json();
|
|
148
|
+
} catch {
|
|
149
|
+
body = {};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const result = body as Partial<IngressResult>;
|
|
153
|
+
return {
|
|
154
|
+
submission_id: result.submission_id ?? input.submissionId,
|
|
155
|
+
accepted: result.accepted ?? response.ok,
|
|
156
|
+
is_replay: result.is_replay ?? false,
|
|
157
|
+
rejection_class: result.rejection_class ?? null,
|
|
158
|
+
correlation_id: result.correlation_id ?? input.correlationId ?? null,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Build a stable idempotency key from tenant + submission ID.
|
|
164
|
+
* This ensures retries within the same submission are deduplicated
|
|
165
|
+
* by the Lagebild API.
|
|
166
|
+
*/
|
|
167
|
+
export function buildIdempotencyKey(tenantId: string, submissionId: string): string {
|
|
168
|
+
return tenantId + ":" + submissionId;
|
|
169
|
+
}
|
|
@@ -31,8 +31,9 @@ import type {
|
|
|
31
31
|
IntegrationSecrets,
|
|
32
32
|
Lead,
|
|
33
33
|
LeadMessage,
|
|
34
|
+
EXECUTION_MODES,
|
|
34
35
|
} from "./port.ts";
|
|
35
|
-
import { DESTINATION_KINDS,
|
|
36
|
+
import { DESTINATION_KINDS, eventToLeadMessage } from "./port.ts";
|
|
36
37
|
import { LifecycleEventPayloadSchema } from "./lifecycle.ts";
|
|
37
38
|
import { z } from "zod";
|
|
38
39
|
|
|
@@ -171,20 +172,14 @@ export const DESTINATION_ADAPTERS: readonly DestinationAdapter[] = [pipedriveDes
|
|
|
171
172
|
* `extraAdapters`; the validator checks vendor + secrets against these catalogs.
|
|
172
173
|
*/
|
|
173
174
|
export const EXTERNAL_DESTINATION_VENDORS: Readonly<Record<DestinationKind, readonly string[]>> = {
|
|
174
|
-
crm: [
|
|
175
|
+
crm: [],
|
|
175
176
|
calendar: [],
|
|
176
177
|
email: [],
|
|
177
178
|
scheduler: [],
|
|
178
179
|
};
|
|
179
180
|
|
|
180
181
|
/** Per `(kind, vendor)` → required secret names for externally-implemented adapters. */
|
|
181
|
-
export const EXTERNAL_DESTINATION_SECRETS: Readonly<Record<string, readonly string[]>> = {
|
|
182
|
-
"crm:supabase-buffer": [
|
|
183
|
-
"SUPABASE_BUFFER_URL",
|
|
184
|
-
"SUPABASE_BUFFER_SERVICE_KEY",
|
|
185
|
-
"SUPABASE_BUFFER_TENANT_ID",
|
|
186
|
-
],
|
|
187
|
-
};
|
|
182
|
+
export const EXTERNAL_DESTINATION_SECRETS: Readonly<Record<string, readonly string[]>> = {};
|
|
188
183
|
|
|
189
184
|
/** Closed per-kind vendor catalog, derived from the registry + external vendors (RFC-0176/0186). */
|
|
190
185
|
export const DESTINATION_VENDORS_BY_KIND: Readonly<Record<DestinationKind, readonly string[]>> =
|
|
@@ -318,8 +313,7 @@ export function authenticateInbound(headers: Headers, secret: string | undefined
|
|
|
318
313
|
* supplied. `seen` is the optional dedup set (RFC-0176).
|
|
319
314
|
*
|
|
320
315
|
* `extraAdapters` lets a specific site's delivery callback inject additional
|
|
321
|
-
* DestinationAdapters
|
|
322
|
-
* the closed shared registry. They are self-enabling (skip if secrets absent).
|
|
316
|
+
* DestinationAdapters without modifying the closed shared registry. They are self-enabling (skip if secrets absent).
|
|
323
317
|
*/
|
|
324
318
|
export function routeEventToReady(
|
|
325
319
|
event: IntegrationEvent,
|
|
@@ -350,8 +344,8 @@ export interface DeliverEventResult {
|
|
|
350
344
|
* destinations (CRM), with the client's own tokens. Email is NOT here — it is sent via
|
|
351
345
|
* Cloudflare Email Routing (send_email binding) in the callback route. `seen` dedups the
|
|
352
346
|
* destination side on redelivery. Best-effort per sink; the caller decides ack vs retry.
|
|
353
|
-
* `extraAdapters` lets a specific site inject additional DestinationAdapters
|
|
354
|
-
*
|
|
347
|
+
* `extraAdapters` lets a specific site inject additional DestinationAdapters
|
|
348
|
+
* without modifying the closed shared registry.
|
|
355
349
|
*/
|
|
356
350
|
export async function deliverEvent(
|
|
357
351
|
event: IntegrationEvent,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<MODULE_CONTRACT>
|
|
3
3
|
<purpose>
|
|
4
4
|
RFC-0168: Integration Port barrel — types and pure contracts only. Re-exports from
|
|
5
|
-
port.ts,
|
|
5
|
+
port.ts, funnel.ts, lifecycle.ts, sharding.ts, dispatch.ts, and qstash.ts.
|
|
6
6
|
Type-only consumers import from here to avoid transitively pulling in adapter
|
|
7
7
|
implementations (orchestration.ts → adapters.ts).
|
|
8
8
|
</purpose>
|
|
@@ -22,33 +22,6 @@ export type { IntegrationEvent, DestinationAdapter } from "./port.ts";
|
|
|
22
22
|
export { DESTINATION_KINDS, EXECUTION_MODES, eventToLeadMessage, eventToLead } from "./port.ts";
|
|
23
23
|
export type { DestinationKind, ExecutionMode } from "./port.ts";
|
|
24
24
|
|
|
25
|
-
export type {
|
|
26
|
-
BufferContact,
|
|
27
|
-
BufferOrganization,
|
|
28
|
-
BufferDeal,
|
|
29
|
-
BufferStageTransition,
|
|
30
|
-
BufferFunnelEvent,
|
|
31
|
-
BufferConsentEvent,
|
|
32
|
-
BufferSubscription,
|
|
33
|
-
BufferInvoice,
|
|
34
|
-
BufferUpsertResult,
|
|
35
|
-
OutboxWriteResult,
|
|
36
|
-
CrmBufferWriter,
|
|
37
|
-
CrmBufferReader,
|
|
38
|
-
CrmBufferClient,
|
|
39
|
-
DealPipedriveIdPatch,
|
|
40
|
-
SyncOutboxRow,
|
|
41
|
-
} from "./crm-buffer.ts";
|
|
42
|
-
export {
|
|
43
|
-
BUFFER_DEAL_STAGES,
|
|
44
|
-
FUNNEL_STAGE_TO_BUFFER_STAGE,
|
|
45
|
-
bridgeFunnelStage,
|
|
46
|
-
isFunnelStage,
|
|
47
|
-
SYNC_OUTBOX_STATUSES,
|
|
48
|
-
SYNC_OUTBOX_OPS,
|
|
49
|
-
} from "./crm-buffer.ts";
|
|
50
|
-
export type { BufferDealStage, SyncOutboxStatus, SyncOutboxOp } from "./crm-buffer.ts";
|
|
51
|
-
|
|
52
25
|
export type {
|
|
53
26
|
VisitorFunnelEventPayload,
|
|
54
27
|
FunnelSystemTrigger,
|
|
@@ -65,6 +38,11 @@ export {
|
|
|
65
38
|
VISITOR_BUYER_TYPES,
|
|
66
39
|
FUNNEL_TRANSITIONS,
|
|
67
40
|
isValidFunnelStage,
|
|
41
|
+
BUFFER_DEAL_STAGES,
|
|
42
|
+
FUNNEL_STAGE_TO_BUFFER_STAGE,
|
|
43
|
+
bridgeFunnelStage,
|
|
44
|
+
SYNC_OUTBOX_STATUSES,
|
|
45
|
+
SYNC_OUTBOX_OPS,
|
|
68
46
|
canTransition,
|
|
69
47
|
nextStages,
|
|
70
48
|
reachableStages,
|
|
@@ -79,6 +57,9 @@ export type {
|
|
|
79
57
|
VisitorFunnelIntent,
|
|
80
58
|
VisitorFunnelSource,
|
|
81
59
|
VisitorBuyerType,
|
|
60
|
+
BufferDealStage,
|
|
61
|
+
SyncOutboxStatus,
|
|
62
|
+
SyncOutboxOp,
|
|
82
63
|
} from "./funnel.ts";
|
|
83
64
|
|
|
84
65
|
export type { LifecycleEventPayload } from "./lifecycle.ts";
|
|
@@ -17,9 +17,9 @@ import {
|
|
|
17
17
|
BUFFER_DEAL_STAGES,
|
|
18
18
|
FUNNEL_STAGE_TO_BUFFER_STAGE,
|
|
19
19
|
bridgeFunnelStage,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
VISITOR_FUNNEL_STAGES,
|
|
21
|
+
isValidFunnelStage,
|
|
22
|
+
} from "../funnel.ts";
|
|
23
23
|
|
|
24
24
|
test("the bridge maps every canonical funnel stage onto a valid generic stage", () => {
|
|
25
25
|
const generic = new Set<string>(BUFFER_DEAL_STAGES);
|
|
@@ -39,8 +39,8 @@ test("terminal funnel stages bridge to their generic equivalents", () => {
|
|
|
39
39
|
expect(bridgeFunnelStage("new_session")).toBe("new");
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
test("
|
|
43
|
-
expect(
|
|
44
|
-
expect(
|
|
45
|
-
expect(
|
|
42
|
+
test("isValidFunnelStage guards the canonical catalog", () => {
|
|
43
|
+
expect(isValidFunnelStage("offer_presented")).toBe(true);
|
|
44
|
+
expect(isValidFunnelStage("q_website_tier")).toBe(false); // legacy UChat string
|
|
45
|
+
expect(isValidFunnelStage("new")).toBe(false); // generic buffer stage, not a funnel stage
|
|
46
46
|
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Unit tests for the Lagebild ingress HTTP client. Verifies payload
|
|
4
|
+
construction, idempotency key generation, and network error handling.</purpose>
|
|
5
|
+
<non-goals>
|
|
6
|
+
<item>Do not make real HTTP calls — fetch is mocked.</item>
|
|
7
|
+
</non-goals>
|
|
8
|
+
</MODULE_CONTRACT>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { test, expect, vi } from "vitest";
|
|
12
|
+
import {
|
|
13
|
+
submitIngress,
|
|
14
|
+
buildIdempotencyKey,
|
|
15
|
+
LAGEBILD_INGRESS_CONTRACT_VERSION,
|
|
16
|
+
type LagebildIngressConfig,
|
|
17
|
+
} from "../lagebild-ingress.ts";
|
|
18
|
+
|
|
19
|
+
const baseConfig: LagebildIngressConfig = {
|
|
20
|
+
apiUrl: "https://lagebild-api.example.workers.dev",
|
|
21
|
+
apiKey: "lbk_test_key_0001",
|
|
22
|
+
tenantId: "ten_warpgogol000001",
|
|
23
|
+
sourceSystemId: "site_warpgogol_com",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
test("buildIdempotencyKey is stable and combines tenant + submission", () => {
|
|
27
|
+
const key = buildIdempotencyKey("ten_abc", "sub_123");
|
|
28
|
+
expect(key).toBe("ten_abc:sub_123");
|
|
29
|
+
expect(buildIdempotencyKey("ten_abc", "sub_123")).toBe(key);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("submitIngress sends POST with correct headers and payload", async () => {
|
|
33
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
34
|
+
new Response(
|
|
35
|
+
JSON.stringify({
|
|
36
|
+
submission_id: "sub_test00000000000001",
|
|
37
|
+
accepted: true,
|
|
38
|
+
is_replay: false,
|
|
39
|
+
correlation_id: null,
|
|
40
|
+
}),
|
|
41
|
+
{ status: 200, headers: { "Content-Type": "application/json" } },
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const result = await submitIngress(baseConfig, {
|
|
46
|
+
submissionId: "sub_test00000000000001",
|
|
47
|
+
idempotencyKey: "ten_warpgogol000001:sub_test00000000000001",
|
|
48
|
+
interactionKind: "contact_message",
|
|
49
|
+
identityClaims: [
|
|
50
|
+
{ claim_type: "email", value: "test@example.com" },
|
|
51
|
+
{ claim_type: "person_name", value: "Test User" },
|
|
52
|
+
],
|
|
53
|
+
message: "Hello from the test",
|
|
54
|
+
origin: { page_uri: "https://example.com/contact", locale: "de" },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(result.accepted).toBe(true);
|
|
58
|
+
expect(result.is_replay).toBe(false);
|
|
59
|
+
|
|
60
|
+
expect(fetchSpy).toHaveBeenCalledOnce();
|
|
61
|
+
const [url, init] = fetchSpy.mock.calls[0] as [string, RequestInit];
|
|
62
|
+
expect(url).toBe("https://lagebild-api.example.workers.dev/v1/ingress");
|
|
63
|
+
expect(init.method).toBe("POST");
|
|
64
|
+
expect((init.headers as Record<string, string>)["Authorization"]).toBe(
|
|
65
|
+
"Bearer lbk_test_key_0001",
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const body = JSON.parse(init.body as string) as Record<string, unknown>;
|
|
69
|
+
expect(body["contract_version"]).toBe(LAGEBILD_INGRESS_CONTRACT_VERSION);
|
|
70
|
+
expect(body["tenant_id"]).toBe("ten_warpgogol000001");
|
|
71
|
+
expect(body["source_system_id"]).toBe("site_warpgogol_com");
|
|
72
|
+
expect(body["interaction_kind"]).toBe("contact_message");
|
|
73
|
+
|
|
74
|
+
fetchSpy.mockRestore();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("submitIngress returns accepted=false on network error", async () => {
|
|
78
|
+
vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("connection refused"));
|
|
79
|
+
|
|
80
|
+
const result = await submitIngress(baseConfig, {
|
|
81
|
+
submissionId: "sub_test00000000000002",
|
|
82
|
+
idempotencyKey: "ten:test2",
|
|
83
|
+
interactionKind: "inquiry",
|
|
84
|
+
identityClaims: [{ claim_type: "email", value: "test@example.com" }],
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(result.accepted).toBe(false);
|
|
88
|
+
expect(result.rejection_class).toBe("network_error");
|
|
89
|
+
|
|
90
|
+
vi.restoreAllMocks();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("submitIngress returns accepted=false on 400 response", async () => {
|
|
94
|
+
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
95
|
+
new Response(
|
|
96
|
+
JSON.stringify({
|
|
97
|
+
submission_id: "sub_test00000000000003",
|
|
98
|
+
accepted: false,
|
|
99
|
+
is_replay: false,
|
|
100
|
+
rejection_class: "schema_invalid",
|
|
101
|
+
correlation_id: null,
|
|
102
|
+
}),
|
|
103
|
+
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const result = await submitIngress(baseConfig, {
|
|
108
|
+
submissionId: "sub_test00000000000003",
|
|
109
|
+
idempotencyKey: "ten:test3",
|
|
110
|
+
interactionKind: "inquiry",
|
|
111
|
+
identityClaims: [{ claim_type: "email", value: "bad" }],
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(result.accepted).toBe(false);
|
|
115
|
+
expect(result.rejection_class).toBe("schema_invalid");
|
|
116
|
+
|
|
117
|
+
vi.restoreAllMocks();
|
|
118
|
+
});
|
|
@@ -4,5 +4,12 @@ export default defineConfig({
|
|
|
4
4
|
test: {
|
|
5
5
|
environment: "node",
|
|
6
6
|
include: ["src/**/*.test.ts"],
|
|
7
|
+
coverage: {
|
|
8
|
+
provider: "v8",
|
|
9
|
+
reporter: ["json", "json-summary", "text-summary", "html"],
|
|
10
|
+
reportsDirectory: "../../.coverage/integration",
|
|
11
|
+
include: ["src/integration/**/*.ts"],
|
|
12
|
+
exclude: ["src/integration/**/*.test.ts", "src/integration/**/*.d.ts"],
|
|
13
|
+
},
|
|
7
14
|
},
|
|
8
15
|
});
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { MetricsPusher } from "./pusher.ts";
|
|
18
|
-
import { WARPGOGOL_METRIC_REGISTRY } from "./metric-registry.ts";
|
|
18
|
+
import type { WARPGOGOL_METRIC_REGISTRY } from "./metric-registry.ts";
|
|
19
19
|
|
|
20
20
|
type LabelKeys<L extends readonly string[]> = L[number];
|
|
21
21
|
type LabelMap<L extends readonly string[]> = Partial<Record<LabelKeys<L>, string>>;
|
|
@@ -135,15 +135,15 @@
|
|
|
135
135
|
"layer": "component"
|
|
136
136
|
},
|
|
137
137
|
{
|
|
138
|
-
"id": "
|
|
139
|
-
"displayName": "
|
|
140
|
-
"semanticRole": "component-
|
|
138
|
+
"id": "cycle-toggle",
|
|
139
|
+
"displayName": "Cycle Toggle",
|
|
140
|
+
"semanticRole": "component-cycle-toggle",
|
|
141
141
|
"version": "1.0.0",
|
|
142
142
|
"layoutHint": "split",
|
|
143
143
|
"acceptedCosmicNames": [
|
|
144
|
-
"
|
|
144
|
+
"Cupid"
|
|
145
145
|
],
|
|
146
|
-
"sourceFile": "packages/werkstatt-site/src/domain/ontology/archetypes/components/
|
|
146
|
+
"sourceFile": "packages/werkstatt-site/src/domain/ontology/archetypes/components/cycle-toggle.yaml",
|
|
147
147
|
"layer": "component"
|
|
148
148
|
},
|
|
149
149
|
{
|
|
@@ -750,7 +750,7 @@
|
|
|
750
750
|
"component-brand-label",
|
|
751
751
|
"component-copyright",
|
|
752
752
|
"component-currency-aware-price-display",
|
|
753
|
-
"component-
|
|
753
|
+
"component-cycle-toggle",
|
|
754
754
|
"component-footer-promo",
|
|
755
755
|
"component-lang-switcher",
|
|
756
756
|
"component-layout",
|
|
@@ -814,9 +814,10 @@
|
|
|
814
814
|
"Proteus": "@warpgogol/werkstatt-site/ui/components/brand-label",
|
|
815
815
|
"Charon": "@warpgogol/werkstatt-site/ui/components/copyright",
|
|
816
816
|
"Rosalind": "@warpgogol/werkstatt-site/ui/components/currency-aware-price-display",
|
|
817
|
-
"
|
|
817
|
+
"Cupid": "@warpgogol/werkstatt-site/ui/components/cycle-toggle",
|
|
818
818
|
"Galatea": "@warpgogol/werkstatt-site/ui/components/donation-card",
|
|
819
819
|
"Puck": "@warpgogol/werkstatt-site/ui/components/effects",
|
|
820
|
+
"Prospero": "@warpgogol/werkstatt-site/ui/components/external-link-qr-modal",
|
|
820
821
|
"Nereid": "@warpgogol/werkstatt-site/ui/components/footer-promo",
|
|
821
822
|
"Ophelia": "@warpgogol/werkstatt-site/ui/components/lang-switcher",
|
|
822
823
|
"Umbriel": "@warpgogol/werkstatt-site/ui/components/layout",
|
|
@@ -892,8 +893,9 @@
|
|
|
892
893
|
"brand-label": "Proteus",
|
|
893
894
|
"copyright": "Charon",
|
|
894
895
|
"currency-aware-price-display": "Rosalind",
|
|
895
|
-
"
|
|
896
|
+
"cycle-toggle": "Cupid",
|
|
896
897
|
"section-shell": "Puck",
|
|
898
|
+
"external-link-qr-modal": "Prospero",
|
|
897
899
|
"footer-promo": "Nereid",
|
|
898
900
|
"lang-switcher": "Ophelia",
|
|
899
901
|
"layout": "Umbriel",
|
|
@@ -975,9 +977,10 @@
|
|
|
975
977
|
"Ariel": "breadcrumbs",
|
|
976
978
|
"Charon": "copyright",
|
|
977
979
|
"Rosalind": "price-display",
|
|
978
|
-
"
|
|
980
|
+
"Cupid": "cycle-toggle",
|
|
979
981
|
"Galatea": "person-profile",
|
|
980
982
|
"Puck": "section-shell",
|
|
983
|
+
"Prospero": "layout-shell",
|
|
981
984
|
"Titania": "footer",
|
|
982
985
|
"Nereid": "footer-promo",
|
|
983
986
|
"Oberon": "header",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
schemaVersion: 1.0.0
|
|
2
|
-
totalCount:
|
|
2
|
+
totalCount: 59
|
|
3
3
|
entries:
|
|
4
4
|
- id: approach
|
|
5
5
|
displayName: Approach
|
|
@@ -100,14 +100,14 @@ entries:
|
|
|
100
100
|
- Rosalind
|
|
101
101
|
sourceFile: packages/werkstatt-site/src/domain/ontology/archetypes/components/currency-aware-price-display.yaml
|
|
102
102
|
layer: component
|
|
103
|
-
- id:
|
|
104
|
-
displayName:
|
|
105
|
-
semanticRole: component-
|
|
103
|
+
- id: cycle-toggle
|
|
104
|
+
displayName: Cycle Toggle
|
|
105
|
+
semanticRole: component-cycle-toggle
|
|
106
106
|
version: 1.0.0
|
|
107
107
|
layoutHint: split
|
|
108
108
|
acceptedCosmicNames:
|
|
109
|
-
-
|
|
110
|
-
sourceFile: packages/werkstatt-site/src/domain/ontology/archetypes/components/
|
|
109
|
+
- Cupid
|
|
110
|
+
sourceFile: packages/werkstatt-site/src/domain/ontology/archetypes/components/cycle-toggle.yaml
|
|
111
111
|
layer: component
|
|
112
112
|
- id: donation-card
|
|
113
113
|
displayName: Donation Card
|
|
@@ -163,6 +163,15 @@ entries:
|
|
|
163
163
|
- Nereid
|
|
164
164
|
sourceFile: packages/werkstatt-site/src/domain/ontology/archetypes/components/footer-promo.yaml
|
|
165
165
|
layer: component
|
|
166
|
+
- id: gratitude
|
|
167
|
+
displayName: Gratitude
|
|
168
|
+
semanticRole: client-gratitude
|
|
169
|
+
version: 1.0.0
|
|
170
|
+
layoutHint: card-grid
|
|
171
|
+
acceptedCosmicNames:
|
|
172
|
+
- Gonggong
|
|
173
|
+
sourceFile: packages/werkstatt-site/src/domain/ontology/archetypes/sections/gratitude.yaml
|
|
174
|
+
layer: section
|
|
166
175
|
- id: hero
|
|
167
176
|
displayName: Hero
|
|
168
177
|
semanticRole: hero
|
|
@@ -528,6 +537,7 @@ sectionRoles:
|
|
|
528
537
|
- article-index
|
|
529
538
|
- audience-self-identification
|
|
530
539
|
- breadcrumbs
|
|
540
|
+
- client-gratitude
|
|
531
541
|
- client-ownership-disclosure
|
|
532
542
|
- consent-gated-chat-launcher
|
|
533
543
|
- credits-gallery
|
|
@@ -560,7 +570,7 @@ componentRoles:
|
|
|
560
570
|
- component-brand-label
|
|
561
571
|
- component-copyright
|
|
562
572
|
- component-currency-aware-price-display
|
|
563
|
-
- component-
|
|
573
|
+
- component-cycle-toggle
|
|
564
574
|
- component-footer-promo
|
|
565
575
|
- component-lang-switcher
|
|
566
576
|
- component-layout
|
|
@@ -599,6 +609,7 @@ planetImportPaths:
|
|
|
599
609
|
Elara: "@warpgogol/werkstatt-site/ui/sections/dynamic-status-block"
|
|
600
610
|
Atlas: "@warpgogol/werkstatt-site/ui/sections/faq-list"
|
|
601
611
|
Dione: "@warpgogol/werkstatt-site/ui/sections/final-cta"
|
|
612
|
+
Gonggong: "@warpgogol/werkstatt-site/ui/sections/gratitude"
|
|
602
613
|
Europa: "@warpgogol/werkstatt-site/ui/sections/hero"
|
|
603
614
|
Phobos: "@warpgogol/werkstatt-site/ui/sections/hero-decision-card"
|
|
604
615
|
Ganymede: "@warpgogol/werkstatt-site/ui/sections/impact"
|
|
@@ -622,9 +633,10 @@ planetImportPaths:
|
|
|
622
633
|
Proteus: "@warpgogol/werkstatt-site/ui/components/brand-label"
|
|
623
634
|
Charon: "@warpgogol/werkstatt-site/ui/components/copyright"
|
|
624
635
|
Rosalind: "@warpgogol/werkstatt-site/ui/components/currency-aware-price-display"
|
|
625
|
-
|
|
636
|
+
Cupid: "@warpgogol/werkstatt-site/ui/components/cycle-toggle"
|
|
626
637
|
Galatea: "@warpgogol/werkstatt-site/ui/components/donation-card"
|
|
627
638
|
Puck: "@warpgogol/werkstatt-site/ui/components/effects"
|
|
639
|
+
Prospero: "@warpgogol/werkstatt-site/ui/components/external-link-qr-modal"
|
|
628
640
|
Nereid: "@warpgogol/werkstatt-site/ui/components/footer-promo"
|
|
629
641
|
Ophelia: "@warpgogol/werkstatt-site/ui/components/lang-switcher"
|
|
630
642
|
Umbriel: "@warpgogol/werkstatt-site/ui/components/layout"
|
|
@@ -675,6 +687,7 @@ blockTypeToCosmicName:
|
|
|
675
687
|
dynamic-status-block: Elara
|
|
676
688
|
faq-list: Atlas
|
|
677
689
|
final-cta: Dione
|
|
690
|
+
gratitude: Gonggong
|
|
678
691
|
hero: Europa
|
|
679
692
|
hero-decision-card: Phobos
|
|
680
693
|
impact: Ganymede
|
|
@@ -698,8 +711,9 @@ blockTypeToCosmicName:
|
|
|
698
711
|
brand-label: Proteus
|
|
699
712
|
copyright: Charon
|
|
700
713
|
currency-aware-price-display: Rosalind
|
|
701
|
-
|
|
714
|
+
cycle-toggle: Cupid
|
|
702
715
|
section-shell: Puck
|
|
716
|
+
external-link-qr-modal: Prospero
|
|
703
717
|
footer-promo: Nereid
|
|
704
718
|
lang-switcher: Ophelia
|
|
705
719
|
layout: Umbriel
|
|
@@ -754,6 +768,7 @@ roleByCosmicName:
|
|
|
754
768
|
Elara: data-driven-status-indicator
|
|
755
769
|
Atlas: approach
|
|
756
770
|
Dione: final-cta
|
|
771
|
+
Gonggong: client-gratitude
|
|
757
772
|
Europa: hero
|
|
758
773
|
Phobos: hero
|
|
759
774
|
Ganymede: impact
|
|
@@ -778,9 +793,10 @@ roleByCosmicName:
|
|
|
778
793
|
Ariel: breadcrumbs
|
|
779
794
|
Charon: copyright
|
|
780
795
|
Rosalind: price-display
|
|
781
|
-
|
|
796
|
+
Cupid: cycle-toggle
|
|
782
797
|
Galatea: person-profile
|
|
783
798
|
Puck: section-shell
|
|
799
|
+
Prospero: layout-shell
|
|
784
800
|
Titania: footer
|
|
785
801
|
Nereid: footer-promo
|
|
786
802
|
Oberon: header
|