diff --git a/README.md b/README.md index 9b25df4a3..1e7745c8d 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ One bundle serves both backends. The extension decides when it starts, first mat | 4 | Install type: an **unpacked** install (`chrome.management.getSelf().installType === 'development'`) is TEST, any other install is PROD | Needs no extra permission; background service worker only | | 5 | Default: PROD | Same as the SDK | -**Tier stamp.** The stamp is the default tier of a store install, so the TEST store item ("UDA TEST") and the PROD item run the same code and differ only in this default. A stamp `TEST` **locks** the extension to TEST: the options page hides the PROD choice, and a PROD request from the options override or from a host page is ignored (the worker and the page side both check). A stamp `PROD` accepts the overrides as before. Without a stamp nothing changes: an unpacked development build (the `test-current` zip has no stamp) is TEST, anything else is PROD. A stamped zip that is loaded unpacked follows its stamp (a PROD-stamped zip gives PROD), so the file you unpack declares its tier. The stamp holds no secret and no URL; the URLs still come from the build settings. The `source` reported by the options page and by `UDASdkReady` is `stamp` when the stamp decided. +**Tier stamp.** The stamp is the default tier of a store install, so the TEST store item ("UDA TEST") and the PROD item ("Universal Digital Assistant by Nistapp (UDAN)") run the same code and differ only in this default. A stamp `TEST` **locks** the extension to TEST: the options page hides the PROD choice, and a PROD request from the options override or from a host page is ignored (the worker and the page side both check). A stamp `PROD` accepts the overrides as before. Without a stamp nothing changes: an unpacked development build (the `test-current` zip has no stamp) is TEST, anything else is PROD. A stamped zip that is loaded unpacked follows its stamp (a PROD-stamped zip gives PROD), so the file you unpack declares its tier. The stamp holds no secret and no URL; the URLs still come from the build settings. The `source` reported by the options page and by `UDASdkReady` is `stamp` when the stamp decided. The URLs for the chosen environment come, per key, from the page (`apiBaseUrl`, `tokenUrl`, `llmUrl`, `keycloakUrl`), then from the build settings above, then from the SDK's own defaults. A key the page set is never overwritten. When the environment (or a backend, realm or client setting) changes while the panel is open, the panel drops the stored session of the old backend, asks the worker for the session again and runs the search again; no page reload is needed. This is `AuthContext` listening to `UDAConfigUpdated`. The Client never calls `authManager.init` (sign-in runs in the worker), so there is nothing else to re-run. @@ -172,7 +172,7 @@ Branch from `dev` and open a PR into `dev`; the PR title must be a [Conventional - Zip: `udan-extension-.zip`, plus `SHA256SUMS` and `build-info.json` (commit, branch, run, attempt, config hash, SDK version and integrity, names of the env keys that were set). Same commit, run number, attempt and build configuration give the same bytes. - Version: CI stamps the built manifest with `version = X.Y.Z.` and `version_name = "X.Y.Z () run . cfg "`. X.Y.Z comes from `package.json` (Release Please owns it); the run number makes each build higher than the last. `version_name` is the build identity: a re-run of the same CI run (new attempt, e.g. after fixing an Actions variable) or any changed build setting changes it, while `version` stays. The config hash is 8 hex of a sha256 over the build settings the bundle inlines (`node scripts/build-env.js production --hash`); the values are not recoverable from it. The source manifests only hold the base version, for local builds. -- Store zips: the same run also writes the two Chrome Web Store zips, `udan-extension--store-test.zip` (for the unlisted TEST item "UDA TEST") and `udan-extension--store-prod.zip` (for the PROD item), each with its line in `SHA256SUMS`. See "Store zips" below. +- Store zips: the same run also writes the two Chrome Web Store zips, `udan-extension--store-test.zip` (for the unlisted TEST item "UDA TEST") and `udan-extension--store-prod.zip` (for the PROD item, "Universal Digital Assistant by Nistapp (UDAN)"), each with its line in `SHA256SUMS`. See "Store zips" below. - Retention: artifact `client-zip`, 90 days for pushes to `dev`, 14 days for PRs. The run summary shows the version, SHA256 and SDK version. - Build settings in CI come from GitHub Actions **variables** (`BASE_PROD_URL`, `TOKEN_TEST_URL`, ..., `KEYCLOAK_PUBLIC_CLIENT_ID`, `KEYCLOAK_IDP_HINT`: `scripts/ci/generate-env.sh --print-names`), never from secrets. They are set once at the organization level and shared with the SDK repo, which uses the same names. @@ -204,7 +204,7 @@ Release Please no longer touches the manifests: CI owns the extension version. A |---|---|---|---| | Used for | `Deploy to TEST` prereleases, unpacked testing | the unlisted TEST store item | the PROD store item | | Manifest `key` | kept (pins the dev/TEST id) | **removed** (the store fixes the item id) | **removed** | -| Manifest `name` | unchanged | `UDA TEST` | unchanged | +| Manifest `name` | unchanged | `UDA TEST` | unchanged (`Universal Digital Assistant by Nistapp (UDAN)` from `public/manifest.json`) | | `tier.json` | none (unpacked = TEST, as before) | `{"tier":"TEST"}` | `{"tier":"PROD"}` | | Icons | `logos/nist-logo-*.png` | TEST badge from `store/test-icons/` | unchanged | diff --git a/docs/store-listing-draft.md b/docs/store-listing-draft.md index 2edf7a7d3..1b070e832 100644 --- a/docs/store-listing-draft.md +++ b/docs/store-listing-draft.md @@ -1,17 +1,105 @@ -# Store listing draft: privacy (owner submits) +# Store listing: copy-paste text for the Chrome Web Store dashboard -Draft only. The Chrome Web Store text is the owner's to submit; check it against the README section "Privacy: what the extension stores" first. +The owner submits this text. It is checked against the README section "Privacy: what the extension stores"; change the README and this file together. Images and the dashboard field list are in `store/listing/README.md`. The two items (PROD and the unlisted TEST item "UDA TEST") use the same text; the item name comes from the manifest in each store zip, not from this file. The PROD name is `Universal Digital Assistant by Nistapp (UDAN)`, exactly 45 characters, the Chrome Web Store limit; keep any copy consistent with it. -**Single purpose.** Record, save and replay step-by-step guides on a web page (Universal Digital Assistant Network). +## Store listing tab -**Data the extension handles** -- Account: when you sign in, UDAN gets a Keycloak account id, your email and your name from your Google account. The extension keeps sign-in tokens in its own browser storage so you stay signed in; Sign Out deletes them and ends the session. -- Content you create: the guides you record and save (the clicks and labels on a page), and search terms you type, are sent to the UDAN service. -- Local: the extension stores its settings and your sign-in tokens in the browser only. +**Title (PROD item).** Universal Digital Assistant by Nistapp (UDAN) + +The title is the manifest `name` of the PROD store zip (45 characters; the documented manifest limit is 75). It is the product name, not a beta label: do not add Beta or Alpha to the listing title or text. The TEST item keeps the name "UDA TEST". + +**Summary** (from the package; 112 of 132 characters) + +``` +Record, save and replay step-by-step guides on any web page. Sign in with Google to keep and search your guides. +``` + +**Description** (plain text, no headings; paste as is) + +``` +Universal Digital Assistant by Nistapp (UDAN) lets you record a task on a web page as a step-by-step guide, save it, and replay it later so that you or someone else can follow the same steps. + +What you can do +- Record: start a recording on a page and click through the task. The extension notes each step (the clicked element and a label you can edit). +- Save: give the guide a name and save it to your UDAN account. +- Replay: open the panel on the page, search for a guide, and follow it step by step. + +How it works +The extension adds a small UDAN button to web pages. Open it to get the panel. Signing in uses Google through UDAN's sign-in page. + +Your data, in plain words +- When you sign in, UDAN receives a Keycloak account id, your email and your name from your Google account. Your Google password is never seen by UDAN: you enter it on Google's or UDAN's own sign-in page. +- The guides you record and save (the clicks and labels on a page, and the page's domain) and the search terms you type are sent to the UDAN service. +- Your sign-in tokens and settings are kept in the extension's own browser storage so that you stay signed in. Sign Out ends the session and deletes the tokens. +- The web page where the panel runs receives a short-lived access token (about 5 minutes). A script on that page can ask the extension for a fresh access token while you are signed in. The long-lived refresh token never leaves the extension. +- Your data is not sold and not used for advertising. + +Privacy policy: https://udan.nistapp.com/legal/privacy +``` + +**Category:** Productivity, subcategory "Workflow & Planning". Closest alternatives: Productivity > "Tools" and Productivity > "Education" (from the dashboard list as I know it; I did not open the dashboard, so if a name has changed, pick the nearest). + +**Language:** English. + +**Privacy policy URL:** `https://udan.nistapp.com/legal/privacy` (renders the policy in a browser, verified 2026-11-01). Do not use `https://udan.nistapp.com/privacy`: the site is a single-page app and that route renders an empty page. + +**Images:** see `store/listing/README.md` (three 1280x800 screenshots, small promo tile 440x280). + +## Privacy practices tab -**Things to say plainly** -- The web page where the panel runs receives a short-lived access token (about 5 minutes). A script on that page can ask the extension for a new one while you are signed in. The long-lived refresh token never leaves the extension. +**Single purpose** (paste into "Single purpose description") + +``` +Record, save and replay step-by-step guides on a web page (Universal Digital Assistant Network). +``` + +**Permission justifications** (one box per permission; unchanged from the earlier submission, `1.0.8.8`) + +| Permission | Text for the dashboard | +|---|---| +| `identity` | Opens the sign-in window (launchWebAuthFlow) for the user's Google sign-in through UDAN's sign-in page, and reads the Chrome profile hint so the sign-in page can suggest the signed-in Chrome account. | +| `identity.email` | Suggests the Chrome account that is already signed in on the sign-in page, so the user does not have to type it. | +| `storage` | Keeps the extension's settings and the user's sign-in tokens in the extension's own storage so the user stays signed in. Sign Out deletes the tokens. | +| `tabs` | Finds the page the user is on, to show the panel on it. | +| `activeTab` | Gives the extension access to the page the user is on when they open the panel or start a recording. | +| Host access (`*://*/*`, content scripts) | The panel and the recorder run on the pages where the user records or replays a guide. The guides are for customer sites that can be on any domain, so the extension cannot name them in advance. | + +**Remote code:** No. All code is in the package; the page script is loaded from the extension's own files. + +**Data usage: what the extension collects** (tick these; leave the others empty) + +| Dashboard category | Tick | Why | +|---|---|---| +| Personally identifiable information | yes | Name and email from the user's Google account, and the Keycloak account id. | +| Authentication information | yes | Access, refresh and ID tokens from the UDAN sign-in (kept in the extension's storage). | +| User activity | yes | The clicks the user records as steps in a guide. | +| Website content | yes | The labels and element data of the steps of a recorded guide. | +| Web history | yes | A saved guide stores the page it was recorded on (`fetchDomain()` / the page URL is saved with the recording), so page addresses are collected. Verified. | +| Health, Financial and payment, Personal communications, Location | no | Not collected. | + +**Certifications** (the three statements; tick all three) + +- I do not sell or transfer user data to third parties, outside of the approved use cases. Data goes to the UDAN service that provides the item's function; the Google sign-in is the approved sign-in use. Not sold. +- I do not use or transfer user data for purposes that are unrelated to my item's single purpose. Not used for advertising or anything else. +- I do not use or transfer user data to determine creditworthiness or for lending purposes. + +**Answers to the questions behind them** + +| Question | Answer | +|---|---| +| Is data sold? | No. | +| Used for purposes unrelated to the single purpose (for example advertising)? | No. | +| Is the data required for the single purpose? | Yes: the account is needed to save and find guides; the guides and search terms are the function itself. | +| Encrypted in transit? | Yes. Evidence: the PROD store zip `1.1.0.204` was unzipped and the bundle grepped; every backend host is `https://` (`udan.nistapp.com`, `authenticate.nistapp.com`, `iam.nistapp.com`, the `*test` equivalents, third-party docs links). The only `http://` strings are `http://localhost` (dev only) and `http://www.w3.org` (a namespace string, not a request). Checkpoint: no PROD request uses plain HTTP. | +| Can the user delete it? | Sign Out deletes the tokens in the extension. For data saved on the service, point to the privacy policy (the policy page is the place to name a deletion route; see the privacy-policy placeholder issue in the PR output). | + +## Facts behind the text (from the README, keep in step) + +- Account: when you sign in, UDAN gets a Keycloak account id, your email and your name from your Google account. The extension keeps sign-in tokens in its own browser storage (`chrome.storage.local`, not readable by websites) so you stay signed in; Sign Out ends the session and deletes them. +- Content you create: the guides you record and save (the clicks and labels on a page), and search terms you type, are sent to the UDAN service. The email is sent as a hash. +- Local: the extension stores its settings and your sign-in tokens in the browser only. +- The web page where the panel runs receives a short-lived access token (about 5 minutes). **A script on that page can ask the extension for a new one while you are signed in** (`UDARequestAccessToken`). The long-lived refresh token never leaves the extension. - Your Google password is never seen by UDAN: you enter it on Google's or Keycloak's own page. - Data is not sold and not used for advertising. - -**Permission reasons** (unchanged by this change): `identity` (sign-in window and the Chrome profile hint), `identity.email` (suggest the signed-in Chrome account on the sign-in page), `storage` (settings and tokens), `tabs` and `activeTab` (find the page to show the panel on), host access (the panel runs on the pages of customer sites). +- The package file `tier.json` (`TEST` or `PROD`) is read from the extension's own package; it stores nothing and sends nothing. +- Not stored or sent: the Google password, browsing history outside the pages where the user records. diff --git a/package.json b/package.json index ab4c1c38f..d355f7fe4 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "UDAN-Frontend", "version": "1.1.0", "private": true, + "license": "AGPL-3.0-only", "dependencies": { "@radix-ui/react-accordion": "^1.2.3", "@radix-ui/react-alert-dialog": "^1.1.6", diff --git a/public/manifest-firefox.json b/public/manifest-firefox.json index ef48260bd..6beb18319 100644 --- a/public/manifest-firefox.json +++ b/public/manifest-firefox.json @@ -1,6 +1,6 @@ { "manifest_version": 3, - "name": "Universal Digital Assistant (UDA) Beta", + "name": "Universal Digital Assistant by Nistapp (UDAN)", "version": "1.1.0", "content_scripts": [ { diff --git a/public/manifest.json b/public/manifest.json index 67c122da2..27559d540 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,6 +1,6 @@ { "manifest_version": 3, - "name": "Universal Digital Assistant (UDA) Beta", + "name": "Universal Digital Assistant by Nistapp (UDAN)", "version": "1.1.0", "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/HrhD5R5x/MgAVFeM9SJelrhbDUuu52lX2TgP2mdsCgeYZCfNbj7ymCrQrydbxKZpC9Lh/AtyqlFtmXgHGfUvxpXbH/JO/vMqNQImDIIej7gdJq4dBunjbMxpGi9L7qB+3aVBAXI+rlxqToaFoTl6s2Xj6prrIlRBh160mWyARtz0yT20jzrEqITq9cydtyUGskFjquVblFwRHxWgfkiQDcq8G7jU212zyFm9z4knnNNXOIUUVZ99+hU3rlMzmgssImJhgG5xGIUEl7TPa86OVpN3RLE+iXC/pf/EEWwYQoHT7qftoKpq1LC0PJKkfzKxb2iadVxwTBDZyLJqt8WHwIDAQAB", "content_scripts": [ diff --git a/scripts/ci/test-known-issues.md b/scripts/ci/test-known-issues.md index 81e9579f5..2f299201d 100644 --- a/scripts/ci/test-known-issues.md +++ b/scripts/ci/test-known-issues.md @@ -1 +1 @@ -- The TEST backend currently rejects requests from browsers (CORS). Known issue, being fixed on the backend side. +- None known at the moment. diff --git a/store/listing/01-title-card-1280x800.png b/store/listing/01-title-card-1280x800.png new file mode 100644 index 000000000..84f84a815 Binary files /dev/null and b/store/listing/01-title-card-1280x800.png differ diff --git a/store/listing/02-panel-sign-in-1280x800.png b/store/listing/02-panel-sign-in-1280x800.png new file mode 100644 index 000000000..254d887f1 Binary files /dev/null and b/store/listing/02-panel-sign-in-1280x800.png differ diff --git a/store/listing/03-page-with-udan-button-1280x800.png b/store/listing/03-page-with-udan-button-1280x800.png new file mode 100644 index 000000000..5f7854ac4 Binary files /dev/null and b/store/listing/03-page-with-udan-button-1280x800.png differ diff --git a/store/listing/README.md b/store/listing/README.md new file mode 100644 index 000000000..4c02f2aa0 --- /dev/null +++ b/store/listing/README.md @@ -0,0 +1,57 @@ +# Store listing assets + +Everything the Chrome Web Store dashboard asks you to supply for the listing. The text lives in `docs/store-listing-draft.md`; this folder holds the images and how to make them again. Nothing here is uploaded by a script: the owner pastes and uploads by hand. + +## Files + +| File | Pixels | What | +|---|---|---| +| `01-title-card-1280x800.png` | 1280x800 | **Generated** title card: logo, product name, one line of value text, white background. Not a UI screenshot. | +| `02-panel-sign-in-1280x800.png` | 1280x800 | **Real UI**: the signed-out panel ("Sign in with Google") open on a synthetic page. | +| `03-page-with-udan-button-1280x800.png` | 1280x800 | **Real UI**: the same synthetic page with the extension's floating UDAN button, panel closed. | +| `promo-tile-440x280.png` | 440x280 | **Generated** small promo tile: logo and name. | +| `demo-page.html` | | The synthetic page ("Example Co.", placeholder text, no real site or person). | +| `make-cards.sh` | | Makes the title card and the tile (ImageMagick, DejaVu Sans). | +| `capture-screenshots.mjs` | | Makes screenshots 02 and 03 from a built extension (Playwright). | + +No screenshot shows a signed-in state: that needs a backend, and nothing here may touch one. The panel shows no TEST badge because the demo page asks for the PROD look (`UDAPluginSDK({ environment: "PROD" })` after `UDASdkReady`, the documented host-page call); the build behind it has only unroutable placeholder URLs. + +## Make them again + +From the repo root, Node 22, ImageMagick, and the Playwright Chromium (`npx playwright-core install chromium` if missing): + +``` +store/listing/make-cards.sh + +printf '%s\n' baseProdURL=http://127.0.0.1:9/api baseTestURL=http://127.0.0.1:9/api \ + tokenProdUrl=http://127.0.0.1:9 tokenTestUrl=http://127.0.0.1:9 \ + llmProdUrl=http://127.0.0.1:9 llmTestUrl=http://127.0.0.1:9 \ + keycloakProdUrl=http://127.0.0.1:9 keycloakTestUrl=http://127.0.0.1:9 \ + keycloakRealm=placeholder-realm keycloakClientId=placeholder-client \ + keycloakPublicClientId=placeholder-public-client keycloakIdpHint= \ + googleAnalyticsMeasurementId= enableGoogleAnalytics=false > /tmp/shot.env +UDAN_ENV_FILE=/tmp/shot.env npm run build +node store/listing/capture-screenshots.mjs build +``` + +The capture serves the page from `127.0.0.1` (content scripts only run on `http(s)` pages), aborts every other request, prints the hosts it aborted and fails on anything but the placeholder `127.0.0.1:9`. The cards are deterministic (same bytes on every run with the same fonts); screenshots 02 and 03 depend on the Chromium build and fonts, so re-run `identify` after regenerating. + +## Dashboard fields, copy-paste + +Text for each field is in `docs/store-listing-draft.md`. + +| Dashboard tab / field | Value | +|---|---| +| Store listing, Summary (max 132) | the "Summary" block (112 characters) | +| Store listing, Description | the "Description" block | +| Store listing, Category | Productivity (alternatives: Tools, Education) | +| Store listing, Language | English | +| Store listing, Screenshots | `01`, `02`, `03` (1280x800) | +| Store listing, Small promo tile | `promo-tile-440x280.png` | +| Store listing, Privacy policy URL | `https://udan.nistapp.com/legal/privacy` (never `/privacy`: it renders an empty page) | +| Privacy practices, Single purpose | the "Single purpose" block | +| Privacy practices, Permission justifications | the table: `identity`, `identity.email`, `storage`, `tabs`, `activeTab`, host access | +| Privacy practices, Remote code | No | +| Privacy practices, Data usage | Personally identifiable information, Authentication information, User activity, Website content, Web history | +| Privacy practices, Certifications | tick all three (not sold, no unrelated use, no creditworthiness use) | +| Distribution, Visibility (TEST item) | Unlisted | diff --git a/store/listing/capture-screenshots.mjs b/store/listing/capture-screenshots.mjs new file mode 100644 index 000000000..5a7b9acc9 --- /dev/null +++ b/store/listing/capture-screenshots.mjs @@ -0,0 +1,66 @@ +// Capture the real-UI store screenshots from a built extension (1280x800). +// npm run build (any build; the capture only needs the unpacked folder) +// node store/listing/capture-screenshots.mjs [] +// What it does: loads unpacked into Chromium (Playwright), serves the synthetic +// page demo-page.html from 127.0.0.1 (the content scripts only run on http(s) pages, so file:// +// would show no panel) and screenshots the page and the signed-out panel. Every request other than +// that page is aborted and listed; the run fails if the extension tried anything else than the +// build's own configured backend URLs, so nothing can reach a real backend, Keycloak, TEST or PROD. +// Build it with placeholder URLs (see README.md) so the aborted requests go nowhere anyway. +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { createServer } from "node:http"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { chromium } from "playwright-core"; + +const here = dirname(fileURLToPath(import.meta.url)); +if (!process.argv[2]) { console.error("usage: capture-screenshots.mjs []"); process.exit(2); } +const buildDir = resolve(process.argv[2]); +const outDir = resolve(process.argv[3] || here); + +const page_html = readFileSync(join(here, "demo-page.html")); +const server = createServer((_req, res) => { res.setHeader("content-type", "text/html; charset=utf-8"); res.end(page_html); }); +await new Promise((r) => server.listen(0, "127.0.0.1", r)); +const pageOrigin = `http://127.0.0.1:${server.address().port}`; + +const profile = mkdtempSync(join(tmpdir(), "udan-shot-")); +const context = await chromium.launchPersistentContext(profile, { + channel: "chromium", + headless: true, + viewport: { width: 1280, height: 800 }, + deviceScaleFactor: 1, + args: [`--disable-extensions-except=${buildDir}`, `--load-extension=${buildDir}`], +}); + +// Allow only the local page itself and the browser's internal schemes; abort and record the rest. +const aborted = []; +await context.route( + (url) => !(url.origin === pageOrigin || /^(chrome-extension|about|blob|data):$/.test(url.protocol)), + (route) => { aborted.push(route.request().url()); route.abort(); }, +); + +try { + const page = await context.newPage(); + await page.goto(`${pageOrigin}/`, { waitUntil: "load" }); + await page.waitForSelector("#udan-react-root", { state: "attached", timeout: 20_000 }); + const button = page.locator("#udan-react-root >> text=UDAN").first(); + await button.waitFor({ state: "visible", timeout: 20_000 }); + await page.waitForTimeout(1500); + await page.screenshot({ path: join(outDir, "03-page-with-udan-button-1280x800.png") }); + + await button.click(); + await page.getByText("Sign in with Google").first().waitFor({ state: "visible", timeout: 20_000 }); + await page.waitForTimeout(1500); + await page.screenshot({ path: join(outDir, "02-panel-sign-in-1280x800.png") }); +} finally { + await context.close().catch(() => {}); + server.close(); + rmSync(profile, { recursive: true, force: true }); +} + +// Report only hosts of aborted requests (placeholders in a placeholder build). +const hosts = [...new Set(aborted.map((u) => new URL(u).host))]; +console.log(`aborted ${aborted.length} non-local request(s); hosts: ${hosts.join(", ") || "none"}`); +const unexpected = hosts.filter((h) => h !== "127.0.0.1:9"); +if (unexpected.length) { console.error(`unexpected host(s): ${unexpected.join(", ")}`); process.exit(1); } diff --git a/store/listing/demo-page.html b/store/listing/demo-page.html new file mode 100644 index 000000000..e685eb22d --- /dev/null +++ b/store/listing/demo-page.html @@ -0,0 +1,50 @@ + + + + + + + Example Co. - Help Center (demo page) + + + + +
+ Example Co. + +
+
+

