janela 0.2.0 → 0.3.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 +54 -1
- data/README.md +203 -21
- data/UPGRADING.md +136 -0
- data/app/assets/javascripts/janela/chart_controller.js +16 -8
- data/app/assets/javascripts/janela/{dashboard_controller.js → frame_controller.js} +11 -4
- data/app/assets/stylesheets/janela.css +163 -0
- data/app/controllers/janela/application_controller.rb +45 -0
- data/app/controllers/janela/frames_controller.rb +57 -0
- data/app/controllers/janela/panes_controller.rb +66 -13
- data/app/controllers/janela/queries_controller.rb +17 -0
- data/app/controllers/janela/{snapshot_panes_controller.rb → snapshot_queries_controller.rb} +7 -5
- data/app/helpers/janela/{dashboard_helper.rb → frames_helper.rb} +25 -9
- data/app/models/janela/frame.rb +28 -0
- data/app/models/janela/pane.rb +107 -85
- data/app/models/janela/query.rb +149 -0
- data/app/models/janela/snapshot.rb +6 -6
- data/app/views/janela/frames/_card.html.erb +7 -0
- data/app/views/janela/frames/_form.html.erb +23 -0
- data/app/views/janela/frames/_frame.html.erb +4 -0
- data/app/views/janela/frames/_pane.html.erb +12 -0
- data/app/views/janela/frames/edit.html.erb +25 -0
- data/app/views/janela/frames/index.html.erb +15 -0
- data/app/views/janela/frames/new.html.erb +5 -0
- data/app/views/janela/frames/show.html.erb +9 -0
- data/app/views/janela/panes/_form.html.erb +58 -0
- data/app/views/janela/panes/_row.html.erb +12 -0
- data/app/views/janela/panes/edit.html.erb +5 -0
- data/app/views/janela/panes/new.html.erb +22 -0
- data/app/views/janela/panes/show.html.erb +3 -44
- data/app/views/janela/queries/_query.html.erb +47 -0
- data/app/views/janela/queries/show.html.erb +4 -0
- data/app/views/janela/shared/_errors.html.erb +7 -0
- data/app/views/layouts/janela/application.html.erb +26 -0
- data/config/importmap.rb +1 -1
- data/config/locales/en.yml +64 -0
- data/config/routes.rb +15 -2
- data/db/migrate/20260916000001_create_janela_frames.rb +13 -0
- data/db/migrate/20260916000002_create_janela_panes.rb +22 -0
- data/docs/decisions/001-built-to-be-forked.md +4 -0
- data/docs/decisions/005-pane-urls-and-mount-path.md +4 -1
- data/docs/decisions/009-snapshots.md +5 -2
- data/docs/decisions/010-agent-guidance-ships-the-agent-waits.md +134 -0
- data/docs/decisions/011-panes-do-not-render-in-the-host-layout.md +81 -0
- data/docs/decisions/012-frames-and-panes-are-data.md +166 -0
- data/docs/decisions/013-naming-and-addressing-frames.md +119 -0
- data/docs/decisions/014-corrections-before-frames-are-built.md +222 -0
- data/docs/decisions/015-how-breaking-change-is-communicated.md +114 -0
- data/docs/decisions/016-the-styling-vocabulary.md +100 -0
- data/docs/decisions/017-janela-owns-no-data-store.md +113 -0
- data/docs/decisions/018-a-table-is-the-universal-renderer.md +75 -0
- data/docs/decisions/019-a-created-frame-asks-the-host-who-owns-it.md +79 -0
- data/docs/decisions/020-formatting-belongs-to-the-measure.md +93 -0
- data/docs/decisions/021-a-check-has-a-name-a-host-can-silence.md +84 -0
- data/docs/decisions/INDEX.md +24 -5
- data/docs/multi-tenancy.md +175 -0
- data/lib/janela/definition.rb +23 -9
- data/lib/janela/dimension.rb +5 -1
- data/lib/janela/doctor.rb +219 -0
- data/lib/janela/engine.rb +15 -2
- data/lib/janela/measure.rb +68 -4
- data/lib/janela/version.rb +1 -1
- data/lib/janela.rb +24 -1
- data/lib/tasks/janela.rake +6 -0
- metadata +57 -4
|
@@ -1,45 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
<span class="janela-value-label"><%= @pane.title %></span>
|
|
5
|
-
<strong class="janela-value-number"><%= number_with_delimiter(@result || 0) %></strong>
|
|
6
|
-
</p>
|
|
7
|
-
<% elsif @result.empty? %>
|
|
8
|
-
<p class="janela-pane janela-empty"><%= @pane.title %>: no data</p>
|
|
9
|
-
<% elsif @pane.chart? %>
|
|
10
|
-
<canvas class="janela-pane janela-chart"
|
|
11
|
-
data-controller="janela--chart"
|
|
12
|
-
data-action="janela--chart:toggle->janela--dashboard#toggle"
|
|
13
|
-
data-janela--chart-type-value="<%= @pane.renderer %>"
|
|
14
|
-
data-janela--chart-title-value="<%= @pane.title %>"
|
|
15
|
-
data-janela--chart-key-value="<%= @pane.filter_key %>"
|
|
16
|
-
data-janela--chart-selected-value="<%= @pane.selected_value %>"
|
|
17
|
-
data-janela--chart-labels-value="<%= @result.keys.to_json %>"
|
|
18
|
-
data-janela--chart-values-value="<%= @result.values.map(&:to_f).to_json %>"
|
|
19
|
-
role="img" aria-label="<%= @pane.title %>"></canvas>
|
|
20
|
-
<% else %>
|
|
21
|
-
<table class="janela-pane">
|
|
22
|
-
<caption><%= @pane.title %></caption>
|
|
23
|
-
<tbody>
|
|
24
|
-
<% @result.each do |value, measured| %>
|
|
25
|
-
<tr>
|
|
26
|
-
<td>
|
|
27
|
-
<% if @pane.clickable? %>
|
|
28
|
-
<button type="button"
|
|
29
|
-
aria-pressed="<%= value.to_s == @pane.selected_value.to_s && @pane.selected_value.present? %>"
|
|
30
|
-
data-action="janela--dashboard#toggle"
|
|
31
|
-
data-janela--dashboard-key-param="<%= @pane.filter_key %>"
|
|
32
|
-
data-janela--dashboard-value-param="<%= value %>">
|
|
33
|
-
<%= value %>
|
|
34
|
-
</button>
|
|
35
|
-
<% else %>
|
|
36
|
-
<span><%= value %></span>
|
|
37
|
-
<% end %>
|
|
38
|
-
</td>
|
|
39
|
-
<td><%= number_with_delimiter(measured) %></td>
|
|
40
|
-
</tr>
|
|
41
|
-
<% end %>
|
|
42
|
-
</tbody>
|
|
43
|
-
</table>
|
|
44
|
-
<% end %>
|
|
1
|
+
<% content_for :title, @query.title %>
|
|
2
|
+
<%= turbo_frame_tag @pane.turbo_frame_id do %>
|
|
3
|
+
<%= render "janela/queries/query", query: @query, result: @result %>
|
|
45
4
|
<% end %>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<%# One pane's content, without its turbo frame: the same markup whether the
|
|
2
|
+
pane came from a URL, a row, or a frame rendered inline. %>
|
|
3
|
+
<% if query.single_value? %>
|
|
4
|
+
<p class="janela-pane janela-value">
|
|
5
|
+
<span class="janela-value-label"><%= query.title %></span>
|
|
6
|
+
<strong class="janela-value-number"><%= query.format(result || 0) %></strong>
|
|
7
|
+
</p>
|
|
8
|
+
<% elsif result.empty? %>
|
|
9
|
+
<p class="janela-pane janela-empty"><%= query.title %>: no data</p>
|
|
10
|
+
<% elsif query.chart? %>
|
|
11
|
+
<canvas class="janela-pane janela-chart"
|
|
12
|
+
data-controller="janela--chart"
|
|
13
|
+
data-action="janela--chart:toggle->janela--frame#toggle"
|
|
14
|
+
data-janela--chart-type-value="<%= query.renderer %>"
|
|
15
|
+
data-janela--chart-title-value="<%= query.title %>"
|
|
16
|
+
data-janela--chart-selected-value="<%= query.selected_value %>"
|
|
17
|
+
data-janela--chart-labels-value="<%= result.keys.to_json %>"
|
|
18
|
+
data-janela--chart-values-value="<%= result.values.map(&:to_f).to_json %>"
|
|
19
|
+
data-janela--chart-formatted-value="<%= result.values.map { |measured| query.format(measured) }.to_json %>"
|
|
20
|
+
data-janela--chart-filters-value="<%= query.filters_for(result.keys).to_json %>"
|
|
21
|
+
role="img" aria-label="<%= query.title %>"></canvas>
|
|
22
|
+
<% else %>
|
|
23
|
+
<table class="janela-pane">
|
|
24
|
+
<caption><%= query.title %></caption>
|
|
25
|
+
<tbody>
|
|
26
|
+
<% result.each do |label, measured| %>
|
|
27
|
+
<% key, value = query.filter_params(label) %>
|
|
28
|
+
<tr>
|
|
29
|
+
<td>
|
|
30
|
+
<% if key %>
|
|
31
|
+
<button type="button"
|
|
32
|
+
aria-pressed="<%= label.to_s == query.selected_value.to_s && query.selected_value.present? %>"
|
|
33
|
+
data-action="janela--frame#toggle"
|
|
34
|
+
data-janela--frame-key-param="<%= key %>"
|
|
35
|
+
data-janela--frame-value-param="<%= value %>">
|
|
36
|
+
<%= label %>
|
|
37
|
+
</button>
|
|
38
|
+
<% else %>
|
|
39
|
+
<span><%= label %></span>
|
|
40
|
+
<% end %>
|
|
41
|
+
</td>
|
|
42
|
+
<td><%= query.format(measured) %></td>
|
|
43
|
+
</tr>
|
|
44
|
+
<% end %>
|
|
45
|
+
</tbody>
|
|
46
|
+
</table>
|
|
47
|
+
<% end %>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<%# Janela's own pages and a pane opened directly, outside a Turbo Frame.
|
|
2
|
+
Deliberately minimal: the engine cannot know the host's asset names or
|
|
3
|
+
bundler, and a host layout's route helpers do not resolve inside an
|
|
4
|
+
isolated engine (ADR 011). It loads Janela's own stylesheet, which the gem
|
|
5
|
+
ships and can therefore name, and no JavaScript: Turbo and Stimulus come
|
|
6
|
+
from the host, so these pages are static dashboards rather than broken
|
|
7
|
+
ones. A host can override this file, or point the controller at its own
|
|
8
|
+
layout. %>
|
|
9
|
+
<!DOCTYPE html>
|
|
10
|
+
<html>
|
|
11
|
+
<head>
|
|
12
|
+
<title><%= yield(:title).presence || "Janela" %></title>
|
|
13
|
+
<meta charset="utf-8">
|
|
14
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
15
|
+
<%= csrf_meta_tags %>
|
|
16
|
+
<%= csp_meta_tag %>
|
|
17
|
+
<%= stylesheet_link_tag "janela" %>
|
|
18
|
+
<%= yield :head %>
|
|
19
|
+
</head>
|
|
20
|
+
<body class="janela-page">
|
|
21
|
+
<% flash.each do |_kind, message| %>
|
|
22
|
+
<p class="janela-flash"><%= message %></p>
|
|
23
|
+
<% end %>
|
|
24
|
+
<%= yield %>
|
|
25
|
+
</body>
|
|
26
|
+
</html>
|
data/config/importmap.rb
CHANGED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
en:
|
|
2
|
+
activerecord:
|
|
3
|
+
models:
|
|
4
|
+
janela/frame:
|
|
5
|
+
one: Frame
|
|
6
|
+
other: Frames
|
|
7
|
+
janela/pane:
|
|
8
|
+
one: Pane
|
|
9
|
+
other: Panes
|
|
10
|
+
attributes:
|
|
11
|
+
janela/frame:
|
|
12
|
+
name: Name
|
|
13
|
+
columns: Columns
|
|
14
|
+
gap: Gap
|
|
15
|
+
janela/pane:
|
|
16
|
+
model: Model
|
|
17
|
+
measure: Measure
|
|
18
|
+
dimension: Dimension
|
|
19
|
+
renderer: Renderer
|
|
20
|
+
granularity: Granularity
|
|
21
|
+
limit: Rows
|
|
22
|
+
span: Width
|
|
23
|
+
title: Title
|
|
24
|
+
janela:
|
|
25
|
+
actions:
|
|
26
|
+
add_pane: "Add a %{name}"
|
|
27
|
+
cancel: Cancel
|
|
28
|
+
continue: Continue
|
|
29
|
+
delete: Remove
|
|
30
|
+
delete_frame: "Delete this %{name}"
|
|
31
|
+
edit: Edit
|
|
32
|
+
move_down: Down
|
|
33
|
+
move_up: Up
|
|
34
|
+
new_frame: "New %{name}"
|
|
35
|
+
save: Save
|
|
36
|
+
view: View
|
|
37
|
+
frames:
|
|
38
|
+
created: Created.
|
|
39
|
+
destroyed: Deleted.
|
|
40
|
+
updated: Saved.
|
|
41
|
+
index:
|
|
42
|
+
blank: "No %{name} yet."
|
|
43
|
+
edit:
|
|
44
|
+
blank: "No %{name} yet."
|
|
45
|
+
frame:
|
|
46
|
+
panes:
|
|
47
|
+
zero: no panes
|
|
48
|
+
one: 1 pane
|
|
49
|
+
other: "%{count} panes"
|
|
50
|
+
changed: "changed %{time} ago"
|
|
51
|
+
panes:
|
|
52
|
+
created: Added.
|
|
53
|
+
destroyed: Removed.
|
|
54
|
+
updated: Saved.
|
|
55
|
+
declared_granularity: As the model declares it
|
|
56
|
+
granularity_hint: "Applies to %{dimensions} only."
|
|
57
|
+
no_dimension: "No breakdown, one number"
|
|
58
|
+
no_limit: Every row
|
|
59
|
+
summary: "%{renderer}, %{span} wide"
|
|
60
|
+
title_placeholder: Janela writes one if you leave this blank
|
|
61
|
+
renderers:
|
|
62
|
+
bar: Bar chart
|
|
63
|
+
line: Line chart
|
|
64
|
+
table: Table
|
data/config/routes.rb
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
Janela::Engine.routes.draw do
|
|
2
2
|
segment = /[a-z0-9_]+/
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
# Frames sit at the mount root and are drawn before the pane grammar, with a
|
|
5
|
+
# numeric constraint: no model's route key is all digits, so /3 is a frame
|
|
6
|
+
# and /orders/revenue is a pane, with nothing to disambiguate (ADR 014).
|
|
7
|
+
resources :frames, path: "", constraints: { id: /\d+/ } do
|
|
8
|
+
# No index: a frame's own edit page is the list of its panes.
|
|
9
|
+
resources :panes, except: :index, constraints: { id: /\d+/, frame_id: /\d+/ } do
|
|
10
|
+
member do
|
|
11
|
+
patch :move_up
|
|
12
|
+
patch :move_down
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
get "snapshots/:snapshot_id/:model/:measure(/:dimension)", to: "snapshot_queries#show", as: :snapshot_pane,
|
|
5
18
|
constraints: { snapshot_id: /\d+/, model: segment, measure: segment, dimension: segment }
|
|
6
19
|
|
|
7
|
-
get ":model/:measure(/:dimension)", to: "
|
|
20
|
+
get ":model/:measure(/:dimension)", to: "queries#show", as: :pane,
|
|
8
21
|
constraints: { model: segment, measure: segment, dimension: segment }
|
|
9
22
|
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class CreateJanelaFrames < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :janela_frames do |t|
|
|
4
|
+
t.string :name, null: false
|
|
5
|
+
t.integer :columns, null: false, default: 3
|
|
6
|
+
t.integer :gap, null: false, default: 4
|
|
7
|
+
# Janela never reads the owner. It is here so a host's Pundit Scope has
|
|
8
|
+
# something to filter a frame on (ADR 014).
|
|
9
|
+
t.references :owner, polymorphic: true, null: true
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class CreateJanelaPanes < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :janela_panes do |t|
|
|
4
|
+
# The composite index below is the one a frame's panes are read by, so
|
|
5
|
+
# references adds none of its own.
|
|
6
|
+
t.references :frame, null: false, index: false, foreign_key: { to_table: :janela_frames }
|
|
7
|
+
t.integer :position, null: false
|
|
8
|
+
t.integer :span, null: false, default: 1
|
|
9
|
+
# A model's route key, the same identifier a pane URL carries (ADR 005).
|
|
10
|
+
t.string :model, null: false
|
|
11
|
+
t.string :measure, null: false
|
|
12
|
+
t.string :dimension
|
|
13
|
+
t.string :renderer, null: false, default: "table"
|
|
14
|
+
t.string :granularity
|
|
15
|
+
t.integer :limit
|
|
16
|
+
t.string :title
|
|
17
|
+
t.timestamps
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
add_index :janela_panes, [ :frame_id, :position ]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -71,3 +71,7 @@ Janela is designed to be forked, not just configured.
|
|
|
71
71
|
special-casing Janela itself.
|
|
72
72
|
- Future scope decisions (what's in the 5%, what isn't) should cite
|
|
73
73
|
this ADR rather than re-litigate the philosophy each time.
|
|
74
|
+
- The exclusion of a report designer was partly superseded by ADR 012:
|
|
75
|
+
composition became data, because this ADR assumed the developer
|
|
76
|
+
authored dashboards and the author is the analyst. The reasoning
|
|
77
|
+
still holds for the query vocabulary, which stays in code.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
Date: 2026-09-15
|
|
3
3
|
Status: Accepted
|
|
4
|
-
Related: ADR 001, ADR 002, ADR 003, ADR 004
|
|
4
|
+
Related: ADR 001, ADR 002, ADR 003, ADR 004, ADR 011
|
|
5
5
|
Triggers:
|
|
6
6
|
- changing the URL of a pane or how a dashboard frame addresses one
|
|
7
7
|
- mounting the engine anywhere other than the default, or with as:
|
|
@@ -134,3 +134,6 @@ author reached for unprompted.
|
|
|
134
134
|
- Looking up the mount in the host's route table means the helper
|
|
135
135
|
finds the first mount of the engine. Mounting Janela twice is
|
|
136
136
|
unsupported and not a goal.
|
|
137
|
+
- The claim above that a directly opened pane renders "inside the
|
|
138
|
+
host's layout" was wrong and is superseded by ADR 011: a host
|
|
139
|
+
layout's route helpers cannot resolve inside an isolated engine.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
Date: 2026-09-15
|
|
3
3
|
Status: Accepted
|
|
4
|
-
Related: ADR 001, ADR 002, ADR 005, ADR 008
|
|
4
|
+
Related: ADR 001, ADR 002, ADR 005, ADR 008, ADR 012
|
|
5
|
+
Superseded in part by: ADR 012
|
|
5
6
|
Triggers:
|
|
6
7
|
- publishing a dashboard or pane for an audience that must not see live data or slicers
|
|
7
8
|
- adding a database table, migration or model to the engine
|
|
@@ -53,7 +54,9 @@ several panes frozen at the same instant under the same filters, so a
|
|
|
53
54
|
`Janela::Snapshot` row holds a name, `taken_at`, the filters, and an
|
|
54
55
|
array of pane results. There is no Dashboard model and no dashboard
|
|
55
56
|
DSL: the host's page is the dashboard, and the host names the panes to
|
|
56
|
-
freeze.
|
|
57
|
+
freeze. (Superseded in part by ADR 012: a dashboard is now a record,
|
|
58
|
+
`Janela::Frame`, and a snapshot of a frame rather than of a list is
|
|
59
|
+
the obvious next shape. Nothing about what a snapshot stores changes.) If the host renders its live page from the same list it passes
|
|
57
60
|
here, nothing is declared twice.
|
|
58
61
|
|
|
59
62
|
```ruby
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
Date: 2026-09-15
|
|
3
|
+
Status: Accepted
|
|
4
|
+
Related: ADR 001, ADR 005, ADR 009, ADR 012, ADR 014
|
|
5
|
+
Superseded in part by: ADR 012
|
|
6
|
+
Triggers:
|
|
7
|
+
- writing or changing guidance for AI agents about using Janela
|
|
8
|
+
- proposing that Janela ship an agent, a skill or an MCP surface to hosts
|
|
9
|
+
- adding generators or introspection tasks
|
|
10
|
+
- changing the public API in a way the guidance describes
|
|
11
|
+
- deciding what a host application receives on install beyond the code
|
|
12
|
+
Topics: ai, agents, jan, skills, documentation, install, dx, scope
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# ADR 010: Agent Guidance Ships, the Agent Waits
|
|
16
|
+
|
|
17
|
+
## Context
|
|
18
|
+
|
|
19
|
+
ADR 001 decided the reasoning behind Janela ships inside the gem, as
|
|
20
|
+
ADRs, so a developer or their agent understands why a piece exists
|
|
21
|
+
before changing it. That covers the *why*. Nothing covers the *how*,
|
|
22
|
+
and the first real installation showed the gap has a price: three
|
|
23
|
+
traps bit within an hour, each a one line fix found only by debugging.
|
|
24
|
+
Ransack's allowlist is per class, so a `through:` dimension needs the
|
|
25
|
+
associated model to allow the attribute. A host that bundles with
|
|
26
|
+
esbuild has no importmap, so none of Janela's JavaScript loads and
|
|
27
|
+
nothing cross filters while everything looks right. Janela's
|
|
28
|
+
controllers are exactly as authenticated as the host's
|
|
29
|
+
`ApplicationController`, which in a host that authenticates per
|
|
30
|
+
controller means not at all.
|
|
31
|
+
|
|
32
|
+
None of that is derivable from reading the code, which is precisely
|
|
33
|
+
the test ADR 001 sets for what belongs in the repository as prose.
|
|
34
|
+
|
|
35
|
+
A second, larger idea came up alongside it: that Janela ship an agent
|
|
36
|
+
of its own, named Jan, so installing the gem gives a host a briefed
|
|
37
|
+
colleague rather than a briefing. Open source has always shipped code
|
|
38
|
+
and documentation; it could now ship the person who knows how to use
|
|
39
|
+
them. It is a genuinely novel idea and nobody is doing it.
|
|
40
|
+
|
|
41
|
+
The two are not the same commitment, and this ADR separates them.
|
|
42
|
+
|
|
43
|
+
## Decision
|
|
44
|
+
|
|
45
|
+
**A skill ships with the gem and installs into the host.**
|
|
46
|
+
`docs/skills/janela/SKILL.md` lives in the gem's `docs/` tree beside
|
|
47
|
+
the ADRs. `rails janela:install:skill` copies it to the host's
|
|
48
|
+
`.claude/skills/janela/` and prints the one line to add to an
|
|
49
|
+
`AGENTS.md` or `CLAUDE.md` for tools that read those instead. Janela
|
|
50
|
+
never edits a host's instruction files itself.
|
|
51
|
+
|
|
52
|
+
The skill is knowledge, not instructions to a particular agent, which
|
|
53
|
+
is why it survives whatever agent formats come and go.
|
|
54
|
+
|
|
55
|
+
**What the skill covers, in this order:**
|
|
56
|
+
|
|
57
|
+
1. The dashboard shape that works, and why: a row of single value
|
|
58
|
+
panes, a time series, then categorical breakdowns.
|
|
59
|
+
2. Choosing measures and dimensions. Name them for what they mean and
|
|
60
|
+
alias through dimensions. Keep categories low cardinality or pass
|
|
61
|
+
`limit`. Declare time dimensions with the granularity people
|
|
62
|
+
actually ask about. A dimension declaration is a promise that
|
|
63
|
+
filtering on it is allowed.
|
|
64
|
+
3. The naming table from ADR 005, so an analyst's sentence becomes a
|
|
65
|
+
pane URL and back.
|
|
66
|
+
4. When to take a snapshot, and that a snapshot holds results, not
|
|
67
|
+
HTML.
|
|
68
|
+
5. The three traps above, each with its one line fix.
|
|
69
|
+
6. What Janela deliberately does not do, so an agent does not build
|
|
70
|
+
natural language query, row level security or a scheduler into the
|
|
71
|
+
host by accident. Composition is no longer on that list: ADR 012
|
|
72
|
+
made frames and panes records and ADR 014 sequenced the editing
|
|
73
|
+
surfaces, so the skill points an agent at those rather than telling
|
|
74
|
+
it to compose dashboards in ERB.
|
|
75
|
+
|
|
76
|
+
**The skill describes the README's API and nothing else.** One API,
|
|
77
|
+
one set of names. If the skill needs to say something the README does
|
|
78
|
+
not, the README is incomplete and gets fixed first. No agent only
|
|
79
|
+
vocabulary.
|
|
80
|
+
|
|
81
|
+
**The demo application is the worked example.** `test/dummy` shows
|
|
82
|
+
every construct on realistic data, deployed and clickable. The skill
|
|
83
|
+
points at specific files rather than duplicating them.
|
|
84
|
+
|
|
85
|
+
**Every code sample is executable.** Each sample in the skill and in
|
|
86
|
+
`docs/guides/` is lifted from a file the test suite exercises, or is
|
|
87
|
+
run by a test. A sample that cannot be run is written as a sentence,
|
|
88
|
+
not a code block. This is the mechanism that stops the guidance
|
|
89
|
+
drifting from the code.
|
|
90
|
+
|
|
91
|
+
**Jan is not shipped to hosts yet.** An agent definition is added to
|
|
92
|
+
this repository only, as the project's own collaborator, and is not
|
|
93
|
+
copied into host applications by any install task. Three reasons:
|
|
94
|
+
|
|
95
|
+
- Its method, read the models, declare dimensions, compose in the
|
|
96
|
+
standard shape, wire the install, verify in a browser, is what a
|
|
97
|
+
capable agent with the skill loaded already does. The marginal value
|
|
98
|
+
over the skill is a name to invoke and a guarantee the skill is
|
|
99
|
+
loaded. That is convenience, not capability, and convenience is not
|
|
100
|
+
worth a public surface.
|
|
101
|
+
- Agent definition formats are unsettled. The skill's content is prose
|
|
102
|
+
about dashboards and survives any format; an agent definition is the
|
|
103
|
+
part most likely to be stale in six months.
|
|
104
|
+
- A gem that writes a named agent into a host arrives with opinions
|
|
105
|
+
about how that team works, not only about what its code does. That
|
|
106
|
+
cuts against the posture in ADR 001, where the invitation is to read
|
|
107
|
+
the code and fork it.
|
|
108
|
+
|
|
109
|
+
**What would change this.** Ship Jan to hosts when either is true:
|
|
110
|
+
|
|
111
|
+
- Jan, used on this repository, demonstrably does something a skill
|
|
112
|
+
loaded agent does not. Name the thing in the ADR that supersedes
|
|
113
|
+
this one.
|
|
114
|
+
- An MCP surface exists. An agent that can query dashboards and take
|
|
115
|
+
snapshots through tools has a job no skill can do, and at that point
|
|
116
|
+
a named agent stops being a wrapper and becomes a user.
|
|
117
|
+
|
|
118
|
+
## Consequences
|
|
119
|
+
|
|
120
|
+
- A host that installs Janela and runs one task gets an opinionated,
|
|
121
|
+
current briefing, including the three traps that cost real debugging
|
|
122
|
+
time. That is the concrete value of this layer and it lands now.
|
|
123
|
+
- `docs/guides/` becomes a real directory shipping in the gem, so a
|
|
124
|
+
forker receives reasoning, rules and how to together.
|
|
125
|
+
- Jan exists but only here. The agent that helps build the gem is the
|
|
126
|
+
agent that would one day help hosts, so drift between what Jan says
|
|
127
|
+
and what the code does surfaces in this repository first. That is
|
|
128
|
+
the trial period, and it is free.
|
|
129
|
+
- Saying no to shipping Jan is recorded rather than remembered, with
|
|
130
|
+
the conditions that would reverse it. A future proposal cites this
|
|
131
|
+
ADR instead of relitigating the idea.
|
|
132
|
+
- Introspection (`rails janela:describe Model`), generators and MCP
|
|
133
|
+
are each one ADR away. Each must be a wrapper over the existing API,
|
|
134
|
+
for the same reason the skill must describe only one.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
Date: 2026-09-15
|
|
3
|
+
Status: Accepted
|
|
4
|
+
Related: ADR 003, ADR 005
|
|
5
|
+
Triggers:
|
|
6
|
+
- changing which layout a pane renders in
|
|
7
|
+
- a host reporting NameError from its own layout when a pane loads
|
|
8
|
+
- adding anything to the engine's own layout
|
|
9
|
+
- making the dummy application unrepresentative of a real host
|
|
10
|
+
Topics: layouts, engines, panes, urls, install
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# ADR 011: Panes Do Not Render in the Host Layout
|
|
14
|
+
|
|
15
|
+
## Context
|
|
16
|
+
|
|
17
|
+
ADR 005 promised that a pane opened directly "gets that pane alone
|
|
18
|
+
inside the host's layout". Dogfooding 0.2.0 in a second application
|
|
19
|
+
showed that promise is a bug. `Janela::ApplicationController` inherits
|
|
20
|
+
the host's `ApplicationController` and therefore the host's layout,
|
|
21
|
+
but the engine is `isolate_namespace`d, so a route helper written in
|
|
22
|
+
that layout resolves against Janela's routes and raises `NameError`.
|
|
23
|
+
Almost every real application layout has a nav, so almost every host
|
|
24
|
+
is broken on install, and a whole dashboard of frames goes blank.
|
|
25
|
+
|
|
26
|
+
The gem's own dummy application has no route helper in its layout,
|
|
27
|
+
which is exactly why the suite, the demo and the first host install
|
|
28
|
+
never saw it. The first host squeaked through because its only layout
|
|
29
|
+
helpers were an Active Storage URL and a PWA manifest path rather
|
|
30
|
+
than nav links.
|
|
31
|
+
|
|
32
|
+
## Decision
|
|
33
|
+
|
|
34
|
+
**A pane requested inside a Turbo Frame renders with no layout.**
|
|
35
|
+
Turbo extracts the matching frame from the response and discards
|
|
36
|
+
everything around it, so a layout was never doing any work there. This
|
|
37
|
+
alone fixes every dashboard, which is how panes are almost always
|
|
38
|
+
requested.
|
|
39
|
+
|
|
40
|
+
**A pane requested directly renders in Janela's own layout.** A
|
|
41
|
+
minimal layout in the engine, carrying only a charset, a viewport, the
|
|
42
|
+
CSRF and CSP tags and a `yield`. It keeps ADR 005's shareable pane
|
|
43
|
+
alive and cannot depend on anything the host has not got.
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
layout -> { turbo_frame_request? ? false : "janela/application" }
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**A directly opened pane is therefore unstyled, and that is
|
|
50
|
+
documented.** Janela ships no CSS (issue #15), and the engine layout
|
|
51
|
+
deliberately loads none of the host's assets, because it cannot know
|
|
52
|
+
their names or whether the host bundles or uses importmap. A host that
|
|
53
|
+
wants its own styling on direct pane URLs sets
|
|
54
|
+
`Janela::ApplicationController.layout "application"` in an
|
|
55
|
+
initializer, and is told the condition: that layout must not call a
|
|
56
|
+
bare host route helper, since inside an engine those need a
|
|
57
|
+
`main_app.` prefix.
|
|
58
|
+
|
|
59
|
+
**The dummy application's layout gains a route helper.** The dummy is
|
|
60
|
+
the gem's stand in for a real host, and a stand in that omits the most
|
|
61
|
+
common thing a layout contains is not doing its job. A `link_to` to
|
|
62
|
+
the dashboard makes the suite fail if a pane ever renders in the host
|
|
63
|
+
layout again.
|
|
64
|
+
|
|
65
|
+
## Consequences
|
|
66
|
+
|
|
67
|
+
- ADR 005's sentence about the host layout is superseded by this ADR.
|
|
68
|
+
The rest of ADR 005, the URL grammar and the naming table, stands.
|
|
69
|
+
- Direct pane URLs render a table but not a chart, because no
|
|
70
|
+
JavaScript is loaded. Shareable pane links are therefore honest for
|
|
71
|
+
data and plain for visuals until a host opts its own layout in.
|
|
72
|
+
Acceptable: the pane URL's job is to show a number to someone, and
|
|
73
|
+
the dashboard is where charts live.
|
|
74
|
+
- The engine now owns a view that a host might want to override.
|
|
75
|
+
`app/views/layouts/janela/application.html.erb` is overridable by
|
|
76
|
+
the usual Rails precedence, which is the Rails answer and needs no
|
|
77
|
+
configuration of Janela's own.
|
|
78
|
+
- The lesson generalises beyond layouts: the dummy application should
|
|
79
|
+
resemble a real host in the ways hosts actually vary. Each time a
|
|
80
|
+
host finds something the dummy could not, the dummy gains that
|
|
81
|
+
characteristic rather than the fix being verified only by hand.
|