@blotout/qinto-cli 0.1.20 → 0.1.22
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 +11 -1
- package/dist/index.js +3801 -3716
- package/dist/index.js.map +1 -1
- package/dist/worker-artifact/app-runtime/index.js +64 -11
- package/dist/worker-artifact/main/index.js +229 -135
- package/dist/worker-artifact/version.json +6 -6
- package/package.json +3 -3
- package/templates/app/full/qinto.app.jsonc +0 -129
- package/templates/app/full/src/browser.ts +0 -15
- package/templates/app/full/src/server.ts +0 -133
- package/templates/app/full/ui/src/pages/index.astro +0 -53
- package/templates/app/minimal/qinto.app.jsonc +0 -20
- package/templates/app/minimal/src/server.ts +0 -9
- package/templates/project/gitignore +0 -3
- package/templates/project/package.json +0 -13
- package/templates/project/tsconfig.json +0 -12
- package/templates/ui/astro.config.mjs +0 -6
- package/templates/ui/package.json +0 -12
- package/templates/ui/src/pages/index.astro +0 -50
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "./node_modules/@blotout/qinto-sdk-app/app.schema.json",
|
|
3
|
-
// The immutable identifier of the app on the Qinto platform.
|
|
4
|
-
"slug": "__APP_SLUG__",
|
|
5
|
-
"name": "__APP_NAME__",
|
|
6
|
-
"description": "Rewrites events on their way through, records purchases, forwards them onward, and ships its own panel.",
|
|
7
|
-
// This app both rewrites events before destinations see them and receives them afterwards.
|
|
8
|
-
"flows": ["transform", "destination"],
|
|
9
|
-
// Events reach the app only when the visitor granted this consent category.
|
|
10
|
-
"consent": { "category": "marketing" },
|
|
11
|
-
// Replace these with your own hosted documents before publishing.
|
|
12
|
-
"legal": {
|
|
13
|
-
"termsUrl": "https://example.com/terms",
|
|
14
|
-
"privacyUrl": "https://example.com/privacy"
|
|
15
|
-
},
|
|
16
|
-
"server": {
|
|
17
|
-
"entry": "src/server.ts",
|
|
18
|
-
// Must match the __hooks the server entry exports.
|
|
19
|
-
"hooks": {
|
|
20
|
-
"transform": ["track"],
|
|
21
|
-
"onEvent": ["track", "identify"],
|
|
22
|
-
"onConnect": false,
|
|
23
|
-
"onLifecycle": ["installed", "configurationChanged", "uninstalled"]
|
|
24
|
-
},
|
|
25
|
-
// Time-based runs, with no visitor behind them — a nightly rollup, a vendor reconcile, a
|
|
26
|
-
// backfill kick-off. Each entry is attempted once per active install per matching window, best
|
|
27
|
-
// effort: a missed run is not caught up, so keep handlers repeatable. The minute field must be a
|
|
28
|
-
// literal, which is what holds a schedule to at most one run an hour; the rest is standard
|
|
29
|
-
// five-field cron in UTC, with day-of-week 0 = Sunday. Ids key the handlers the bundle
|
|
30
|
-
// registers under "schedules", and the publish refuses a mismatch either way.
|
|
31
|
-
"schedules": [{ "id": "daily-rollup", "cron": "0 2 * * *", "description": "Nightly per-install rollup" }]
|
|
32
|
-
},
|
|
33
|
-
// The browser bundle ships to the visitor's page alongside the tag. "entry" is optional: an app
|
|
34
|
-
// that only wants "capture" declares this block without shipping any browser code.
|
|
35
|
-
//
|
|
36
|
-
// "capture" names the query params and cookies the platform records against each visitor for
|
|
37
|
-
// this app, which only this app reads back. Each name belongs to one list. Uncomment to use it —
|
|
38
|
-
// a site manager sees the list and must accept any name a later version adds.
|
|
39
|
-
//
|
|
40
|
-
// "type" says what a value is: "attribution" for a click or campaign id, "identity" (the default)
|
|
41
|
-
// for a vendor id that names the visitor across visits. "expiresInSeconds" is optional and runs
|
|
42
|
-
// from the first sighting of that value; seeing the same value again never extends it. Omit it to
|
|
43
|
-
// keep the value for the life of the visitor profile.
|
|
44
|
-
// "capture": { "queryParams": [{ "name": "fbclid", "type": "attribution", "expiresInSeconds": 604800 }], "cookies": [{ "name": "_fbp" }] },
|
|
45
|
-
// eventResults lets the matching server install receive this browser handler's return value on
|
|
46
|
-
// the same action as action.browser. It is untrusted page input and delivery stays fail-open.
|
|
47
|
-
"browser": { "entry": "src/browser.ts", "eventResults": ["track"] },
|
|
48
|
-
"capabilities": {
|
|
49
|
-
"api": [],
|
|
50
|
-
// "pii" is what puts the visitor's email in ctx.user.properties.
|
|
51
|
-
"data": ["pii", "customProperties", "eventContext"]
|
|
52
|
-
},
|
|
53
|
-
// The platform creates this database, binds it as env.PURCHASES, and applies these immutable,
|
|
54
|
-
// ordered entries. Expand entries are additive and run before rollout. Use backfill handlers for
|
|
55
|
-
// resumable data work and contract entries for destructive SQL after old bundles are gone.
|
|
56
|
-
"resources": [
|
|
57
|
-
{
|
|
58
|
-
"type": "d1",
|
|
59
|
-
"identifier": "purchases",
|
|
60
|
-
"binding": "PURCHASES",
|
|
61
|
-
"migrations": [
|
|
62
|
-
{
|
|
63
|
-
"id": "001-create-purchases",
|
|
64
|
-
"phase": "expand",
|
|
65
|
-
"statements": [
|
|
66
|
-
"CREATE TABLE IF NOT EXISTS purchases (order_id TEXT PRIMARY KEY, value REAL NOT NULL, email_sha256 TEXT, recorded_at INTEGER NOT NULL)"
|
|
67
|
-
]
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
"id": "002-index-purchases",
|
|
71
|
-
"phase": "expand",
|
|
72
|
-
"statements": ["CREATE INDEX IF NOT EXISTS purchases_recorded_at ON purchases (recorded_at)"]
|
|
73
|
-
}
|
|
74
|
-
]
|
|
75
|
-
}
|
|
76
|
-
],
|
|
77
|
-
// What a site manager fills in per install; a secret never reaches the browser.
|
|
78
|
-
"variables": [
|
|
79
|
-
{
|
|
80
|
-
"name": "ENDPOINT_URL",
|
|
81
|
-
"kind": "plain",
|
|
82
|
-
"type": "text",
|
|
83
|
-
"label": "Delivery endpoint",
|
|
84
|
-
"required": true,
|
|
85
|
-
"helpText": "The HTTPS endpoint each event is forwarded to. Until it is set the install stays inactive and sends nothing."
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
"name": "API_KEY",
|
|
89
|
-
"kind": "secret",
|
|
90
|
-
"type": "password",
|
|
91
|
-
"label": "API key",
|
|
92
|
-
"required": true,
|
|
93
|
-
"helpText": "The key this app authenticates to that endpoint with."
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
"name": "SITE_TAG",
|
|
97
|
-
"kind": "plain",
|
|
98
|
-
"type": "text",
|
|
99
|
-
"label": "Site tag",
|
|
100
|
-
"browser": true,
|
|
101
|
-
"helpText": "Sent with every event, and readable from the browser bundle."
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
"name": "LAST_SYNCED_AT",
|
|
105
|
-
"kind": "plain",
|
|
106
|
-
"type": "text",
|
|
107
|
-
"label": "Last synced",
|
|
108
|
-
"helpText": "Written by the panel's own /refresh handler; nobody fills this in by hand."
|
|
109
|
-
}
|
|
110
|
-
],
|
|
111
|
-
// A site installs this app once per destination, so the panel switches between them.
|
|
112
|
-
"install": { "multiple": true, "labelHint": "e.g. EU endpoint" },
|
|
113
|
-
// Values you set once for the whole app with `qinto app var set` / `qinto app secret set`; every
|
|
114
|
-
// site that installs it gets the same one, and a required one blocks the publish until it is set.
|
|
115
|
-
// They are never written here: this file is snapshotted into the published version and readable.
|
|
116
|
-
"appVariables": [
|
|
117
|
-
{
|
|
118
|
-
"name": "VENDOR_APP_SECRET",
|
|
119
|
-
"kind": "secret",
|
|
120
|
-
"type": "password",
|
|
121
|
-
"label": "Vendor app secret",
|
|
122
|
-
"required": true,
|
|
123
|
-
"helpText": "Your own credential with the vendor, the same for every site. Set it with `qinto app secret set`."
|
|
124
|
-
}
|
|
125
|
-
],
|
|
126
|
-
// The panel the platform renders in an iframe, and the command that builds it. "writesSettings"
|
|
127
|
-
// lets its server handler change this site's settings for the app; a site manager accepts it once.
|
|
128
|
-
"ui": { "dir": "ui/dist", "build": "npm run build", "writesSettings": true }
|
|
129
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { defineBrowserApp } from '@blotout/qinto-sdk-app/browser'
|
|
2
|
-
|
|
3
|
-
export default defineBrowserApp((app) => {
|
|
4
|
-
// The factory runs once per active install, with that install's
|
|
5
|
-
// browser-exposed plain variables in app.variables.
|
|
6
|
-
app.onEvent((event) => {
|
|
7
|
-
console.info('event on the page', event.event, app.variables.SITE_TAG)
|
|
8
|
-
|
|
9
|
-
return { observedAt: Date.now(), siteTag: app.variables.SITE_TAG }
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
app.onPush((message) => {
|
|
13
|
-
console.info('server push', message.from, message.topic, message.data)
|
|
14
|
-
})
|
|
15
|
-
})
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import type { AppUiContext } from '@blotout/qinto-sdk-app/server'
|
|
2
|
-
import { defineApp } from '@blotout/qinto-sdk-app/server'
|
|
3
|
-
|
|
4
|
-
type Env = { PURCHASES: D1Database }
|
|
5
|
-
|
|
6
|
-
// The purchases table comes from the migrations qinto.app.jsonc declares, applied at deploy.
|
|
7
|
-
const INSERT_PURCHASE = 'INSERT OR REPLACE INTO purchases (order_id, value, email_sha256, recorded_at) VALUES (?, ?, ?, ?)'
|
|
8
|
-
|
|
9
|
-
const sha256Hex = async (value: string): Promise<string> => {
|
|
10
|
-
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value.trim().toLowerCase()))
|
|
11
|
-
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('')
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// The panel posts an install id; it is input, never authority. The handler resolves it against
|
|
15
|
-
// ctx.installs, which the platform built from the site's own registry.
|
|
16
|
-
const analytics = async (request: Request, ctx: AppUiContext<Env>): Promise<Response> => {
|
|
17
|
-
const { installId } = (await request.json()) as { installId: string }
|
|
18
|
-
const target = ctx.installs.find((entry) => entry.id === installId)
|
|
19
|
-
if (target === undefined) {
|
|
20
|
-
return Response.json({ error: 'unknown install' }, { status: 404 })
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// That install's own secret, plus the app-scope secret you set once with `qinto app secret set`.
|
|
24
|
-
const token = target.secrets.API_KEY
|
|
25
|
-
const stats = await fetch(`${String(target.variables.ENDPOINT_URL)}/stats`, {
|
|
26
|
-
headers: { authorization: `Bearer ${token}`, 'x-app-secret': ctx.appSecrets.VENDOR_APP_SECRET }
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
return Response.json({ install: target.label, stats: await stats.json() })
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Writing a setting back needs `ui.writesSettings: true` in the manifest, and a viewer with write
|
|
33
|
-
// access. The value is authoritative at once but reaches ctx.variables on the next 5-minute sync —
|
|
34
|
-
// state your own code must read back immediately belongs in your own KV or D1.
|
|
35
|
-
const refresh = async (request: Request, ctx: AppUiContext<Env>): Promise<Response> => {
|
|
36
|
-
const { installId } = (await request.json()) as { installId: string }
|
|
37
|
-
if (ctx.viewer.access !== 'write') {
|
|
38
|
-
return Response.json({ error: 'read-only viewer' }, { status: 403 })
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
await ctx.settings.write(installId, { LAST_SYNCED_AT: new Date().toISOString() })
|
|
42
|
-
|
|
43
|
-
return Response.json({ refreshed: true })
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export default defineApp<Env>({
|
|
47
|
-
ui: async (request, ctx) => {
|
|
48
|
-
const path = new URL(request.url).pathname
|
|
49
|
-
if (path === '/analytics') {
|
|
50
|
-
return analytics(request, ctx)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (path === '/refresh') {
|
|
54
|
-
return refresh(request, ctx)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return Response.json({ error: 'not found' }, { status: 404 })
|
|
58
|
-
},
|
|
59
|
-
transform: {
|
|
60
|
-
track: async (action, ctx) => {
|
|
61
|
-
// Reshape the payload with ctx.replace, discard the event with ctx.drop, or let it through.
|
|
62
|
-
if (action.payload.event === 'Blocked Event') {
|
|
63
|
-
return ctx.drop()
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return ctx.replace({ ...action.payload, properties: { ...action.payload.properties, siteTag: ctx.variables.SITE_TAG } })
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
onEvent: {
|
|
70
|
-
track: async (action, ctx) => {
|
|
71
|
-
if (action.payload.event === 'Purchase') {
|
|
72
|
-
const email = ctx.user.properties.email
|
|
73
|
-
const emailHash = typeof email === 'string' ? await sha256Hex(email) : null
|
|
74
|
-
await ctx.env.PURCHASES.prepare(INSERT_PURCHASE)
|
|
75
|
-
.bind(String(action.payload.properties.orderId), Number(action.payload.properties.value), emailHash, action.receivedAt)
|
|
76
|
-
.run()
|
|
77
|
-
ctx.observability.log('purchase recorded', { orderId: String(action.payload.properties.orderId) })
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// A plain `fetch` here is traced for you and appears in the site's real-time console;
|
|
81
|
-
// report a delivery that failed with ctx.observability.error so the trace marks it.
|
|
82
|
-
const response = await fetch(String(ctx.variables.ENDPOINT_URL), {
|
|
83
|
-
method: 'POST',
|
|
84
|
-
headers: { 'content-type': 'application/json', authorization: `Bearer ${ctx.secrets.API_KEY}` },
|
|
85
|
-
body: JSON.stringify({
|
|
86
|
-
event: action.payload.event,
|
|
87
|
-
properties: action.payload.properties,
|
|
88
|
-
userId: action.userId,
|
|
89
|
-
context: action.context,
|
|
90
|
-
browser: action.browser
|
|
91
|
-
})
|
|
92
|
-
})
|
|
93
|
-
if (!response.ok) {
|
|
94
|
-
ctx.observability.error('delivery refused', undefined, { status: response.status, install: ctx.install.label })
|
|
95
|
-
|
|
96
|
-
return
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
ctx.observability.log('track delivered', { event: action.payload.event, install: ctx.install.label })
|
|
100
|
-
},
|
|
101
|
-
identify: async (action, ctx) => {
|
|
102
|
-
ctx.observability.log('identify received', { userId: action.userId, traits: Object.keys(action.payload.traits).length })
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
onLifecycle: {
|
|
106
|
-
installed: async (event, ctx) => {
|
|
107
|
-
ctx.observability.log('install ready', { eventId: event.eventId, install: ctx.install.label })
|
|
108
|
-
},
|
|
109
|
-
configurationChanged: async (event, ctx) => {
|
|
110
|
-
ctx.observability.log('configuration changed', {
|
|
111
|
-
eventId: event.eventId,
|
|
112
|
-
variables: event.changedVariables.length,
|
|
113
|
-
secrets: event.changedSecrets.length
|
|
114
|
-
})
|
|
115
|
-
},
|
|
116
|
-
uninstalled: async (event, ctx) => {
|
|
117
|
-
ctx.observability.log('install removed', { eventId: event.eventId, install: ctx.install.label })
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
// Keyed by the ids qinto.app.jsonc declares. Delivery is best effort — a run can be missed with no
|
|
121
|
-
// catch-up, and one whose response was lost may still have happened, so a handler must be safe to
|
|
122
|
-
// repeat. Work that must not be lost belongs in a workflow the handler starts.
|
|
123
|
-
schedules: {
|
|
124
|
-
'daily-rollup': async (event, ctx) => {
|
|
125
|
-
const counted = await ctx.env.PURCHASES.prepare('SELECT count(*) AS total FROM purchases').first<{ total: number }>()
|
|
126
|
-
ctx.observability.log('rollup complete', {
|
|
127
|
-
schedule: event.id,
|
|
128
|
-
install: ctx.install.label,
|
|
129
|
-
purchases: counted?.total ?? 0
|
|
130
|
-
})
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
})
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
<html lang='en'>
|
|
2
|
-
<head>
|
|
3
|
-
<meta charset='utf-8' />
|
|
4
|
-
<meta name='viewport' content='width=device-width, initial-scale=1' />
|
|
5
|
-
<title>__APP_NAME__</title>
|
|
6
|
-
</head>
|
|
7
|
-
<body>
|
|
8
|
-
<main>
|
|
9
|
-
<h1>__APP_NAME__</h1>
|
|
10
|
-
<p>The whole loop: pick an install, the panel calls this app's own <code>ui</code> server handler, and the handler reads that install's secret and calls out with the app's own credential.</p>
|
|
11
|
-
|
|
12
|
-
<label for='install'>Install</label>
|
|
13
|
-
<select id='install'></select>
|
|
14
|
-
<button id='load' type='button'>Load stats</button>
|
|
15
|
-
<button id='refresh' type='button'>Record a sync</button>
|
|
16
|
-
|
|
17
|
-
<pre id='out'>Pick an install.</pre>
|
|
18
|
-
</main>
|
|
19
|
-
<script>
|
|
20
|
-
// Resolved from the app's own dependency one directory up; the panel declares only its build tooling.
|
|
21
|
-
import { createAppUi } from '@blotout/qinto-sdk-app/ui'
|
|
22
|
-
|
|
23
|
-
const ui = createAppUi()
|
|
24
|
-
const context = await ui.getContext()
|
|
25
|
-
const out = document.getElementById('out')
|
|
26
|
-
const select = document.getElementById('install')
|
|
27
|
-
|
|
28
|
-
select.replaceChildren(
|
|
29
|
-
...context.installs.map((install) => {
|
|
30
|
-
const option = document.createElement('option')
|
|
31
|
-
option.value = install.id
|
|
32
|
-
option.textContent = install.enabled ? install.label : `${install.label} (disabled)`
|
|
33
|
-
return option
|
|
34
|
-
})
|
|
35
|
-
)
|
|
36
|
-
select.value = context.appInstallId
|
|
37
|
-
|
|
38
|
-
// ui.fetch resolves the app's own handler and attaches the panel token. It returns the raw
|
|
39
|
-
// Response: parse it, and decide yourself what to do with a 429 or a 5xx.
|
|
40
|
-
const call = async (path) => {
|
|
41
|
-
const response = await ui.fetch(path, {
|
|
42
|
-
method: 'POST',
|
|
43
|
-
headers: { 'content-type': 'application/json' },
|
|
44
|
-
body: JSON.stringify({ installId: select.value })
|
|
45
|
-
})
|
|
46
|
-
out.textContent = `${response.status} ${JSON.stringify(await response.json(), null, 2)}`
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
document.getElementById('load').addEventListener('click', () => call('/analytics'))
|
|
50
|
-
document.getElementById('refresh').addEventListener('click', () => call('/refresh'))
|
|
51
|
-
</script>
|
|
52
|
-
</body>
|
|
53
|
-
</html>
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "./node_modules/@blotout/qinto-sdk-app/app.schema.json",
|
|
3
|
-
// The immutable identifier of the app on the Qinto platform.
|
|
4
|
-
"slug": "__APP_SLUG__",
|
|
5
|
-
"name": "__APP_NAME__",
|
|
6
|
-
// Where the app plugs into the event pipeline: "transform" and/or "destination".
|
|
7
|
-
"flows": ["destination"],
|
|
8
|
-
// Events reach the app only when the visitor granted this consent category.
|
|
9
|
-
"consent": { "category": "analytics" },
|
|
10
|
-
// Replace these with your own hosted documents before publishing.
|
|
11
|
-
"legal": {
|
|
12
|
-
"termsUrl": "https://example.com/terms",
|
|
13
|
-
"privacyUrl": "https://example.com/privacy"
|
|
14
|
-
},
|
|
15
|
-
"server": {
|
|
16
|
-
"entry": "src/server.ts",
|
|
17
|
-
// Must match the __hooks the server entry exports.
|
|
18
|
-
"hooks": { "transform": [], "onEvent": ["track"], "onConnect": false }
|
|
19
|
-
}
|
|
20
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "__APP_SLUG__",
|
|
3
|
-
"private": true,
|
|
4
|
-
"type": "module",
|
|
5
|
-
"dependencies": {
|
|
6
|
-
"@blotout/qinto-sdk-app": "__APP_SDK_SPECIFIER__",
|
|
7
|
-
"zod": "4.5.4"
|
|
8
|
-
},
|
|
9
|
-
"devDependencies": {
|
|
10
|
-
"@cloudflare/workers-types": "5.20260819.1",
|
|
11
|
-
"typescript": "7.0.2"
|
|
12
|
-
}
|
|
13
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
<html lang='en'>
|
|
2
|
-
<head>
|
|
3
|
-
<meta charset='utf-8' />
|
|
4
|
-
<meta name='viewport' content='width=device-width, initial-scale=1' />
|
|
5
|
-
<title>App panel</title>
|
|
6
|
-
</head>
|
|
7
|
-
<body>
|
|
8
|
-
<main>
|
|
9
|
-
<h1>App panel</h1>
|
|
10
|
-
<p>This panel renders inside the Qinto platform. Outside it the bridge stays inert, so the fields below stay empty.</p>
|
|
11
|
-
<dl>
|
|
12
|
-
<dt>Site</dt>
|
|
13
|
-
<dd id='site'></dd>
|
|
14
|
-
<dt>Version</dt>
|
|
15
|
-
<dd id='version'></dd>
|
|
16
|
-
</dl>
|
|
17
|
-
<h2>Installs</h2>
|
|
18
|
-
<p>Every install the site made of this app, with the values it configured. A variable the manifest declares as a secret never reaches this panel — your server handler receives those.</p>
|
|
19
|
-
<ul id='installs'></ul>
|
|
20
|
-
</main>
|
|
21
|
-
<script>
|
|
22
|
-
// Resolved from the app's own dependency one directory up; the panel declares only its build tooling.
|
|
23
|
-
import { createAppUi } from '@blotout/qinto-sdk-app/ui'
|
|
24
|
-
|
|
25
|
-
const ui = createAppUi()
|
|
26
|
-
const context = await ui.getContext()
|
|
27
|
-
|
|
28
|
-
document.getElementById('site').textContent = context.siteOrigin
|
|
29
|
-
document.getElementById('version').textContent = context.appVersion
|
|
30
|
-
|
|
31
|
-
const line = (install) => {
|
|
32
|
-
const values = Object.entries(install.variables).map(([name, value]) => `${name}=${value}`)
|
|
33
|
-
const own = install.id === context.appInstallId ? ' — this panel' : ''
|
|
34
|
-
return `${install.label}${own}: ${values.join(', ') || 'nothing configured yet'}`
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
document.getElementById('installs').replaceChildren(
|
|
38
|
-
...context.installs.map((install) => {
|
|
39
|
-
const item = document.createElement('li')
|
|
40
|
-
item.textContent = line(install)
|
|
41
|
-
return item
|
|
42
|
-
})
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
// ui.fetch(path) calls this app's own `ui` server handler with the panel token attached —
|
|
46
|
-
// that is where a secret, an app-scope credential, or a settings write belongs. getToken() is
|
|
47
|
-
// for calling your own endpoints instead.
|
|
48
|
-
</script>
|
|
49
|
-
</body>
|
|
50
|
-
</html>
|