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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -0
  3. data/README.md +26 -8
  4. data/UPGRADING.md +59 -0
  5. data/app/assets/javascripts/janela/chart_controller.js +148 -42
  6. data/app/assets/stylesheets/janela.css +7 -0
  7. data/app/controllers/janela/panes_controller.rb +2 -2
  8. data/app/controllers/janela/queries_controller.rb +12 -0
  9. data/app/controllers/janela/snapshot_queries_controller.rb +2 -0
  10. data/app/helpers/janela/frames_helper.rb +37 -9
  11. data/app/models/janela/pane.rb +33 -2
  12. data/app/models/janela/query.rb +252 -27
  13. data/app/models/janela/snapshot.rb +2 -2
  14. data/app/views/janela/frames/show.html.erb +2 -2
  15. data/app/views/janela/panes/_form.html.erb +53 -0
  16. data/app/views/janela/queries/_query.html.erb +18 -5
  17. data/config/locales/en.yml +26 -1
  18. data/db/migrate/20261013000001_add_sort_to_janela_panes.rb +7 -0
  19. data/db/migrate/20261029000001_add_series_to_janela_panes.rb +9 -0
  20. data/db/migrate/20261029000003_add_ignore_unknown_filters_to_janela_panes.rb +8 -0
  21. data/docs/agents.md +33 -2
  22. data/docs/decisions/018-a-table-is-the-universal-renderer.md +1 -0
  23. data/docs/decisions/053-an-agent-reaches-janela-through-tools-the-host-scopes.md +1 -0
  24. data/docs/decisions/054-a-chart-reads-numbers-the-way-its-measure-formats-them.md +1 -1
  25. data/docs/decisions/055-a-category-pane-can-be-sorted-by-its-measure-either-way-or-by-its-label.md +204 -0
  26. data/docs/decisions/056-the-demo-counts-its-own-visits-and-an-agent-arranges-the-dashboard.md +51 -0
  27. data/docs/decisions/057-a-host-is-told-about-the-agent-tools-and-a-generator-sets-them-up.md +197 -0
  28. data/docs/decisions/058-the-deployed-demo-is-reached-over-ssh-and-its-visits-frame-survives-a-reset.md +94 -0
  29. data/docs/decisions/059-a-host-can-tell-janelas-own-pages-that-its-layout-draws-charts.md +91 -0
  30. data/docs/decisions/060-the-demo-tells-search-engines-what-it-is-and-shows-its-own-adoption.md +79 -0
  31. data/docs/decisions/061-the-demo-speaks-brazilian-portuguese-under-a-prefix-and-the-gem-stays-english.md +118 -0
  32. data/docs/decisions/062-a-time-pane-can-accumulate-and-carry-more-series-on-two-axes.md +282 -0
  33. data/docs/decisions/063-a-pane-can-ignore-a-filter-its-model-cannot-apply.md +147 -0
  34. data/docs/decisions/INDEX.md +25 -16
  35. data/docs/roadmap.md +10 -4
  36. data/lib/generators/janela/mcp/mcp_generator.rb +51 -0
  37. data/lib/generators/janela/mcp/templates/janela-mcp.tt +50 -0
  38. data/lib/janela/definition.rb +73 -4
  39. data/lib/janela/dimension.rb +9 -1
  40. data/lib/janela/doctor.rb +10 -1
  41. data/lib/janela/measure.rb +8 -0
  42. data/lib/janela/tools.rb +37 -4
  43. data/lib/janela/version.rb +1 -1
  44. data/lib/janela.rb +12 -0
  45. 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.
@@ -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.