janela 0.12.0 → 0.14.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.
@@ -0,0 +1,221 @@
1
+ ---
2
+ Date: 2026-10-06
3
+ Status: Accepted
4
+ Related: ADR 004, ADR 020, ADR 029, ADR 037, ADR 038, ADR 046, ADR 047, ADR 050, ADR 053
5
+ Triggers:
6
+ - a chart's axis showing a number the tooltip shows differently
7
+ - drawing text on a chart canvas, or adding a Chart.js plugin
8
+ - adding a boolean to a pane, a column to janela_panes, or a parameter to the pane URL
9
+ - a ratio, a currency or a percentage on a chart
10
+ - a chart too dense to label every bar
11
+ Topics: charts, formatting, measures, public-api, panes, urls
12
+ ---
13
+
14
+ # ADR 054: A Chart Reads Numbers the Way Its Measure Formats Them
15
+
16
+ ## Context
17
+
18
+ #72 and #76, raised from a real install building a client report: a share
19
+ of rows by category, drawn as bars, with the percentage on each bar. Two
20
+ things stood in the way, and both are the same gap. A chart is the one
21
+ place a number reaches a reader without the measure's format applied
22
+ (ADR 020), except the tooltip, which a reader has to hover to see.
23
+
24
+ Both were reproduced against the demo on `main` (Chart.js 4.5.1), with the
25
+ chart built live and the drawn scale read from it.
26
+
27
+ **The axis is raw.** A bar chart of a ratio drew ticks `0, 0.1 ... 1.0`
28
+ beside tooltips reading `0.0%`. Revenue drew `0, 50,000 ... 500,000` beside
29
+ tooltips reading `$469,097.85`. `chart_controller.js` sets
30
+ `scales: { y: { beginAtZero: true } }` and formats only the tooltip.
31
+
32
+ **#72 said this was never decided. It was.** ADR 020 closes its Decision
33
+ with "The chart still plots raw numbers, because an axis is a scale and not
34
+ a label." That sentence is wrong for a reader, who reads the axis as labels
35
+ whatever it is to Chart.js. A ratio's axis at 0.5 against a tooltip at 50.0%
36
+ reads as two different numbers (ADR 038 keeps the stored value a fraction
37
+ for the data's sake, which is not a reason for the axis to show one). The
38
+ rest of ADR 020 stands: the number is never rounded or transformed, and the
39
+ measure is the layer that owns its format.
40
+
41
+ **Tick precision does not need a rule of Janela's own.** The worry in #72
42
+ was that ticks fall between data points, so the precision that suits a
43
+ value (`25.0%`) is wrong for a tick (`25%`). Chart.js's own numeric tick
44
+ formatter already derives its decimals from the spacing between ticks.
45
+ Calling it with a ratio's ticks scaled by 100 and appending `%` gave, on the
46
+ live chart:
47
+
48
+ | ratio data up to | ticks |
49
+ | --- | --- |
50
+ | 0.53 | `0%, 10% ... 60%` |
51
+ | 0.03 | `0%, 0.5%, 1% ... 3%` |
52
+ | 1 | `0% ... 100%` |
53
+
54
+ Whole percentages where the spacing is whole, half percentages only where
55
+ the ticks really are half a percent apart. One catch: scaling by 100 leaves
56
+ float noise (`3.5000000000000004%`), so the scaled value has to be rounded
57
+ before it is formatted.
58
+
59
+ **A bar label has to fit, and nothing guarantees it does.** Measured with a
60
+ label drawn at each bar's end:
61
+
62
+ - The tallest bar of a default chart touches the top of the plot area, so a
63
+ label above it is clipped. `scales.y.grace = "10%"` left 30px of
64
+ headroom in a 300px chart, and Chart.js 4.5.1 supports it.
65
+ - A label inside the bar was never an option: the shortest bars are about
66
+ 2px tall.
67
+ - With 30 bars in a 600px chart a bar was 13px wide and a label such as
68
+ `$88,000.00` was 60px, so neighbouring labels overlap several deep.
69
+ Hiding only the ones that collide would label some bars and not others
70
+ for a reason a reader cannot see.
71
+ - A negative bar ends below its base, so a label placed above the end would
72
+ sit on the bar.
73
+ - A plugin cannot be added to a chart after it is built, so the drawing has
74
+ to be handed over when the controller constructs the chart, as
75
+ `maintainAspectRatio` is for a height (ADR 047).
76
+
77
+ ### What was considered
78
+
79
+ **Formatted tick strings from the server.** The server formats a value
80
+ (ADR 020), but the client chooses the ticks, where they fall depends on the
81
+ chart's size and data, and the server cannot know them. It would have to
82
+ format values it is never asked about.
83
+
84
+ **A `precision` of the measure's own for ticks.** A measure's precision is
85
+ what one value means. A scale spanning `$0` to `$500,000` in steps of
86
+ 50,000 does not want cents, and a ratio at one decimal place would print
87
+ `20.0%` at every tick. It is the answer to a different question.
88
+
89
+ **A host-supplied tick callback.** A setting, to do what one formatter
90
+ already does for every measure. ADR 001 prefers one obvious way, and ADR 021
91
+ and ADR 023 only allow a setting where a judgement is made once at boot.
92
+
93
+ **`chartjs-plugin-datalabels` for the bar labels.** A dependency, and a
94
+ second way to format a number on a chart. ADR 004 keeps Chart.js the one
95
+ thing the engine ships, and an inline plugin that draws a string the server
96
+ already produced is about twenty lines.
97
+
98
+ **Labels on by default.** Changes every bar chart that exists, and a chart
99
+ of thirty bars gains nothing from it. Unset must change nothing, as it does
100
+ for height and prominence.
101
+
102
+ **A setting for where the label sits, or whether it fits.** Outside the end
103
+ is the only place that works for every bar, including the shortest and the
104
+ negative, and a fit that is a judgement per chart is a decision a reader of
105
+ the dashboard cannot see made.
106
+
107
+ **Labels on a line, or on a ring.** A line has a point per bucket, often
108
+ sixty or more, and a ring already prints every value beside it (ADR 046).
109
+ Neither gains from it.
110
+
111
+ ## Decision
112
+
113
+ **A chart's axis is formatted by the measure, always, and a bar chart can
114
+ be asked to draw each bar's value as a label.** The first supersedes the one
115
+ sentence in ADR 020 above and nothing else in it. The second is opt in.
116
+
117
+ ### The axis
118
+
119
+ The server sends what a tick needs as one more data attribute on the canvas:
120
+ the measure's `prefix`, its `suffix`, and whether it is a ratio.
121
+
122
+ ```erb
123
+ data-janela--chart-tick-format-value='{"prefix":"$","suffix":"","ratio":false}'
124
+ ```
125
+
126
+ The controller formats each tick with Chart.js's own numeric formatter and
127
+ adds those. A ratio's ticks are scaled by 100, rounded to remove float
128
+ noise, and given a `%`. The measure's `precision` is not used: the tick's
129
+ decimals come from the spacing between ticks, which is what answers "25%,
130
+ not 25.0%". It applies to a bar and a line, both of which have a value axis,
131
+ and it is not a setting: it is what the axis always was, now correct.
132
+
133
+ A measure with no prefix, suffix or ratio draws what it draws today.
134
+
135
+ ### The labels
136
+
137
+ **A pane may carry `value_labels`, a boolean. Unset is exactly what it is
138
+ today.** When set on a bar chart, each bar's formatted string, the same one
139
+ the tooltip shows, is drawn beyond the end of the bar, above for a positive
140
+ value and below for a negative one. The value axis is given 10% of grace so
141
+ the tallest bar keeps its label in the plot.
142
+
143
+ **All or none.** The labels are drawn only if every one of them fits within
144
+ its category's slot, the chart area's width divided by the number of bars.
145
+ If any does not, none is drawn and the chart is what it was without the flag.
146
+ A chart never labels some bars and not others, and a host that wants the
147
+ labels on a dense chart makes the chart wider or the pane a bar chart of
148
+ fewer categories (`limit:`, ADR 007).
149
+
150
+ **One vocabulary in three places,** as height and prominence do:
151
+
152
+ ```erb
153
+ <%= janela_pane Order, :expedited_rate, by: :status, as: :bar, value_labels: true %>
154
+ ```
155
+
156
+ - `Janela::Pane#value_labels`, a nullable boolean, offered on the pane form
157
+ as a checkbox.
158
+ - `janela_pane` and `janela_snapshot_pane` take `value_labels:`.
159
+ - The pane URL carries it as `?value_labels=1`, because the server draws the
160
+ pane again from that URL on every cross-filter.
161
+
162
+ **The name avoids `labels`.** In this codebase a chart's labels are its
163
+ categories: the controller's `labels` value is the x axis. `value_labels`
164
+ says which labels, and `values` would collide the same way with the
165
+ controller's `values`. It is the one name that reads the same at the helper,
166
+ the column, the URL and the controller (ADR 037).
167
+
168
+ **It is not part of the pane's identity.** ADR 029 identifies a frame by who
169
+ it is, and whether its bars carry numbers does not say which query it is. It
170
+ stays out of `Query.turbo_frame_id`.
171
+
172
+ **On anything that is not a bar chart it is ignored, not an error.** A
173
+ table, a single value, a line and a ring keep what they have, and a pane
174
+ switched between renderers keeps the flag, as height does on a ring
175
+ (ADR 047).
176
+
177
+ ## Consequences
178
+
179
+ - **Every existing chart's axis can change.** A revenue chart gains a `$`
180
+ on its ticks and a ratio chart reads in percentages. Nothing else about it
181
+ moves, and a measure with no prefix, suffix or ratio is untouched. It is
182
+ in `CHANGELOG.md` under Changed as a visible correction, with no action for
183
+ a host; it is not an `UPGRADING.md` step (ADR 015), because nothing a host
184
+ wrote has to change. A host that overrode the tick callback on its own
185
+ chart is unaffected, since Janela does not set one for it.
186
+ - A ratio chart is readable end to end: a percentage on the axis, in the
187
+ tooltip, and on the bars if asked.
188
+ - **A migration for stored frames.** `janela_panes` gains a nullable
189
+ boolean `value_labels`, additive, so every existing pane is unset and
190
+ unchanged. A host using stored frames runs `bin/rails
191
+ janela:install:migrations` and `db:migrate`, as it did for `height`,
192
+ `prominence` and `companions`. The doctor's `unmigrated-columns` check
193
+ (ADR 035) is taught the new column, so a host that upgrades and skips the
194
+ migration is told rather than meeting an error on the pane form. A host that
195
+ does not use stored frames has nothing to do.
196
+ - **`Janela::Tools` has to learn the attribute.** Its `PANE_ATTRIBUTES` is the
197
+ list of a pane's own attributes an agent may set (ADR 053), and an
198
+ attribute not on it is refused. `value_labels` joins it in the same change as
199
+ the column, or an agent could arrange every other part of a pane and not
200
+ this one.
201
+ - **The pane URL grammar grows by one optional parameter.** ADR 037 counts
202
+ that as public surface, which is why this belongs before 1.0.
203
+ - **Ticks use the browser's number locale, and the tooltip uses the host's
204
+ delimiter** (ADR 020). A host whose `number.format.delimiter` differs from
205
+ its readers' browsers would see two styles on one chart. This is from how
206
+ Chart.js formats and is not something measured here; if it turns up in
207
+ practice the answer is to pass the delimiter in the same attribute, not to
208
+ stop using Chart.js's formatter.
209
+ - The all or none rule means the flag can do nothing on a dense chart, and
210
+ the pane form cannot say so. That is a real cost, accepted because the
211
+ alternative is a chart that labels bars selectively.
212
+ - Drawn pixels are not available to a screen reader. That is as true of the
213
+ bars themselves, which ADR 042 answers with the figure's caption and the
214
+ table beside it, and this decision does not change it.
215
+ - Tests read what the code produced: the drawn tick labels, and the position
216
+ and text of each drawn bar label, not the attribute that asked for them
217
+ (ADR 035).
218
+ - What would change this decision: a need for a label on a line or a ring,
219
+ which is an argument about those renderers and not about this one; or a
220
+ host that needs the labels to survive on a dense chart, which would be a
221
+ case for a step in how they are thinned and not for a placement setting.
@@ -23,26 +23,26 @@ them when in doubt: `grep -l "Triggers:.*fork" docs/decisions/*.md`.
23
23
  | **Vision, scope, forkability** | 001, 010, 012, 037, 052 |
24
24
  | **Open-source & host-decoupling** | 001, 022, 036, 041, 052 |
25
25
  | **DSL & query layer** | 002, 006, 007, 020, 025, 038, 044, 049, 051 |
26
- | **Dependencies** | 002, 003, 004, 006, 017, 025 |
27
- | **Authorisation** | 002, 003, 004, 009, 017, 019, 022, 032, 033, 034, 035, 039, 040, 048 |
26
+ | **Dependencies** | 002, 003, 004, 006, 017, 025, 053 |
27
+ | **Authorisation** | 002, 003, 004, 009, 017, 019, 022, 032, 033, 034, 035, 039, 040, 048, 053 |
28
28
  | **Performance & storage** | 007, 017, 025, 048, 051 |
29
- | **Ordering & formatting** | 007, 020, 038 |
29
+ | **Ordering & formatting** | 007, 020, 038, 054 |
30
30
  | **Cross-filtering & Hotwire** | 003, 004, 005, 008, 024, 025, 040, 043, 044, 045, 048, 049 |
31
31
  | **Layouts & views** | 011, 012, 016, 018, 020, 027, 039, 047, 050 |
32
32
  | **CSS & styling** | 016, 018, 023, 026, 027, 036, 042, 046, 047, 050 |
33
- | **Frames, panes & persistence** | 012, 013, 014, 019, 029, 030, 033, 039, 040, 041, 043, 044, 047, 048, 050, 051 |
33
+ | **Frames, panes & persistence** | 012, 013, 014, 019, 029, 030, 033, 039, 040, 041, 043, 044, 047, 048, 050, 051, 053, 054 |
34
34
  | **Naming rule** | 014, 023, 036 |
35
- | **JavaScript delivery & charts** | 004, 006, 026, 042, 046, 047 |
35
+ | **JavaScript delivery & charts** | 004, 006, 026, 042, 046, 047, 054 |
36
36
  | **Time dimensions** | 006, 025, 045 |
37
- | **Routes, URLs & naming** | 005, 007, 008, 009, 011, 013, 022, 024, 025, 040, 041, 045, 047, 050, 051 |
37
+ | **Routes, URLs & naming** | 005, 007, 008, 009, 011, 013, 022, 024, 025, 040, 041, 045, 047, 050, 051, 054 |
38
38
  | **Snapshots & publishing** | 009, 020, 028, 033, 034, 051 |
39
- | **AI agents & guidance** | 010, 015, 021 |
39
+ | **AI agents & guidance** | 010, 015, 021, 053 |
40
40
  | **The doctor & checks** | 021, 025, 032, 033, 035 |
41
41
  | **Releases & upgrades** | 015, 021, 032, 034, 035, 036, 037, 042, 052 |
42
42
  | **Accessibility & keyboard** | 024, 042, 045, 046, 049 |
43
43
  | **Security** | 003, 025, 028, 031, 032, 034, 035, 044, 052 |
44
44
  | **Testing** | 003 |
45
- | **Roadmap & planning** | 001, 037, 045, 046, 052 |
45
+ | **Roadmap & planning** | 001, 037, 045, 046, 052, 053 |
46
46
 
47
47
  ## Chronological
48
48
 
@@ -100,7 +100,9 @@ them when in doubt: `grep -l "Triggers:.*fork" docs/decisions/*.md`.
100
100
  | 050 | A Single Value's Prominence Is One of Three Steps, and Unset Changes Nothing | 2026-09-30 | Accepted |
101
101
  | 051 | A Table Can Carry Companion Columns, and a Fact Is Shown Only When It Is Shared | 2026-09-30 | Accepted |
102
102
  | 052 | Two People Cut Releases, Reports Go Private, and Support Is Best Effort | 2026-09-30 | Accepted |
103
+ | 053 | An Agent Reaches Janela Through Tools the Host Scopes, and Janela Registers None | 2026-10-02 | Accepted |
104
+ | 054 | A Chart Reads Numbers the Way Its Measure Formats Them | 2026-10-06 | Accepted |
103
105
 
104
106
  ## Next number
105
107
 
106
- Next ADR: 053
108
+ Next ADR: 055
@@ -36,9 +36,10 @@ without asking first:
36
36
  - A snapshot, read through `policy_scope(Janela::Snapshot)`.
37
37
 
38
38
  A pane rendered inline in your own page runs in your request, so the
39
- same scope applies there as in the engine's controllers. Nothing is
40
- calculated in a background context where the current tenant would have
41
- gone missing.
39
+ same scope applies there as in the engine's controllers, so a live pane
40
+ is never calculated where the current tenant would have gone missing. A
41
+ scheduled snapshot is the one thing that runs in the background, and it
42
+ is told whose it is (see Snapshots below).
42
43
 
43
44
  ## With Pundit
44
45
 
@@ -69,6 +70,23 @@ class ApplicationController < ActionController::Base
69
70
  end
70
71
  ```
71
72
 
73
+ A snapshot is read through its own scope as well. Without a policy for it
74
+ Pundit raises, and a hand rolled `policy_scope` that does not name it
75
+ would hand every tenant every snapshot, so write the second one too:
76
+
77
+ ```ruby
78
+ # app/policies/janela/snapshot_policy.rb
79
+ module Janela
80
+ class SnapshotPolicy < ApplicationPolicy
81
+ class Scope < ApplicationPolicy::Scope
82
+ def resolve
83
+ scope.where(owner: Current.account)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ ```
89
+
72
90
  Your own models keep the policies they already have. Janela calls
73
91
  `policy_scope(Order)` and gets whatever `OrderPolicy::Scope` returns.
74
92
 
@@ -85,7 +103,7 @@ class ApplicationController < ActionController::Base
85
103
  private
86
104
  def policy_scope(model)
87
105
  case model.name
88
- when "Janela::Frame" then model.where(owner: ActsAsTenant.current_tenant)
106
+ when "Janela::Frame", "Janela::Snapshot" then model.where(owner: ActsAsTenant.current_tenant)
89
107
  else model.all # acts_as_tenant has already scoped your own models
90
108
  end
91
109
  end
data/docs/roadmap.md CHANGED
@@ -8,7 +8,7 @@ Topics: roadmap, releases, scope, planning
8
8
 
9
9
  <svg viewBox="0 0 680 360" width="100%" role="img" aria-labelledby="vista-title vista-desc" class="vista-art" style="display: block; margin: 1.75rem 0; border-radius: 10px;">
10
10
  <title id="vista-title">Vista</title>
11
- <desc id="vista-desc">Sea and sky with a horizon across them. The near water is dark, because every issue in 1.0 has shipped, and three lights sit far off at the horizon for the work still in sight past it. A low sun rises and sets on the horizon as the pointer moves up and down, and never climbs higher: the sky above is empty, because what is not coming is not in view.</desc>
11
+ <desc id="vista-desc">Sea and sky with a horizon across them. The near water is dark, because every issue in 1.0 has shipped, and five lights sit far off at the horizon for the work still in sight past it. A low sun rises and sets on the horizon as the pointer moves up and down, and never climbs higher: the sky above is empty, because what is not coming is not in view.</desc>
12
12
 
13
13
  <style>
14
14
  .vista-art .v-far { transform: translate3d(calc(var(--vitral-shift-x, 0) * 13px), calc(var(--vitral-shift-y, 0) * 7px), 0); transition: transform .45s cubic-bezier(.2,.7,.3,1); }
@@ -89,6 +89,10 @@ Topics: roadmap, releases, scope, planning
89
89
  <circle cx="322" cy="201" r="1.5" fill="#FFF3DC" opacity="0.55"/>
90
90
  <circle cx="540" cy="202.5" r="8" fill="url(#vLamp)" opacity="0.2"/>
91
91
  <circle cx="540" cy="202.5" r="1.6" fill="#FFF3DC" opacity="0.55"/>
92
+ <circle cx="112" cy="201.5" r="8" fill="url(#vLamp)" opacity="0.2"/>
93
+ <circle cx="112" cy="201.5" r="1.5" fill="#FFF3DC" opacity="0.55"/>
94
+ <circle cx="430" cy="202" r="8" fill="url(#vLamp)" opacity="0.2"/>
95
+ <circle cx="430" cy="202" r="1.6" fill="#FFF3DC" opacity="0.55"/>
92
96
  </g>
93
97
 
94
98
  <g class="v-glint" stroke="#FFE2A6" stroke-linecap="round">
@@ -109,7 +113,7 @@ Topics: roadmap, releases, scope, planning
109
113
  Janela is alpha. It works, it is tested against a real Rails application
110
114
  in a real browser, and its public surface has changed in three of the
111
115
  last four releases. This page says what has to be true before that stops,
112
- what is in the next release, and what is deliberately not coming. The
116
+ what is left before 1.0, and what is deliberately not coming. The
113
117
  reasoning behind it is ADR 037.
114
118
 
115
119
  ## What 1.0 means
@@ -128,11 +132,11 @@ change in any release, and every change of that kind carries an entry in
128
132
  `UPGRADING.md`.
129
133
 
130
134
  **A pane can be read.** Janela draws tables, bars, lines, doughnuts and
131
- pies. A table row still cannot carry context beside its label, a single
132
- number has no way to say how prominent it is, and a chart takes whatever
133
- height its width gives it whether or not that suits the page. Those are
134
- not extra features. They are the 5% not finished, and most of them were
135
- found by people installing the gem rather than reading it.
135
+ pies. A table row can carry context beside its label, a single number says
136
+ how prominent it is, a chart has a height that suits the page, its axis reads
137
+ in the measure's own format, and a bar can show its value. Those
138
+ were not extra features. They were the 5% not finished, and most of them
139
+ were found by people installing the gem rather than reading it.
136
140
 
137
141
  **The doctor can be trusted.** `rails janela:doctor` checks an
138
142
  installation for the mistakes that produce a dashboard showing numbers
@@ -144,17 +148,24 @@ working is a check nobody should rely on.
144
148
 
145
149
  The near ground is clear. Every issue that was in 1.0 has shipped: chart
146
150
  height, a ratio measure, single value prominence, companion columns, Sprockets
147
- hosts, and how the project is run. What 1.0 waits on now is time with real
148
- installs, since much of that surface was added in the last days and has only
149
- met this repository. When nothing surprising comes back, the surface is
150
- frozen and 1.0 is tagged.
151
+ hosts, and how the project is run. 0.13.0 added tools an agent can use
152
+ (ADR 053). 0.14.0 followed: a chart's axis reads in the measure's own format,
153
+ so a ratio is a percentage down the side as well as in the tooltip, and a bar
154
+ chart can show each bar's value (ADR 054). Both came from a real install, and
155
+ the second adds a pane option, so it starts its own short soak. What 1.0 waits
156
+ on now is a week of real installs of 0.14.0 ([#71](https://github.com/retail-tasker/janela/issues/71)),
157
+ since much of that surface was added in the last days and has only met this
158
+ repository. When nothing surprising comes back, the surface is frozen and 1.0
159
+ is tagged. The agent tools are the one new piece of surface: the read tools are
160
+ frozen at 1.0, and the write tools are frozen with them only if a real host has
161
+ used them by the end of that week. Otherwise they wait for 1.1.
151
162
 
152
163
  Progress is tracked on the
153
164
  [1.0 milestone](https://github.com/retail-tasker/janela/milestone/2).
154
165
 
155
166
  ## Horizonte
156
167
 
157
- The three lights far off at the horizon. Wanted, not blocking a stable
168
+ The lights far off at the horizon. Wanted, not blocking a stable
158
169
  release, and being on this list is not a refusal.
159
170
 
160
171
  - **Drilling down on time panes** ([#65](https://github.com/retail-tasker/janela/issues/65)).
@@ -162,6 +173,17 @@ release, and being on this list is not a refusal.
162
173
  that pane itself to the weeks within it is a different gesture, with
163
174
  its own questions about getting back out and about the URL, and it
164
175
  needs a decision record before any code.
176
+ - **Telling a frame to refresh** ([#68](https://github.com/retail-tasker/janela/issues/68)).
177
+ One action a host's own timer or push can call, and no timer or stream of
178
+ Janela's own (ADR 048). Additive, so it can land after 1.0.
179
+ - **A frame toolbar** ([#77](https://github.com/retail-tasker/janela/issues/77)).
180
+ A small strip of standard controls a host does not have to build, starting
181
+ with full screen. Additive, so it can land after 1.0, and it needs a decision
182
+ record first.
183
+ - **A theme panel in the demo** ([#84](https://github.com/retail-tasker/janela/issues/84)).
184
+ Setting how every pane is presented, starting with colours, as a page of
185
+ controls over the properties a theme already publishes. The demo, not the
186
+ gem, and any property beyond colour needs a decision record first.
165
187
  - **A command palette for the demo** ([#41](https://github.com/retail-tasker/janela/issues/41)).
166
188
  The demo site, not the gem.
167
189
  - **A scroll drift on the gallery page** ([#45](https://github.com/retail-tasker/janela/issues/45)).
@@ -178,7 +200,9 @@ run does it better.
178
200
  Natural-language query. A separate data warehouse. A row-level-security
179
201
  subsystem, because your application already has Pundit or CanCanCan and
180
202
  Janela reads through it. A refresh-scheduling interface, because you
181
- already have a scheduler and snapshots are an ActiveJob. An embedding
203
+ already have a scheduler and snapshots are an ActiveJob (a way to tell a
204
+ frame to refresh, which your scheduler can call, is
205
+ [#68](https://github.com/retail-tasker/janela/issues/68)). An embedding
182
206
  SDK. A mobile application. Print and paginated reports. A drag-and-drop
183
207
  visual dashboard designer, though frames and panes are database records,
184
208
  so an application can build its own editor on top of them.
data/docs/theming.md CHANGED
@@ -82,6 +82,7 @@ the names a theme spends most of its time on.
82
82
  | `janela-legend` | `<table>` | its legend, one row of swatch, button and value per slice |
83
83
  | `janela-swatch` | `<span>` | the colour beside a legend label |
84
84
  | `janela-empty` | `<p>` | a pane whose query returned nothing |
85
+ | `janela-muted` | `<p>` | the note under a ring that could not be drawn and became a table (ADR 046) |
85
86
  | `janela-error` | `<p>` | a pane that could not be read |
86
87
  | `janela-content` | `<div>` | a stored pane holding words or a host partial rather than a query (ADR 039) |
87
88
  | `janela-content-heading` | `<h2>` | a text pane's heading |
@@ -121,8 +122,9 @@ here rather than being left for each host to write.
121
122
  `janela.css` also styles Janela's own pages, the frame index and the
122
123
  editing forms: `janela-page`, `janela-card`, `janela-button`,
123
124
  `janela-form`, `janela-field`, `janela-list`, `janela-crumb`,
124
- `janela-flash` and the rest. They are scoped under `janela-page`, which
125
- only the engine's own layout sets, so they cannot touch your pages.
125
+ `janela-flash` and the rest. They are plain class names that the engine's
126
+ own pages use, not scoped to `janela-page`, so they affect your pages
127
+ only if your own markup uses the same names. Avoid them.
126
128
 
127
129
  **Those names may change in any release.** They are the engine's own
128
130
  chrome rather than an interface. Restyle them if you want Janela's
@@ -140,8 +142,8 @@ Janela.theme = "midnight"
140
142
 
141
143
  The name is resolved against your own asset paths, so `midnight.css` in
142
144
  your application works exactly as the gem's own `vitral` does. A name
143
- that resolves to nothing raises rather than quietly rendering an
144
- unthemed page.
145
+ that your asset pipeline cannot find raises when the page renders,
146
+ rather than quietly rendering an unthemed one.
145
147
 
146
148
  That setting links the theme into **Janela's own pages**. Your pages load
147
149
  whatever your layout says, so if you want the same look around a pane you
data/lib/janela/doctor.rb CHANGED
@@ -9,7 +9,7 @@ module Janela
9
9
 
10
10
  # Run in this order, and each one names the finding it produces: a host
11
11
  # silences a check by that name (ADR 021).
12
- CHECKS = %i[stale_identifiers unmounted_engine unmigrated_tables unregistered_controllers
12
+ CHECKS = %i[stale_identifiers unmounted_engine unmigrated_tables unmigrated_columns unregistered_controllers
13
13
  through_dimensions_without_an_allowlist unscoped_reads frames_nobody_will_own
14
14
  snapshots_nobody_will_see unauthenticated_endpoints
15
15
  hardcoded_disallowed_predicates].freeze
@@ -21,10 +21,22 @@ module Janela
21
21
  "janela/dashboard_controller" => "janela/frame_controller",
22
22
  "janela/dashboard_controller.js" => "janela/frame_controller.js",
23
23
  "Janela::DashboardHelper" => "Janela::FramesHelper",
24
- "Janela::PanesController" => "Janela::QueriesController",
25
24
  "Janela::SnapshotPanesController" => "Janela::SnapshotQueriesController"
26
25
  }.freeze
27
26
 
27
+ # A name that was renamed away and later given back to something else, so
28
+ # finding it cannot say which one the host means. An error would send a
29
+ # host patching the stored-pane form to a controller without pane_params,
30
+ # and the extra param would silently stop saving (#74). A warning says what
31
+ # the name means now and leaves the judgement to the host.
32
+ REUSED = {
33
+ "Janela::PanesController" =>
34
+ "This name was the controller that renders a pane's query until 0.7, when it became " \
35
+ "Janela::QueriesController. Since 0.12 it is the stored-pane form's controller again, " \
36
+ "and it owns pane_params. A patch to the form belongs here; a patch to how a query " \
37
+ "renders belongs on Janela::QueriesController."
38
+ }.freeze
39
+
28
40
  SEARCHED = %w[app config lib].freeze
29
41
  READABLE = %w[.rb .erb .js .erb.html .html.erb .haml .slim .yml].freeze
30
42
 
@@ -79,14 +91,26 @@ module Janela
79
91
  end
80
92
 
81
93
  def stale_identifiers
82
- RENAMED.filter_map do |old, new|
83
- files = source_files.select { |file| file.read.include?(old) }
84
- next if files.empty?
94
+ renamed = RENAMED.filter_map do |old, new|
95
+ next unless (files = files_naming(old)).any?
85
96
 
86
- Finding.new(severity: :error,
87
- summary: "#{old} is now #{new}",
88
- detail: files.map { |file| " #{file.relative_path_from(@root)}" }.join("\n"))
97
+ Finding.new(severity: :error, summary: "#{old} is now #{new}", detail: listed(files))
89
98
  end
99
+ reused = REUSED.filter_map do |name, meaning|
100
+ next unless (files = files_naming(name)).any?
101
+
102
+ Finding.new(severity: :warning, summary: "#{name} has meant two things",
103
+ detail: " #{meaning}\n#{listed(files)}")
104
+ end
105
+ renamed + reused
106
+ end
107
+
108
+ def files_naming(identifier)
109
+ source_files.select { |file| file.read.include?(identifier) }
110
+ end
111
+
112
+ def listed(files)
113
+ files.map { |file| " #{file.relative_path_from(@root)}" }.join("\n")
90
114
  end
91
115
 
92
116
  def unmounted_engine
@@ -116,6 +140,40 @@ module Janela
116
140
  nil # no database to ask yet
117
141
  end
118
142
 
143
+ # The columns a release added to a table that already existed. A host that
144
+ # upgraded and did not run the migration has the table, so the check above
145
+ # passes, and meets an error the first time a pane form or a stored pane
146
+ # reads the column. Only the tables that exist are asked: a missing table
147
+ # is that check's finding, and listing every column of it here would say
148
+ # the same thing twice.
149
+ ADDED_COLUMNS = {
150
+ "Janela::Frame" => %w[key default_model default_where],
151
+ "Janela::Pane" => %w[kind heading body link partial height prominence companions value_labels],
152
+ "Janela::Snapshot" => %w[owner_type owner_id]
153
+ }.freeze
154
+
155
+ def unmigrated_columns
156
+ return unless engine_mounted?
157
+
158
+ absent = ADDED_COLUMNS.filter_map do |model_name, columns|
159
+ model = model_name.constantize
160
+ next unless model.table_exists?
161
+
162
+ missing = columns - model.column_names
163
+ "#{model.table_name} (#{missing.join(', ')})" if missing.any?
164
+ end
165
+ return if absent.empty?
166
+
167
+ Finding.new(severity: :error,
168
+ summary: "#{absent.to_sentence} #{absent.one? ? 'is' : 'are'} missing columns a release added",
169
+ detail: " The tables exist, so the migrations from an earlier release ran, but a later one has not.\n" \
170
+ " Run:\n" \
171
+ " bin/rails janela:install:migrations\n" \
172
+ " bin/rails db:migrate")
173
+ rescue StandardError
174
+ nil # no database to ask yet
175
+ end
176
+
119
177
  def engine_mounted?
120
178
  Rails.application.routes.routes.any? { |route| route.app.respond_to?(:app) && route.app.app == Janela::Engine }
121
179
  end