@bridgeline-digital/hawk-app-cli 0.1.1-dev.20261021204606 → 0.1.1-dev.20261026194933

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.
Files changed (38) hide show
  1. package/README.md +150 -8
  2. package/agents/AGENTS.md +34 -4
  3. package/claude-plugin/hawk-apps/skills/add-function/SKILL.md +17 -5
  4. package/claude-plugin/hawk-apps/skills/new-screen/SKILL.md +4 -0
  5. package/claude-plugin/hawk-apps/skills/prepare-review/SKILL.md +6 -2
  6. package/docs/bridge/README.md +375 -27
  7. package/docs/showcase/src/App.tsx +22 -2
  8. package/docs/ui/README.md +15 -3
  9. package/package.json +11 -1
  10. package/schema/hawk-app.schema.json +225 -1
  11. package/src/cli.mjs +48 -7
  12. package/src/deploy.mjs +16 -8
  13. package/src/dev.mjs +177 -4
  14. package/src/doctor.mjs +6 -0
  15. package/src/error-brief.mjs +128 -0
  16. package/src/function-test-runner.mjs +16 -0
  17. package/src/function-test.d.ts +51 -0
  18. package/src/function-test.mjs +303 -0
  19. package/src/functions.mjs +35 -21
  20. package/src/manifest.mjs +256 -1
  21. package/src/mcp-hawksearch.mjs +4 -3
  22. package/src/mcp.mjs +26 -3
  23. package/src/platform.mjs +66 -0
  24. package/src/project-checks.mjs +17 -3
  25. package/src/scope-check.mjs +9 -2
  26. package/src/sourcemaps.mjs +168 -0
  27. package/src/telemetry.mjs +103 -5
  28. package/src/template-history.json +8 -4
  29. package/src/test-command.mjs +88 -0
  30. package/templates/next/next.config.ts +3 -0
  31. package/templates/next/src/app/page.tsx +6 -0
  32. package/templates/next-mui/next.config.ts +3 -0
  33. package/templates/next-mui/src/app/page.tsx +3 -1
  34. package/templates/preact/src/App.tsx +6 -0
  35. package/templates/preact/vite.config.ts +3 -0
  36. package/templates/react/src/App.tsx +6 -0
  37. package/templates/react/vite.config.ts +3 -0
  38. package/templates/vanilla/public/app.js +2 -0
package/README.md CHANGED
@@ -69,7 +69,8 @@ separately, see [Clients](#clients)):
69
69
  // client can request it from the store (needs "store", below) (clients: see "Clients")
70
70
  "icon": "icon.svg", // the app's icon: an .svg (drawn in the host's color) or a square .png, next to this file
71
71
  "scopes": ["reports:read"] // reports:read = analytics; hawksearch:read = dashboard API (read) and preview tools;
72
- // hawksearch:write = create/update/delete synonyms, redirects, banners, …
72
+ // hawksearch:write = create/update/delete synonyms, redirects, banners, …,
73
+ // and pin items (hawksearch.pinning)
73
74
  // through the dashboard API (see the bridge's "Writing to HawkSearch");
74
75
  // data:read = your own SQL queries in queries/ (see "Custom data");
75
76
  // functions:invoke = server-side functions (see "Functions");
@@ -176,8 +177,8 @@ npx hawk-app dev --client e3141 --engine 3141
176
177
 
177
178
  The first time, it asks for part of a client's name and lets you pick the client, then saves it in `.env.local` as
178
179
  `HAWK_APPS_DEV_CLIENT` (`--client <id>` uses another). A new app is registered on the platform the first time (not
179
- released: only Bridgeline staff see it). Queries and functions always run from the **deployed** version, even here:
180
- deploy after changing them.
180
+ released: only Bridgeline staff see it). Queries and functions called from the app run from the **deployed** version,
181
+ even here: deploy after changing them (or test them locally with `hawk-app test`).
181
182
 
182
183
  Starts your app (`npm run dev`) and a local stand-in for HawkSearch at `http://localhost:3200`, which embeds the app
183
184
  the way HawkSearch does (the bridge's own `<hawk-app-frame>`). The app gets **real tokens and real data**: the CLI asks
@@ -195,7 +196,19 @@ CORS setup, on dev or prod; so does the AI stream (`app.ai.stream`), which the p
195
196
  `https://dev.hawksearch.net`; `HAWKSEARCH_GROUP_KEY=true` for a group key): the platform seals it into the token. It is
196
197
  never written anywhere else.
197
198
  - Queries and functions run against your **current deployed version** (deploy first after changing them); the UI is your
