also looking at this
Expand BunPress guide and pre-E2E quality gates
#35
40 files
+1524
-373
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -42,14 +42,16 @@ const config: BunPressOptions = { | ||
| 42 | 42 | text: 'Getting started', |
| 43 | 43 | items: [ |
| 44 | 44 | { text: 'Quickstart', link: '/quickstart' }, |
| 45 | { text: 'Reporting concepts', link: '/concepts' }, | |
| 45 | 46 | { text: 'Limits', link: '/limits' }, |
| 46 | 47 | ], |
| 47 | 48 | }, |
| 48 | 49 | { |
| 49 | 50 | text: 'Installing it', |
| 50 | 51 | items: [ |
| 52 | { text: 'Stacks package', link: '/stacks' }, | |
| 51 | 53 | { text: 'Laravel package', link: '/laravel' }, |
| 52 | { text: 'Query API', link: '/api' }, | |
| 54 | { text: 'Configuration', link: '/configuration' }, | |
| 53 | 55 | ], |
| 54 | 56 | }, |
| 55 | 57 | { |
| @@ -66,12 +68,13 @@ const config: BunPressOptions = { | ||
| 66 | 68 | { text: 'Self-hosting', link: '/self-hosting' }, |
| 67 | 69 | { text: 'Deploying', link: '/deploy' }, |
| 68 | 70 | { text: 'Launch checklist', link: '/launch-checklist' }, |
| 69 | { text: 'API', link: '/api' }, | |
| 71 | { text: 'Troubleshooting', link: '/troubleshooting' }, | |
| 70 | 72 | ], |
| 71 | 73 | }, |
| 72 | 74 | { |
| 73 | 75 | text: 'Reference', |
| 74 | 76 | items: [ |
| 77 | { text: 'Query API', link: '/api' }, | |
| 75 | 78 | { text: 'Design brief', link: '/design-brief' }, |
| 76 | 79 | ], |
| 77 | 80 | }, |
| @@ -0,0 +1,44 @@ | ||
| 1 | # Reporting concepts | |
| 2 | ||
| 3 | ReportsHQ is an in-process reporting engine. It reads only the models, measures, time fields, dimensions and relations the application declares, compiles a bounded query, and returns one stable block shape to the included chart components or JSON API. | |
| 4 | ||
| 5 | ## Registry | |
| 6 | ||
| 7 | The registry is the allowlist. A model description names the table and the fields that reporting may use. A database column that is not declared is not available to the builder or query compiler. | |
| 8 | ||
| 9 | ## Measure | |
| 10 | ||
| 11 | A measure is a value to aggregate, such as revenue, orders or users. It defines the aggregate operation, optional source column and unit. | |
| 12 | ||
| 13 | ```ts | |
| 14 | measures: { | |
| 15 | revenue: { aggregate: 'sum', column: 'total_amount', unit: 'currency' }, | |
| 16 | orders: { aggregate: 'count' }, | |
| 17 | } | |
| 18 | ``` | |
| 19 | ||
| 20 | ## Dimension | |
| 21 | ||
| 22 | A dimension groups a measure, such as status, country or plan. Dimensions are bounded where necessary so a chart cannot accidentally request millions of categories. Values outside the displayed set are reported honestly as `Other` when the block supports it. | |
| 23 | ||
| 24 | ## Time grain | |
| 25 | ||
| 26 | A time field can be bucketed by a supported grain such as day, week or month. Buckets are calculated in the report timezone, not by applying a fixed offset. This matters across daylight-saving changes and at both edges of a date range. | |
| 27 | ||
| 28 | ## Relation and fan-out | |
| 29 | ||
| 30 | A relation explains how two declared models join. Mark a relation `fansOut` when one source row can match several target rows. ReportsHQ refuses measures that would be multiplied by that join. | |
| 31 | ||
| 32 | For example, summing an order total while grouping through its line items counts the same order once per item. The compiler refuses that query. Declare line revenue on the line-item model instead. | |
| 33 | ||
| 34 | ## Block | |
| 35 | ||
| 36 | A block is one report tile. Its kind controls presentation, while its query selects a model, measure, time field, dimension and filters. All block kinds receive the same result contract: series, totals and an optional error. | |
| 37 | ||
| 38 | ## Draft and published report | |
| 39 | ||
| 40 | The builder edits the draft. Publishing creates the version shown by ordinary report views and share links. A half-finished layout therefore cannot leak through a public share merely because an editor moved a tile. | |
| 41 | ||
| 42 | ## Refusal | |
| 43 | ||
| 44 | When a query cannot produce a defensible number, the block returns an explanation instead of a plausible guess. The rest of the report can still render. A refusal is part of the data contract, not a transport failure. | |
| @@ -0,0 +1,82 @@ | ||
| 1 | # Configuration reference | |
| 2 | ||
| 3 | ReportsHQ has two integrations with the same reporting semantics: `@reportshq/stacks` and `reportshq/laravel`. Both require a model registry. Routes, API access, timezone, storage and licensing are explicit application decisions. | |
| 4 | ||
| 5 | ## Stacks | |
| 6 | ||
| 7 | ```ts | |
| 8 | // config/reportshq.ts | |
| 9 | export default { | |
| 10 | models, | |
| 11 | license: null, | |
| 12 | timezone: 'UTC', | |
| 13 | routes: { | |
| 14 | enabled: true, | |
| 15 | prefix: '/reports', | |
| 16 | middleware: ['auth'], | |
| 17 | shareMiddleware: [], | |
| 18 | }, | |
| 19 | api: { | |
| 20 | enabled: false, | |
| 21 | prefix: '/api/reportshq', | |
| 22 | middleware: ['auth'], | |
| 23 | }, | |
| 24 | } | |
| 25 | ``` | |
| 26 | ||
| 27 | The Stacks package does not ship database tables. Provide a `ReportStore` backed by code, configuration, your own models or another persistence layer. The optional write methods determine whether the browser builder is editable or read-only. | |
| 28 | ||
| 29 | ## Laravel | |
| 30 | ||
| 31 | ```php | |
| 32 | // config/reportshq.php | |
| 33 | return [ | |
| 34 | 'models' => [/* declared model descriptions */], | |
| 35 | 'license' => env('REPORTSHQ_LICENSE'), | |
| 36 | 'timezone' => env('REPORTSHQ_TIMEZONE', 'UTC'), | |
| 37 | 'routes' => [ | |
| 38 | 'enabled' => env('REPORTSHQ_ROUTES', true), | |
| 39 | 'prefix' => env('REPORTSHQ_PREFIX', 'reports'), | |
| 40 | 'middleware' => ['web', 'auth'], | |
| 41 | 'share_middleware' => ['web'], | |
| 42 | ], | |
| 43 | 'api' => [ | |
| 44 | 'enabled' => env('REPORTSHQ_API', false), | |
| 45 | 'prefix' => env('REPORTSHQ_API_PREFIX', 'api/reportshq'), | |
| 46 | 'middleware' => ['api', 'auth:sanctum'], | |
| 47 | ], | |
| 48 | ]; | |
| 49 | ``` | |
| 50 | ||
| 51 | Publish the package configuration and migrations using the commands documented in [the Laravel guide](/docs/laravel). Review generated routes before enabling the JSON API. | |
| 52 | ||
| 53 | ## Model descriptions | |
| 54 | ||
| 55 | Keep the registry narrow: | |
| 56 | ||
| 57 | - Declare only tables and columns intended for reporting. | |
| 58 | - Put row-level restrictions in the reporting description. ORM global scopes and soft-delete behavior do not automatically apply to compiled SQL. | |
| 59 | - Mark fan-out relations accurately. | |
| 60 | - Use explicit currency and timezone semantics. | |
| 61 | - Bound high-cardinality dimensions. | |
| 62 | ||
| 63 | ## Middleware | |
| 64 | ||
| 65 | Page, API and share routes have separate middleware because they serve different callers. Page routes commonly use sessions, API routes use tokens, and a share link may be intentionally public to anyone holding its revocable token. | |
| 66 | ||
| 67 | An empty share middleware list does not make every report public. Only an active share token exposes its published report. Add rate limiting, trusted proxy and application-specific policy middleware when required. | |
| 68 | ||
| 69 | ## Environment values | |
| 70 | ||
| 71 | Common Laravel environment settings include: | |
| 72 | ||
| 73 | | Variable | Purpose | | |
| 74 | | --- | --- | | |
| 75 | | `REPORTSHQ_LICENSE` | Optional offline license key | | |
| 76 | | `REPORTSHQ_TIMEZONE` | Default report timezone | | |
| 77 | | `REPORTSHQ_ROUTES` | Enable packaged page routes | | |
| 78 | | `REPORTSHQ_PREFIX` | Page route prefix | | |
| 79 | | `REPORTSHQ_API` | Enable the JSON API | | |
| 80 | | `REPORTSHQ_API_PREFIX` | JSON API route prefix | | |
| 81 | ||
| 82 | The exact configuration surface is versioned with each package. Treat the installed package's typed config or published config file as the final source of truth. | |
| @@ -11,14 +11,14 @@ hero: | ||
| 11 | 11 | text: Quickstart |
| 12 | 12 | link: /quickstart |
| 13 | 13 | - theme: alt |
| 14 | text: Ingestion API | |
| 15 | link: /quickstart | |
| 14 | text: Stacks package | |
| 15 | link: /stacks | |
| 16 | 16 | - theme: alt |
| 17 | 17 | text: View on GitHub |
| 18 | 18 | link: https://github.com/stacksjs/reportshq |
| 19 | 19 | features: |
| 20 | - title: One package | |
| 21 | details: composer require, a migration, and a config file naming your models. Nothing to deploy beside it and no tracking calls to write. | |
| 20 | - title: One engine | |
| 21 | details: Install it in Stacks or Laravel, name the models it may read, and keep every query on the application's existing database connection. | |
| 22 | 22 | - title: Describe once, query safely |
| 23 | 23 | details: Name your models, measures and dimensions in one config file. That description is an allowlist, so a block can only ever reach what you named. |
| 24 | 24 | - title: It refuses rather than guesses |
| @@ -36,11 +36,14 @@ features: | ||
| 36 | 36 | | You want to | Read | |
| 37 | 37 | |---|---| |
| 38 | 38 | | Get from nothing to a report | [Quickstart](/quickstart) | |
| 39 | | Understand measures, dimensions and refusals | [Reporting concepts](/concepts) | | |
| 39 | 40 | | Install it into a Stacks application | [Stacks package](/stacks) | |
| 40 | 41 | | Install it into a Laravel application | [Laravel package](/laravel) | |
| 42 | | Review every integration setting | [Configuration](/configuration) | | |
| 41 | 43 | | Read the JSON the charts consume | [Query API](/api) | |
| 42 | 44 | | Build a report of your own | [Report builder](/builder) | |
| 43 | 45 | | Send a report to somebody outside | [Sharing](/sharing) | |
| 44 | 46 | | Get the numbers as a file or an email | [Schedules and exports](/schedules-exports) | |
| 45 | 47 | | Know what a licence covers | [Limits](/limits) | |
| 46 | 48 | | Run it on your own machines | [Self-hosting](/self-hosting) | |
| 49 | | Diagnose a report or deployment | [Troubleshooting](/troubleshooting) | | |
| @@ -30,7 +30,7 @@ An export is generated on the request and streamed straight out. Nothing is | ||
| 30 | 30 | written to disk and nothing is linked. |
| 31 | 31 | |
| 32 | 32 | The hosted product did store a file, sign a URL for it and expire it after an |
| 33 | hour — all of which existed because the file lived on a different machine from | |
| 33 | hour - all of which existed because the file lived on a different machine from | |
| 34 | 34 | the reader. Here it does not: the numbers are one query away, so generating on |
| 35 | 35 | demand has nothing to clean up and cannot serve somebody a stale copy. |
| 36 | 36 | |
| @@ -45,7 +45,7 @@ The schedule is evaluated in your project's timezone rather than by adding a | ||
| 45 | 45 | fixed offset to UTC. That means 09:00 stays 09:00 after the clocks change, |
| 46 | 46 | which is the whole reason to store a timezone rather than an offset. |
| 47 | 47 | |
| 48 | ### Recipients are not validated — review them yourself | |
| 48 | ### Recipients are not validated - review them yourself | |
| 49 | 49 | |
| 50 | 50 | Be explicit about this one. `recipients` is a free-text column; |
| 51 | 51 | `Schedule::recipientList()` splits it on commas and `Delivery` hands the result |
| @@ -58,7 +58,7 @@ application, since the package does not do it for you. | ||
| 58 | 58 | |
| 59 | 59 | ### What a run records |
| 60 | 60 | |
| 61 | `last_run_at` is stamped, and that is all — it is what decides whether a | |
| 61 | `last_run_at` is stamped, and that is all - it is what decides whether a | |
| 62 | 62 | schedule is due, so it is written *before* the mail is queued. A send that |
| 63 | 63 | fails afterwards still leaves the schedule looking like it ran. There is no |
| 64 | 64 | failure log here: if you need one, watch your queue's failed jobs. |
| @@ -13,7 +13,7 @@ From a report, create a share. You get a URL of the form: | ||
| 13 | 13 | https://your-app.example.com/reports/shared/<token> |
| 14 | 14 | ``` |
| 15 | 15 | |
| 16 | The link is served by your own application, on your own domain — the route is | |
| 16 | The link is served by your own application, on your own domain - the route is | |
| 17 | 17 | `/reports/shared/{token}`, mounted alongside the rest of the package. Nothing |
| 18 | 18 | is hosted here. |
| 19 | 19 | |
| @@ -0,0 +1,54 @@ | ||
| 1 | # Troubleshooting | |
| 2 | ||
| 3 | ## A block shows an error but the page returns 200 | |
| 4 | ||
| 5 | This is expected when one block requests a query ReportsHQ cannot answer safely. Read the block's `error` value. Common causes are a fan-out relation, undeclared model or field, unsupported grain, invalid filter or an unavailable draft. | |
| 6 | ||
| 7 | Fix the report or registry rather than converting the block error into a zero. Zero means the query ran and found no value; a refusal means no defensible value was calculated. | |
| 8 | ||
| 9 | ## Totals are too high after adding a dimension | |
| 10 | ||
| 11 | The measure is probably crossing a one-to-many relation. Mark the relation `fansOut` and move the measure to the grain where it is additive. For example, use line revenue when grouping orders by product. | |
| 12 | ||
| 13 | ## Soft-deleted or tenant rows appear | |
| 14 | ||
| 15 | The runner executes compiled SQL on the application's connection. ORM scopes do not apply automatically. Add the equivalent restrictions to the model description and ensure tenant identity comes from trusted application context, not the request body. | |
| 16 | ||
| 17 | ## Day or month buckets do not match another report | |
| 18 | ||
| 19 | Confirm the report timezone, source timestamp and range boundaries. A UTC report and a local-time report can disagree at both range edges while each appears internally consistent. | |
| 20 | ||
| 21 | ## The API returns 404 | |
| 22 | ||
| 23 | The JSON API is disabled by default. Enable it, confirm the configured prefix and make sure the package routes are registered in the application. In Stacks, the route file must be named by `app/Routes.ts`. | |
| 24 | ||
| 25 | ## The API redirects to login | |
| 26 | ||
| 27 | Session middleware is guarding an API route. Give the API its own token-aware middleware group. Keep browser page middleware, API middleware and share middleware separate. | |
| 28 | ||
| 29 | ## A share link asks the recipient to sign in | |
| 30 | ||
| 31 | Authentication middleware was added to share routes. Remove it if the intended security model is possession of the revocable token. Keep application-wide controls such as trusted proxies or rate limiting if they do not require an account. | |
| 32 | ||
| 33 | ## A revoked share still works | |
| 34 | ||
| 35 | Confirm the request is reaching the current application release and that the share lookup is not cached beyond the revocation boundary. A share is checked on every request so revocation should take effect immediately. | |
| 36 | ||
| 37 | ## A scheduled report did not arrive | |
| 38 | ||
| 39 | Check that the scheduler and queue worker are running, the report has an eligible next-run time, mail is configured, recipients are valid and the export format is permitted by the installation's license. Inspect failed jobs before retrying. | |
| 40 | ||
| 41 | ## XLSX or scheduled delivery is unavailable | |
| 42 | ||
| 43 | CSV is available on every plan. XLSX and scheduled email are licensed capabilities. See [limits](/docs/limits). License checks are offline and do not disable ordinary report rendering. | |
| 44 | ||
| 45 | ## The docs build succeeds locally but fails during deploy | |
| 46 | ||
| 47 | Run the same command as the static site deployment: | |
| 48 | ||
| 49 | ```bash | |
| 50 | PATH="$PWD/node_modules/.bin:$PATH" bun node_modules/@stacksjs/buddy/dist/cli.js build docs | |
| 51 | test -d dist/docs/.bunpress | |
| 52 | ``` | |
| 53 | ||
| 54 | The explicit path matters on a clean runner because the docs build launches the local `bunpress` executable by name. | |