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 +4 -4
- data/CHANGELOG.md +19 -1
- data/README.md +28 -25
- data/UPGRADING.md +38 -1
- data/app/assets/stylesheets/vitral.css +21 -0
- data/app/views/janela/queries/_query.html.erb +15 -15
- data/docs/agents.md +97 -0
- data/docs/composing.md +14 -14
- data/docs/decisions/053-an-agent-reaches-janela-through-tools-the-host-scopes.md +187 -0
- data/docs/decisions/INDEX.md +7 -6
- data/docs/multi-tenancy.md +22 -4
- data/docs/roadmap.md +27 -13
- data/docs/theming.md +6 -4
- data/lib/janela/doctor.rb +66 -8
- data/lib/janela/tools.rb +220 -0
- data/lib/janela/version.rb +1 -1
- data/lib/janela.rb +1 -0
- metadata +12 -8
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
---
|
|
2
|
+
Date: 2026-10-02
|
|
3
|
+
Status: Accepted
|
|
4
|
+
Related: ADR 001, ADR 010, ADR 012, ADR 014, ADR 017, ADR 019, ADR 025, ADR 032, ADR 033, ADR 034, ADR 037, ADR 041
|
|
5
|
+
Triggers:
|
|
6
|
+
- proposing an MCP server, an agent tool or any machine facing surface for Janela
|
|
7
|
+
- an agent needs to read or arrange a frame without a browser
|
|
8
|
+
- adding a dependency on an MCP library
|
|
9
|
+
- deciding whether Janela should detect, register or install anything in a host's own tooling
|
|
10
|
+
- running a Janela query outside a controller, from a tool, a job or a console
|
|
11
|
+
- changing what an agent may do to a pane
|
|
12
|
+
Topics: ai, agents, mcp, tools, authorisation, scope, host-integration, frames, dependencies, 1.0
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# ADR 053: An Agent Reaches Janela Through Tools the Host Scopes, and Janela Registers None
|
|
16
|
+
|
|
17
|
+
## Context
|
|
18
|
+
|
|
19
|
+
Janela dashboards are meant to be composed by agents, and an agent in a
|
|
20
|
+
host application has no way in. Frames and panes are rows (ADR 012), so
|
|
21
|
+
the only route is `Janela::Frame` and `Janela::Pane` written from a
|
|
22
|
+
console. A host that already runs an MCP server writes its own wrappers
|
|
23
|
+
by hand, and a host without one gets nothing. #73 asks for an MCP
|
|
24
|
+
surface, and ADR 010 named it as "one ADR away" and as one of the two
|
|
25
|
+
conditions for shipping Jan.
|
|
26
|
+
|
|
27
|
+
The issue proposed that Janela detect a host's MCP server and offer to
|
|
28
|
+
install tools into it. Four facts, measured on the demo before this was
|
|
29
|
+
written, change what that should mean.
|
|
30
|
+
|
|
31
|
+
**Reading a pane outside a controller reads everything.** The refusal in
|
|
32
|
+
ADR 032 lives in `Janela::ApplicationController`, where `policy_scope`
|
|
33
|
+
is asked. `Query#result` underneath takes an optional `on:` relation,
|
|
34
|
+
and without it reads the model's default scope. With two orders in the
|
|
35
|
+
demo, `pane.query.result` returned both statuses, and
|
|
36
|
+
`pane.query.result(on: Order.where(status: "paid"))` returned one. A tool
|
|
37
|
+
that called the first form would show an agent, and so a person, rows
|
|
38
|
+
the host never scoped, with no error. This is the same shape as ADR 034:
|
|
39
|
+
a thing that runs outside a request and has to be told what may be read.
|
|
40
|
+
|
|
41
|
+
**The host's scope is an answer given at the point of asking.** ADR 019
|
|
42
|
+
found that a decision needing the current user and tenant belongs inside
|
|
43
|
+
a request rather than in a setting, and ADR 032 rejected a global
|
|
44
|
+
`Janela.scope = ->(model, controller)` for it. An MCP call is not a
|
|
45
|
+
request to Janela's controllers. Who is asking arrives through whatever
|
|
46
|
+
authentication the host's own MCP server has, which Janela does not own
|
|
47
|
+
and cannot see.
|
|
48
|
+
|
|
49
|
+
**A pane row already refuses what it should.** A pane naming a measure or
|
|
50
|
+
model that is not declared is invalid, and says so in words an agent can
|
|
51
|
+
act on: `Measure "nonsense" is not a measure of Order` and `Model
|
|
52
|
+
"no_such_model" is not a janela model`. The tools do not need a
|
|
53
|
+
validation layer of their own.
|
|
54
|
+
|
|
55
|
+
**Tools can be built from data.** The official Ruby SDK, `mcp` 1.6.1,
|
|
56
|
+
builds a tool from a name, a description, a JSON schema and a block
|
|
57
|
+
(`MCP::Tool.define`), and hands the caller's context to the block as
|
|
58
|
+
`server_context`. Other libraries take the same four things. A tool
|
|
59
|
+
definition is therefore plain data plus a call, and none of it needs to
|
|
60
|
+
live in an MCP library to be written once.
|
|
61
|
+
|
|
62
|
+
One more fact corrects the issue. #73 describes the install task as "in
|
|
63
|
+
the same spirit as `rails janela:install:skill`". That task does not
|
|
64
|
+
exist. ADR 010 records the skill, the install task and the agent
|
|
65
|
+
definition as not implemented, and `docs/skills/` is absent. This would
|
|
66
|
+
be the first install task Janela has, not the second.
|
|
67
|
+
|
|
68
|
+
### What was considered
|
|
69
|
+
|
|
70
|
+
**Janela serves MCP itself**, as an engine endpoint. Rejected. A served
|
|
71
|
+
endpoint needs authentication, a notion of who is calling and a way to
|
|
72
|
+
answer ADR 032's question for them, and Janela has none of the three: its
|
|
73
|
+
controllers are exactly as authenticated as the host's (ADR 010). Adding
|
|
74
|
+
them is the subsystem ADR 001 refused, and getting scope wrong shows one
|
|
75
|
+
tenant's numbers to another.
|
|
76
|
+
|
|
77
|
+
**Detect the host's MCP server and install into it**, as #73 proposed.
|
|
78
|
+
Rejected for the reason ADR 034 rejected detecting the kind of host: the
|
|
79
|
+
signal does not distinguish anything. A host's MCP setup is a gem, an
|
|
80
|
+
endpoint or a hand-written server, and the one thing Janela can reliably
|
|
81
|
+
know is none of those. It would also be Janela editing a host's tooling,
|
|
82
|
+
which ADR 010 says it never does.
|
|
83
|
+
|
|
84
|
+
**A `janela:install:mcp` task that writes tool files into the host.**
|
|
85
|
+
Rejected. Once copied the files are the host's, they drift from the
|
|
86
|
+
gem's public API, and the doctor would have to grow a check for each
|
|
87
|
+
release to say so. A host that wants the shape reads it in the guide and
|
|
88
|
+
owns the ten lines.
|
|
89
|
+
|
|
90
|
+
**A global scope setting**, so the tools need no argument. Rejected on
|
|
91
|
+
ADR 019 and ADR 032, above.
|
|
92
|
+
|
|
93
|
+
**`mcp` as a dependency of the gem.** Rejected. It is the right library
|
|
94
|
+
today and is not the only one a host may use, so a hard dependency would
|
|
95
|
+
make Janela's choice the host's. It stays out of the gemspec, the way the
|
|
96
|
+
demo's markdown renderer does, and the adapter below is optional.
|
|
97
|
+
|
|
98
|
+
**Writing and reading in one step.** Rejected as the shipped shape, not
|
|
99
|
+
as an idea. The write tools change records a person arranged, so they are
|
|
100
|
+
off until asked for.
|
|
101
|
+
|
|
102
|
+
## Decision
|
|
103
|
+
|
|
104
|
+
**Janela ships its agent tools as plain Ruby that the host builds with a
|
|
105
|
+
scope and registers wherever it likes. Janela registers nothing,
|
|
106
|
+
detects nothing and serves nothing.**
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
tools = Janela::Tools.new(scope: ->(model) { policy_scope(model) }, write: true)
|
|
110
|
+
|
|
111
|
+
tools.all # name, description, input_schema, read_only
|
|
112
|
+
tools.call("add_pane", frame_id: 3, model: "orders", measure: "revenue")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**`scope:` is required and has no default.** It is a callable that takes
|
|
116
|
+
a model class and returns a relation: the same answer `policy_scope`
|
|
117
|
+
gives. Leaving it out raises `Janela::Unscoped` when the tools are built,
|
|
118
|
+
not when one is called, which is ADR 032's refusal moved to the place
|
|
119
|
+
this surface begins. Every read the tools make passes the relation it
|
|
120
|
+
returns as `on:`, and frames and panes are read through the same callable
|
|
121
|
+
(`scope.call(Janela::Frame)`). It is an argument and not a setting,
|
|
122
|
+
because it is built where the host knows who is asking, as ADR 034's
|
|
123
|
+
`scope_for` is.
|
|
124
|
+
|
|
125
|
+
**Read tools first, and writes only when asked for.** The 1.0 set:
|
|
126
|
+
|
|
127
|
+
| Tool | Does |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| `describe_vocabulary` | The models, measures, dimensions, renderers and granularities `Janela.definitions` declares, so an agent offers only what can be asked |
|
|
130
|
+
| `list_frames` | Id, name, owner and pane count of the frames the scope returns |
|
|
131
|
+
| `get_frame` | One frame with its panes in position order |
|
|
132
|
+
| `read_pane` | A pane's values, through the host's scope, with `q[...]` filters bounded as ADR 025 bounds them |
|
|
133
|
+
| `add_pane`, `update_pane`, `remove_pane`, `move_pane` | Only with `write: true` |
|
|
134
|
+
|
|
135
|
+
Inputs are the pane's own attributes and the README's own names. There is
|
|
136
|
+
no agent only vocabulary (ADR 010). A rejected row returns the pane's
|
|
137
|
+
validation messages unchanged. A frame is found or made by the host with
|
|
138
|
+
`Frame.for` (ADR 041) or by an analyst on the engine's pages, so there is
|
|
139
|
+
no `create_frame` in 1.0.
|
|
140
|
+
|
|
141
|
+
**A tool definition is data, and an adapter is a few lines the host
|
|
142
|
+
owns.** `Janela::Tools#all` carries what any library needs. The guide
|
|
143
|
+
shows the mapping for the official `mcp` SDK, and a host using another
|
|
144
|
+
writes the same few lines. Janela adds no MCP library to its gemspec.
|
|
145
|
+
|
|
146
|
+
**Snapshots are not in 1.0.** A snapshot persists and publishes, so it
|
|
147
|
+
needs an owner (ADR 033) and a scope the caller has named (ADR 034). That
|
|
148
|
+
is two more decisions an agent should not be making by default, and it
|
|
149
|
+
waits for a host that wants it.
|
|
150
|
+
|
|
151
|
+
**Jan still does not ship.** ADR 010's second condition, that an MCP
|
|
152
|
+
surface exists, is met by this. Its first reason, that a skill loaded
|
|
153
|
+
agent already does the work, has not been tested, because the skill was
|
|
154
|
+
never built. The skill ships first and teaches these tools. An agent
|
|
155
|
+
definition is revisited after a host has used both, and ADR 010 is not
|
|
156
|
+
superseded here.
|
|
157
|
+
|
|
158
|
+
## Consequences
|
|
159
|
+
|
|
160
|
+
- **It ships as 0.13.0, with its own short soak.** This adds public
|
|
161
|
+
surface, and ADR 037 says anything that changes the surface resets the
|
|
162
|
+
soak clock in #71. The earliest 1.0 tag moves by the length of that
|
|
163
|
+
soak. The rule for what 1.0 then contains is set now, before anyone has
|
|
164
|
+
seen the result: if a real host has exercised the write tools and found
|
|
165
|
+
nothing wrong, 1.0 ships with them. If no host has used them by the end
|
|
166
|
+
of the soak, 1.0 ships read only and the write tools wait for 1.1.
|
|
167
|
+
Freezing four schemas that nobody has tried is what a soak is for
|
|
168
|
+
avoiding.
|
|
169
|
+
- **The tool names and input schemas join the contract at 1.0**, so the
|
|
170
|
+
set is small on purpose and every name is a verb and a README noun. A
|
|
171
|
+
tool added after 1.0 is a minor release. A tool changed is a major one.
|
|
172
|
+
- **A host does the registering.** That is a few lines in the place its
|
|
173
|
+
MCP server already lives, and the guide says where. A host with no MCP
|
|
174
|
+
server gets nothing from this, which is the honest answer: Janela does
|
|
175
|
+
not run servers.
|
|
176
|
+
- **The first test is the measured failure.** A tool built with a scope
|
|
177
|
+
that returns `Order.none` reads no values, and tools built without a
|
|
178
|
+
scope raise. Both belong in the suite before the code.
|
|
179
|
+
- **No doctor check.** Construction raises, so there is nothing for the
|
|
180
|
+
doctor to see that the raise does not say (ADR 035).
|
|
181
|
+
- **The pane `order` option (#75) reaches agents for free**, since the
|
|
182
|
+
write tools take a pane's attributes. It does not change this ADR.
|
|
183
|
+
- **What would change this decision:** two hosts writing the same
|
|
184
|
+
adapter by hand, which would earn a shipped one for that library; or a
|
|
185
|
+
host with no MCP server that needs one, which would be the day Janela
|
|
186
|
+
is asked to run something and has to answer ADR 032's question for
|
|
187
|
+
itself.
|
data/docs/decisions/INDEX.md
CHANGED
|
@@ -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
29
|
| **Ordering & formatting** | 007, 020, 038 |
|
|
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 |
|
|
34
34
|
| **Naming rule** | 014, 023, 036 |
|
|
35
35
|
| **JavaScript delivery & charts** | 004, 006, 026, 042, 046, 047 |
|
|
36
36
|
| **Time dimensions** | 006, 025, 045 |
|
|
37
37
|
| **Routes, URLs & naming** | 005, 007, 008, 009, 011, 013, 022, 024, 025, 040, 041, 045, 047, 050, 051 |
|
|
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,8 @@ 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 |
|
|
103
104
|
|
|
104
105
|
## Next number
|
|
105
106
|
|
|
106
|
-
Next ADR:
|
|
107
|
+
Next ADR: 054
|
data/docs/multi-tenancy.md
CHANGED
|
@@ -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
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
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
|
|
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,10 @@ 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
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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, and a chart has a height that suits the page. Those
|
|
137
|
+
were not extra features. They were the 5% not finished, and most of them
|
|
138
|
+
were found by people installing the gem rather than reading it.
|
|
136
139
|
|
|
137
140
|
**The doctor can be trusted.** `rails janela:doctor` checks an
|
|
138
141
|
installation for the mistakes that produce a dashboard showing numbers
|
|
@@ -144,17 +147,20 @@ working is a check nobody should rely on.
|
|
|
144
147
|
|
|
145
148
|
The near ground is clear. Every issue that was in 1.0 has shipped: chart
|
|
146
149
|
height, a ratio measure, single value prominence, companion columns, Sprockets
|
|
147
|
-
hosts, and how the project is run.
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
frozen and 1.0
|
|
150
|
+
hosts, and how the project is run. 0.13.0 is out, and adds tools an agent
|
|
151
|
+
can use (ADR 053). What 1.0 waits on now is a week of real installs ([#71](https://github.com/retail-tasker/janela/issues/71)),
|
|
152
|
+
since much of that surface was added in the last days and has only met this
|
|
153
|
+
repository. When nothing surprising comes back, the surface is frozen and 1.0
|
|
154
|
+
is tagged. The agent tools are the one new piece of surface: the read tools are
|
|
155
|
+
frozen at 1.0, and the write tools are frozen with them only if a real host has
|
|
156
|
+
used them by the end of that week. Otherwise they wait for 1.1.
|
|
151
157
|
|
|
152
158
|
Progress is tracked on the
|
|
153
159
|
[1.0 milestone](https://github.com/retail-tasker/janela/milestone/2).
|
|
154
160
|
|
|
155
161
|
## Horizonte
|
|
156
162
|
|
|
157
|
-
The
|
|
163
|
+
The lights far off at the horizon. Wanted, not blocking a stable
|
|
158
164
|
release, and being on this list is not a refusal.
|
|
159
165
|
|
|
160
166
|
- **Drilling down on time panes** ([#65](https://github.com/retail-tasker/janela/issues/65)).
|
|
@@ -162,6 +168,12 @@ release, and being on this list is not a refusal.
|
|
|
162
168
|
that pane itself to the weeks within it is a different gesture, with
|
|
163
169
|
its own questions about getting back out and about the URL, and it
|
|
164
170
|
needs a decision record before any code.
|
|
171
|
+
- **Telling a frame to refresh** ([#68](https://github.com/retail-tasker/janela/issues/68)).
|
|
172
|
+
One action a host's own timer or push can call, and no timer or stream of
|
|
173
|
+
Janela's own (ADR 048). Additive, so it can land after 1.0.
|
|
174
|
+
- **Vitral's own palette** ([#67](https://github.com/retail-tasker/janela/issues/67)).
|
|
175
|
+
The optional theme setting its own series colours instead of Janela's
|
|
176
|
+
neutral ones.
|
|
165
177
|
- **A command palette for the demo** ([#41](https://github.com/retail-tasker/janela/issues/41)).
|
|
166
178
|
The demo site, not the gem.
|
|
167
179
|
- **A scroll drift on the gallery page** ([#45](https://github.com/retail-tasker/janela/issues/45)).
|
|
@@ -178,7 +190,9 @@ run does it better.
|
|
|
178
190
|
Natural-language query. A separate data warehouse. A row-level-security
|
|
179
191
|
subsystem, because your application already has Pundit or CanCanCan and
|
|
180
192
|
Janela reads through it. A refresh-scheduling interface, because you
|
|
181
|
-
already have a scheduler and snapshots are an ActiveJob
|
|
193
|
+
already have a scheduler and snapshots are an ActiveJob (a way to tell a
|
|
194
|
+
frame to refresh, which your scheduler can call, is
|
|
195
|
+
[#68](https://github.com/retail-tasker/janela/issues/68)). An embedding
|
|
182
196
|
SDK. A mobile application. Print and paginated reports. A drag-and-drop
|
|
183
197
|
visual dashboard designer, though frames and panes are database records,
|
|
184
198
|
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
|
|
125
|
-
|
|
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
|
|
144
|
-
unthemed
|
|
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 =
|
|
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],
|
|
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
|