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.
| @@ -81,7 +81,9 @@ Layout is packed on the server as well as in the browser, and the response | ||
| 81 | 81 | carries the canonical positions back. What is stored and what you saw cannot |
| 82 | 82 | disagree. |
| 83 | 83 | |
| 84 | ## Sharing | |
| 84 | ## Laravel sharing | |
| 85 | ||
| 86 | The current Laravel source exposes the following share endpoints when its API is enabled and guarded. The Stacks `reportRoutes` list does not contain share routes, and this site's code-defined viewer does not mount them. | |
| 85 | 87 | |
| 86 | 88 | ```bash |
| 87 | 89 | curl -X POST .../api/reportshq/reports/commerce-overview/shares -d '{"label":"Board"}' |
| @@ -1,144 +1,50 @@ | ||
| 1 | 1 | # Report builder |
| 2 | 2 | |
| 3 | A report is a grid of blocks. Each block runs one query and draws one thing. | |
| 4 | ||
| 5 | The terms here are the terms the interface uses, because both come from the | |
| 6 | same validation schema in `app/Reports/schema.ts`. A test asserts this page | |
| 7 | lists exactly the values that schema accepts, so a new measure cannot appear in | |
| 8 | the product without appearing here. | |
| 9 | ||
| 10 | ## Blocks | |
| 11 | ||
| 12 | | Kind | Draws | | |
| 13 | |---|---| | |
| 14 | | `line` | A measure over time | | |
| 15 | | `area` | The same, filled, for one dominant series | | |
| 16 | | `bar` | A measure across a dimension, or over coarse time | | |
| 17 | | `donut` | Composition of a total across a dimension | | |
| 18 | | `big_number` | One number, and whether it moved | | |
| 19 | | `table` | The underlying rows, when the shape is not a chart | | |
| 20 | | `funnel` | Conversion across ordered steps | | |
| 21 | | `heatmap` | A measure across two dimensions | | |
| 22 | | `text` | Prose. The only kind that runs no query | | |
| 23 | ||
| 24 | ## The query | |
| 25 | ||
| 26 | Every block except `text` carries a query with these parts. | |
| 27 | ||
| 28 | ### events | |
| 29 | ||
| 30 | The event names the block reads. Empty means every event in the project. Names | |
| 31 | come from the models you described in `config/reportshq.php`. | |
| 32 | ||
| 33 | ### measure | |
| 34 | ||
| 35 | What to compute: | |
| 36 | ||
| 37 | | Measure | Means | | |
| 38 | |---|---| | |
| 39 | | `count` | Rows. The default, and what a count of events means | | |
| 40 | | `count_unique` | Distinct `user_key`. Unique visitors, active users, buyers | | |
| 41 | | `sum` | Sum of the named field. Revenue, duration, quantity | | |
| 42 | | `avg` | Mean of the named field. Order value, session length | | |
| 43 | | `min` | Smallest value of the named field | | |
| 44 | | `max` | Largest value of the named field | | |
| 45 | ||
| 46 | `sum`, `avg`, `min` and `max` require a **field**. `count` and `count_unique` | |
| 47 | ignore one. | |
| 48 | ||
| 49 | ### field | |
| 50 | ||
| 51 | Which value the measure reads. One of `name`, `user_key`, `session_key`, | |
| 52 | `value`, `currency`, `occurred_at`, or `properties.<key>` for anything in your | |
| 53 | own property bag. | |
| 54 | ||
| 55 | This is an allowlist rather than a pattern. These names reach a query builder, | |
| 56 | and "anything that looks like an identifier" is how a column name becomes an | |
| 57 | injection point the day somebody builds the query with a template string. | |
| 58 | ||
| 59 | ### dimension | |
| 60 | ||
| 61 | Group the result into series by a field, using the same set as above. Bounded | |
| 62 | by **limit** with an explicit other bucket: when two hundred tags become the top | |
| 63 | ten, the report says how many went into Other rather than letting a reader | |
| 64 | assume the tail does not exist. | |
| 65 | ||
| 66 | ### filters | |
| 67 | ||
| 68 | Narrow what the block counts. Each filter is a field, an operator and usually a | |
| 69 | value: | |
| 70 | ||
| 71 | | Operator | Means | | |
| 72 | |---|---| | |
| 73 | | `is` | Equal to | | |
| 74 | | `is_not` | Not equal to | | |
| 75 | | `contains` | Substring | | |
| 76 | | `starts_with` | Prefix | | |
| 77 | | `gt` | Greater than | | |
| 78 | | `lt` | Less than | | |
| 79 | | `exists` | The field is present | | |
| 80 | | `not_exists` | The field is absent | | |
| 81 | ||
| 82 | `exists` and `not_exists` carry no value, because the test is presence. | |
| 83 | ||
| 84 | ### grain | |
| 85 | ||
| 86 | How time is bucketed: `hour`, `day`, `week` or `month`. | |
| 87 | ||
| 88 | ### compare | |
| 89 | ||
| 90 | Compare against the previous period of equal length. When there was no previous | |
| 91 | period, the block says so rather than inventing a percentage. This is how | |
| 92 | dashboards end up claiming a four thousand percent rise, and it is always | |
| 93 | because the previous period was zero. | |
| 94 | ||
| 95 | ### limit | |
| 96 | ||
| 97 | Top N series when a dimension is set. The rest become Other, and the count is | |
| 98 | stated. | |
| 99 | ||
| 100 | ### steps | |
| 101 | ||
| 102 | Ordered event names, for `funnel` blocks only. A funnel measures people, not | |
| 103 | events, so when data is sampled the sampling keeps whole subjects: somebody is | |
| 104 | either wholly in the sample or wholly out. Sampling events independently makes | |
| 105 | every conversion rate noise, because the steps stop belonging to the same | |
| 106 | people. | |
| 107 | ||
| 108 | ## The grid | |
| 109 | ||
| 110 | Twelve columns. Drag a block to move it, its corner to resize, and everything | |
| 111 | has a keyboard path. Blocks push each other out of the way while you hold one, | |
| 112 | and the server settles the layout before saving, so two blocks can never share | |
| 113 | a cell. That last part is on the server deliberately: a layout agreed by one | |
| 114 | browser is a layout that disagrees with itself when two people edit at once. | |
| 115 | ||
| 116 | ## Draft and published | |
| 117 | ||
| 118 | Edits go to a draft. The published version is what a teammate opens and what a | |
| 119 | [share link](/docs/sharing) serves, and it does not change until you publish. A | |
| 120 | half finished experiment is never what somebody else walks into. | |
| 3 | A report is a grid of blocks. The current source has two storage models: `@reportshq/stacks` asks the host for a `ReportStore`, while `reportshq/laravel` ships Eloquent report tables and a browser editor. The editor in this site's old hosted app was removed. Its read-only Accounts report is defined in `config/reportshq.ts`. | |
| 4 | ||
| 5 | ## Block kinds | |
| 6 | ||
| 7 | Both current packages accept the same nine kind names in `packages/stacks/src/http/handlers.ts` and `packages/laravel/src/Reports/Block.php`: | |
| 8 | ||
| 9 | | Kind | Use | | |
| 10 | | --- | --- | | |
| 11 | | `big_number` | A headline value | | |
| 12 | | `line` | A time series | | |
| 13 | | `area` | A filled time series | | |
| 14 | | `bar` | Values over time or categories | | |
| 15 | | `donut` | Category composition | | |
| 16 | | `table` | Values in rows | | |
| 17 | | `funnel` | A funnel chart frame | | |
| 18 | | `heatmap` | A heatmap chart frame | | |
| 19 | | `note` | Text without a query | | |
| 20 | ||
| 21 | The current query engines do not have a separate ordered-step funnel calculation or two-dimensional heatmap aggregation. Do not infer those analytical semantics from the renderer kind alone. | |
| 22 | ||
| 23 | ## Query vocabulary | |
| 24 | ||
| 25 | Each query-bearing block starts with `model` and `measure`, both looked up in the semantic registry. Measures are described with `count`, `sum`, `avg`, `min`, or `max`. Only `count` needs no column. The TypeScript block shape is `BlockQuery` in `packages/stacks/src/types.ts`: | |
| 26 | ||
| 27 | ```ts | |
| 28 | { | |
| 29 | model: 'order', | |
| 30 | measure: 'revenue', | |
| 31 | time: { key: 'placed' }, | |
| 32 | grain: 'day', | |
| 33 | from: '2026-08-01', | |
| 34 | to: '2026-09-01', | |
| 35 | } | |
| 36 | ``` | |
| 121 | 37 | |
| 122 | ## Every block explains itself | |
| 38 | `dimension` groups by an allowlisted column. `time` selects an allowlisted date column on the measured model; `grain` can be `hour`, `day`, `week`, or `month`. `from` is inclusive and `to` is exclusive. A date range without `time` uses the model's first described time column. `limit` bounds a dimensioned result to at most 500 rows in the TypeScript compiler. The current TypeScript compiler accepts `=`, `!=`, `>`, `>=`, `<`, `<=`, and `like` for filters. Laravel's `Filter` uses the named operators `is`, `is_not`, `contains`, `starts_with`, `gt`, `lt`, `exists`, and `not_exists`. Do not copy a filter payload between the two runtimes without translating the operator vocabulary. | |
| 123 | 39 | |
| 124 | Each block carries one sentence derived from the query it actually runs, not | |
| 125 | from a label somebody typed. Two charts both called Revenue can count different | |
| 126 | things, and a reader deserves to know which one is in front of them before they | |
| 127 | quote a number in a meeting. | |
| 40 | `compare` is present in the TypeScript type, but the current TypeScript compiler returns `comparison: null`. It does not yet compute a prior-period change. The retired event fields `events`, `user_key`, `session_key`, `properties.<key>`, and `steps` are not this reporting query schema. | |
| 128 | 41 | |
| 129 | ## Report filters | |
| 42 | The registry supplies every SQL identifier and the caller supplies only keys and parameterized values. A column not described in the registry is unreachable by a block request. | |
| 130 | 43 | |
| 131 | A whole report can be narrowed from its URL: | |
| 44 | ## Editing and publishing | |
| 132 | 45 | |
| 133 | ``` | |
| 134 | /reports/my-report?f=properties.plan:is:pro | |
| 135 | ``` | |
| 46 | A Stacks host can keep reports in code and make the viewer read-only. The browser builder is writable only when the store implements `addBlock`, `saveBlock`, `removeBlock`, and `publish`; a partially implemented editor is not advertised as writable. The host mounts the package's route descriptions and chooses auth and persistence. This site intentionally does not mount the builder. | |
| 136 | 47 | |
| 137 | The form is `f=field:operator:value`, repeated, up to five. It lives in the | |
| 138 | query string and nowhere else, so a narrowed view is linkable: paste the URL | |
| 139 | into a conversation and the reader sees what you saw. | |
| 48 | In Laravel, `Builder` adds blocks, validates their kind, packs a 12-column layout on the server, and publishes a revision. The package's standalone editor is under `/reports/{slug}/edit` only after `routes.enabled` is set and the host chooses suitable middleware. The Filament plugin is another optional surface. | |
| 140 | 49 | |
| 141 | Operators here are limited to `is`, `is_not` and `contains`. A malformed filter | |
| 142 | is dropped rather than erroring, so a link with one bad filter still shows the | |
| 143 | report. Funnels are left alone, because quietly narrowing their steps would | |
| 144 | change what the conversion rate measures without saying so. | |
| 50 | One impossible block returns its reason in `error` while the report can still render other blocks. A fan-out relation that would multiply a sum is refused instead of shown as a plausible wrong number. See [the query API](/docs/api) for the result shape. | |
| @@ -19,7 +19,7 @@ measures: { | ||
| 19 | 19 | |
| 20 | 20 | ## Dimension |
| 21 | 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. | |
| 22 | A dimension groups a measure, such as status, country or plan. The current TypeScript compiler can limit a dimensioned result to at most 500 rows. It does not synthesize an `Other` bucket for omitted values, so a limited chart is not a complete total. | |
| 23 | 23 | |
| 24 | 24 | ## Time grain |
| 25 | 25 | |
| @@ -37,7 +37,7 @@ A block is one report tile. Its kind controls presentation, while its query sele | ||
| 37 | 37 | |
| 38 | 38 | ## Draft and published report |
| 39 | 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. | |
| 40 | In the current Laravel source, the builder edits a draft and publishing creates the revision shown by ordinary views and share links. In a Stacks host, draft storage and publishing depend on its `ReportStore`; this site's code-defined report has no browser editor or share link. | |
| 41 | 41 | |
| 42 | 42 | ## Refusal |
| 43 | 43 | |
| @@ -33,22 +33,22 @@ The Stacks package does not ship database tables. Provide a `ReportStore` backed | ||
| 33 | 33 | return [ |
| 34 | 34 | 'models' => [/* declared model descriptions */], |
| 35 | 35 | 'license' => env('REPORTSHQ_LICENSE'), |
| 36 | 'timezone' => env('REPORTSHQ_TIMEZONE', 'UTC'), | |
| 36 | 'connection' => env('REPORTSHQ_CONNECTION'), | |
| 37 | 37 | 'routes' => [ |
| 38 | 'enabled' => env('REPORTSHQ_ROUTES', true), | |
| 39 | 'prefix' => env('REPORTSHQ_PREFIX', 'reports'), | |
| 38 | 'enabled' => env('REPORTSHQ_ROUTES', false), | |
| 39 | 'prefix' => env('REPORTSHQ_ROUTE_PREFIX', 'reports'), | |
| 40 | 40 | 'middleware' => ['web', 'auth'], |
| 41 | 41 | 'share_middleware' => ['web'], |
| 42 | 42 | ], |
| 43 | 43 | 'api' => [ |
| 44 | 44 | 'enabled' => env('REPORTSHQ_API', false), |
| 45 | 45 | 'prefix' => env('REPORTSHQ_API_PREFIX', 'api/reportshq'), |
| 46 | 'middleware' => ['api', 'auth:sanctum'], | |
| 46 | 'middleware' => ['api'], | |
| 47 | 47 | ], |
| 48 | 48 | ]; |
| 49 | 49 | ``` |
| 50 | 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. | |
| 51 | These are values in the current repository source, not the published v0.1.0 package. Once a reporting release exists, publish its configuration with the command in [the Laravel guide](/docs/laravel). The package loads its own migrations. Set page and API middleware for your host before enabling either surface; the source defaults shown here do not add an auth guard. | |
| 52 | 52 | |
| 53 | 53 | ## Model descriptions |
| 54 | 54 | |
| @@ -62,9 +62,9 @@ Keep the registry narrow: | ||
| 62 | 62 | |
| 63 | 63 | ## Middleware |
| 64 | 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. | |
| 65 | The current Laravel source has separate page, API and share middleware. The page and API defaults are `web` and `api`, respectively, without an application-specific auth guard. Add your own auth policy before enabling them. Its share link may be intentionally public to anyone holding a revocable token. The Stacks package describes no share route today. | |
| 66 | 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. | |
| 67 | An empty share middleware list does not make every report public. Only an active share token exposes its published report in Laravel. Add rate limiting, trusted proxy and application-specific policy middleware when required. | |
| 68 | 68 | |
| 69 | 69 | ## Environment values |
| 70 | 70 | |
| @@ -73,9 +73,9 @@ Common Laravel environment settings include: | ||
| 73 | 73 | | Variable | Purpose | |
| 74 | 74 | | --- | --- | |
| 75 | 75 | | `REPORTSHQ_LICENSE` | Optional offline license key | |
| 76 | | `REPORTSHQ_TIMEZONE` | Default report timezone | | |
| 76 | | `REPORTSHQ_CONNECTION` | Reporting database connection | | |
| 77 | 77 | | `REPORTSHQ_ROUTES` | Enable packaged page routes | |
| 78 | | `REPORTSHQ_PREFIX` | Page route prefix | | |
| 78 | | `REPORTSHQ_ROUTE_PREFIX` | Page route prefix | | |
| 79 | 79 | | `REPORTSHQ_API` | Enable the JSON API | |
| 80 | 80 | | `REPORTSHQ_API_PREFIX` | JSON API route prefix | |
| 81 | 81 | |
| @@ -1,11 +1,11 @@ | ||
| 1 | 1 | --- |
| 2 | 2 | layout: home |
| 3 | 3 | title: ReportsHQ Documentation |
| 4 | description: Reports that run inside your own Laravel application, against your own database. Installing the package, describing your models, the query API and the report builder. | |
| 4 | description: In-process reporting source for Stacks and Laravel. Release status, model allowlists, query APIs, and builder behavior. | |
| 5 | 5 | hero: |
| 6 | 6 | name: ReportsHQ |
| 7 | text: Reports that build themselves | |
| 8 | tagline: "Describe the models you already have. The Commerce, Users and Content reports appear with real numbers in them, and nothing leaves your servers." | |
| 7 | text: Reports inside your application | |
| 8 | tagline: "Describe the models and columns reporting may read, then define a report against your own database. Check release status before installing." | |
| 9 | 9 | actions: |
| 10 | 10 | - theme: brand |
| 11 | 11 | text: Quickstart |
| @@ -17,28 +17,28 @@ hero: | ||
| 17 | 17 | text: View on GitHub |
| 18 | 18 | link: https://github.com/stacksjs/reportshq |
| 19 | 19 | features: |
| 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. | |
| 20 | - title: Two in-process packages | |
| 21 | details: The current Stacks and Laravel source query the host application's database. Their reporting rewrites are not yet in the published package versions. | |
| 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 |
| 25 | 25 | details: Ask for a measure at a grain it does not have and the block says why, instead of returning a number that is quietly wrong. |
| 26 | - title: Honest about its numbers | |
| 27 | details: No previous period is said rather than guessed at, bounded dimensions state what went into Other, and sampling keeps whole people. | |
| 28 | - title: Share without an account | |
| 29 | details: A link shows one published report and nothing else. Revoke it and it stops working on the next request. | |
| 26 | - title: Honest about refusals | |
| 27 | details: A block reports why it could not run instead of presenting a plausible wrong number. The current TypeScript compiler does not calculate prior-period comparison. | |
| 28 | - title: Laravel sharing | |
| 29 | details: The current Laravel source has revocable links for one published report. The Stacks package does not yet describe a share route. | |
| 30 | 30 | - title: It never sees your data |
| 31 | details: Queries run in process through your own ORM. No connection to hand out, no endpoint to send to, and an offline licence check that opens no sockets. | |
| 31 | details: Reporting queries run in process on the application's own connection, not through ORM scopes. The Laravel event forwarder is separate and optional. | |
| 32 | 32 | --- |
| 33 | 33 | |
| 34 | 34 | ## Start here |
| 35 | 35 | |
| 36 | 36 | | You want to | Read | |
| 37 | 37 | |---|---| |
| 38 | | Get from nothing to a report | [Quickstart](/quickstart) | | |
| 38 | | Check release status and preview the current source | [Quickstart](/quickstart) | | |
| 39 | 39 | | Understand measures, dimensions and refusals | [Reporting concepts](/concepts) | |
| 40 | | Install it into a Stacks application | [Stacks package](/stacks) | | |
| 41 | | Install it into a Laravel application | [Laravel package](/laravel) | | |
| 40 | | Integrate the Stacks source after release | [Stacks package](/stacks) | | |
| 41 | | Integrate the Laravel source after release | [Laravel package](/laravel) | | |
| 42 | 42 | | Review every integration setting | [Configuration](/configuration) | |
| 43 | 43 | | Read the JSON the charts consume | [Query API](/api) | |
| 44 | 44 | | Build a report of your own | [Report builder](/builder) | |
| @@ -4,20 +4,28 @@ | ||
| 4 | 4 | models you already have, queries them in place on the connection your |
| 5 | 5 | application already holds, and renders through the routes you already guard. |
| 6 | 6 | |
| 7 | Nothing leaves the application. There is no endpoint to send to, no connection | |
| 8 | to hand out, and the licence check is offline. | |
| 7 | The report engine does not send rows to a vendor endpoint and the licence check | |
| 8 | is offline. A separate optional event forwarder can send taxonomy events to a | |
| 9 | collector explicitly configured by the host; it is not required for reports. | |
| 9 | 10 | |
| 10 | 11 | ## Install |
| 11 | 12 | |
| 13 | Do not run that install sequence yet for the reporting product. Packagist | |
| 14 | currently serves v0.1.0, whose published metadata describes the earlier event | |
| 15 | forwarder. This repository's current source adds in-process reporting, but that | |
| 16 | source has not been released on Packagist. Verify a release containing it before | |
| 17 | following the install sequence. The current source loads migrations for | |
| 18 | reports, blocks, revisions, shares and schedules. Your own tables are only ever | |
| 19 | read by the report engine. | |
| 20 | ||
| 21 | After a reporting release exists, the host installation sequence is: | |
| 22 | ||
| 12 | 23 | ```bash |
| 13 | 24 | composer require reportshq/laravel |
| 14 | 25 | php artisan vendor:publish --tag=reportshq-config |
| 15 | 26 | php artisan migrate |
| 16 | 27 | ``` |
| 17 | 28 | |
| 18 | The migration creates the tables the reports live in: reports, blocks, | |
| 19 | revisions, shares and schedules. Your own tables are only ever read. | |
| 20 | ||
| 21 | 29 | ## Describing a model |
| 22 | 30 | |
| 23 | 31 | ```php |
| @@ -103,9 +111,9 @@ same compiled components in all three, so they cannot drift. | ||
| 103 | 111 | |
| 104 | 112 | ## Exports, sharing, schedules |
| 105 | 113 | |
| 106 | CSV and XLSX are generated on demand rather than stored and linked. The numbers | |
| 107 | are one query away, so there is nothing to clean up and no way to serve a stale | |
| 108 | copy. | |
| 114 | The current Laravel source generates CSV and XLSX on demand. Its CSV is one | |
| 115 | long table with block, point, series and value columns. Its XLSX uses one sheet | |
| 116 | per query-bearing block. The current Stacks package generates CSV only. | |
| 109 | 117 | |
| 110 | 118 | Sharing and scheduling are documented in [sharing](/docs/sharing) and |
| 111 | 119 | [schedules and exports](/docs/schedules-exports). |
| @@ -22,9 +22,12 @@ a good quarter. | ||
| 22 | 22 | | XLSX export | | Yes | Yes | |
| 23 | 23 | | Priority support | | | Yes | |
| 24 | 24 | |
| 25 | `app/Billing/limits.ts` is the only place these numbers live. The pricing page | |
| 26 | renders from it rather than restating it, because a marketing table maintained | |
| 27 | separately is a promise somebody eventually breaks by editing one of them. | |
| 25 | `app/Billing/limits.ts` supplies pricing copy. It is not a runtime feature gate | |
| 26 | or a proof that every listed capability exists. The current Stacks package has | |
| 27 | CSV but not XLSX, sharing or scheduled delivery. The Laravel source implements | |
| 28 | CSV, XLSX, sharing and schedules, but its reporting rewrite has not yet been | |
| 29 | published on Packagist. Check [release status](/docs/quickstart) before treating | |
| 30 | this table as an installation plan. | |
| 28 | 31 | |
| 29 | 32 | ## Reports are unlimited on every tier |
| 30 | 33 | |
| @@ -1,36 +1,28 @@ | ||
| 1 | 1 | # Quickstart |
| 2 | 2 | |
| 3 | Empty application to first report, in about five minutes. Nothing here talks to | |
| 4 | a service, because there is not one: everything below runs on your machine | |
| 5 | against your own database. | |
| 3 | ReportsHQ runs reporting queries inside the application that owns the data. The current reporting implementations live in this repository under `packages/stacks/` and `packages/laravel/`. Neither needs a hosted collector for reports. | |
| 6 | 4 | |
| 7 | ## 1. Install the package | |
| 5 | ## Check the release before installing | |
| 8 | 6 | |
| 9 | Two ecosystems, one engine. Pick yours; everything after this step is the same | |
| 10 | report either way. | |
| 7 | As of September 2026, npm serves `@reportshq/stacks` 0.1.0 and Packagist serves `reportshq/laravel` v0.1.0. Those published artifacts predate the reporting rewrite. Installing either version does **not** give you the reporting API described below. The TypeScript reporting source is versioned 0.2.0 in this checkout, but it has not been published. The Laravel reporting source also differs from its published artifact. | |
| 8 | ||
| 9 | Do not use `bun add @reportshq/stacks` or `composer require reportshq/laravel` as a reporting setup step until releases containing the current source are available. You can inspect and test the current implementations in a working checkout of this repository: | |
| 11 | 10 | |
| 12 | 11 | ```bash |
| 13 | # Stacks. No migration: the package ships no tables and asks your | |
| 14 | # application where reports are kept. See /docs/stacks. | |
| 15 | bun add @reportshq/stacks | |
| 12 | cd packages/stacks | |
| 13 | bun test ./tests | |
| 16 | 14 | ``` |
| 17 | 15 | |
| 18 | 16 | ```bash |
| 19 | # Laravel | |
| 20 | composer require reportshq/laravel | |
| 21 | php artisan vendor:publish --tag=reportshq-config | |
| 22 | php artisan migrate | |
| 17 | cd ../.. | |
| 18 | php packages/laravel/tests/run.php | |
| 23 | 19 | ``` |
| 24 | 20 | |
| 25 | In Laravel the migration creates the tables the reports themselves live in: the | |
| 26 | report, its blocks, its revisions, its share links and its schedules. In Stacks | |
| 27 | there is no migration, because the package keeps no tables of its own and asks | |
| 28 | the application where reports live instead. Either way your own tables are never | |
| 29 | touched, read-only or otherwise altered. | |
| 21 | The following steps describe the current source and what a host application must supply when the reporting packages are released. They are not a claim that a fresh customer install works today. | |
| 30 | 22 | |
| 31 | ## 2. Describe a model | |
| 23 | ## 1. Describe queryable models | |
| 32 | 24 | |
| 33 | Name something you already have. | |
| 25 | Name only the tables and columns a report may read. This is the security boundary for query identifiers. | |
| 34 | 26 | |
| 35 | 27 | ```ts |
| 36 | 28 | // Stacks: config/reportshq.ts |
| @@ -43,18 +35,14 @@ export const models: Record<string, ModelDescription> = { | ||
| 43 | 35 | revenue: { aggregate: 'sum', column: 'total_amount', unit: 'currency' }, |
| 44 | 36 | orders: { aggregate: 'count' }, |
| 45 | 37 | }, |
| 46 | time: { | |
| 47 | placed: 'created_at', | |
| 48 | }, | |
| 49 | dimensions: { | |
| 50 | status: 'status', | |
| 51 | }, | |
| 38 | time: { placed: 'created_at' }, | |
| 39 | dimensions: { status: 'status' }, | |
| 52 | 40 | }, |
| 53 | 41 | } |
| 54 | 42 | ``` |
| 55 | 43 | |
| 56 | 44 | ```php |
| 57 | // Laravel: config/reportshq.php | |
| 45 | // Laravel: config/reportshq.php, inside the returned array | |
| 58 | 46 | 'models' => [ |
| 59 | 47 | 'order' => [ |
| 60 | 48 | 'class' => App\Models\Order::class, |
| @@ -72,96 +60,32 @@ export const models: Record<string, ModelDescription> = { | ||
| 72 | 60 | ], |
| 73 | 61 | ``` |
| 74 | 62 | |
| 75 | That is the whole description. A measure says what to add up and how, a | |
| 76 | `time` column is what a date range applies to, and every dimension is something | |
| 77 | you may group by. Where a relation would multiply rows, saying so is what lets | |
| 78 | the compiler refuse a question that would double-count rather than answer it | |
| 79 | approximately. | |
| 80 | ||
| 81 | **Only what you name is reachable.** The compiler will not touch a column that | |
| 82 | is not in this file, which is why a password hash cannot end up as a dimension | |
| 83 | by somebody typing its name into a URL. | |
| 84 | ||
| 85 | ## 3. Open the reports | |
| 86 | ||
| 87 | The routes are off until you say otherwise, because the package cannot know | |
| 88 | which of your users may see a total of everybody's orders. Turn them on: | |
| 89 | ||
| 90 | ```bash | |
| 91 | REPORTSHQ_ROUTES=true | |
| 92 | ``` | |
| 93 | ||
| 94 | ```bash | |
| 95 | php artisan serve | |
| 96 | ``` | |
| 97 | ||
| 98 | Visit `/reports`. It is empty -- nothing creates a report for you, and the page | |
| 99 | says as much. Make the first one: | |
| 63 | Compiled SQL does not apply ORM scopes or soft deletes automatically. Declare the same row restrictions in the reporting model description when those rows must be excluded. | |
| 100 | 64 | |
| 101 | ```php | |
| 102 | use ReportsHQ\Laravel\Reports\Builder; | |
| 103 | ||
| 104 | // The slug is derived from the name, and kept unique for you. | |
| 105 | $report = app(Builder::class)->create('Commerce'); | |
| 106 | ``` | |
| 107 | ||
| 108 | Or post the same thing from the page itself, which is what the "New report" | |
| 109 | form does. | |
| 65 | ## 2. Decide where reports are stored | |
| 110 | 66 | |
| 111 | ## 4. Build it | |
| 67 | The Stacks package brings no report tables. Supply a `ReportStore` with `list`, `find`, `blocks`, and `saveLayout`, backed by code or your own persistence. Write methods are optional. This site defines its one Accounts report in `config/reportshq.ts` and keeps the viewer read-only. See [the Stacks integration](/docs/stacks) for the store and route descriptions. | |
| 112 | 68 | |
| 113 | Visit `/reports/commerce/edit`. Drag a block from the palette onto the grid, | |
| 114 | pick a measure and a dimension, and it redraws as you change it. Drag a corner | |
| 115 | to resize, or nudge with the arrow keys. | |
| 69 | The Laravel package source ships migrations for reports, blocks, revisions, shares, and schedules. Its `Builder` creates and publishes reports. The standalone pages are off by default; enable them only after choosing middleware that protects the data they expose. See [the Laravel integration](/docs/laravel). | |
| 116 | 70 | |
| 117 | There is no data to accumulate and nothing to wait for: a block reads rows that | |
| 118 | are already in your database, so the first one you drop covers everything you | |
| 119 | have ever sold. | |
| 71 | ## 3. Build a first report | |
| 120 | 72 | |
| 121 | Publish when it looks right. A draft stays yours until you do, so a teammate | |
| 122 | never opens a half arranged grid. | |
| 73 | A block names a model and one of that model's described measures. A time key or dimension is optional: | |
| 123 | 74 | |
| 124 | ## 5. Share it, or send it | |
| 125 | ||
| 126 | A published report can be given a link that works for somebody with no account: | |
| 127 | ||
| 128 | ```php | |
| 129 | use ReportsHQ\Laravel\Reports\Share; | |
| 130 | ||
| 131 | $share = Share::create([ | |
| 132 | 'report_id' => $report->id, | |
| 133 | 'token' => Share::newToken(), | |
| 134 | 'label' => 'For the board', | |
| 135 | 'expires_at' => now()->addDays(30), | |
| 136 | ]); | |
| 75 | ```ts | |
| 76 | { model: 'order', measure: 'revenue', time: { key: 'placed' }, grain: 'day' } | |
| 137 | 77 | ``` |
| 138 | 78 | |
| 139 | Or emailed on a cadence, from your own queue, in the report's own timezone: | |
| 140 | ||
| 141 | ```php | |
| 142 | use ReportsHQ\Laravel\Reports\Schedule; | |
| 143 | ||
| 144 | Schedule::create([ | |
| 145 | 'report_id' => $report->id, | |
| 146 | 'cadence' => 'weekly', | |
| 147 | 'hour' => 8, | |
| 148 | 'recipients' => 'ops@example.com', | |
| 149 | 'format' => 'xlsx', | |
| 150 | ]); | |
| 151 | ``` | |
| 79 | In a Stacks host, put this query in a stored block and mount the package's route descriptions behind your application's auth policy. In a Laravel host using the current source, `Builder::create('Commerce')` creates a draft; `Builder::addBlock()` adds a block; `Builder::publish()` records the published revision. The Laravel browser editor exists only when the standalone routes are enabled. This site's viewer does not mount an editor. | |
| 152 | 80 | |
| 153 | Add the runner to `routes/console.php` and the application does the rest: | |
| 81 | The compiler returns an explicit refusal on a block when a query names an undeclared field or would multiply a measure across a fan-out relation. It does not send a query to ReportsHQ servers. | |
| 154 | 82 | |
| 155 | ```php | |
| 156 | Schedule::command('reportshq:send')->hourly(); | |
| 157 | ``` | |
| 83 | ## 4. Export or distribute | |
| 158 | 84 | |
| 159 | It is registered by the package but scheduled by you, on purpose. A package that | |
| 160 | adds itself to your scheduler is a package that sends email nobody asked for. | |
| 85 | The Stacks package currently downloads CSV only. The Laravel source supports CSV and XLSX, plus local sharing and scheduled delivery. Those Laravel features require the host's route, middleware, queue, and mail decisions. See [exports and schedules](/docs/schedules-exports) and [sharing](/docs/sharing) for the package-specific behavior. | |
| 161 | 86 | |
| 162 | 87 | ## What next |
| 163 | 88 | |
| 164 | - [The Stacks package](/docs/stacks) or [the Laravel package](/docs/laravel) | |
| 165 | for the full description format in your own language. | |
| 166 | - [The query API](/docs/api) for the JSON the charts read. | |
| 167 | - [What a licence covers](/docs/limits), and why none of it gates a report. | |
| 89 | - [Reporting concepts](/docs/concepts) explains measures, dimensions, time grains, and fan-out. | |
| 90 | - [The builder](/docs/builder) gives the current block and query vocabulary. | |
| 91 | - [Configuration](/docs/configuration) shows the separate route and API switches. | |
| @@ -5,24 +5,25 @@ without anybody asking. | ||
| 5 | 5 | |
| 6 | 6 | ## Exports |
| 7 | 7 | |
| 8 | Export any report as CSV or XLSX. | |
| 8 | The current Stacks source exports CSV only. Its handler rejects XLSX. The | |
| 9 | current Laravel source exports both CSV and XLSX. The published package | |
| 10 | versions still predate this reporting source, so check [release status](/docs/quickstart) | |
| 11 | before using either as a customer installation. | |
| 9 | 12 | |
| 10 | Both come out in **long format**: one row per series per period, rather than a | |
| 11 | grid that looks right on screen and fights every pivot table. A wide export is | |
| 12 | easier to read and harder to use, and an export exists to be used. | |
| 13 | CSV is one long table with block, point, series and value columns. Laravel XLSX | |
| 14 | uses a separate sheet per query-bearing block, with point, series and value | |
| 15 | columns. The two formats are deliberately different. | |
| 13 | 16 | |
| 14 | | period | series | value | | |
| 15 | |---|---|---| | |
| 16 | | 2026-08-01 | pro | 4250 | | |
| 17 | | 2026-08-01 | starter | 1180 | | |
| 18 | | 2026-08-02 | pro | 3990 | | |
| 17 | | block | point | series | value | | |
| 18 | |---|---|---|---| | |
| 19 | | Signups | 2026-08-01 | pro | 4250 | | |
| 20 | | Signups | 2026-08-01 | starter | 1180 | | |
| 21 | | Signups | 2026-08-02 | pro | 3990 | | |
| 19 | 22 | |
| 20 | 23 | CSV suits anything that reads text. XLSX gives a workbook with the types |
| 21 | 24 | already correct, so dates are dates rather than strings that need coercing. |
| 22 | Both come from the same query the chart ran, so an exported number and a drawn | |
| 23 | number cannot disagree. | |
| 24 | ||
| 25 | XLSX is a paid-tier capability; CSV is on every plan. See [limits](/docs/limits). | |
| 25 | Both are generated from the rendered report result. The offline licence does | |
| 26 | not gate either format in the current source. See [limits](/docs/limits). | |
| 26 | 27 | |
| 27 | 28 | ## Nothing is stored to expire |
| 28 | 29 | |
| @@ -36,12 +37,14 @@ demand has nothing to clean up and cannot serve somebody a stale copy. | ||
| 36 | 37 | |
| 37 | 38 | ## Schedules |
| 38 | 39 | |
| 39 | A schedule sends a report's headline numbers by email, daily, weekly or | |
| 40 | monthly, at an hour you choose, optionally with a spreadsheet attached. | |
| 40 | The current Laravel source can send a report by email on a schedule, optionally | |
| 41 | with a spreadsheet. The Stacks package has no schedule runner. Laravel registers | |
| 42 | the `reportshq:send` command but the host must put it on its own scheduler and | |
| 43 | run a queue worker for queued mail. | |
| 41 | 44 | |
| 42 | 45 | ### The hour stays the hour |
| 43 | 46 | |
| 44 | The schedule is evaluated in your project's timezone rather than by adding a | |
| 47 | The schedule is evaluated in the report's timezone rather than by adding a | |
| 45 | 48 | fixed offset to UTC. That means 09:00 stays 09:00 after the clocks change, |
| 46 | 49 | which is the whole reason to store a timezone rather than an offset. |
| 47 | 50 | |
| @@ -63,4 +66,6 @@ schedule is due, so it is written *before* the mail is queued. A send that | ||
| 63 | 66 | fails afterwards still leaves the schedule looking like it ran. There is no |
| 64 | 67 | failure log here: if you need one, watch your queue's failed jobs. |
| 65 | 68 | |
| 66 | Scheduled delivery is a paid-tier capability. See [limits](/docs/limits). | |
| 69 | The current offline licence does not gate scheduled delivery. Pricing copy may | |
| 70 | describe a commercial tier, but the host remains responsible for mail access, | |
| 71 | recipients and delivery monitoring. See [limits](/docs/limits). | |
| @@ -2,10 +2,13 @@ | ||
| 2 | 2 | |
| 3 | 3 | There is no other kind, and that is the whole point. |
| 4 | 4 | |
| 5 | The package runs inside your application, on your servers, against your | |
| 6 | database. Nothing is hosted on our side, nothing is sent anywhere, and the | |
| 7 | licence is checked offline. What follows is therefore not a special deployment | |
| 8 | mode: it is a description of what you already have once you install it. | |
| 5 | The reporting engines run inside your application, on your servers, against | |
| 6 | your database. Reporting sends no rows to ReportsHQ and the licence is checked | |
| 7 | offline. The Laravel package also contains a separate, optional event | |
| 8 | forwarder. That half sends events to an explicitly configured collector only | |
| 9 | when its endpoint, key and sample rate enable it. The collector's future is an | |
| 10 | unmade product decision, so do not assume it is either a required part of | |
| 11 | reporting or permanently retired. | |
| 9 | 12 | |
| 10 | 13 | If you are looking for how to deploy `reportshq.org`, the marketing site and |
| 11 | 14 | account pages, that is [deploying](/docs/deploy) and it has nothing to do with |
| @@ -13,27 +16,31 @@ running reports. | ||
| 13 | 16 | |
| 14 | 17 | ## What you are actually running |
| 15 | 18 | |
| 16 | Your application. That is it. | |
| 19 | Your application. The currently published package versions do not yet include | |
| 20 | the reporting source described here. Check [release status](/docs/quickstart) | |
| 21 | before installation. | |
| 17 | 22 | |
| 18 | 23 | ```bash |
| 19 | 24 | composer require reportshq/laravel |
| 20 | 25 | php artisan migrate |
| 21 | 26 | ``` |
| 22 | 27 | |
| 23 | The package adds routes, views, a console command and five tables. It does not | |
| 24 | add a service, a daemon, a queue worker of its own, or a port to open. If your | |
| 25 | application deploys, the reports deploy with it. | |
| 28 | After a reporting release exists, the Laravel package can load routes, views, | |
| 29 | a console command and five tables. Routes and the JSON API remain off until | |
| 30 | the host enables them. It does not add a service, daemon, queue worker of its | |
| 31 | own, or port to open. Scheduled email uses the host's worker and scheduler. | |
| 26 | 32 | |
| 27 | 33 | ## Requirements |
| 28 | 34 | |
| 29 | PHP 8.2+, Laravel 11+, and whatever database you already use. SQL is built for | |
| 30 | whatever driver your connection reports, so anything Laravel supports works, | |
| 31 | including SQLite. | |
| 35 | PHP 8.2+, Laravel 11+, and a supported SQLite, MySQL or Postgres connection. | |
| 36 | The current source has a query dialect for each of those three; do not assume | |
| 37 | every database driver Laravel supports has one. | |
| 32 | 38 | |
| 33 | 39 | ## Without a licence key |
| 34 | 40 | |
| 35 | Leave `REPORTSHQ_LICENSE` unset and every limit falls away. Unlimited | |
| 36 | applications, unlimited reports, everything the Pro tier lists. | |
| 41 | Leave `REPORTSHQ_LICENSE` unset and the current reporting engine still runs. | |
| 42 | The plans in pricing copy do not gate functionality or guarantee that every | |
| 43 | listed feature is implemented in both packages. | |
| 37 | 44 | |
| 38 | 45 | Nothing about the software changes. The pages say the installation is |
| 39 | 46 | unlicensed and that is the entire difference. This is deliberate: an offline |
| @@ -49,10 +56,10 @@ paying for, not because something will stop. | ||
| 49 | 56 | Worth stating explicitly, because it is the property most self-hosting is |
| 50 | 57 | chasing. |
| 51 | 58 | |
| 52 | Queries run in process, through your own ORM, against your own connection. There | |
| 53 | is no ingest endpoint, no export to a third party, and no telemetry. The licence | |
| 54 | class opens no sockets, and a test asserts that by reading its source for | |
| 55 | anything that could. | |
| 59 | Queries run in process on your own connection, not through ORM scopes. The | |
| 60 | report engine has no ingest endpoint or vendor telemetry. The separate Laravel | |
| 61 | event forwarder can POST taxonomy events to an endpoint the host explicitly | |
| 62 | configures. The licence class opens no sockets. | |
| 56 | 63 | |
| 57 | 64 | For anyone under a data processing agreement, this is usually the shortest |
| 58 | 65 | section of a security review you will ever write. |
| @@ -82,7 +89,8 @@ data was restored with them. | ||
| 82 | 89 | |
| 83 | 90 | ## Upgrades |
| 84 | 91 | |
| 85 | `composer update reportshq/laravel`, then `php artisan migrate`. Read the | |
| 92 | After a reporting release exists, `composer update reportshq/laravel`, then | |
| 93 | `php artisan migrate`. Read the | |
| 86 | 94 | changelog for anything with a `!` in the commit subject, which is how a breaking |
| 87 | 95 | change is marked. |
| 88 | 96 | |
| @@ -1,9 +1,9 @@ | ||
| 1 | 1 | # Sharing |
| 2 | 2 | |
| 3 | A share link shows one published report and nothing around it: no navigation | |
| 4 | into the rest of your project, no sign-in wall, no account for the recipient to | |
| 5 | create. The people who most need to see a number are usually the least willing | |
| 6 | to make a login for it. | |
| 3 | The current Laravel source can serve one published report through a revocable | |
| 4 | token, without a recipient account. The current Stacks package has no share | |
| 5 | route in `reportRoutes`, and reportshq.org does not mount one. These instructions | |
| 6 | apply to a Laravel host after a reporting release containing this source exists. | |
| 7 | 7 | |
| 8 | 8 | ## Creating a link |
| 9 | 9 | |
| @@ -18,7 +18,7 @@ The link is served by your own application, on your own domain - the route is | ||
| 18 | 18 | is hosted here. |
| 19 | 19 | |
| 20 | 20 | The token is the credential. Anyone holding it sees that one report, as |
| 21 | published. They cannot reach anything else in the project, and the link grants | |
| 21 | published. They cannot reach other reports through this route, and the link grants | |
| 22 | 22 | no ability to change anything. |
| 23 | 23 | |
| 24 | 24 | ## Revoking and rotating |
| @@ -26,10 +26,9 @@ no ability to change anything. | ||
| 26 | 26 | **Revoke** and the link stops working on the next request, not at the end of a |
| 27 | 27 | cache window. |
| 28 | 28 | |
| 29 | **Rotate** and the same report becomes available at a new token while the old | |
| 30 | one dies. That is the honest fix when a URL has been forwarded further than you | |
| 31 | meant: the people who should still have it get the new link, and everyone else | |
| 32 | is simply out. | |
| 29 | There is no dedicated rotate operation in the current API. Revoke the old share | |
| 30 | and create a new one, then distribute the new URL. The API list reveals only a | |
| 31 | token hint, not the complete token. | |
| 33 | 32 | |
| 34 | 33 | ## Views are recorded |
| 35 | 34 | |
| @@ -37,16 +36,17 @@ Each view is recorded against the link, so you know whether the weekly report | ||
| 37 | 36 | somebody asked for is actually being opened. It settles a lot of arguments |
| 38 | 37 | about what is worth continuing to produce. |
| 39 | 38 | |
| 40 | Embedding is available on the paid tiers, and the top tier removes our footer | |
| 41 | from shared and embedded reports so what you send a client looks like your work. | |
| 42 | See [limits](/docs/limits) for which tier carries what. | |
| 39 | The current reporting package has no embed route or tier-based share guard. | |
| 40 | Pricing copy is not an inventory of shipped link features. See [limits](/docs/limits) | |
| 41 | for how the offline licence behaves. | |
| 43 | 42 | |
| 44 | 43 | ## What a share is not |
| 45 | 44 | |
| 46 | 45 | A share link is shared with whoever holds it. It is not a login, there is no |
| 47 | 46 | per-recipient identity behind it, and it should not be treated as private |
| 48 | beyond the secrecy of the URL. `robots.txt` keeps `/s/` and `/embed` out of | |
| 49 | search indexes, but a link pasted into a public page is public. | |
| 47 | beyond the secrecy of the URL. The host application is responsible for cache, | |
| 48 | logging and crawler policy for `/reports/shared/{token}`. A link pasted into a | |
| 49 | public page is public. | |
| 50 | 50 | |
| 51 | If a report should only be seen by named people, add them to the project as | |
| 52 | members instead. | |
| 51 | If a report should be seen only by named people, protect a normal report route | |
| 52 | with the host application's authorization policy instead. | |
| @@ -7,20 +7,25 @@ application already holds, and renders through the routes you already guard. | ||
| 7 | 7 | Nothing leaves the application. There is no endpoint to send to, no connection |
| 8 | 8 | to hand out, and the licence check is offline. |
| 9 | 9 | |
| 10 | This is the same engine as [the Laravel package](/docs/laravel) and the same | |
| 11 | compiled chart components, so the two cannot drift on what a number means. What | |
| 12 | differs is only how you describe a model and where you mount it. | |
| 10 | The Stacks and [Laravel source](/docs/laravel) share a result contract and | |
| 11 | compiled chart components. Their query implementations are separate and their | |
| 12 | filter operator vocabularies currently differ. Verify semantics in each | |
| 13 | runtime instead of assuming they cannot drift. | |
| 13 | 14 | |
| 14 | 15 | ## Install |
| 15 | 16 | |
| 17 | Do not run that install command yet for the reporting product. npm currently | |
| 18 | serves `0.1.0`, the earlier event-forwarding SDK. The reporting source in this | |
| 19 | checkout is versioned `0.2.0` but has not been published. Verify that npm serves | |
| 20 | a release containing the reporting source before installing it. See the | |
| 21 | [quickstart release status](/docs/quickstart) for the current boundary. | |
| 22 | ||
| 23 | After a reporting release exists, install it with: | |
| 24 | ||
| 16 | 25 | ```bash |
| 17 | 26 | bun add @reportshq/stacks |
| 18 | 27 | ``` |
| 19 | 28 | |
| 20 | Take `0.2.0` or later. `0.1.0` carries the same name but is a different | |
| 21 | library: it was an event-forwarding SDK from before this package became the | |
| 22 | reporting engine, and none of the API below exists in it. | |
| 23 | ||
| 24 | 29 | **There is no migration, and that is deliberate.** This package ships no tables |
| 25 | 30 | of its own and never writes to yours: it reads the models you describe below, |
| 26 | 31 | and it asks the application where reports themselves are kept through a |
| @@ -86,9 +91,9 @@ rest of `ReportsHQConfig` is optional: | ||
| 86 | 91 | } |
| 87 | 92 | ``` |
| 88 | 93 | |
| 89 | `shareMiddleware` is empty on purpose. A share link is read by somebody with no | |
| 90 | account, so it does not inherit `routes.middleware`; adding a guard there means | |
| 91 | the link stops working for the people it was sent to. | |
| 94 | The config type includes `shareMiddleware`, but the current Stacks | |
| 95 | `reportRoutes` list has no share route. This setting does not publish a link by | |
| 96 | itself. The Laravel source has the share route and its own middleware decision. | |
| 92 | 97 | |
| 93 | 98 | ## Wiring it up |
| 94 | 99 | |
| @@ -217,12 +222,12 @@ Or [the JSON API](/docs/api), for a front end of your own. | ||
| 217 | 222 | |
| 218 | 223 | ## Exports, sharing, schedules |
| 219 | 224 | |
| 220 | CSV and XLSX are generated on demand rather than stored and linked. The numbers | |
| 221 | are one query away, so there is nothing to clean up and no way to serve a stale | |
| 222 | copy. | |
| 225 | The current Stacks package generates CSV on demand. Its download handler | |
| 226 | rejects XLSX. The Laravel source implements both formats. Neither format is | |
| 227 | stored as a report file by the Stacks handler. | |
| 223 | 228 | |
| 224 | Sharing and scheduling are documented in [sharing](/docs/sharing) and | |
| 225 | [schedules and exports](/docs/schedules-exports). | |
| 229 | Sharing and scheduling in the current source are Laravel features, documented | |
| 230 | in [sharing](/docs/sharing) and [schedules and exports](/docs/schedules-exports). | |
| 226 | 231 | |
| 227 | 232 | ## Requirements |
| 228 | 233 | |
| @@ -36,11 +36,11 @@ Confirm the request is reaching the current application release and that the sha | ||
| 36 | 36 | |
| 37 | 37 | ## A scheduled report did not arrive |
| 38 | 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. | |
| 39 | For the current Laravel source, check that the host scheduler and queue worker are running, the report has an eligible next-run time, mail is configured and recipients are valid. The offline licence does not gate export formats. Inspect failed jobs before retrying. The Stacks package has no schedule runner. | |
| 40 | 40 | |
| 41 | 41 | ## XLSX or scheduled delivery is unavailable |
| 42 | 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. | |
| 43 | The current Stacks package exports CSV only and has no scheduled delivery. The current Laravel source implements CSV, XLSX and scheduled email, but the published v0.1.0 artifact predates the reporting rewrite. The offline licence does not gate these methods. See [limits](/docs/limits) and [release status](/docs/quickstart). | |
| 44 | 44 | |
| 45 | 45 | ## The docs build succeeds locally but fails during deploy |
| 46 | 46 | |
| @@ -16,6 +16,7 @@ interface PageAudit { | ||
| 16 | 16 | overflow: number |
| 17 | 17 | unresolved: boolean |
| 18 | 18 | unnamedControls: string[] |
| 19 | visibleTextLength: number | |
| 19 | 20 | } |
| 20 | 21 | |
| 21 | 22 | interface Pending { |
| @@ -32,17 +33,20 @@ const OUTPUT = resolve('storage/logs/visual-contract') | ||
| 32 | 33 | const VIEWPORTS: Viewport[] = [ |
| 33 | 34 | { name: 'desktop', width: 1440, height: 1000, mobile: false }, |
| 34 | 35 | { name: 'tablet', width: 768, height: 1024, mobile: false }, |
| 36 | { name: 'small-mobile', width: 320, height: 780, mobile: true }, | |
| 35 | 37 | { name: 'mobile', width: 390, height: 844, mobile: true }, |
| 38 | { name: 'large-mobile', width: 430, height: 932, mobile: true }, | |
| 36 | 39 | ] |
| 37 | 40 | const THEMES = ['light', 'dark'] as const |
| 38 | 41 | const ROUTE_CANDIDATES = [ |
| 39 | 42 | '/', |
| 43 | '/pricing', | |
| 44 | '/features', | |
| 45 | '/use-cases', | |
| 40 | 46 | '/login', |
| 41 | 47 | '/register', |
| 42 | '/dashboard', | |
| 43 | '/dashboard/commshq', | |
| 44 | 48 | '/reports', |
| 45 | '/projects', | |
| 49 | '/account', | |
| 46 | 50 | ] |
| 47 | 51 | |
| 48 | 52 | function chromePath(): string { |
| @@ -212,6 +216,7 @@ const auditExpression = `(() => { | ||
| 212 | 216 | overflow: Math.max(0, document.documentElement.scrollWidth - document.documentElement.clientWidth), |
| 213 | 217 | unresolved: document.body.innerText.includes('{{') || document.body.innerText.includes('@if'), |
| 214 | 218 | unnamedControls, |
| 219 | visibleTextLength: document.body.innerText.trim().length, | |
| 215 | 220 | } |
| 216 | 221 | })()` |
| 217 | 222 | |
| @@ -281,6 +286,14 @@ async function main(): Promise<void> { | ||
| 281 | 286 | await cdp.send('Page.navigate', { url: `http://127.0.0.1:${server.port}${route}` }) |
| 282 | 287 | await waitFor(() => cdp.evaluate("document.readyState === 'complete'")) |
| 283 | 288 | await Bun.sleep(100) |
| 289 | // Protected stx static builds wrap the server-rendered guest branch in a | |
| 290 | // top-level template. Scripts are disabled in this contract, so expose | |
| 291 | // that branch for layout inspection instead of capturing a blank page. | |
| 292 | await cdp.evaluate(`(() => { | |
| 293 | if (document.body.innerText.trim()) return | |
| 294 | const shell = document.querySelector('body > template') | |
| 295 | if (shell) document.body.append(shell.content.cloneNode(true)) | |
| 296 | })()`) | |
| 284 | 297 | for (const theme of THEMES) { |
| 285 | 298 | await cdp.evaluate(`(() => { |
| 286 | 299 | const theme = ${JSON.stringify(theme)} |
| @@ -293,6 +306,7 @@ async function main(): Promise<void> { | ||
| 293 | 306 | const audit = await cdp.evaluate<PageAudit>(auditExpression) |
| 294 | 307 | const key = `${route} at ${viewport.name}/${theme}` |
| 295 | 308 | if (audit.overflow > 1) failures.push(`${key}: ${audit.overflow}px horizontal overflow`) |
| 309 | if (audit.visibleTextLength < 20) failures.push(`${key}: no meaningful visible page content`) | |
| 296 | 310 | if (audit.unresolved) failures.push(`${key}: unresolved template expression is visible`) |
| 297 | 311 | if (audit.imagesWithoutAlt) failures.push(`${key}: ${audit.imagesWithoutAlt} image(s) have no alt attribute`) |
| 298 | 312 | if (audit.unnamedControls.length) failures.push(`${key}: unnamed controls ${audit.unnamedControls.join(', ')}`) |