janela 0.4.0 → 0.4.1
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 +7 -0
- data/README.md +1 -1
- data/docs/decisions/010-agent-guidance-ships-the-agent-waits.md +1 -0
- data/docs/decisions/025-janela-bounds-what-a-filter-can-ask-for.md +131 -0
- data/docs/decisions/INDEX.md +18 -16
- data/lib/janela/host_routes.rb +6 -0
- data/lib/janela/version.rb +1 -1
- metadata +2 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 00b7cab2c2ce57d7d9457affeb3d5f647b177f37d4f78349d4a12fe3fe298bd6
|
|
4
|
+
data.tar.gz: 5092f072ec8c4e282f09321fdff541e9eac613f557ae2f9697de830468c52778
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1bfea2cbc91408fa6c8879f2cd64b3c7cd51cc53b075da9268553ff9125716054afa68b8abc6f714e7b25c70a63f7a295f1c16353d5f31779a76ca326affbf6a
|
|
7
|
+
data.tar.gz: d9303b22989d5951d847bd56b3337f4576aa352c5887a2cc73b82d15e77d9967230a2984cd06bb2c428abed4f08065cd612461687abe7c91548cab4e88857669
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.4.1] - 2026-09-17
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- `Janela::HostRoutes.forwarded` could return an empty set. Route loading is lazy, so calling it before anything had drawn the host's routes, such as from a host's own initializer, silently forwarded nothing rather than raising. It now draws the routes first if they are not already loaded (#37).
|
|
13
|
+
|
|
8
14
|
## [0.4.0] - 2026-09-17
|
|
9
15
|
|
|
10
16
|
### Added
|
|
@@ -122,6 +128,7 @@ First alpha, installed from GitHub for testing in a single host application.
|
|
|
122
128
|
- Only models that declare a `janela` block are addressable over HTTP.
|
|
123
129
|
- ADRs 001 to 004 in `docs/decisions/`, shipped inside the gem.
|
|
124
130
|
|
|
131
|
+
[0.4.1]: https://github.com/retail-tasker/janela/releases/tag/v0.4.1
|
|
125
132
|
[0.4.0]: https://github.com/retail-tasker/janela/releases/tag/v0.4.0
|
|
126
133
|
[0.3.0]: https://github.com/retail-tasker/janela/releases/tag/v0.3.0
|
|
127
134
|
[0.2.1]: https://github.com/retail-tasker/janela/releases/tag/v0.2.1
|
data/README.md
CHANGED
|
@@ -435,7 +435,7 @@ Deliberately out of scope: natural-language query, a separate data warehouse, a
|
|
|
435
435
|
|
|
436
436
|
## Status
|
|
437
437
|
|
|
438
|
-
**v0.4.
|
|
438
|
+
**v0.4.1 alpha.** The measures/dimensions DSL, time dimensions, cross-filtering with multi-selection, bar and line charts, pane URLs, shareable dashboard URLs, snapshots, database-backed frames, the engine's own pages for reading and editing them and the optional vitral theme work and are covered by unit and real-browser tests. Not yet built: a visual editor, drill-down on time panes, other chart types. Open work is in [GitHub Issues](https://github.com/retail-tasker/janela/issues).
|
|
439
439
|
|
|
440
440
|
## Development
|
|
441
441
|
|
|
@@ -3,6 +3,7 @@ Date: 2026-09-15
|
|
|
3
3
|
Status: Accepted
|
|
4
4
|
Related: ADR 001, ADR 005, ADR 009, ADR 012, ADR 014
|
|
5
5
|
Superseded in part by: ADR 012
|
|
6
|
+
Not implemented: the host skill, the install task and the agent definition
|
|
6
7
|
Triggers:
|
|
7
8
|
- writing or changing guidance for AI agents about using Janela
|
|
8
9
|
- proposing that Janela ship an agent, a skill or an MCP surface to hosts
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
Date: 2026-09-17
|
|
3
|
+
Status: Accepted
|
|
4
|
+
Related: ADR 002, ADR 005, ADR 006, ADR 007, ADR 008, ADR 015, ADR 024
|
|
5
|
+
Triggers:
|
|
6
|
+
- a filter reaching Ransack from a URL
|
|
7
|
+
- adding a predicate, or widening what a filter may contain
|
|
8
|
+
- a query with no LIMIT, or a grouped query over a high cardinality dimension
|
|
9
|
+
- deciding whether a bound belongs to Janela or to the host
|
|
10
|
+
Topics: security, ransack, urls, performance, cross-filtering
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# ADR 025: Janela Bounds What a Filter Can Ask For
|
|
14
|
+
|
|
15
|
+
## Context
|
|
16
|
+
|
|
17
|
+
ADR 002 chose Ransack so that a host's filters are the Ransack params a
|
|
18
|
+
Rails developer already reads, and made a dimension declaration the
|
|
19
|
+
allowlist of attributes. That part holds. What it left open is which
|
|
20
|
+
*predicates* may be used on an allowed attribute, and issue #8 asked the
|
|
21
|
+
question.
|
|
22
|
+
|
|
23
|
+
Measured against the demo before writing this:
|
|
24
|
+
|
|
25
|
+
| Filter passed | Result today |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| `status_eq: "paid"` | 300.0 |
|
|
28
|
+
| `status_cont: "pai"` | 300.0 |
|
|
29
|
+
| `status_start: "p"` | 325.0 |
|
|
30
|
+
| `status_matches: "%aid"` | 300.0 |
|
|
31
|
+
| `customer_name_cont: "cm"` | 150.0 |
|
|
32
|
+
| `amount_gt: 60` | `BadRequest`, `amount` is not a dimension |
|
|
33
|
+
| `status_frobnicate: "x"` | `BadRequest`, no such predicate |
|
|
34
|
+
|
|
35
|
+
So the attribute allowlist works, and every predicate Ransack knows is
|
|
36
|
+
reachable on an allowed attribute, including through an association.
|
|
37
|
+
`Ransack::Predicate.names` has 62 entries. The sharpest is `_matches`,
|
|
38
|
+
which takes an arbitrary `LIKE` pattern, so a leading wildcard scan of an
|
|
39
|
+
indexed column is one URL edit away, in a page a host has already
|
|
40
|
+
authorised someone to read.
|
|
41
|
+
|
|
42
|
+
Two other bounds are missing. A grouped query with no `limit` carries no
|
|
43
|
+
`LIMIT` in SQL at all, so a breakdown by a high cardinality dimension
|
|
44
|
+
returns a row per value. And a single filter can carry any number of
|
|
45
|
+
values: `status_in` with 5001 of them was answered rather than refused,
|
|
46
|
+
which matters more since ADR 024 made `_in` the shape a click writes.
|
|
47
|
+
|
|
48
|
+
**The question worth deciding is whose job this is.** ADR 002
|
|
49
|
+
deliberately spent Ransack's familiarity on the host, and the obvious
|
|
50
|
+
objection to bounding anything here is that a host can restrict filtering
|
|
51
|
+
itself by defining `ransackable_attributes`. That objection is false, and
|
|
52
|
+
the measurement above is why: `ransackable_attributes` allows *columns*,
|
|
53
|
+
and Ransack has no per-attribute predicate allowlist. Once a host allows
|
|
54
|
+
a column for equality, it has allowed `_matches` on that column too.
|
|
55
|
+
There is nowhere else for this bound to live.
|
|
56
|
+
|
|
57
|
+
Options considered and rejected:
|
|
58
|
+
|
|
59
|
+
- **Leave it, and document it.** A dashboard is often the first page a
|
|
60
|
+
company exposes to people it would not give SQL to, and the library
|
|
61
|
+
hands them a pattern scan. Documenting a sharp edge is not the same as
|
|
62
|
+
not having one.
|
|
63
|
+
- **Restrict to `_eq` and `_in`,** as issue #8 proposed. It would delete
|
|
64
|
+
documented behaviour: ADR 006 allows `placed_on_gteq` and
|
|
65
|
+
`placed_on_lt` so a host can narrow a time range, and the README says
|
|
66
|
+
so. A range filter on a time dimension is a dashboard's ordinary
|
|
67
|
+
question, not an escape hatch.
|
|
68
|
+
- **A setting listing the allowed predicates.** Rejected on the test
|
|
69
|
+
ADR 021 and ADR 023 set: a setting earns itself when the judgement is
|
|
70
|
+
made once about the whole application and cannot be inferred. This one
|
|
71
|
+
can be inferred, from what kind of dimension is being filtered.
|
|
72
|
+
|
|
73
|
+
## Decision
|
|
74
|
+
|
|
75
|
+
**Janela allows only the predicates a dashboard asks in, bounds what a
|
|
76
|
+
grouped query returns, and bounds how many values one filter may carry.**
|
|
77
|
+
|
|
78
|
+
**Predicates are allowed by the kind of dimension.** A categorical
|
|
79
|
+
dimension takes `eq`, `in` and `null`, which is exactly what a click
|
|
80
|
+
produces (ADR 024). A time dimension additionally takes `gteq`, `gt`,
|
|
81
|
+
`lteq` and `lt`, because narrowing a range is how a time dimension is
|
|
82
|
+
filtered (ADR 006). Anything else raises `Janela::BadRequest` naming the
|
|
83
|
+
attribute and the predicates that are allowed, the way a disallowed
|
|
84
|
+
attribute already does.
|
|
85
|
+
|
|
86
|
+
The check belongs beside the existing one in `Definition#filter`, which
|
|
87
|
+
already raises for a filter Ransack silently dropped. `Ransack::Predicate
|
|
88
|
+
.detect_from_string` splits a key into attribute and predicate, so this
|
|
89
|
+
is a comparison, not a parser.
|
|
90
|
+
|
|
91
|
+
**One ceiling, 1000, in both places.** A grouped query with no `limit`
|
|
92
|
+
gets `LIMIT 1000`, and a single filter may carry at most 1000 values.
|
|
93
|
+
1000 is not a new number: ADR 007 already fixed it as the maximum a host
|
|
94
|
+
may ask for, so the ceiling is the existing maximum applied by default
|
|
95
|
+
rather than a second constant to reason about. A pane that hits the
|
|
96
|
+
ceiling is unreadable long before it is reached, so this is a bound on
|
|
97
|
+
harm, not on usefulness.
|
|
98
|
+
|
|
99
|
+
**Time panes keep their exemption.** ADR 007 exempted them from `limit`
|
|
100
|
+
because a time series shows its whole range, and gap filled buckets are
|
|
101
|
+
generated rather than returned by the database. The ceiling does not
|
|
102
|
+
apply to them. A host that wants fewer buckets narrows the range or
|
|
103
|
+
coarsens the granularity, which ADR 006 already allows.
|
|
104
|
+
|
|
105
|
+
**No setting is added.** A host that genuinely needs a pattern search
|
|
106
|
+
already has the documented way in: pass a pre-filtered relation through
|
|
107
|
+
`on:`, where it writes the condition itself in its own code, under its
|
|
108
|
+
own authorisation, rather than accepting one from a URL.
|
|
109
|
+
|
|
110
|
+
## Consequences
|
|
111
|
+
|
|
112
|
+
- The reachable filter surface goes from 62 predicates to three on a
|
|
113
|
+
categorical dimension and seven on a time dimension. That is the point.
|
|
114
|
+
- **Breaking for a host that passes anything else.** A `_cont` or
|
|
115
|
+
`_matches` filter that works today raises `BadRequest` after this. It
|
|
116
|
+
needs an `UPGRADING.md` entry naming the allowed predicates and the
|
|
117
|
+
`on:` alternative, and the release carrying it is minor (ADR 015).
|
|
118
|
+
The doctor cannot find this one by reading source, because the filter
|
|
119
|
+
is usually built at runtime; the upgrade note is the whole mitigation.
|
|
120
|
+
- A stored snapshot holds the filters it was taken under (ADR 009). One
|
|
121
|
+
taken with a now disallowed predicate would raise when read. Whether to
|
|
122
|
+
refuse those at read time or leave stored results alone is not decided
|
|
123
|
+
here and should be settled while building this.
|
|
124
|
+
- The default ceiling changes a number a host can see: a breakdown over
|
|
125
|
+
more than 1000 values silently showed all of them and will now show
|
|
126
|
+
1000. It is ordered by the measure, so what is cut is the smallest, and
|
|
127
|
+
a host that wants a different cut passes `limit`.
|
|
128
|
+
- What would change this: a host with a real need for a predicate not on
|
|
129
|
+
the list. The answer is to add that predicate to the list for that kind
|
|
130
|
+
of dimension, in an ADR that says which dashboard question needed it,
|
|
131
|
+
not to make the list configurable.
|
data/docs/decisions/INDEX.md
CHANGED
|
@@ -21,23 +21,24 @@ them when in doubt: `grep -l "Triggers:.*fork" docs/decisions/*.md`.
|
|
|
21
21
|
| Topic | ADRs |
|
|
22
22
|
|-------|------|
|
|
23
23
|
| **Vision, scope, forkability** | 001, 010, 012 |
|
|
24
|
-
| **Open-source & host-decoupling** | 001 |
|
|
25
|
-
| **DSL & query layer** | 002, 006, 007 |
|
|
26
|
-
| **Dependencies** | 002, 003, 004, 006, 017 |
|
|
27
|
-
| **Authorisation** | 002, 003, 004, 009, 017, 019 |
|
|
28
|
-
| **Performance & storage** | 007, 017 |
|
|
29
|
-
| **Cross-filtering & Hotwire** | 003, 004, 005, 008 |
|
|
30
|
-
| **Layouts & views** | 011, 012, 016, 018 |
|
|
31
|
-
| **CSS & styling** | 016, 018 |
|
|
24
|
+
| **Open-source & host-decoupling** | 001, 022 |
|
|
25
|
+
| **DSL & query layer** | 002, 006, 007, 020, 025 |
|
|
26
|
+
| **Dependencies** | 002, 003, 004, 006, 017, 025 |
|
|
27
|
+
| **Authorisation** | 002, 003, 004, 009, 017, 019, 022 |
|
|
28
|
+
| **Performance & storage** | 007, 017, 025 |
|
|
29
|
+
| **Cross-filtering & Hotwire** | 003, 004, 005, 008, 024, 025 |
|
|
30
|
+
| **Layouts & views** | 011, 012, 016, 018, 020 |
|
|
31
|
+
| **CSS & styling** | 016, 018, 023 |
|
|
32
32
|
| **Frames, panes & persistence** | 012, 013, 014, 019 |
|
|
33
|
-
| **Naming rule** | 014 |
|
|
33
|
+
| **Naming rule** | 014, 023 |
|
|
34
34
|
| **JavaScript delivery & charts** | 004, 006 |
|
|
35
|
-
| **Time dimensions** | 006 |
|
|
36
|
-
| **Routes, URLs & naming** | 005, 007, 008, 009, 011, 013 |
|
|
37
|
-
| **Snapshots & publishing** | 009 |
|
|
38
|
-
| **AI agents & guidance** | 010, 015 |
|
|
39
|
-
| **Releases & upgrades** | 015 |
|
|
40
|
-
| **
|
|
35
|
+
| **Time dimensions** | 006, 025 |
|
|
36
|
+
| **Routes, URLs & naming** | 005, 007, 008, 009, 011, 013, 022, 024, 025 |
|
|
37
|
+
| **Snapshots & publishing** | 009, 020 |
|
|
38
|
+
| **AI agents & guidance** | 010, 015, 021 |
|
|
39
|
+
| **Releases & upgrades** | 015, 021 |
|
|
40
|
+
| **Accessibility & keyboard** | 024 |
|
|
41
|
+
| **Security** | 003, 025 |
|
|
41
42
|
| **Testing** | 003 |
|
|
42
43
|
|
|
43
44
|
## Chronological
|
|
@@ -68,7 +69,8 @@ them when in doubt: `grep -l "Triggers:.*fork" docs/decisions/*.md`.
|
|
|
68
69
|
| 022 | A Host's Route Helpers Work Inside the Engine | 2026-09-16 | Accepted |
|
|
69
70
|
| 023 | Vitral Is a Theme, Not the Stylesheet | 2026-09-16 | Accepted |
|
|
70
71
|
| 024 | Selecting More Than One Value | 2026-09-16 | Accepted |
|
|
72
|
+
| 025 | Janela Bounds What a Filter Can Ask For | 2026-09-17 | Accepted |
|
|
71
73
|
|
|
72
74
|
## Next number
|
|
73
75
|
|
|
74
|
-
Next ADR:
|
|
76
|
+
Next ADR: 026
|
data/lib/janela/host_routes.rb
CHANGED
|
@@ -25,6 +25,12 @@ module Janela
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def self.forwarded(host: Rails.application.routes, engine: Janela::Engine.routes)
|
|
28
|
+
# Route loading is lazy, so a call before anything else has drawn the
|
|
29
|
+
# host's routes would otherwise see an empty set rather than an error
|
|
30
|
+
# (issue #37). This does not recurse: define! runs from
|
|
31
|
+
# after_routes_loaded, by which point the reloader has already marked
|
|
32
|
+
# itself loaded.
|
|
33
|
+
Rails.application.reload_routes_unless_loaded
|
|
28
34
|
host.named_routes.helper_names - engine.named_routes.helper_names
|
|
29
35
|
end
|
|
30
36
|
end
|
data/lib/janela/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: janela
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jay Killeen
|
|
@@ -160,6 +160,7 @@ files:
|
|
|
160
160
|
- docs/decisions/022-host-route-helpers-work-inside-the-engine.md
|
|
161
161
|
- docs/decisions/023-vitral-is-a-theme-not-the-stylesheet.md
|
|
162
162
|
- docs/decisions/024-selecting-more-than-one-value.md
|
|
163
|
+
- docs/decisions/025-janela-bounds-what-a-filter-can-ask-for.md
|
|
163
164
|
- docs/decisions/INDEX.md
|
|
164
165
|
- docs/multi-tenancy.md
|
|
165
166
|
- docs/naming.md
|
|
@@ -182,15 +183,6 @@ metadata:
|
|
|
182
183
|
changelog_uri: https://github.com/retail-tasker/janela/blob/main/CHANGELOG.md
|
|
183
184
|
bug_tracker_uri: https://github.com/retail-tasker/janela/issues
|
|
184
185
|
rubygems_mfa_required: 'true'
|
|
185
|
-
post_install_message: |
|
|
186
|
-
Janela 0.4.0 selects more than one value in a dimension. Most applications
|
|
187
|
-
need do nothing. You need to act only if you override a pane view, where
|
|
188
|
-
selected_value is now selected_values, or if something of yours reads
|
|
189
|
-
Janela's URLs, where a click now writes q[field_in][] rather than
|
|
190
|
-
q[field_eq]. Links already shared keep working.
|
|
191
|
-
|
|
192
|
-
Steps: UPGRADING.md in this gem, or
|
|
193
|
-
https://github.com/retail-tasker/janela/blob/main/UPGRADING.md
|
|
194
186
|
rdoc_options: []
|
|
195
187
|
require_paths:
|
|
196
188
|
- lib
|