janela 0.12.0 → 0.13.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2416bb876bf67b3cda96853cc4cee216fd8c153a0ec4930d8243512be5c8c163
4
- data.tar.gz: 1b4a35484a9d04d696771376389ab35dc17b76469ce772de54e48ed7017f5cc5
3
+ metadata.gz: 76a888911baddb25729442ecf07cf8975467f05792eb1881d1a6c9ec1c3d05e9
4
+ data.tar.gz: e3718fafdaf889aed3fd4459b722e5ed0db5b39ecffb771cb4c5599c34c0bfb9
5
5
  SHA512:
6
- metadata.gz: 6f20d48d6dfcb473861d0e20b9116e135498d6879a79f9c5720a184b35a66be4189d7e74bc51431d116f4955c73b458af59ff428062e237ee0a2421564436ad0
7
- data.tar.gz: 5b9c28323e356a9c6833ea25d0c0e3ef8c92225848ef7611fb4c3aeafb95bd75e7c0087d9b3957d819c161fedd1bfacd915b3731382ae78cf6874dca94202645
6
+ metadata.gz: 3cb99e213c2afbf7e7a570fe4c02b34f1c07fb6d7dc70e0d0fad4753c50264a3466e6f7661c52fb545a4ffa6b50a8fb1b63a6eed5e3ecbd4dd6b7670f21ed9ec
7
+ data.tar.gz: d9fbcebdd34dd825aeb98c98dc449ce92be38b50b1839e83c838730c0c5e9c3258e42d8410d818702032642496688279c0a9e1c361621d228d50dd7e92b9ea6d
data/CHANGELOG.md CHANGED
@@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.13.0] - 2026-10-02
9
+
10
+ ### Changed
11
+
12
+ - **The vitral theme draws charts in its own colours.** Bars, doughnut slices and pie slices were the stained glass accent for the first and Janela's neutral set for the other seven; vitral now sets all eight, as soft glass tones: a muted blue, sage, lavender, plum, clay, teal, ochre and dusty rose, with the grey of lead came for anything past the eighth. A first version in saturated jewel tones was rejected by eye as a rainbow against the pale tiles, so these sit at about half the chroma and lean on differences in lightness. They were chosen as an ordered set and checked together, with adjacent colour-vision separation of 12.4 (the target is 8) and adjacent normal-vision separation of 15.8 (the floor is 15). Slot 1 is a softer blue of vitral's own; `--janela-accent` stays the stronger blue for what is selected. A host that uses vitral and has set its own `--janela-series-N` keeps them. No action needed (ADR 026, #67).
13
+
14
+ ### Fixed
15
+
16
+ - **The engine's own chart partial compiles through Herb.** Rails 8.2 renders HTML templates through Herb, which refuses ERB that writes an attribute's name or sits in attribute position, and `janela/queries/_query.html.erb` did both for the chart's canvas. It is now one `tag.canvas` call that renders the same attributes, so a host on the 8.2 defaults is not broken by Janela's own template. A host that has copied that partial keeps its copy, which Herb will reject if it still has ERB in an attribute position: `bin/rails app:herb:check` on Rails 8.2 lists any. No action needed otherwise.
17
+ - **The doctor no longer calls `Janela::PanesController` a stale name.** It reported an error that the name "is now `Janela::QueriesController`", true from 0.7 until 0.12 added a `Janela::PanesController` back as the stored-pane form's controller, which owns `pane_params`. A host patching that form was told to move the patch to a controller with no `pane_params`, which would have silently stopped its extra pane param saving, and the error failed `janela:doctor` until the check was silenced. It is now a warning that says what the name means today. The check is still silenced by `stale-identifiers`, and `Janela::SnapshotPanesController` is unchanged (#74).
18
+
19
+ ### Added
20
+
21
+ - **Tools for an agent: `Janela::Tools`.** An agent in a host had no way to read or arrange a dashboard except by writing `Janela::Frame` and `Janela::Pane` rows from a console. `Janela::Tools.new(scope:)` is plain Ruby with a name, a description and a JSON schema for each of `describe_vocabulary`, `list_frames`, `get_frame` and `read_pane`, and with `write: true`, `add_pane`, `update_pane`, `remove_pane` and `move_pane`. `scope:` is required and takes a model class and returns a relation, the answer `policy_scope` gives, so a read or a change reaches only what the caller may see; leaving it out raises `Janela::Unscoped` when the tools are built. Janela registers nothing, serves nothing and depends on no MCP library: [docs/agents.md](docs/agents.md) shows the adapter for the official Ruby SDK, which Janela's own tests run. No action needed (ADR 053, #73).
22
+
23
+ - **The doctor notices a skipped migration, not only a missing table.** `bin/rails janela:doctor` reported a missing `janela_panes` and said nothing when the table was there but a release had added a column to it, so a host that upgraded and did not run `db:migrate` was told all was well, and met an error the first time a pane form or a stored pane read `height`, `prominence` or `companions`. A new `unmigrated-columns` check lists the missing columns by table and names the two commands. It can be silenced by that name like any other (ADR 021, ADR 035).
24
+
8
25
  ## [0.12.0] - 2026-10-01
9
26
 
10
27
  ### Added
@@ -40,7 +57,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
40
57
  ### Changed
41
58
 
42
59
  - **A range in the URL no longer narrows the time pane it names.** `q[placed_on_gteq]` and `q[placed_on_lt]` from a reader now scope every other pane and leave a time pane on that dimension showing its whole series with the range marked, where before it narrowed that pane's own series. A range a host fixes with `where:` or `default_where` still narrows it. See `UPGRADING.md` (ADR 045).
43
- - **A bar chart's bars are no longer all one colour.** Each bar is now drawn in the palette colour for its position, so a host that has seen every bar in `--janela-accent` will see the first bar in the accent and the rest in new colours. A line chart is unchanged. To keep the old look, set `--janela-series-2` to `--janela-series-8` to your accent; see `UPGRADING.md` (ADR 046).
60
+ - **A bar chart's bars are no longer all one colour.** Each bar is now drawn in the palette colour for its position, so a host that has seen every bar in `--janela-accent` will see the first bar in the accent and the rest in new colours. A line chart is unchanged. To keep the old look, set `--janela-series-2` to `--janela-series-8`, and `--janela-series-other`, to your accent; see `UPGRADING.md` (ADR 046).
44
61
 
45
62
  ### Fixed
46
63
 
@@ -256,6 +273,7 @@ First alpha, installed from GitHub for testing in a single host application.
256
273
  - Only models that declare a `janela` block are addressable over HTTP.
257
274
  - ADRs 001 to 004 in `docs/decisions/`, shipped inside the gem.
258
275
 
276
+ [0.13.0]: https://github.com/retail-tasker/janela/releases/tag/v0.13.0
259
277
  [0.12.0]: https://github.com/retail-tasker/janela/releases/tag/v0.12.0
260
278
  [0.11.0]: https://github.com/retail-tasker/janela/releases/tag/v0.11.0
261
279
  [0.10.0]: https://github.com/retail-tasker/janela/releases/tag/v0.10.0
data/README.md CHANGED
@@ -6,7 +6,7 @@ PowerBI-style dashboards and cross-filtering slicers, native to Rails and Active
6
6
 
7
7
  ## First principle
8
8
 
9
- Janela is a PowerBI-style library built on Ruby and Stimulus, meant to drop onto any Ruby on Rails application. No JS framework, no build step of its own, no separate frontend app. Just a gem you add to an existing Rails app's Gemfile and two Stimulus controllers that ship with it.
9
+ Janela is a PowerBI-style library built on Ruby and Stimulus, meant to drop onto any Ruby on Rails application. No JS framework, no build step of its own, no separate frontend app. Just a gem you add to an existing Rails app's Gemfile and two Stimulus controllers that ship with it, plus an optional third for the theme.
10
10
 
11
11
  ## Why
12
12
 
@@ -14,7 +14,7 @@ Every Rails BI option today is one of:
14
14
 
15
15
  - **SQL-first** (Blazer). Powerful, but the query is a black box to your models and associations.
16
16
  - **Admin-panel-first** (RailsAdmin, ActiveAdmin, Motor Admin, Avo). Association-aware filtering, but built for CRUD, not for composing multiple charts that filter each other.
17
- - **A dead end for cross-filtering**. None of the above let clicking one chart re-scope every other chart on the page. That's the actual PowerBI/Tableau slicer experience, and nothing in the Rails ecosystem does it as a first-class citizen.
17
+ - **A dead end for cross-filtering**. None of the above are built around clicking one chart to re-scope every other chart on the page. That's the PowerBI/Tableau slicer experience, and it's what Janela is for.
18
18
 
19
19
  Janela's bet: the same dashboard definition should serve two audiences without being two systems.
20
20
 
@@ -23,11 +23,11 @@ Janela's bet: the same dashboard definition should serve two audiences without b
23
23
 
24
24
  ## Installation
25
25
 
26
- Janela is an alpha on [rubygems.org](https://rubygems.org/gems/janela). It has two halves, a gem and an npm package:
26
+ Janela is an alpha on [rubygems.org](https://rubygems.org/gems/janela). It is a gem, plus a JavaScript package installed from GitHub if your app bundles its JavaScript. An app on importmap-rails needs only the gem. It needs Rails 8.0+ and Ruby 3.3+ (see Requirements below).
27
27
 
28
28
  ```ruby
29
29
  # Gemfile
30
- gem "janela", "~> 0.12"
30
+ gem "janela", "~> 0.13"
31
31
  ```
32
32
 
33
33
  ```ruby
@@ -35,6 +35,15 @@ gem "janela", "~> 0.12"
35
35
  mount Janela::Engine => "/dashboards" # or /reports, or wherever you like
36
36
  ```
37
37
 
38
+ Janela reads nothing until your `ApplicationController` says what a visitor may see, so the first visit to `/dashboards` raises `Janela::Unscoped` without it. An app with nothing to hide answers once:
39
+
40
+ ```ruby
41
+ # app/controllers/application_controller.rb
42
+ private def policy_scope(model) = model.all
43
+ ```
44
+
45
+ If you use stored frames or snapshots, run `bin/rails janela:install:migrations && bin/rails db:migrate` (see Frames below), and finish with `bin/rails janela:doctor`, which checks the install and says what to fix. The rest of this section is the JavaScript.
46
+
38
47
  Then register the two Stimulus controllers. How depends on how your app ships JavaScript.
39
48
 
40
49
  **With jsbundling (esbuild, bun, webpack):**
@@ -322,7 +331,7 @@ The engine serves an index and a page per frame at the mount root, so you can in
322
331
 
323
332
  No model's route key is all digits, so a frame id and a pane URL cannot be confused. Both pages read through `policy_scope(Janela::Frame)`, so a frame your scope does not return is a 404 rather than a page, and so is a pane row under it.
324
333
 
325
- These pages render in Janela's own minimal layout, which loads the gem's stylesheet and nothing else. It does not load Turbo or Stimulus, because those come from your bundler and the engine cannot name them. So Janela's own pages are correct, styled, **static** dashboards: every pane is rendered inline and the numbers are right, filters in the URL apply, and nothing cross-filters when you click. A chart pane needs Chart.js, so on these pages it draws nothing; put a frame on your own page, where your JavaScript is, for the interactive version.
334
+ These pages render in Janela's own minimal layout, which loads the gem's stylesheet and nothing else. It does not load Turbo or Stimulus, because those come from your bundler and the engine cannot name them. So Janela's own pages are correct, styled, **static** dashboards: every pane is rendered inline and the numbers are right, filters in the URL apply, and nothing cross-filters when you click. A chart pane needs Chart.js, so on these pages it renders as its table (ADR 018); put a frame on your own page, where your JavaScript is, for the interactive version. Each page links back to your application's root, which you can rename in your own locale file under `janela.actions.home`.
326
335
 
327
336
  The noun in the headings is `Janela::Frame.model_name.human`, so rename it in your own locale file rather than in a setting:
328
337
 
@@ -418,7 +427,7 @@ Render a stored pane the same way you render a live one:
418
427
  <%= janela_snapshot_pane @snapshot, Order, :revenue, by: :status, as: :bar %>
419
428
  ```
420
429
 
421
- Inside a dashboard a pane is a Turbo Frame and carries no layout at all. Opened directly it renders in Janela's own minimal layout, which deliberately loads no assets, because the gem cannot know your asset names or whether you bundle. A direct pane link therefore shows its numbers unstyled, and a chart pane shows nothing, since the chart needs Stimulus.
430
+ Inside a dashboard a pane is a Turbo Frame and carries no layout at all. Opened directly it renders in Janela's own minimal layout, which loads the gem's stylesheet and none of your JavaScript, because the gem cannot know your asset names or whether you bundle. A direct pane link therefore shows its numbers styled, and a bar or line chart draws nothing, since the chart needs Stimulus. A table and a ring (which is drawn as SVG on the server) still show.
422
431
 
423
432
  To make direct pane links styled and chart-capable, give Janela a small layout of your own that loads your assets and nothing else:
424
433
 
@@ -487,25 +496,19 @@ Scoping is automatic when you use Pundit: `Janela::ApplicationController` calls
487
496
 
488
497
  **Multi tenancy** has its own guide: [docs/multi-tenancy.md](docs/multi-tenancy.md). It covers what goes through your scope, worked wiring for Pundit, acts_as_tenant and CanCanCan, what owns a frame the analyst creates, and what rows a scheduled snapshot freezes.
489
498
 
490
- ### The pages Janela serves
499
+ ### Giving an agent access
491
500
 
492
- Mounting the engine gives you an index of frames and a page per frame with no
493
- work at all, which is enough to navigate on the day you install it:
501
+ Frames and panes are rows, so an agent can arrange a dashboard if it has tools. Janela gives you the tool definitions as plain Ruby and leaves the registering to you, because your application knows who is asking and Janela does not (ADR 053):
494
502
 
495
- ```
496
- /insights every frame your policy scope returns
497
- /insights/3 one frame
498
- ```
503
+ ```ruby
504
+ tools = Janela::Tools.new(scope: ->(model) { policy_scope(model) }) # read only
505
+ tools = Janela::Tools.new(scope: ->(model) { policy_scope(model) }, write: true)
499
506
 
500
- Both go through your `policy_scope`, so a frame another tenant owns is a 404. Each page carries a link back to your application's root, so they are not a dead end; rename it in your own locale file under `janela.actions.home`, or override the engine's layout if you want your whole navigation there.
507
+ Janela::Tools.all # name, description, input_schema, read_only
508
+ tools.call("read_pane", pane_id: 4)
509
+ ```
501
510
 
502
- These pages load Janela's own stylesheet and nothing of yours, because the gem
503
- cannot know your asset names or bundler. Two consequences worth knowing. They
504
- do not cross-filter, since that needs Stimulus. And a pane whose row asks for a
505
- chart renders as its **table** here, because there is no chart runtime on the
506
- page and a table needs nothing: the same frame rendered in your own page with
507
- `janela_frame(@frame)` draws the chart. A renderer is a viewing choice, not part
508
- of the pane (ADR 018).
511
+ `scope:` is required and is the same answer your `policy_scope` gives, so an agent reads and changes only what the person it acts for can. The read tools list the vocabulary and frames and read a pane's values. The write tools add, change, remove and move panes, and are off unless asked for. Janela serves no MCP endpoint, edits no configuration and depends on no MCP library; [docs/agents.md](docs/agents.md) shows the few lines that register them with the official Ruby SDK.
509
512
 
510
513
  ### Checking an installation
511
514
 
@@ -558,11 +561,11 @@ Deliberately out of scope: natural-language query, a separate data warehouse, a
558
561
 
559
562
  ## Status
560
563
 
561
- **v0.12.0 alpha.** The measures/dimensions DSL, time dimensions, cross-filtering with multi-selection, bar, line, doughnut and pie charts, pane URLs, shareable dashboard URLs, snapshots, database-backed frames found by owner and key, panes that hold words or a host partial as well as a query, a host-fixed filter no click can remove and a frame's own permanent one beside it, STI subclasses, the engine's own pages for reading and editing them and the optional vitral theme work and are covered by unit and real-browser tests, with the classes a theme may target documented in [Theming Janela](docs/theming.md). Not yet built: a visual editor, drill-down on time panes, other chart types. [Vista](docs/roadmap.md), the roadmap, says what 1.0 means and which of these are in it; open work is in [GitHub Issues](https://github.com/retail-tasker/janela/issues).
564
+ **v0.13.0 alpha.** The measures/dimensions DSL, time dimensions, cross-filtering with multi-selection, bar, line, doughnut and pie charts, pane URLs, shareable dashboard URLs, snapshots, database-backed frames found by owner and key, panes that hold words or a host partial as well as a query, a host-fixed filter no click can remove and a frame's own permanent one beside it, STI subclasses, the engine's own pages for reading and editing them, tools an agent can use to read and arrange a dashboard, and the optional vitral theme work and are covered by unit and real-browser tests, with the classes a theme may target documented in [Theming Janela](docs/theming.md). Not yet built: a visual editor, and narrowing a time pane's own granularity by clicking one of its buckets (a click on a bucket does filter the others). [Vista](docs/roadmap.md), the roadmap, says what 1.0 means and which of these are in it; open work is in [GitHub Issues](https://github.com/retail-tasker/janela/issues).
562
565
 
563
566
  ## Development
564
567
 
565
- Janela is a Rails engine. It ships with a minimal host application in `test/dummy` that mounts the engine at `/janela`, so the gem is always developed and tested against a real Rails app with a real (SQLite) database.
568
+ Janela is a Rails engine. It ships with a minimal host application in `test/dummy` that mounts the engine at `/dashboards`, so the gem is always developed and tested against a real Rails app with a real (SQLite) database.
566
569
 
567
570
  After checking out the repo, run `bin/setup` to install dependencies. Then:
568
571
 
@@ -575,13 +578,13 @@ bin/rails console # console inside the dummy app, engine loaded
575
578
 
576
579
  ## Contributing
577
580
 
578
- Bug reports and pull requests are welcome on GitHub at https://github.com/retail-tasker/janela. Pull requests are reviewed on the merits of the diff, whether a person or an agent wrote them. [CONTRIBUTING.md](CONTRIBUTING.md) says how to run the tests, when a change needs a decision record first, and what to write down for the people who upgrade. Contributors are expected to adhere to the [code of conduct](https://github.com/retail-tasker/janela/blob/main/CODE_OF_CONDUCT.md).
581
+ Bug reports and pull requests are welcome on GitHub at https://github.com/retail-tasker/janela. Pull requests are reviewed on the merits of the diff, whether a person or an agent wrote them. [CONTRIBUTING.md](https://github.com/retail-tasker/janela/blob/main/CONTRIBUTING.md) says how to run the tests, when a change needs a decision record first, and what to write down for the people who upgrade. Contributors are expected to adhere to the [code of conduct](https://github.com/retail-tasker/janela/blob/main/CODE_OF_CONDUCT.md).
579
582
 
580
583
  ## Support and security
581
584
 
582
585
  Janela is pre-1.0 and its public surface can still move between releases, with an upgrade note each time. Issues and pull requests are read when the maintainers can get to them; there is no support contract and no promised response time.
583
586
 
584
- To report a vulnerability, use the private form described in [SECURITY.md](SECURITY.md) and not a public issue. Releases are cut by the two maintainers and published from a protected workflow; the steps are in [RELEASING.md](RELEASING.md).
587
+ To report a vulnerability, use the private form described in [SECURITY.md](https://github.com/retail-tasker/janela/blob/main/SECURITY.md) and not a public issue. Releases are cut by the two maintainers and published from a protected workflow; the steps are in [RELEASING.md](https://github.com/retail-tasker/janela/blob/main/RELEASING.md).
585
588
 
586
589
  ## License
587
590
 
data/UPGRADING.md CHANGED
@@ -12,6 +12,38 @@ bin/rails janela:doctor
12
12
 
13
13
  It reads your application and lists what still needs changing.
14
14
 
15
+ ## 0.12.0 to 0.13.0
16
+
17
+ No migration, and nothing you must change. Two things you may see.
18
+
19
+ **The doctor treats `Janela::PanesController` as a warning.**
20
+
21
+ 0.7 renamed it to `Janela::QueriesController`, and 0.12 added a
22
+ `Janela::PanesController` back as the stored-pane form's controller, which owns
23
+ `pane_params`. The doctor used to call every mention of the name an error. It now
24
+ warns, and says what the name means today. If you patch the form's
25
+ `pane_params`, leave it where it is. If you patched how a query renders, that
26
+ patch belongs on `Janela::QueriesController`.
27
+
28
+ **A host that copied `janela/queries/_query.html.erb` should check it.**
29
+
30
+ Rails 8.2 renders HTML templates through Herb, which refuses ERB that writes an
31
+ attribute's name or sits in attribute position. Janela's own copy of the partial
32
+ no longer does either, so it compiles. A copy of yours keeps the old markup,
33
+ which Herb will reject if it has ERB in an attribute position. On Rails 8.2:
34
+
35
+ ```bash
36
+ bin/rails herb:check
37
+ ```
38
+
39
+ lists what it rejects. On earlier Rails there is nothing to do.
40
+
41
+ **Optional: tools for an agent.**
42
+
43
+ `Janela::Tools` gives an agent the tools to read and arrange a dashboard. It is
44
+ not on unless you build it, and nothing registers itself. See
45
+ [docs/agents.md](docs/agents.md).
46
+
15
47
  ## 0.11.0 to 0.12.0
16
48
 
17
49
  One migration step, if you use stored frames.
@@ -63,7 +95,7 @@ in `--janela-series-2` to `--janela-series-8`, and every bar after the
63
95
  eighth in `--janela-series-other`. Until now every bar was the accent.
64
96
  Line charts are unchanged.
65
97
 
66
- If you want the old look, set the seven to your accent:
98
+ If you want the old look, set the other eight to your accent:
67
99
 
68
100
  ```css
69
101
  :root {
@@ -549,6 +581,11 @@ Most applications do not.
549
581
  `Janela::Pane` is reserved for a database record in a later release,
550
582
  which is why the runtime object had to give the name up.
551
583
 
584
+ `Janela::PanesController` was renamed in this table, and 0.12 added a
585
+ `Janela::PanesController` back: the stored-pane form's controller, which owns
586
+ `pane_params`. The doctor warns about the name without calling it an error,
587
+ because only you know which one your code means.
588
+
552
589
  If you override Janela's view, move your copy from
553
590
  `app/views/janela/panes/` to `app/views/janela/queries/`.
554
591
 
@@ -69,6 +69,27 @@
69
69
  janela.css draws agree with the parts this one does. */
70
70
  --janela-line: var(--vitral-came-soft);
71
71
  --janela-accent: rgb(40, 110, 205);
72
+
73
+ /* The colours a bar, a doughnut slice or a pie slice is drawn in, by
74
+ position (ADR 046, ADR 026). Soft glass rather than bright: a first
75
+ version in saturated jewel tones read as a rainbow against the pale tiles,
76
+ so these sit at about half the chroma and lean on differences in lightness
77
+ to keep neighbours apart (the plum and the dusty rose are the darker ones).
78
+ Chosen as an ordered set and run through the palette checker: adjacent
79
+ colour vision separation 12.4 (target 8) and adjacent normal vision
80
+ separation 15.8 (floor 15). The order is what keeps neighbours apart, so a
81
+ reorder has to be re-checked. Slot 1 is a softer blue of its own and not
82
+ the accent, which stays the stronger blue for what is selected. The neutral
83
+ past the eighth is the grey of lead came. */
84
+ --janela-series-1: #608fcb;
85
+ --janela-series-2: #56ab81;
86
+ --janela-series-3: #a492da;
87
+ --janela-series-4: #91507d;
88
+ --janela-series-5: #be774e;
89
+ --janela-series-6: #159da9;
90
+ --janela-series-7: #c29e51;
91
+ --janela-series-8: #994b59;
92
+ --janela-series-other: #8794a6;
72
93
  }
73
94
 
74
95
  /* Opt in, because a theme that repaints a host's <body> uninvited is not a
@@ -15,21 +15,21 @@
15
15
  <figcaption class="janela-chart-title" id="<%= title_id %>"><%= query.title %></figcaption>
16
16
  <%# A height puts the canvas in a box of fixed size for the chart to fill;
17
17
  with none there is no box and the chart is what it always was (ADR 047). %>
18
- <% canvas = capture do %>
19
- <canvas class="janela-chart"
20
- data-controller="janela--chart"
21
- data-action="janela--chart:toggle->janela--frame#toggle"
22
- data-janela--chart-type-value="<%= query.renderer %>"
23
- data-janela--chart-title-value="<%= query.title %>"
24
- data-janela--chart-selected-value="<%= query.selected_values.to_json %>"
25
- data-janela--chart-labels-value="<%= result.keys.to_json %>"
26
- data-janela--chart-values-value="<%= result.values.map(&:to_f).to_json %>"
27
- data-janela--chart-formatted-value="<%= result.values.map { |measured| query.format(measured) }.to_json %>"
28
- data-janela--chart-filters-value="<%= query.filters_for(result.keys).to_json %>"
29
- <%= tag.attributes(aria: { description: (t("janela.time.click_hint") if query.time? && query.clickable?) }) %>
30
- <%= "data-janela--chart-fixed-height-value=true".html_safe if query.boxed? %>
31
- role="img" aria-labelledby="<%= title_id %>"></canvas>
32
- <% end %>
18
+ <% canvas = tag.canvas(
19
+ class: "janela-chart", role: "img",
20
+ aria: { labelledby: title_id, description: (t("janela.time.click_hint") if query.time? && query.clickable?) },
21
+ data: {
22
+ controller: "janela--chart",
23
+ action: "janela--chart:toggle->janela--frame#toggle",
24
+ "janela--chart-type-value": query.renderer,
25
+ "janela--chart-title-value": query.title,
26
+ "janela--chart-selected-value": query.selected_values.to_json,
27
+ "janela--chart-labels-value": result.keys.to_json,
28
+ "janela--chart-values-value": result.values.map(&:to_f).to_json,
29
+ "janela--chart-formatted-value": result.values.map { |measured| query.format(measured) }.to_json,
30
+ "janela--chart-filters-value": query.filters_for(result.keys).to_json,
31
+ "janela--chart-fixed-height-value": (true if query.boxed?)
32
+ }) %>
33
33
  <%= query.boxed? ? tag.div(canvas, class: "janela-chart-box janela-h-#{query.height}") : canvas %>
34
34
  </figure>
35
35
  <% else %>
data/docs/agents.md ADDED
@@ -0,0 +1,97 @@
1
+ ---
2
+ Topics: agents, mcp, tools, authorisation, host-integration
3
+ ---
4
+
5
+ # Giving an Agent Access to Janela
6
+
7
+ Frames and panes are rows (ADR 012), so an agent could always arrange a
8
+ dashboard by writing them from a console. This page is for giving it tools
9
+ instead: read a frame, read a pane's values, and, if you choose, add, change,
10
+ remove and move panes. The reasoning is in ADR 053.
11
+
12
+ The short version: Janela gives you the tool definitions as plain Ruby. You
13
+ build them with the answer to "what may this caller read", and you register
14
+ them with whatever MCP library your application already runs. Janela
15
+ registers nothing, detects nothing and serves nothing, because it cannot
16
+ know who is asking and your application can.
17
+
18
+ ## Building the tools
19
+
20
+ ```ruby
21
+ tools = Janela::Tools.new(scope: ->(model) { policy_scope(model) })
22
+ ```
23
+
24
+ `scope:` is required. It takes a model class and returns a relation: the same
25
+ answer your `policy_scope` gives a Janela controller (ADR 032). Leave it out
26
+ and `Janela::Unscoped` is raised when the tools are built, because a tool with
27
+ no scope would read every row of every model. Every read the tools make passes
28
+ that relation, and frames and panes are found through it too, so a caller can
29
+ change only what it can see.
30
+
31
+ Build the tools where you know who is asking, which for an MCP server is when a
32
+ tool is called. They are cheap to construct.
33
+
34
+ ## What they do
35
+
36
+ | Tool | Does | Needs `write: true` |
37
+ | --- | --- | --- |
38
+ | `describe_vocabulary` | The models, measures, dimensions, renderers and granularities the `janela` blocks declare. An agent should offer nothing else. | no |
39
+ | `list_frames` | The frames the scope returns: id, name, owner, pane count. | no |
40
+ | `get_frame` | One frame with its panes in position order. | no |
41
+ | `read_pane` | A pane's values, through the scope. Filters are Ransack predicates on declared dimensions, bounded as a reader's are (ADR 025). | no |
42
+ | `add_pane` | Add a pane to the end of a frame. | yes |
43
+ | `update_pane` | Change a pane. An invalid change leaves it as it was. | yes |
44
+ | `remove_pane` | Remove a pane and close the gap. | yes |
45
+ | `move_pane` | Move a pane one place up or down. | yes |
46
+
47
+ The write tools are off unless you build the tools with `write: true`. Their
48
+ inputs are the pane's own attributes, the ones the README
49
+ names, and nothing else: an attribute that is not a pane's is refused rather
50
+ than dropped. A pane Janela would refuse is refused with its own reason, for
51
+ example `Measure "nonsense" is not a measure of Order`.
52
+
53
+ There is no tool to create a frame or take a snapshot. Your code finds or makes
54
+ a frame with `Janela::Frame.for` (ADR 041), and a snapshot needs an owner and a
55
+ scope you have named (ADR 033, ADR 034).
56
+
57
+ ## Registering them
58
+
59
+ `Janela::Tools.all` is the list of definitions, and needs no scope: each has a
60
+ `name`, a `description`, an `input_schema` (JSON Schema) and `read_only`.
61
+ `tools.call(name, arguments)` runs one and returns a Hash, or raises a
62
+ `Janela::Error` (`NotFound`, `BadRequest` or `Unscoped`) that an adapter reports
63
+ to the agent as the tool's own error.
64
+
65
+ For the official Ruby SDK, the `mcp` gem, an adapter looks like this. It is run
66
+ by Janela's own tests, so it cannot drift from the code. `server_context` is
67
+ whatever your server was built with, and is where you keep the caller; the
68
+ scope is built from it at the moment of the call.
69
+
70
+ ```ruby
71
+ def janela_server(write: false)
72
+ tools = Janela::Tools.all(write: write).map do |tool|
73
+ MCP::Tool.define(name: tool.name, description: tool.description, input_schema: tool.input_schema,
74
+ annotations: { read_only_hint: tool.read_only }) do |server_context:, **arguments|
75
+ # The scope is built per call, because who is asking is known only now.
76
+ scope = ->(model) { server_context[:scope].call(model) }
77
+ result = Janela::Tools.new(scope: scope, write: write).call(tool.name, arguments)
78
+ MCP::Tool::Response.new([ { type: "text", text: result.to_json } ])
79
+ rescue Janela::Error => error
80
+ MCP::Tool::Response.new([ { type: "text", text: error.message } ], error: true)
81
+ end
82
+ end
83
+
84
+ MCP::Server.new(name: "janela", tools: tools, server_context: { scope: yield })
85
+ end
86
+ ```
87
+
88
+ `mcp` is not a dependency of Janela, and a host using another library writes the
89
+ same few lines against it.
90
+
91
+ ## What Janela does not do
92
+
93
+ It does not serve MCP, detect a server, edit your configuration or install
94
+ anything. It holds no credentials and has no idea who your agent is. Whether an
95
+ agent may reach your MCP server at all is your server's authentication, which
96
+ Janela never sees. Everything it adds is the question it already asks: what may
97
+ this caller read?
data/docs/composing.md CHANGED
@@ -23,12 +23,12 @@ with your own classes plus the hooks in [Theming Janela](theming).
23
23
  | `janela_frame do ... end` | anywhere inside the block | the page needs a heading, text, links or a layout of its own |
24
24
  | `janela_frame @frame` | none | the dashboard is data an analyst edits without a deploy |
25
25
 
26
- A stored frame renders panes and nothing else. That is deliberate (ADR
27
- 012): a row names a measure the model declared, so an analyst arranges
28
- what is shown and cannot put arbitrary content on the page. If a
29
- dashboard needs a heading and an explanation, write them in the page
30
- around the frame, or use the block form. Whether a stored frame should
31
- hold content of its own, such as text or an image, has not been decided.
26
+ A stored frame renders panes, and a pane is either a query, a few words
27
+ the analyst writes (a heading, a sentence, a link), or a partial you
28
+ wrote and they place by name (ADR 039). What an analyst writes is
29
+ escaped, never markup, so a row cannot put arbitrary content on the page
30
+ (ADR 012). Anything richer than that, an icon, an image, a layout of your
31
+ own, goes in the page around the frame, or use the block form.
32
32
 
33
33
  ```erb
34
34
  <h2>Orders</h2>
@@ -58,9 +58,9 @@ same classes a stored frame gets from its integers:
58
58
 
59
59
  Every direct child is a grid item, your own elements included, so a
60
60
  heading can take a whole row and a text box can sit between two panes.
61
- `janela_pane` takes no class of its own yet
62
- ([#29](https://github.com/retail-tasker/janela/issues/29)), so wrap a
63
- pane to span it:
61
+ `janela_pane` takes no class of its own, and will not: arranging panes
62
+ beyond the shipped grid is the host's (see the roadmap). So wrap a pane
63
+ to span it:
64
64
 
65
65
  ```erb
66
66
  <div class="janela-span-2"><%= janela_pane Order, :revenue, by: :status, as: :bar %></div>
@@ -195,11 +195,11 @@ and number, so the figure needs a `<div>` rather than a `<p>` around it,
195
195
  and a line of CSS to put the numbers side by side. Without it they
196
196
  stack.
197
197
 
198
- A done out of total figure cannot be two panes yet. A measure has no
199
- condition of its own, so there is no pane for the done half. Compute it
200
- as the progress bar below does. A measure that is itself the ratio is
201
- proposed in ADR 038
202
- ([#27](https://github.com/retail-tasker/janela/issues/27)).
198
+ A done out of total figure over a yes or no column is one pane: a
199
+ `ratio:` measure is the share of rows where a boolean is true (ADR 038).
200
+ When the condition is not a column, a measure has no condition of its own,
201
+ so there is no pane for the done half; compute it as the progress bar
202
+ below does.
203
203
 
204
204
  ### A progress bar
205
205