198
- local code. Every dev token is audit-logged with your publisher key.
199
+ local code. Every dev token is audit-logged with your publisher key. To run functions from your local code, use
200
+ `hawk-app test` ([Testing functions](#testing-functions-hawk-app-test)).
201
+ - **Actions:** when `hawk-app.json` has `actions`, the toolbar's **Action** menu opens an action's view in a dialog as
202
+ HawkSearch will (type the value its targets would read from the page); `app.action.done()` closes it.
203
+ - **Export as PDF:** the toolbar has HawkSearch's PDF icon. It shows while the page allows it (`app.setPdfExport` /
204
+ `usePdfExport`; the events list logs it), and a click downloads the page as a PDF.
205
+ - **Explain this page:** the toolbar has HawkSearch's **Explain this page** button. It shows once the page says what
206
+ it shows (`app.assistant.setContext` / `usePageContext`; the events list logs each one), and explains it with the
207
+ platform's AI for the client.
208
+ - **Client API:** `http://localhost:3200/v1/clients/<id>/apps/<slug>/api/<name>` (with `X-HawkSearch-ApiKey`) runs the
209
+ endpoint's function from your **local** `functions/` (bundled on each call), with a real token for the client whose
210
+ HawkSearch connection is the key you send. The platform's own checks (the key's engine, API access, the limit) are
211
+ skipped; secrets and config are empty.
199
212
 
200
213
  ## Deploy
201
214
 
@@ -217,7 +230,7 @@ npx hawk-app deploy --dry-run # what hawk-app.json would change, without a v
217
230
  | Command | What it does |
218
231
  |---|---|