Request a service visit

+

A made-up page for illustration. Fill in the form below to book a visit.

+
+ + + + + + + +
+
+ + diff --git a/store/listing/make-cards.sh b/store/listing/make-cards.sh new file mode 100755 index 000000000..0cb8044b0 --- /dev/null +++ b/store/listing/make-cards.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Generate the title card (1280x800) and the small promo tile (440x280) with ImageMagick. +# Deterministic: fixed fonts, fixed colours, no timestamps (png:exclude-chunk drops date chunks). +# Usage: store/listing/make-cards.sh (run from anywhere) +# Needs: ImageMagick 6 or 7 (magick or convert) and the DejaVu Sans fonts (override with FONT_BOLD / FONT_REG). +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +logo="$here/../../public/logos/nist-logo-128x128.png" +FONT_BOLD="${FONT_BOLD:-/usr/share/fonts/TTF/DejaVuSans-Bold.ttf}" +FONT_REG="${FONT_REG:-/usr/share/fonts/TTF/DejaVuSans.ttf}" +for f in "$logo" "$FONT_BOLD" "$FONT_REG"; do [ -f "$f" ] || { echo "missing: $f" >&2; exit 2; }; done + +if command -v magick >/dev/null 2>&1; then im=(magick); else im=(convert); fi +bg="#ffffff"; ink="#14213d"; mute="#4a5568" +png=(-define png:exclude-chunk=date,time -strip) + +# Title card, 1280x800: logo, product name on two lines, one line of value text. +"${im[@]}" -size 1280x800 "xc:$bg" \ + "$logo" -gravity north -geometry +0+210 -composite \ + -gravity north -font "$FONT_BOLD" -fill "$ink" -pointsize 58 -annotate +0+370 "Universal Digital Assistant" \ + -gravity north -font "$FONT_BOLD" -fill "$ink" -pointsize 58 -annotate +0+440 "by Nistapp (UDAN)" \ + -gravity north -font "$FONT_REG" -fill "$mute" -pointsize 30 -annotate +0+560 "Record, save and replay step-by-step guides on any web page." \ + "${png[@]}" "PNG24:$here/01-title-card-1280x800.png" + +# Small promo tile, 440x280: logo and name only. +"${im[@]}" -size 440x280 "xc:$bg" \ + "$logo" -gravity north -geometry +0+36 -composite \ + -gravity north -font "$FONT_BOLD" -fill "$ink" -pointsize 22 -annotate +0+184 "Universal Digital Assistant" \ + -gravity north -font "$FONT_BOLD" -fill "$ink" -pointsize 22 -annotate +0+214 "by Nistapp (UDAN)" \ + "${png[@]}" "PNG24:$here/promo-tile-440x280.png" + +identify "$here/01-title-card-1280x800.png" "$here/promo-tile-440x280.png" diff --git a/store/listing/promo-tile-440x280.png b/store/listing/promo-tile-440x280.png new file mode 100644 index 000000000..3fdc4d8a0 Binary files /dev/null and b/store/listing/promo-tile-440x280.png differ