janela 0.14.0 → 0.16.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 +33 -0
- data/README.md +26 -8
- data/UPGRADING.md +59 -0
- data/app/assets/javascripts/janela/chart_controller.js +148 -42
- data/app/assets/stylesheets/janela.css +7 -0
- data/app/controllers/janela/panes_controller.rb +2 -2
- data/app/controllers/janela/queries_controller.rb +12 -0
- data/app/controllers/janela/snapshot_queries_controller.rb +2 -0
- data/app/helpers/janela/frames_helper.rb +37 -9
- data/app/models/janela/pane.rb +33 -2
- data/app/models/janela/query.rb +252 -27
- data/app/models/janela/snapshot.rb +2 -2
- data/app/views/janela/frames/show.html.erb +2 -2
- data/app/views/janela/panes/_form.html.erb +53 -0
- data/app/views/janela/queries/_query.html.erb +18 -5
- data/config/locales/en.yml +26 -1
- data/db/migrate/20261013000001_add_sort_to_janela_panes.rb +7 -0
- data/db/migrate/20261029000001_add_series_to_janela_panes.rb +9 -0
- data/db/migrate/20261029000003_add_ignore_unknown_filters_to_janela_panes.rb +8 -0
- data/docs/agents.md +33 -2
- data/docs/decisions/018-a-table-is-the-universal-renderer.md +1 -0
- data/docs/decisions/053-an-agent-reaches-janela-through-tools-the-host-scopes.md +1 -0
- data/docs/decisions/054-a-chart-reads-numbers-the-way-its-measure-formats-them.md +1 -1
- data/docs/decisions/055-a-category-pane-can-be-sorted-by-its-measure-either-way-or-by-its-label.md +204 -0
- data/docs/decisions/056-the-demo-counts-its-own-visits-and-an-agent-arranges-the-dashboard.md +51 -0
- data/docs/decisions/057-a-host-is-told-about-the-agent-tools-and-a-generator-sets-them-up.md +197 -0
- data/docs/decisions/058-the-deployed-demo-is-reached-over-ssh-and-its-visits-frame-survives-a-reset.md +94 -0
- data/docs/decisions/059-a-host-can-tell-janelas-own-pages-that-its-layout-draws-charts.md +91 -0
- data/docs/decisions/060-the-demo-tells-search-engines-what-it-is-and-shows-its-own-adoption.md +79 -0
- data/docs/decisions/061-the-demo-speaks-brazilian-portuguese-under-a-prefix-and-the-gem-stays-english.md +118 -0
- data/docs/decisions/062-a-time-pane-can-accumulate-and-carry-more-series-on-two-axes.md +282 -0
- data/docs/decisions/063-a-pane-can-ignore-a-filter-its-model-cannot-apply.md +147 -0
- data/docs/decisions/INDEX.md +25 -16
- data/docs/roadmap.md +10 -4
- data/lib/generators/janela/mcp/mcp_generator.rb +51 -0
- data/lib/generators/janela/mcp/templates/janela-mcp.tt +50 -0
- data/lib/janela/definition.rb +73 -4
- data/lib/janela/dimension.rb +9 -1
- data/lib/janela/doctor.rb +10 -1
- data/lib/janela/measure.rb +8 -0
- data/lib/janela/tools.rb +37 -4
- data/lib/janela/version.rb +1 -1
- data/lib/janela.rb +12 -0
- metadata +22 -8
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
---
|
|
2
|
+
Date: 2026-10-14
|
|
3
|
+
Status: Accepted
|
|
4
|
+
Related: ADR 010, ADR 015, ADR 032, ADR 035, ADR 053, ADR 056
|
|
5
|
+
Triggers:
|
|
6
|
+
- adding an install step, a generator or a rake task that writes into a host
|
|
7
|
+
- changing what janela:doctor prints when nothing is wrong
|
|
8
|
+
- deciding how a host learns that a feature exists
|
|
9
|
+
- registering Janela::Tools with an MCP library
|
|
10
|
+
- an agent needs to change a frame's columns or gap
|
|
11
|
+
- an agent needs to reach a dashboard on a deployed host
|
|
12
|
+
Topics: ai, agents, mcp, install, dx, generators, doctor, host-integration, discoverability
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# ADR 057: A Host Is Told About the Agent Tools, and a Generator Sets Them Up
|
|
16
|
+
|
|
17
|
+
## Context
|
|
18
|
+
|
|
19
|
+
ADR 053 gave Janela agent tools and deliberately stopped short of helping a
|
|
20
|
+
host use them. It rejected a `janela:install:mcp` task because copied files
|
|
21
|
+
drift, rejected detecting a host's MCP setup because the signal does not
|
|
22
|
+
distinguish anything, and added no doctor check because construction already
|
|
23
|
+
raises. A host that wants the tools reads `docs/agents.md` and writes about
|
|
24
|
+
ten lines.
|
|
25
|
+
|
|
26
|
+
That left one thing unconsidered: **how a host learns the tools exist.** The
|
|
27
|
+
guide is a document the host has to go looking for, so a host that does not
|
|
28
|
+
know to look never finds out. The demo showed it. Its `/visits` dashboard was
|
|
29
|
+
arranged through `Janela::Tools` by an agent over stdio, using
|
|
30
|
+
`test/dummy/bin/janela-mcp`, and the maintainer's reaction to being told how it
|
|
31
|
+
was done was that he had assumed a setup tool already existed, and that anyone
|
|
32
|
+
using the library should not have to ask for one. Nothing in the install path,
|
|
33
|
+
the doctor's output or the README's install section mentions the tools at all.
|
|
34
|
+
The feature is real and, to a new host, invisible.
|
|
35
|
+
|
|
36
|
+
### Measured
|
|
37
|
+
|
|
38
|
+
Against the demo, 2026-10-14.
|
|
39
|
+
|
|
40
|
+
**A generator in the gem is found and ships.** A file at
|
|
41
|
+
`lib/generators/janela/mcp/mcp_generator.rb` is listed by
|
|
42
|
+
`bin/rails generate --help` under `Janela:` and runs as
|
|
43
|
+
`bin/rails generate janela:mcp`. The gemspec's `lib/**/*` glob already packages
|
|
44
|
+
it. Janela has no generators today, so this would be the first.
|
|
45
|
+
|
|
46
|
+
**A server file cannot borrow the host's `policy_scope`.** It is a controller
|
|
47
|
+
method, and a stdio server has no request. Called on a bare controller it does
|
|
48
|
+
not raise, which would be a safe failure. It answers a different question:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
policy_scope(Janela::Frame) from inside a request: owner is the first customer
|
|
52
|
+
policy_scope(Janela::Frame) with no request: WHERE owner_id IS NULL
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
ADR 035 found the same shape for the doctor, where a real host's
|
|
56
|
+
`policy_scope` reaches for the session, which is nil outside a request. So the
|
|
57
|
+
scope has to be written into the server file by the host, who knows who an
|
|
58
|
+
agent acts for. `Janela::Tools.new` with no scope still raises `Janela::Unscoped`
|
|
59
|
+
(ADR 053), so a generated file that leaves the scope unwritten fails loudly
|
|
60
|
+
and does not read the wrong rows.
|
|
61
|
+
|
|
62
|
+
**The demo's server file is the first copy of the thing.** It is 25 lines, of
|
|
63
|
+
which three are the host's: who is asking, what the scope is, and which
|
|
64
|
+
environment it may run in. The rest is `docs/agents.md`'s adapter, unchanged.
|
|
65
|
+
|
|
66
|
+
### What was considered
|
|
67
|
+
|
|
68
|
+
**Keep ADR 053 as it is and improve the guide.** Rejected. The guide is not
|
|
69
|
+
the problem, finding it is. A better page nobody opens is the same failure.
|
|
70
|
+
|
|
71
|
+
**A doctor finding.** Rejected as a finding. ADR 035 says a check reads what
|
|
72
|
+
Janela does or says what it saw, and the doctor's contract is "what to fix".
|
|
73
|
+
The tools are optional, so their absence is not something to fix, and a
|
|
74
|
+
warning would be wrong for the host that has deliberately chosen not to use
|
|
75
|
+
them. It also could not be silenced without teaching hosts to silence the
|
|
76
|
+
doctor. See the decision for what the doctor says instead.
|
|
77
|
+
|
|
78
|
+
**A `post_install_message` for the tools.** Rejected as the main route. ADR 015
|
|
79
|
+
sets it only on a release a host must act on and removes it in the next, so it
|
|
80
|
+
reaches a host once, at the moment it is least interested. It is used for the
|
|
81
|
+
release that ships the generator and no other.
|
|
82
|
+
|
|
83
|
+
**Detect the host's MCP server and register into it.** Still rejected, for
|
|
84
|
+
ADR 053's reason: it edits a host's tooling on a signal that distinguishes
|
|
85
|
+
nothing.
|
|
86
|
+
|
|
87
|
+
**A generator that borrows the host's scope by calling `policy_scope`.**
|
|
88
|
+
Rejected on the measurement above. It would silently read the wrong rows.
|
|
89
|
+
|
|
90
|
+
**`mcp` as a dependency, added to the host's Gemfile by the generator.**
|
|
91
|
+
Rejected. The generator prints the line to add and never edits the Gemfile,
|
|
92
|
+
which keeps ADR 053's "Janela depends on no MCP library" and ADR 010's rule that
|
|
93
|
+
Janela edits nothing it did not create.
|
|
94
|
+
|
|
95
|
+
## Decision
|
|
96
|
+
|
|
97
|
+
**A host is told the agent tools exist in the places it already looks, and a
|
|
98
|
+
generator writes the server file the host then owns.**
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
bin/rails generate janela:mcp
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
It writes two things and edits nothing else:
|
|
105
|
+
|
|
106
|
+
- `bin/janela-mcp`, an executable stdio server built on the official Ruby
|
|
107
|
+
SDK. It is `docs/agents.md`'s adapter with the host's three decisions left
|
|
108
|
+
as plain code to fill in. The scope is a lambda that **raises
|
|
109
|
+
`Janela::Unscoped` until the host replaces it**, so an unedited file reads
|
|
110
|
+
nothing. It refuses to run outside development, with the guard in the file
|
|
111
|
+
where the host can see it, because removing it is a decision about who can
|
|
112
|
+
reach real data and should be made on purpose.
|
|
113
|
+
- An entry in `.mcp.json`, created if it is absent and merged if it is not, so
|
|
114
|
+
Claude Code and any client that reads the same file find the server.
|
|
115
|
+
|
|
116
|
+
If the `mcp` gem is not in the bundle the generator says which line to add to
|
|
117
|
+
the Gemfile and does not add it. If `bin/janela-mcp` already exists it is left
|
|
118
|
+
alone and the generator says so.
|
|
119
|
+
|
|
120
|
+
**The file is the host's from the moment it is written.** This is the case
|
|
121
|
+
ADR 053 rejected, and it is accepted now for a reason that did not apply then:
|
|
122
|
+
the file is the adapter itself and nothing else. It calls `Janela::Tools.all`
|
|
123
|
+
and `Janela::Tools#call`, which are the public contract from 1.0, so there is
|
|
124
|
+
nothing for it to drift from except a change that would already be a major
|
|
125
|
+
release. The doctor does not grow a check for it.
|
|
126
|
+
|
|
127
|
+
**Discovery, in the order a new host meets it:**
|
|
128
|
+
|
|
129
|
+
1. **The README's install steps** gain one line after the doctor step: that
|
|
130
|
+
agents can arrange dashboards, the command, and a link to `docs/agents.md`.
|
|
131
|
+
It stays in the README permanently, since a host installing next year needs
|
|
132
|
+
it as much as one installing today.
|
|
133
|
+
2. **`janela:doctor` prints a closing line, not a finding**, when
|
|
134
|
+
`bin/janela-mcp` does not exist: `Optional: let an agent arrange
|
|
135
|
+
dashboards. bin/rails generate janela:mcp (docs/agents.md).` It states what
|
|
136
|
+
it saw, which is the absence of the file. It does not change the exit
|
|
137
|
+
status, is not counted in "things to fix" and needs no silencing. It is
|
|
138
|
+
printed on a clean run too, so the host that has nothing to fix still hears
|
|
139
|
+
it, and a host that has generated the file stops hearing it.
|
|
140
|
+
3. **The gem's `post_install_message`** carries it once, on the release that
|
|
141
|
+
ships the generator, and goes in the release after (ADR 015).
|
|
142
|
+
4. **The generator's own output** says what to fill in and how to start the
|
|
143
|
+
server, so the first run ends with a working next step.
|
|
144
|
+
|
|
145
|
+
**An agent can change a frame's columns and gap.** ADR 053 gave agents no
|
|
146
|
+
frame-level tool because "a frame is found or made by the host". Arranging
|
|
147
|
+
`/visits` showed that a layout is not finished without them: the demo's frame
|
|
148
|
+
went from three columns to four and could only be changed from a console. One
|
|
149
|
+
tool is added, and only with `write: true`:
|
|
150
|
+
|
|
151
|
+
| Tool | Does |
|
|
152
|
+
| --- | --- |
|
|
153
|
+
| `update_frame` | Change a frame's `columns` and `gap`. Found through the host's scope like every other frame, and refused with the frame's own reason (`Columns is not included in the list`) when the value is not one Janela draws |
|
|
154
|
+
|
|
155
|
+
Its inputs are the frame's own attributes and nothing else, so a `name`, an
|
|
156
|
+
`owner` or a `key` is refused and not dropped, as a pane's are. A frame is
|
|
157
|
+
still found or made by the host: there is no `create_frame`, and a frame's
|
|
158
|
+
owner is the host's to say (ADR 033). This is a new tool, so it is a minor
|
|
159
|
+
release, and its name and schema join the contract at 1.0 with the rest.
|
|
160
|
+
|
|
161
|
+
**A deployed host's dashboard stays out of an agent's reach, and that is
|
|
162
|
+
named, not solved.** The generated server runs in development against a local
|
|
163
|
+
database, so a change made through it is not a change to a live site. Reaching
|
|
164
|
+
a deployed host needs an authenticated path that only the host can provide
|
|
165
|
+
(ADR 053: Janela serves no endpoint and does not know who is calling). The
|
|
166
|
+
guide says so, so it is a known limit and not a surprise.
|
|
167
|
+
|
|
168
|
+
## Consequences
|
|
169
|
+
|
|
170
|
+
- **A new host hears about the tools without asking**, in its install steps,
|
|
171
|
+
from the doctor it was told to run, and from the generator's output. A host
|
|
172
|
+
that does not want them has nothing to silence.
|
|
173
|
+
- **This is the first generator Janela ships**, and ADR 010 lists generators
|
|
174
|
+
as something that needs a decision. This is that decision, scoped to one
|
|
175
|
+
generator that writes a file and a config entry.
|
|
176
|
+
- **The doctor's contract widens by one printed line.** It says something on a
|
|
177
|
+
clean run for the first time. That is accepted because the line is not a
|
|
178
|
+
finding and cannot change the exit status or the count.
|
|
179
|
+
- **ADR 053 is superseded in part**: its rejection of an install task and its
|
|
180
|
+
statement that no doctor output is added. Its tool surface, its required
|
|
181
|
+
scope and its rule that Janela registers nothing on a host's behalf stand.
|
|
182
|
+
- **A host that already wrote its own adapter loses nothing.** The generator
|
|
183
|
+
leaves an existing `bin/janela-mcp` alone, and the doctor line stops once it
|
|
184
|
+
exists.
|
|
185
|
+
- **A scope left as written raises.** The cost is that the first run needs one
|
|
186
|
+
edit, which is the point.
|
|
187
|
+
- **The write tools are five, not four.** ADR 053's table gains `update_frame`,
|
|
188
|
+
and its rule that the write tools are off until asked for applies to it.
|
|
189
|
+
- **Tests first:** the generator writes both files into a scratch host and
|
|
190
|
+
leaves an existing one alone; the unedited server file raises
|
|
191
|
+
`Janela::Unscoped` rather than reading; the doctor's closing line appears
|
|
192
|
+
without the file, is absent with it, and never changes `check` or the exit
|
|
193
|
+
status; and `docs/agents.md`'s adapter and the generated file are asserted to
|
|
194
|
+
match so they cannot drift (ADR 010).
|
|
195
|
+
- **What would change this decision:** a second MCP library with enough hosts
|
|
196
|
+
to want its own generator option, or the doctor line turning out to be
|
|
197
|
+
noise, in which case it moves to the README alone.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
Date: 2026-10-14
|
|
3
|
+
Status: Accepted
|
|
4
|
+
Related: ADR 053, ADR 056, ADR 057
|
|
5
|
+
Triggers:
|
|
6
|
+
- an agent needs to change a dashboard on the deployed demo
|
|
7
|
+
- adding an endpoint to the demo that writes
|
|
8
|
+
- changing demo:reset, or what the demo keeps through it
|
|
9
|
+
- changing which group the mcp gem is in
|
|
10
|
+
Topics: demo, agents, mcp, deployment, security, dogfooding
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# ADR 058: The Deployed Demo Is Reached Over SSH, and Its Visits Frame Survives a Reset
|
|
14
|
+
|
|
15
|
+
## Context
|
|
16
|
+
|
|
17
|
+
ADR 057 left one gap named and unsolved: a server run from `bin/janela-mcp`
|
|
18
|
+
changes the database of the environment it starts in, so an agent could
|
|
19
|
+
arrange `/visits` locally and not on the live demo. The maintainer's reading
|
|
20
|
+
was that talking to an agent should change the dashboard people see.
|
|
21
|
+
|
|
22
|
+
### Measured
|
|
23
|
+
|
|
24
|
+
Against the deployed demo, 2026-10-14.
|
|
25
|
+
|
|
26
|
+
**The production image has no `mcp` gem.** It is built with
|
|
27
|
+
`BUNDLE_WITHOUT="development test"`, and `mcp` was in the development and
|
|
28
|
+
test group, so `bundle list` on the running container shows Janela and not
|
|
29
|
+
`mcp`. The server could not have started there.
|
|
30
|
+
|
|
31
|
+
**Production logs to stdout.** `config.logger` writes to `$stdout`, which for
|
|
32
|
+
this server is the protocol stream, so a single log line would corrupt it.
|
|
33
|
+
Run in production mode at debug level against a scratch database, with
|
|
34
|
+
`$stdout` pointed at stderr during boot, stdout carried only valid JSON-RPC
|
|
35
|
+
and every log line went to stderr.
|
|
36
|
+
|
|
37
|
+
**The hourly reset deletes every frame.** `demo:reset` runs `Frame.delete_all`
|
|
38
|
+
and reseeds, so a change made to "Site visits" would be gone within the hour.
|
|
39
|
+
Its owner is also a customer the reset deletes and recreates under a new id.
|
|
40
|
+
|
|
41
|
+
**SSH reaches the box.** `docker exec -i` into the running container is a
|
|
42
|
+
clean stdio pipe with no terminal allocated.
|
|
43
|
+
|
|
44
|
+
### What was considered
|
|
45
|
+
|
|
46
|
+
**An authenticated HTTP endpoint on the demo.** Rejected. It would put a write
|
|
47
|
+
path to a public site behind a token, which is a new surface to secure and
|
|
48
|
+
rotate, for a dashboard one maintainer arranges. ADR 053 says Janela serves
|
|
49
|
+
nothing and a host that wants an endpoint owns its authentication. The demo is
|
|
50
|
+
a host, and it does not need one.
|
|
51
|
+
|
|
52
|
+
**Letting the reset run over the frame.** Rejected. It makes a live change
|
|
53
|
+
impossible to keep.
|
|
54
|
+
|
|
55
|
+
## Decision
|
|
56
|
+
|
|
57
|
+
**The deployed demo is reached by running the same server on the box over SSH,
|
|
58
|
+
and the authentication is the SSH key.** `bin/janela-mcp` runs in production
|
|
59
|
+
only when `JANELA_MCP_PRODUCTION=1` is set, which the SSH command sets and
|
|
60
|
+
nothing else does. There is no endpoint, no token and no new login.
|
|
61
|
+
|
|
62
|
+
**`mcp` moves out of the development and test group** so it is in the
|
|
63
|
+
production bundle. It stays out of the gemspec (ADR 053).
|
|
64
|
+
|
|
65
|
+
**The server points `$stdout` at stderr while Rails boots**, and gives it back
|
|
66
|
+
to the transport afterwards, so production's stdout logger cannot corrupt the
|
|
67
|
+
stream.
|
|
68
|
+
|
|
69
|
+
**`demo:reset` keeps the visits frame.** Every other frame is still put back.
|
|
70
|
+
The frame is handed to the new first customer after the reseed, because its
|
|
71
|
+
owner was deleted. Page views were already left alone (ADR 056).
|
|
72
|
+
|
|
73
|
+
## Consequences
|
|
74
|
+
|
|
75
|
+
- **A change made through the tools reaches the live dashboard** and stays
|
|
76
|
+
there. The seeds hold the first design and the live frame is the one that
|
|
77
|
+
evolves, so the seeds no longer describe it once it has been changed.
|
|
78
|
+
- **Keeping one frame broke two assumptions the demo made, found on the live
|
|
79
|
+
site an hour after the deploy.** The seeds created the original frames only
|
|
80
|
+
when there were no frames at all, and the orders page linked to whichever
|
|
81
|
+
frame had the lowest id. With the visits frame kept, the first never ran and
|
|
82
|
+
the second picked the wrong one, so the live `/dashboards` lost two frames.
|
|
83
|
+
Each frame is now guarded by its own name and the page finds its frame by
|
|
84
|
+
name. The reset test now asserts every original frame comes back, since it
|
|
85
|
+
had only checked one that happened to have its own guard.
|
|
86
|
+
- **The reach is exactly the people with SSH to the box**, which is the people
|
|
87
|
+
who can already change the demo.
|
|
88
|
+
- **The connection is not committed.** The SSH entry goes in a developer's own
|
|
89
|
+
local MCP config, because a contributor without access would see a server
|
|
90
|
+
that fails.
|
|
91
|
+
- **The demo image carries one more gem**, with no endpoint that uses it.
|
|
92
|
+
- **What would change this decision:** a second person needing to arrange the
|
|
93
|
+
demo without SSH, which is the day an authenticated endpoint is worth its
|
|
94
|
+
cost.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
Date: 2026-10-14
|
|
3
|
+
Status: Proposed
|
|
4
|
+
Related: ADR 011, ADR 018, ADR 023, ADR 046
|
|
5
|
+
Triggers:
|
|
6
|
+
- a chart pane renders as a table on one of the engine's own pages
|
|
7
|
+
- changing what the engine's frame page passes to janela_frame
|
|
8
|
+
- adding a setting that says what a host's layout loads
|
|
9
|
+
- a host gives Janela's own pages its own layout
|
|
10
|
+
Topics: rendering, charts, layouts, engines, host-integration, dogfooding, settings
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# ADR 059: A Host Can Tell Janela's Own Pages That Its Layout Draws Charts
|
|
14
|
+
|
|
15
|
+
## Context
|
|
16
|
+
|
|
17
|
+
The demo's Site visits dashboard had bar charts, and the page people reach
|
|
18
|
+
from the Dashboards list showed them as tables. That page is the engine's own
|
|
19
|
+
frame page, which passes `charts: false` to `janela_frame` because its minimal
|
|
20
|
+
layout loads no chart runtime (ADR 018). The maintainer looked at it three
|
|
21
|
+
times and called it trash each time. The data was right and the page was
|
|
22
|
+
doing exactly what ADR 018 says, which is the trouble: a decision written for
|
|
23
|
+
a host with no layout of its own was applied to a host that has one.
|
|
24
|
+
|
|
25
|
+
ADR 018 closed with its own condition: "If the engine ever gains a way to
|
|
26
|
+
know a host's asset setup, this decision is worth revisiting, because the
|
|
27
|
+
better answer is a chart that draws everywhere."
|
|
28
|
+
|
|
29
|
+
### Measured
|
|
30
|
+
|
|
31
|
+
Against the live demo, 2026-10-14.
|
|
32
|
+
|
|
33
|
+
**The demo gives the engine's frame page its own layout, and that layout
|
|
34
|
+
loads the runtime.** `Janela::FramesController.layout` is overridden (ADR 011,
|
|
35
|
+
ADR 022), the page carries the `janela--frame` Stimulus controller and has no
|
|
36
|
+
console errors, and `/visits`, which renders the same frame through the host's
|
|
37
|
+
own page, draws both bar charts. The only difference between the two pages is
|
|
38
|
+
the `charts: false` in the engine's view.
|
|
39
|
+
|
|
40
|
+
**The engine cannot tell.** Whether a layout loads Chart.js is a fact about the
|
|
41
|
+
host's bundler and importmap. A layout other than Janela's own may load
|
|
42
|
+
nothing, and Janela cannot read it.
|
|
43
|
+
|
|
44
|
+
### What was considered
|
|
45
|
+
|
|
46
|
+
**Detect a host layout and assume it draws charts.** Rejected for ADR 018's
|
|
47
|
+
reason: it would guess. A host layout that loads Turbo and no Chart.js would
|
|
48
|
+
get empty holes where tables were, which is the failure ADR 018 was written
|
|
49
|
+
to remove.
|
|
50
|
+
|
|
51
|
+
**Detect JavaScript at runtime and swap.** Rejected. The page is meant to be
|
|
52
|
+
correct before any JavaScript, and a swap would flash a table first.
|
|
53
|
+
|
|
54
|
+
**Always draw charts on the engine's pages.** Rejected. The default install,
|
|
55
|
+
with Janela's own minimal layout, has no runtime.
|
|
56
|
+
|
|
57
|
+
## Decision
|
|
58
|
+
|
|
59
|
+
**A host says so, once.**
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
# config/initializers/janela.rb
|
|
63
|
+
Janela.charts_on_own_pages = true
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
It defaults to `false`, so a host that sets nothing renders exactly what it
|
|
67
|
+
does today. When it is `true` the engine's frame page passes `charts: true`,
|
|
68
|
+
and a bar or line pane draws. It is a setting and not a convention because it
|
|
69
|
+
is the one thing the engine cannot see and cannot guess, which is what ADR 021
|
|
70
|
+
says a setting is for. A host turns it on when it has given Janela's own pages
|
|
71
|
+
a layout that registers the Stimulus controllers and loads Chart.js, and not
|
|
72
|
+
before.
|
|
73
|
+
|
|
74
|
+
The demo sets it, because its layout is such a layout.
|
|
75
|
+
|
|
76
|
+
## Consequences
|
|
77
|
+
|
|
78
|
+
- **Nothing changes for an existing host.** The default is ADR 018's behaviour.
|
|
79
|
+
- **A host that sets it without the runtime gets empty chart holes** on the
|
|
80
|
+
engine's pages. The README says what the setting promises, and the doctor
|
|
81
|
+
does not check it, because what a layout loads is not something it can read
|
|
82
|
+
(ADR 035).
|
|
83
|
+
- **ADR 018 is superseded in part**: the engine's frame page is no longer
|
|
84
|
+
always table-only. Its rule for a page with no runtime stands.
|
|
85
|
+
- **The pane page opened directly is unchanged.** It is not given a setting
|
|
86
|
+
here, and a host that wants a chart there still uses a layout and a frame of
|
|
87
|
+
its own, as README says. This covers the frame page, which is where the
|
|
88
|
+
Dashboards list leads.
|
|
89
|
+
- **What would change this decision:** the engine gaining a reliable way to
|
|
90
|
+
read a host's asset setup, at which point the setting is removed and the
|
|
91
|
+
chart simply draws.
|
data/docs/decisions/060-the-demo-tells-search-engines-what-it-is-and-shows-its-own-adoption.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
Date: 2026-10-14
|
|
3
|
+
Status: Proposed
|
|
4
|
+
Related: ADR 010, ADR 052, ADR 056
|
|
5
|
+
Triggers:
|
|
6
|
+
- changing what the demo tells a search engine or a link preview
|
|
7
|
+
- adding a call from the demo to a service outside it
|
|
8
|
+
- showing a number in the demo that the demo did not count itself
|
|
9
|
+
- adding a page to the demo that should or should not be indexed
|
|
10
|
+
Topics: demo, seo, marketing, privacy, dependencies, dogfooding
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# ADR 060: The Demo Tells Search Engines What It Is, and Shows Its Own Adoption
|
|
14
|
+
|
|
15
|
+
## Context
|
|
16
|
+
|
|
17
|
+
The demo is Janela's front door, and a search for it is how most hosts will meet
|
|
18
|
+
the gem. The maintainer wants to measure how well it is found, and the first
|
|
19
|
+
thing that measuring needs is that there is something to find.
|
|
20
|
+
|
|
21
|
+
### Measured
|
|
22
|
+
|
|
23
|
+
Against the live demo, 2026-10-14.
|
|
24
|
+
|
|
25
|
+
| Thing | State |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| `/robots.txt`, `/sitemap.xml` | both 404 |
|
|
28
|
+
| meta description, canonical link, Open Graph tags | none on any page |
|
|
29
|
+
| `lang` on the `html` element | absent |
|
|
30
|
+
| `<h1>` on `/name` | none |
|
|
31
|
+
| titles | one per page, though `Gallery` and `Vitral` say nothing a search result could use |
|
|
32
|
+
| RubyGems | the gem answers `api/v1/gems/janela.json`: 8,315 downloads, 147 on the current version |
|
|
33
|
+
| GitHub | the repository answers `api.github.com/repos/...`: 1 star |
|
|
34
|
+
|
|
35
|
+
The Dashboards, snapshot and frame pages are the engine's own, and they take any
|
|
36
|
+
filter as a query string, so a crawler that follows their links meets an
|
|
37
|
+
unbounded number of URLs that all show the same page.
|
|
38
|
+
|
|
39
|
+
## Decision
|
|
40
|
+
|
|
41
|
+
**The demo says what each page is, in the head of the page, and keeps crawlers
|
|
42
|
+
off the pages that are only filters.**
|
|
43
|
+
|
|
44
|
+
- Every page has a `lang`, a title, a meta description, a canonical link to its
|
|
45
|
+
own path with no query string, and Open Graph and Twitter tags. A page with
|
|
46
|
+
nothing of its own to say gets the site's description rather than none.
|
|
47
|
+
- A documentation page's description is its first paragraph, read from the
|
|
48
|
+
document, so it cannot drift from what ships in the gem (ADR 010).
|
|
49
|
+
- `robots.txt` names the sitemap and disallows `/dashboards`, `/snapshots` and
|
|
50
|
+
`/frames`, the pages that are one dashboard under unbounded filters.
|
|
51
|
+
`sitemap.xml` lists the pages worth finding: the home page, the guides, the
|
|
52
|
+
decisions, the gallery, the name, the example and the visits page. One
|
|
53
|
+
mechanism for keeping a page out, and it is the robots file.
|
|
54
|
+
|
|
55
|
+
**The footer shows RubyGems downloads and GitHub stars, fetched by the server
|
|
56
|
+
and cached for an hour.** They link to the gem's page and the repository. A
|
|
57
|
+
failed fetch shows the plain links as before and is not retried for five
|
|
58
|
+
minutes, so a service being down costs the page nothing and the demo does not
|
|
59
|
+
hammer it. The numbers are fetched, not stored: Janela owns no data store, and a
|
|
60
|
+
count that is only ever read from its source cannot disagree with it.
|
|
61
|
+
|
|
62
|
+
**Nothing is fetched in the test suite.** The footer is on every page, so a call
|
|
63
|
+
from it would put the network in every test. It is switched on in development
|
|
64
|
+
and production, and a test that wants it says so.
|
|
65
|
+
|
|
66
|
+
## Consequences
|
|
67
|
+
|
|
68
|
+
- **The demo makes two outbound calls**, at most one pair an hour. They carry no
|
|
69
|
+
information about a visitor, because the server makes them and not the
|
|
70
|
+
browser, so no visitor's address reaches either service.
|
|
71
|
+
- **A visitor's first view after the cache expires waits for the fetch**, up to
|
|
72
|
+
the timeout. That is accepted for a demo, and the stale-while-refresh answer
|
|
73
|
+
is for when it is not.
|
|
74
|
+
- **This is the baseline, not the measuring.** Search Console and page speed
|
|
75
|
+
data, a referrer on the visits recorder and the adoption numbers as stored
|
|
76
|
+
history are separate decisions, and the first of them needs `PageView` to
|
|
77
|
+
change, which ADR 056 keeps cookie free.
|
|
78
|
+
- **What would change this decision:** the counts being wanted as history, which
|
|
79
|
+
is the day they stop being fetched and become rows.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
Date: 2026-10-14
|
|
3
|
+
Status: Proposed
|
|
4
|
+
Related: ADR 023, ADR 036, ADR 054, ADR 056, ADR 060
|
|
5
|
+
Triggers:
|
|
6
|
+
- adding a language to the demo, or a page that is translated
|
|
7
|
+
- generating a route helper for a page under the /pt-BR prefix
|
|
8
|
+
- deciding whether the gem should ship a second language
|
|
9
|
+
- mounting the engine inside a scope with a dynamic segment
|
|
10
|
+
- writing a month, a number or a date label in a chart
|
|
11
|
+
Topics: i18n, demo, routing, seo, engines, host-integration, dogfooding
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# ADR 061: The Demo Speaks Brazilian Portuguese Under a Prefix, and the Gem Stays English
|
|
15
|
+
|
|
16
|
+
## Context
|
|
17
|
+
|
|
18
|
+
Janela is the Portuguese word for window, and the demo has never said so in
|
|
19
|
+
Portuguese. The maintainer wants a language picker and Brazilian Portuguese
|
|
20
|
+
(`pt-BR`) as a homage to the name. Two questions decide the shape: whether the
|
|
21
|
+
gem or only the demo carries the language, and where the language lives in the
|
|
22
|
+
address.
|
|
23
|
+
|
|
24
|
+
The README already tells a host to override Janela's words in its own locale
|
|
25
|
+
file, and every string the gem shows is one `en.yml` of 63 keys under `janela.*`.
|
|
26
|
+
|
|
27
|
+
### Measured
|
|
28
|
+
|
|
29
|
+
Against the demo, 2026-10-14.
|
|
30
|
+
|
|
31
|
+
**The language can be a prefix, including for the mounted engine.** With the
|
|
32
|
+
whole demo inside `scope "(:locale)", locale: /pt-BR/` and the engine mounted
|
|
33
|
+
inside it, `/pt-BR/dashboards` serves the engine's index, its frame page and its
|
|
34
|
+
editor. Every `href`, `action`, `data-` URL and Turbo Frame `src` on those pages
|
|
35
|
+
carries `/pt-BR/`, so cross-filtering stays in the language. `/fr/orders` is a
|
|
36
|
+
404, because the constraint admits only the language we have.
|
|
37
|
+
|
|
38
|
+
**A positional route argument fills the locale slot.** With an optional leading
|
|
39
|
+
segment, `doc_path("agents")` generated `locale: "agents"` and then failed for
|
|
40
|
+
want of a slug, on every English page, until the controller's
|
|
41
|
+
`default_url_options` always carried a `locale` key, nil for English. The same
|
|
42
|
+
call in a test outside a request has no controller, so the route set carries
|
|
43
|
+
`locale: nil`, which an integration test merges, and the system tests' base class
|
|
44
|
+
says it for their own helper object.
|
|
45
|
+
|
|
46
|
+
**The engine repeated the language as a query parameter.** Its own links came out
|
|
47
|
+
as `/pt-BR/dashboards/684?locale=pt-BR`: the mount point already carries the
|
|
48
|
+
language in its path, so a `locale` option has nowhere to go but the query
|
|
49
|
+
string. `_routes` inside `url_options` says which route set is being generated
|
|
50
|
+
for, so the host drops `locale` for the engine's routes and keeps it for its own.
|
|
51
|
+
|
|
52
|
+
**A month label was English only.** `Dimension::LABELS["month"]` used
|
|
53
|
+
`strftime("%b %Y")`, which does not read a locale. A clicked label maps back to
|
|
54
|
+
its bucket through a lookup built from the labels (`Query`), not by parsing the
|
|
55
|
+
text, so a translated label cannot break a time click.
|
|
56
|
+
|
|
57
|
+
**Numbers already follow the locale.** The measure reads `number.format` for its
|
|
58
|
+
delimiter and `number_to_rounded` for the separator, so with Rails' own Portuguese
|
|
59
|
+
data `$375.00` reads `$375,00`.
|
|
60
|
+
|
|
61
|
+
### What was considered
|
|
62
|
+
|
|
63
|
+
**The gem ships `pt-BR`.** Rejected for now, by the maintainer. A second language
|
|
64
|
+
in the gem is upkeep for every release, and the demo adding it in its own locale
|
|
65
|
+
file is the worked example of how any host does.
|
|
66
|
+
|
|
67
|
+
**A cookie or the `Accept-Language` header.** Rejected. Neither is in the
|
|
68
|
+
address, so a link cannot carry the language, a search engine cannot see the
|
|
69
|
+
second page, and the visits page promises no cookie.
|
|
70
|
+
|
|
71
|
+
**A subdomain.** Rejected. It is a second site to certify and deploy for one
|
|
72
|
+
language.
|
|
73
|
+
|
|
74
|
+
**Translating every page.** Rejected as a first step. The guides and decisions are
|
|
75
|
+
the gem's own markdown, rendered as they ship (ADR 010), and are English.
|
|
76
|
+
|
|
77
|
+
## Decision
|
|
78
|
+
|
|
79
|
+
**English is the root and Brazilian Portuguese is under `/pt-BR`, one address per
|
|
80
|
+
language. The gem ships English only, and the demo adds Portuguese in its own
|
|
81
|
+
locale files.**
|
|
82
|
+
|
|
83
|
+
- The pages translated are the home page, the orders dashboard, the visits page,
|
|
84
|
+
the menu, the footer and every string Janela's own pages show. A page that is
|
|
85
|
+
not translated is still reachable under `/pt-BR`, says so at the top, and
|
|
86
|
+
names the English page as its canonical address, so there is never a second
|
|
87
|
+
copy of English text competing with the first.
|
|
88
|
+
- A translated page carries `hreflang` alternates for both languages and
|
|
89
|
+
`x-default`. The sitemap lists the translated pages in both. `robots.txt`
|
|
90
|
+
keeps the Portuguese dashboards out as it does the English ones (ADR 060).
|
|
91
|
+
- The picker is a link in the menu, named in its own language, that goes to the
|
|
92
|
+
same page in the other language and keeps its filter.
|
|
93
|
+
- Brazilian Portuguese for Rails' own words (month names, number formats) is the
|
|
94
|
+
`rails-i18n` gem, in the demo's Gemfile and not the gem's.
|
|
95
|
+
- **The gem's month label reads the locale** when the host has month names for it,
|
|
96
|
+
and falls back to English when it has not, so a host that never set a locale
|
|
97
|
+
sees exactly what it did. This is the one change to the gem.
|
|
98
|
+
- A frame is `quadro` and a pane is `painel`, the words a Brazilian reader of a
|
|
99
|
+
dashboard already uses, and not the window words (`esquadria`, `vidraça`) that
|
|
100
|
+
are prettier and mean nothing to someone looking for a chart.
|
|
101
|
+
|
|
102
|
+
## Consequences
|
|
103
|
+
|
|
104
|
+
- **The Portuguese is written by an assistant and has not been read by a native
|
|
105
|
+
speaker.** It should be before this is shown to people. It is a draft to be
|
|
106
|
+
corrected, and the copy is in two files, so correcting it is an edit.
|
|
107
|
+
- **Every `Rails.application.routes` helper in the demo is under an optional
|
|
108
|
+
segment.** A new helper called with a positional argument outside a controller
|
|
109
|
+
needs `locale: nil` or a named argument. The route set and the system test
|
|
110
|
+
base class carry the former.
|
|
111
|
+
- **A missing key fails a test.** Test runs raise on a missing translation, and a
|
|
112
|
+
test compares the Portuguese files with the English ones, because fallbacks
|
|
113
|
+
would otherwise hide a gap behind English text.
|
|
114
|
+
- **The visits dashboard records Portuguese pages at their own path**, so the
|
|
115
|
+
language a visitor read is visible in the data the demo already shows.
|
|
116
|
+
- **What would change this decision:** a host asking for Portuguese from the
|
|
117
|
+
gem, which is the day `config/locales/pt-BR.yml` moves from the demo into the
|
|
118
|
+
gem and `janela.pt-BR.yml` here is deleted.
|