219
232
  | `init` | Creates `hawk-app.json` (never overwrites an existing one) and adds [AGENTS.md](#ai-coding-assistants-agentsmd). |
220
- | `validate [--build [<folder>]]` | Checks `hawk-app.json` locally, `queries/` and `functions/` if the app has them, and the project: the bridge is used and current, and there's a build script (or `--no-build` deploys). `--build` also builds the app with a stand-in base path (`/<slug>/v0`) and fails on absolute URLs outside it; the folder comes from the `app:deploy` script unless given. |
233
+ | `validate [--build [<folder>]]` | Checks `hawk-app.json` locally, `queries/` and `functions/` if the app has them, and the project: the bridge is used and current, the app says what its page shows ("Explain this page"), and there's a build script (or `--no-build` deploys). `--build` also builds the app with a stand-in base path (`/<slug>/v0`) and fails on absolute URLs outside it; the folder comes from the `app:deploy` script unless given. |
221
234
  | `deploy [<folder>] [--no-build] [--no-audit] [--alias <name>] [--dry-run]` | Sends hawk-app.json first (its scopes and settings go with every deploy), then reserves the next version number, runs `npm run build` with `HAWK_APP_BASE_PATH=/<slug>/v<n>` (when package.json has a `build` script, unless `--no-build`), then uploads every file in the build folder (the template's, `dist`, `out` or `public`, unless given) to `/<slug>/v<n>/` on the apps CDN, plus `queries/` and the bundled `functions` (if any) to the platform's private storage, and deploys the functions. Runs `npm audit` of the production dependencies first, asks before deploying with high or critical findings, and reports the counts to reviewers (see [Dependency check](#dependency-check); `--no-audit` skips it). Warns when the HTML loads absolute URLs outside `/<slug>/v<n>/`, which wouldn't load, and when the code calls a bridge feature whose scope `hawk-app.json` lacks (e.g. `app.ai.generate` without `ai:invoke`; `validate` and `doctor` check it too). Says whether the version is a draft (staff only until it's approved) or auto-approved, and when it cleared an approval request. `--alias staging` points the alias at the new version (see [Versions and aliases](#versions-and-aliases)). `--dry-run` only shows what hawk-app.json would change. |
222
235
  | `register` | Sends `hawk-app.json` without a new version (for scripts; `deploy` and `dev` already send it). It never releases anything and never changes the app's clients. On a released app, changes wait for review. |
223
236
  | `versions` | Every version with its status (draft, waiting, approved, rejected) and the aliases on it. |
@@ -293,13 +306,40 @@ HawkSearch). The full picture, with diagrams: docs/versions-and-aliases.md in th
293
306
 
294
307
  ## Errors and usage (`hawk-app errors`, `hawk-app usage`)
295
308
 
296
- The bridge reports the app's page errors and one open per page load (platform/docs/telemetry.md):
309
+ The bridge reports the app's page errors, one open per page load, and the screens and actions used
310
+ (platform/docs/telemetry.md):
297
311
 
298
312
  ```bash
299
- npx hawk-app errors # the last 7 days (--days 1-30): grouped, the most frequent first, with versions and clients
313
+ npx hawk-app errors # the last 7 days (--days 1-30): grouped, the most frequent first, with versions, clients and an [id]
314
+ npx hawk-app errors --explain 3fa1c2d9 # one error's brief, then Claude's likely cause and patch (--no-ai: the brief only)
300
315
  npx hawk-app usage # the last 30 days (--days 1-400): opens, view opens, users and clients; staff apart
316
+ npx hawk-app usage --screens # screens and actions: the most used, then the least used, with the 3 newest versions
317
+ npx hawk-app usage --screens --version 4 # one version's counts
301
318
  ```
302
319
 
320
+ **What people use.** Screens are the app's routes (ids as `:id`), its views, and what it names with `app.usage.screen`
321
+ (the UI kit's `useScreen('Logs')`, for a tab); actions are clicks on `data-hawk-usage="Export CSV"` elements and
322
+ `app.usage.action`. Compare versions to see whether a change is used, and the least used to see what to simplify.
323
+
324
+ **Errors that come with a fix.** `--explain <id>` writes the error's **brief** and asks Claude for the cause:
325
+
326
+ - **The stack, mapped to your source:** each of the app's frames as `src/app/page.tsx:42:17`, with the code around it
327
+ and the line that threw marked `>`. The maps are the version's own, so the code is what ran, not what's in your
328
+ checkout now. Library frames are listed on one line each.
329
+ - **The platform calls that failed** in the same versions and days (route, status, code, detail), often the reason
330
+ (e.g. a report that answered 502 left `rows` undefined).
331
+ - **What changed since the last version without it:** a diff of the files in the stack between that version and the
332
+ one that threw.
333
+ - **Then Claude** (the platform's, on its account, no scope or key needed) answers with the likely cause, a patch as a
334
+ unified diff, and how to check it. Check the patch before you apply it; nothing is changed for you.
335
+ - **In Claude Code,** the MCP server's `error_brief` tool gives it the same brief, so it can fix the code in your repo
336
+ directly (ask: "fix error 3fa1c2d9").
337
+
338
+ It needs the version's **source maps**: the templates build them (`productionBrowserSourceMaps` in next.config.ts,
339
+ `build.sourcemap: 'hidden'` in vite.config.ts), and `hawk-app deploy` sends every `.map` file of the build to the
340
+ platform's private bucket instead of the CDN, so your source is never public. Without maps the stack stays minified and
341
+ the brief says so. Errors of versions deployed before keep their minified stacks; the next deploy maps the new ones.
342
+
303
343
  ## Accessibility check
304
344
 
305
345
  `hawk-app deploy` runs axe-core (WCAG 2.1 A/AA and best practices) over up to 10 built pages and sends the result with
@@ -388,6 +428,7 @@ Protocol](https://modelcontextprotocol.io)), so they can look things up and chec
388
428
  | `doctor` | `hawk-app doctor`: what's wrong with the setup, and how to fix it. |
389
429
  | `versions`, `why` | Every version with its status and aliases; what a client sees of the app, and why. |
390
430
  | `errors`, `usage` | The app's page errors; its opens, users and clients per day. |
431
+ | `error_brief` | One error's brief (`errors --explain <id> --no-ai`): the stack mapped to the source with the code, the failed platform calls, what changed since the last version without it. For fixing it in the repo. |
391
432
  | `ui_components` | The UI kit's components for the app's template (next-mui: the provider, components and theme; the others: the stylesheet classes and elements), or what's said about one, from the kit's README, with a link to the live [gallery](https://developers.hawksearchapps.com/reference/ui-kit-gallery/). |
392
433
  | `analytics_reports` | The analytics API's reports for `app.analytics.report()`, with one report's columns, filters and an example call, read from the developer portal's dev spec (public; no client data). |
393
434
  | `hawksearch_api` | HawkSearch's API reference (developerdocs.hawksearch.com): an operation's parameters, request and response fields, the bridge call (`app.dashboard.*`, `app.hawksearch.*`) and its scope, or why apps can't call it (public; no client data). |
@@ -643,6 +684,107 @@ with 6 fields (`minutes hours day-of-month month day-of-week year`, no `cron( )`
643
684
  scope. `schedules` is a setting sent with each deploy: on a released app a change waits for review like scopes,
644
685
  and `hawk-app status` lists the registered schedules.
645
686
 
687
+ `events` run a job function when something happens for a client, so the app doesn't need a schedule to notice it.
688
+ `client.added` runs when a client's users get the app (a store request approved, the client added to the app's list,
689
+ or the app released to the clients on its list), `config.changed` when a config value changes for a client (its own,
690
+ or a global one), `version.released` when another version reaches a client that had the app, and, read from HawkSearch
691
+ once a minute, `hawksearch.changed` (a dashboard change; `"sections": ["synonym", …]` to follow only some) and
692
+ `index.rebuilt` (a rebuild finished):
693
+
694
+ ```jsonc
695
+ "functions": { "welcome": { "job": true }, "migrate": { "job": true } },
696
+ "events": { "client.added": { "function": "welcome" }, "version.released": { "function": "migrate" } }
697
+ ```
698
+
699
+ The run is a job for that client, without a site or HawkSearch connection, except the HawkSearch events, which get the
700
+ engine and the client's connection (the bridge README's **Events** section has the details). `hawk-app validate` checks the event names, that each `function` is a job function of the same
701
+ `hawk-app.json`, and the `functions:invoke` scope; `events` is reviewed like scopes, and `hawk-app status` lists them.
702
+
703
+ `api` turns functions into endpoints the client's own systems call (a developer's script, a job on their side) with
704
+ their engine's HawkSearch site API key, once Bridgeline turns API access on for the client in HawkSearch (the app's
705
+ kebab menu › API access, which shows each endpoint's URL and a curl):
706
+
707
+ ```jsonc
708
+ "functions": { "design-html": {} },
709
+ "api": { "design-html": { "function": "design-html", "method": "GET", "description": "A saved design's email HTML" } }
710
+ ```
711
+
712
+ The function isn't a job (the caller waits); its input is the query string (GET) or the JSON body (POST), and
713
+ `apiResponse()` from the bridge's `/server` entry answers HTML or any other type (the bridge README's **Client API**
714
+ section). `hawk-app validate` checks the names (at most 10), that each `function` is a function of the same
715
+ `hawk-app.json` without `"job": true`, the method (GET or POST) and the `functions:invoke` scope; `api` is reviewed like
716
+ scopes, and `hawk-app status` lists the endpoints.
717
+
718
+ ### Actions (start the app from HawkSearch's screens)
719
+
720
+ `actions` put an app where the work is: a button in HawkSearch's own pages that opens one of the app's views in a
721
+ dialog, with values read from the page as the view's params. The app says where with **targets**: the page (a regular
722
+ expression on its path), a CSS selector for the things (one button per match), where the button goes, and how each
723
+ param is read:
724
+
725
+ ```jsonc
726
+ "views": { "fix": { "path": "views/fix", "title": "Fix a search", "params": ["keyword"] } },
727
+ "actions": {
728
+ "fix-keyword": {
729
+ "label": "Fix with Zero Results Fixer",
730
+ "view": "fix",
731
+ "targets": { "hawksearch": [{
732
+ "path": "/admin/advanced-analytics/top-queries/?$",
733
+ "selector": "bl-table tbody.table__body > tr.table__row",
734
+ "anchor": "td.table__cell:first-child",
735
+ "params": { "keyword": { "selector": "td.table__cell:first-child" } }
736
+ }] }
737
+ }
738
+ }
739
+ ```
740
+
741
+ Deploy; once the version is approved, the buttons appear in HawkSearch with no change there. `actions` always needs an
742
+ approver (never auto-approve): reviewers see each target and what it reads. Pick stable selectors (ids, `data-*`
743
+ attributes, element names): if HawkSearch changes its markup, deploy new targets. A param can also name a `data-*`
744
+ attribute (`"data": "hawk-keyword"`), used as is when the page has it, with its selector and pattern as the fallback. The view reads
745
+ `app.context.view.params` and `app.context.view.action`, and calls `app.action.done({ changed })` to close the dialog
746
+ (the bridge README's **Actions**, with every target setting). `hawk-app validate` checks the targets (paths and
747
+ patterns are regular expressions, params the view declares); `hawk-app status` lists the actions. `hawk-app dev` has an
748
+ **Action** menu: pick an action, type a value, and the view opens in a dialog as in HawkSearch. For complete recipes
749
+ (actions, schedules, events, a client API endpoint and their tests), see the [Recipes](../../docs/examples.md).
750
+
751
+ ### Testing functions (`hawk-app test`)
752
+
753
+ Functions can be tested without deploying. Put tests next to them as `functions/<name>.test.ts` (Node's own
754
+ `node:test` and `node:assert`: nothing to install) and run `npx hawk-app test` (or `npm run app:test`):
755
+
756
+ ```ts
757
+ // functions/design-html.test.ts
758
+ import assert from 'node:assert/strict';
759
+ import { test } from 'node:test';
760
+ import { runFunction } from '@bridgeline-digital/hawk-app-cli/testing';
761
+
762
+ test('answers the design as JSON', async () => {
763
+ const run = await runFunction('design-html', {
764
+ trigger: 'api', // call (default), job, schedule, event or api
765
+ input: { designId: 'spring' },
766
+ storage: { 'design#spring': design }, // storage in memory
767
+ answers: [{ method: 'POST', path: 'hawksearch/recommendations', body: recommendations }],
768
+ });
769
+ assert.equal(run.error, null, run.logs);
770
+ assert.equal(run.result.subject, 'New for spring');
771
+ });
772
+ ```
773
+
774
+ - `runFunction(name, options)` bundles the function exactly as `hawk-app deploy` does and runs it in its own Node
775
+ process with the event the platform sends (`input`, `trigger`, `client`, `engine`, `user`, `secrets`,
776
+ `clientSecrets`, `config`), with its `timeoutSeconds`. It answers `{ result, error, calls, progress, storage, logs }`:
777
+ what it returned or threw, every platform call it made (with its body and what answered it), `ctx.job.progress`
778
+ reports, the storage afterwards, and its console output.
779
+ - Its platform calls go to a fake platform: `answers` first, then storage and config in memory (when the test gives
780
+ `storage` or `config`), then a **recording** (`recording: 'spring'` reads `functions/__recordings__/spring.json`).
781
+ A call nothing answers fails with 501 `not_recorded`.
782
+ - `hawk-app test --record` makes the recordings: calls nothing else answers go to the platform once, as the test's
783
+ `client` (and `engine`; `HAWKSEARCH_API_KEY` for HawkSearch), and are saved. Recordings hold real answers: review them
784
+ before committing. They are never deployed with the function's source.
785
+ - `hawk-app test <filter>` runs only the test files whose path contains the filter. Tests aren't functions: they are
786
+ never deployed as one.
787
+
646
788
  ## Email
647
789
 
648
790
  Apps with the `email:send` scope can send email with `app.email.send(...)` or, from a function,
@@ -719,7 +861,7 @@ with it is never auto-approved. Prompts and answers are never logged or stored.
719
861
  the Apps page (see [Versions and aliases](#versions-and-aliases)).
720
862
  - **Released apps.** Every deploy is a new version. Clients stay on the approved version until the new one is approved in
721
863
  HawkSearch (staff can open it there first). Changes to settings in `hawk-app.json` (name, description,
722
- icon, category, version, visibility, hosting, scopes, `systemParameters`, `schedules`, `config`, `externalOrigins`, `listed`, `minProtocolVersion`) wait for approval
864
+ icon, category, version, visibility, hosting, scopes, `systemParameters`, `schedules`, `events`, `api`, `actions`, `config`, `externalOrigins`, `listed`, `minProtocolVersion`) wait for approval
723
865
  the same way. For `config` only the declarations are reviewed: its values are set at any time without review.
724
866
  - **Functions.** Engineering enables functions per app. A version whose functions changed (added, removed, new code,
725
867
  dependencies or settings) always waits for review, even with auto-approve; reviewers see each function's settings,
package/agents/AGENTS.md CHANGED
@@ -50,13 +50,14 @@ app.onContextChange(render); // the user switched client, engine or dates
50
50
  | Call | Scope |
51
51
  | --- | --- |
52
52
  | `app.analytics.report('queries', { limit, startDate, endDate, filters })` | `reports:read` |
53
- | `app.dashboard.get('Synonym')`: HawkSearch dashboard API, no API version in the path | `hawksearch:read` |
53
+ | `app.dashboard.get('Synonym')`: HawkSearch dashboard API, no API version in the path. Typed routes: `app.dashboard.resource('Synonym').list()/get(id)` (reads) and `.create/update/delete` (`hawksearch:write`), models as `HawkSearchModel<'Synonym'>` | `hawksearch:read` |
54
54
  | `app.dashboard.post/put/delete(path, body)`: merchandising and relevancy resources only, never retried | `hawksearch:write` |
55
+ | `app.hawksearch.pinning.set(itemPinId, docId, isPinned)`, `.order(itemPinId, docIds)`: the Item Pin rule a search applied (its `ItemPinId`), saved as the user | `hawksearch:write` |
55
56
  | `app.hawksearch.search(...)`, `app.hawksearch.autocomplete(...)` on the user's engine; preview: `IsInPreview`, `SmartBar`, `ClientData.PreviewBuckets`, `IndexName`, `ExplainDocId` | `hawksearch:search` |
56
57
  | `app.hawksearch.preview.settings/indexes/item/explain`: the preview page's visitor targets, indexes, item details, keyword explain | `hawksearch:read` |
57
58
  | `app.hawkai.agents/agent/smartResponses/smartResponse`: the client's HawkAI agents and smart responses (names and settings); `runSmartResponse(id, { query, search })`: its answer for a (preview) search; `assistant(agentId)`: the values for HawkAI's assistant widget | `hawkai:read` |
58
59
  | `app.data.query('name', params)`: your SQL in `queries/<name>.sql` + `.json` | `data:read` |
59
- | `app.functions.call('name', input)`, `.start/.wait/.job` for jobs: `functions/<name>.ts` exporting `handler` | `functions:invoke` |
60
+ | `app.functions.call('name', input)`, `.start/.wait/.job` for jobs: `functions/<name>.ts` exporting `handler`. `schedules` and `events` in hawk-app.json run job functions on a cron, or when something happens for a client (`client.added`, `config.changed`, `version.released`, `hawksearch.changed` with optional `sections`, `index.rebuilt`; input types `HawkAppEvent` from the bridge's `/server`). `api` in hawk-app.json makes a function (not a job) an endpoint the client's own systems call with their HawkSearch site API key (`{ "<name>": { "function", "method": "GET" \| "POST" } }`; `apiResponse(body, { contentType })` for HTML). Test functions without deploying: `functions/<name>.test.ts` with `node:test` and `runFunction` from `@bridgeline-digital/hawk-app-cli/testing` (storage, answers, recordings), run by `npx hawk-app test` | `functions:invoke` |
60
61
  | `app.email.send(...)` | `email:send` |
61
62
  | `app.storage.get/put/delete/list/batch/usage`: the app's own JSON data per client (350 KB a value, `ttlDays`, `ifVersion`); `app.storage.files.upload/url/download/list/delete`: its files (100 MB each) | `storage` |
62
63
  | `app.hawksearch.systemParameters.list/get/set`: names listed in `systemParameters` | `hawksearch:sysparams:write` |
@@ -103,7 +104,7 @@ template: use them before writing your own), `validate`, `status` (with the revi
103
104
  `analytics_reports` (every report `app.analytics.report()` can call, with its columns and filters),
104
105
  `hawksearch_api` (HawkSearch's dashboard, search, recommendation and indexing operations: fields, the bridge call, its scope),
105
106
  `data_tables` and `data_describe` (names and columns, never data), `logs`, `init`, `upgrade`,
106
- `deploy` (with `alias`, and `dryRun` to only show what hawk-app.json would change), `versions`, `why` (what a client sees, and why), `doctor` (what's wrong with the setup), `errors`, `usage`, and `clients` and `clients_add`, which only ever use the dev platform. Prefer them to guessing. When the
107
+ `deploy` (with `alias`, and `dryRun` to only show what hawk-app.json would change), `versions`, `why` (what a client sees, and why), `doctor` (what's wrong with the setup), `errors`, `error_brief` (one error mapped to the source, with the failed calls and what changed since the last version without it: use it to fix production errors here), `usage`, and `clients` and `clients_add`, which only ever use the dev platform. Prefer them to guessing. When the
107
108
  user names a client ("deploy to OTC"), call `clients_add` with their words: it shows them a form to pick the exact
108
109
  client. With `alias` (`hawk-app clients add <id> --alias review`) the client gets that alias's version: on a version that
109
110
  isn't approved, only Bridgeline staff in it see the app. Only approvers change what a client whose users see the app
@@ -140,6 +141,33 @@ Each view is a route, e.g. `src/app/views/preview/page.tsx` for `"path": "views/
140
141
  `{ cache: 'no-store' }` on a call that writes (e.g. an analytics report that records something), or
141
142
  `{ cache: 'reload' }` to refresh. The full app page is never cached.
142
143
 
144
+ **Say what each page shows**: HawkSearch's "Explain this page" button explains the app's page from it (no scope).
145
+ With the UI kit: `usePageContext(rows ? { page: '<the page>', dateRange, totals, top10 } : null)`; otherwise
146
+ `app.assistant.setContext({ ... })` whenever the data or filters change. Plain names and numbers of what's on screen,
147
+ at most 50 KB; summarize long tables. `hawk-app validate` warns when an app never sets one.
148
+
149
+ **Comments** (scope `comments`): `<HawkComments target="keyword:…" label="…" />` from the UI kit on what people discuss
150
+ (a row, a widget, `page:summary`); targets are stable ids, never personal data. Declare `config.commentEmails`
151
+ (`{ "type": "emails" }`) so new comments are emailed (`hawk-app config set commentEmails …`, a person or a Slack
152
+ channel's email).
153
+
154
+ **Export as PDF** (only on pages worth keeping: a report, a summary): `usePdfExport({ fileName })` from the UI kit
155
+ (`false` while loading), or `app.setPdfExport({ fileName })` and `app.setPdfExport(null)` when the page leaves. The
156
+ host shows a PDF icon and downloads the whole page; mark buttons and filters `data-hawk-pdf-ignore`.
157
+
158
+ **Usage** is counted by the bridge: routes and views as screens. Name what they don't show: `useScreen('Logs')` from the
159
+ UI kit in each tab (or `app.usage.screen`), and mark the main buttons `data-hawk-usage="Export CSV"` (or
160
+ `app.usage.action`). Name the thing, never data (a keyword, a client).
161
+
162
+ **Actions** (`actions` in `hawk-app.json`) add a button to HawkSearch's own pages that opens a view in a dialog:
163
+ `{ "<name>": { "label": "Fix with …", "view": "fix", "targets": { "hawksearch": [{ "path": "<regex on the page path>",
164
+ "selector": "<CSS: each thing>", "anchor"?: "<CSS inside it>", "position"?: "after", "params": { "keyword": { "data"?,
165
+ "selector"?, "attribute"?, "pattern"? } } }] } } }`. `"data": "keyword"` reads `data-keyword` as is when the page has it,
166
+ else the rest of the reader applies. Prefer stable selectors (ids, `data-*`, element names); the view must list the
167
+ params in its `params` and handle them missing. The view reads `app.context.view.params` (and `context.view.action`:
168
+ `{ name }`), its toasts show, and `app.action.done({ changed: true })` closes the dialog once the work is saved. Actions
169
+ always need an approver. Try it with `hawk-app dev`'s Action menu.
170
+
143
171
  A slot can also hold the **entire app** (its own page, with no `view` in the context): it then runs inside another
144
172
  page, at the slot's height, so don't rely on the full window either.
145
173
 
@@ -173,8 +201,10 @@ npx hawk-app versions # every version with its status and aliases
173
201
  npx hawk-app open # the app's page in HawkSearch (review, submit for approval)
174
202
  npx hawk-app logs <function> # a function's logs
175
203
  npx hawk-app config list # config values (config set <key> <value> [--client <id>], config unset <key>)
176
- npx hawk-app errors # errors in the app's pages (deployed versions), the most frequent first
204
+ npx hawk-app errors # errors in the app's pages (deployed versions), the most frequent first, with [id]s
205
+ npx hawk-app errors --explain <id> # one error mapped to the source, the failed calls, what changed; then a likely fix
177
206
  npx hawk-app usage # opens, users and clients per day
207
+ npx hawk-app usage --screens # the screens and actions used, most and least, per version (--version <n>)
178
208
  npx hawk-app upgrade # bring the app (and this file) up to the CLI's current template
179
209
  npx hawk-app upgrade --channel latest # a dev app (bridge/CLI on the `dev` tag) to the released versions
180
210
  ```
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: add-function
3
- description: Add or change a server-side function (functions/<name>.ts, AWS Lambda), a background job or a schedule in a Hawk App. Use when the app needs server code, a third-party API with a secret key, work longer than a page should wait, or something on a timer.
3
+ description: Add or change a server-side function (functions/<name>.ts, AWS Lambda), a background job, a schedule, an event handler or a client API endpoint in a Hawk App, with tests. Use when the app needs server code, a third-party API with a secret key, work longer than a page should wait, something on a timer, setup when a client gets the app, or data the client's own systems fetch.
4
4
  ---
5
5
 
6
6
  # Add a function
@@ -27,8 +27,20 @@ whether the bridge already does it from the page (`docs`): no function is simple
27
27
  themselves (hidden input). A value each client sets for itself is a `clientSecrets` entry instead.
28
28
  4. **Long work** is a job (`"job": true`, up to 900 s): the app calls `app.functions.start(name, input)` and
29
29
  `app.functions.wait(jobId)`. **On a timer** it's a `schedules` entry (a job function, a 6-field EventBridge cron at
30
- most hourly, an IANA `timezone`) that users turn on per client with `app.schedules.enable(name)`.
30
+ most hourly, an IANA `timezone`) that users turn on per client with `app.schedules.enable(name)`. **When something
31
+ happens for a client** it's an `events` entry (`"<event>": { "function": "<job>" }`): `client.added` (it got the
32
+ app), `config.changed` (its config changed), `version.released` (a new version reached it): these run without a site
33
+ or HawkSearch connection, so they can't call HawkSearch. `hawksearch.changed` (a dashboard change, `sections` to
34
+ narrow it) and `index.rebuilt` come from HawkSearch within 1–2 minutes and run with the engine and connection; keep
35
+ them idempotent.
31
36
  5. **Call it** from the app with `app.functions.call('<name>', input)` and handle `function_failed`,
32
- `function_timeout`, `secret_missing` and `functions_not_enabled`.
33
- 6. **Check it.** Run `validate`. Functions run only from a **deployed** version; deploy only when the user asks. After
34
- a deploy, `logs` shows the function's output.
37
+ `function_timeout`, `secret_missing` and `functions_not_enabled`. **For the client's own systems** (a script, a job
38
+ on their side) make it an endpoint instead: `"api": { "<name>": { "function": "<function>", "method": "GET" } }`
39
+ (not a job; input from the query or JSON body; `apiResponse(body, { contentType })` from `/server` for HTML).
40
+ Bridgeline turns API access on per client; the client calls it with its HawkSearch site API key.
41
+ 6. **Test it** without deploying: `functions/<name>.test.ts` with `node:test` and `runFunction` from
42
+ `@bridgeline-digital/hawk-app-cli/testing` (input, trigger, `storage` in memory, `answers` for platform calls,
43
+ recordings), then `npx hawk-app test`. A client API endpoint also runs locally under `hawk-app dev` at
44
+ `http://localhost:3200/v1/clients/<id>/apps/<slug>/api/<name>`.
45
+ 7. **Check it.** Run `validate`. Called from the app, functions run from the **deployed** version; deploy only when the
46
+ user asks. After a deploy, `logs` shows the function's output.
@@ -28,5 +28,9 @@ colors and light/dark mode. Never restyle the kit or add another font.
28
28
  - accessible: labelled fields, `alt` on images, one `<main>`, headings in order.
29
29
  5. **A view** (a preview card or an embeddable slot) is a route declared in `views` in `hawk-app.json`: keep it small,
30
30
  read stored results, transparent background, height from the content. See the Views section of `AGENTS.md`.
31
+ **Opened from HawkSearch's own pages** (a button by a search term in a report, on an AI Advisor insight…), it's an
32
+ `actions` entry: the view, and `targets` per host (a regex on the page path, CSS selectors, how each param is read;
33
+ the bridge README's **Actions**). The view reads `app.context.view.params`, handles them missing, and calls
34
+ `app.action.done({ changed: true })` when the work is saved. Try it with `npm run app:dev`'s Action menu.
31
35
  6. **Check it.** Run the hawk-apps `validate` tool (with `build: true` when the build setup changed) and fix what it
32
36
  says. Offer `npm run app:dev` to see it locally. Deploy only when the user asks.
@@ -22,11 +22,15 @@ green, or explain to the user why one can't be.
22
22
  never auto-approved.
23
23
  - **Queries:** each has `{{partitions(...)}}`, names its columns and has a `LIMIT` on lists.
24
24
  - **Functions and external Lambdas:** shown to the approver with their code and settings; Engineering must have
25
- enabled functions.
25
+ enabled functions. `npx hawk-app test` passes, if the app has function tests.
26
+ - **Client API endpoints (`api`):** each answers only what the client's own users could see in the app.
27
+ - **Actions (`actions`):** each target reads only what its view needs, with stable selectors (ids, `data-*`
28
+ attributes, element names); approvers see every target, and an app with actions is never auto-approved.
26
29
  - **No secrets** anywhere in the app or the build.
27
30
  3. **What approvers will see.** Run `status`: the review checks on the newest version, and `deploy` with
28
31
  `dryRun: true`: what `hawk-app.json` would change. If the newest version is older than the code, say so: the user
29
32
  decides when to deploy.
30
- 4. **Errors.** Run `errors` and fix what the deployed versions threw, the most frequent first.
33
+ 4. **Errors.** Run `errors` and fix what the deployed versions threw, the most frequent first: `error_brief` with an
34
+ error's id gives its stack mapped to the source, the failed platform calls and what changed since it last worked.
31
35
  5. **Finish** with a short list: ready to submit (open the app with `npx hawk-app open` and click **Submit for
32
36
  approval**), or what's left. Never deploy to prod or submit for the user.