Commit 48f6c7a
* feat(#103): metrics page — per-user, per-box draggable layout (GridStack)
Closes #103. The Metrics page's chart grid becomes drag + resize
editable, with the layout persisted per (user, box) so each operator
gets their own view per box. Reuses Home gear's GridStack vendor
bundle — no new dependency.
DB (new metrics_layouts table):
- Primary key (user_id, server_id) → layout_json blob. JSON beats
per-tile rows because every read/write is the whole layout and
there's no querying inside it. The blob is GridStack's save()
output verbatim.
- New file: internal/framework/database/metrics_layouts.go with
GetMetricsLayout / SaveMetricsLayout / DeleteMetricsLayout +
ErrNoMetricsLayout sentinel for the "no row" case.
- Schema wired into database.New() right after the Home schema.
Handler + routes:
- New file: internal/framework/handler/api_metrics_layout.go.
- GET /api/{boxID}/metrics/layout — returns saved JSON or 204
(caller falls back to template defaults; mid-rollout-safe).
- PATCH /api/{boxID}/metrics/layout — upsert. Body validated as
a JSON array of {id,x,y,w,h}; capped at 16 KiB. Tile IDs aren't
re-validated against the known set because the dashboard
silently ignores unknown ids — keeps the storage layer
agnostic to GridStack version changes.
- DELETE /api/{boxID}/metrics/layout — drops the row so the next
load uses defaults. Drives the "Reset layout" button.
- Routes registered in cmd/server/main.go alongside the existing
/metrics endpoints.
Templ (metrics.templ):
- Wrapper panel removed; each chart card now sits inside a
.grid-stack-item with default gs-x / gs-y / gs-w / gs-h
attributes. Card order rearranged to the layout proposed in
#103: CPU/Memory · Network/Response Time · Health/Errors ·
Sessions & Requests (full-width) · per-source rows.
- Edit toggle button (.metrics-edit-off-label / .metrics-edit-
on-label) + Reset button (.metrics-edit-only class, hidden
outside edit mode) added to the controls bar. Mirrors Home's
edit-toggle UX.
- GridStack vendor CSS loaded inline; vendor JS + new
metrics-layout.js script tags added to the page bottom (defer).
- applyCapabilities() now dispatches a 'metrics:capabilities-
applied' CustomEvent on every run so metrics-layout.js can
reflow GridStack without either script knowing about the
other's globals.
JS (new static/js/metrics-layout.js):
- Initialises GridStack on #charts-grid (read-only by default).
- Loads saved layout via GET; 204 falls back to template defaults
so first-visit users see the documented order without an
endpoint round-trip's worth of flicker.
- Edit-mode toggle calls setStatic(false), shows .metrics-edit-
only controls, and persists on exit. PATCH is debounced so a
multi-tile drag flush ends in one save call, not N.
- Capability-driven reflow: listens for the CustomEvent above
and calls gs.removeWidget(item, false) / gs.makeWidget(item)
per-tile based on each card's .hidden class. Unavailable
sources drop out of the grid (other tiles compact up) and
return to their saved positions when the source reappears.
- Reset button DELETEs the row and reloads, restoring template
defaults.
Out of scope (deferred):
- Layout import/export. Single (user, box) layouts only — bulk
ops can land as a follow-up when multi-box editing is a thing.
- Sharing layouts between users.
- Mobile-specific layouts. GridStack auto-compacts on narrow
viewports — same behaviour Home relies on, no metrics-specific
treatment needed.
go build / go vet / go test / templ generate all clean. ~580 LOC
new code (DB + handler + JS), plus templ edits across the page.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#103): address Copilot review findings on PR #104
JS:
- metrics-layout.js: getServerID was missing the single-server
fallback (#default-server-id) that metrics.templ's inline helper
consults. Single-box installs silently no-op'd every layout
GET/PATCH/DELETE because serverID came back empty. Now matches
the inline helper byte-for-byte: select → default-server-id →
URL.
- metrics-layout.js: saveLayout now takes a {flush: true} option
used by the edit-mode exit handler. Without flush, a tab-close
right after "Done editing" could drop the layout because the
debounce was still pending. Drag/resize calls still use the
debounced path so a multi-tile drag flushes once, not N times.
- metrics-layout.js: resetLayout now cancels any pending debounced
PATCH AND suppresses further saves before issuing the DELETE.
Without this a fast drag-then-reset race could land a PATCH
after the DELETE and resurrect the row the user just threw away.
- metrics-layout.js: GridStack margin 8 → 16. Comment had claimed
16 (matches Tailwind gap-4 and Home's gridstack init); value
was the typo. Matched to Home now so the visual idiom is
consistent across pages.
Handler:
- api_metrics_layout.go: PATCH now runs validateLayoutTiles before
persisting. Previously a malformed client could land tiles with
empty IDs, negative coords, zero/negative dimensions, or
duplicate IDs that would silently corrupt the saved blob. New
validator enforces: non-empty id, x/y in [0, maxCoord], w/h in
[1, maxDim], unique ids across the array, tile count in [1,
maxTilesPerLayout=64]. First failure returned with an actionable
message (one good error beats a list when the source is a JS
caller, not a hand-edited file).
- api_metrics_layout_test.go (new): table-driven coverage of
every validateLayoutTiles rejection path plus the happy path.
- metrics_layouts_test.go (new): DB round-trip coverage for
GetMetricsLayout (empty → ErrNoMetricsLayout sentinel), Save
upsert behaviour, isolation across (user, box) pairs, Delete
resets-to-default flow, Delete no-op on absent row.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#103): chart canvases overflowing GridStack tiles
The chart-aspect-container's padding-bottom: 72% trick sizes the
inner canvas from the parent's width — which worked under the old
flex grid (every card was the same shape, height matched width
naturally) but breaks under GridStack: the tile's height is
gs-h × cellHeight (320px on a default tile), independent of the
tile's width. Where width × 72% > 320px, the canvas overflowed
the tile.
Visible result on a typical viewport: legends + time-axis labels
bleeding out of every chart card into the rows below them
(screenshot in the PR thread).
Fix: scope `.chart-aspect-container` so it behaves as a flex-fill
container inside the GridStack grid:
- #charts-grid .grid-stack-item-content → display: flex,
flex-direction: column, height: 100%, overflow: hidden so the
chart container can flex to the GridStack-set height and
stragglers can't escape during resize transitions.
- #charts-grid .chart-aspect-container → flex: 1 1 auto, min-height: 0,
padding-bottom: 0. min-height: 0 is the flex-child idiom that
lets the container shrink below its content's intrinsic size —
without it, the chart refused to render below ~432px.
- The original aspect-ratio rule stays in place for usages
outside the metrics grid (drill-down drawer, etc.).
The canvas absolute-fill rule (width/height: 100% !important)
was already correct; it just needed a parent with a real flex-
computed height to fill.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#103): chart-grid container height + bottom spacing
Two issues from PR #104 manual testing:
1. The GridStack container's auto-height tracking lagged the
actual tile layout when capability-driven removeWidget() calls
ran during init. Result: whatever section came after the grid
(Error Insights panel) was rendering on top of the bottom row
of tiles because the grid's height didn't reflect them.
Fix: compute min-height manually as max(y+h) * cellHeight and
set it on the container after every load / capability flip /
layout change. GridStack's own auto-sizing still runs as
normal — this is belt + suspenders for the staticGrid +
removeWidget combination where the engine's height update
was racing the visible layout.
2. No bottom margin between the grid and the next section.
Fix: add mb-6 to #charts-grid so it pushes following content
down by the same 24px gap that separates the KPI band from
the grid above.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#103): capability-hidden tiles staying visible (CSS specificity bug)
Root cause: my new flex-fill rule
#charts-grid .grid-stack-item-content { display: flex; ... }
has higher specificity (0,1,2,0) than Tailwind's .hidden utility
(0,0,1,0) and was winning the cascade. So when applyCapabilities()
correctly added .hidden to capability-not-installed cards (caddy /
traefik / nginx / apache on a HAProxy-only host like light-hugger),
the display: flex from my rule overrode display: none from .hidden
— the cards stayed visible with their titles + empty chart areas.
The agent's probes and the JS hide logic were both doing the right
thing the whole time; the override was silent because dev-tools'
computed-style panel just shows "display: flex" without flagging
which rule it lost to.
Fix: scope the flex-fill rule with :not(.hidden) so .hidden wins
the cascade for capability-hidden cards. Now display: none kicks
in as intended, the tile vanishes visually, and the container-
height recompute brings the bottom edge up to whatever the last
*visible* tile ends — no more ghost slots reserving space below
the layout.
Same bug would have hit no-HAProxy hosts (the HAProxy cards
would stay visible despite haproxyAvailable=false). Catching it
before that footgun lands in front of a real user.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#103): bump GridStack margin to 24 for vertical breathing room
16px between rows blended together in dark mode — the card
backgrounds and the page background are similar enough greys that
the gap visually disappeared, especially between Server Health /
Error Rates and the full-width Sessions & Requests tile below.
24 (12px around every tile → 24px between neighbours) gives the
rows a visible separation without spreading the grid wide enough
to hurt density on smaller displays.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#103): asymmetric GridStack margins (taller rows, same columns)
A uniform margin applied the same gap horizontally and vertically.
With charts having axis labels stacked top + bottom of each tile,
the visual content of adjacent rows ends up closer together than
adjacent columns — so a single margin value that looked spacious
horizontally felt cramped vertically (rows visibly merged together
under the full-width Sessions & Requests tile).
GridStack supports four directional margins. Going with:
marginTop / marginBottom: 18 → 36px between rows
marginLeft / marginRight: 12 → 24px between side-by-side
(The dev-tools "outer frame doesn't match inner content" the user
noticed is GridStack's design — the outer holds the grid slot and
the inner is inset by margin/2 on each edge. With asymmetric
margins the inset is asymmetric too, which is correct.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#103): GridStack cellHeight() side-effects + bump vertical margins
Two interlocking issues that together produced the merged-row look.
1. GridStack's cellHeight() (the bare getter, no args) is
actually an IMPLICIT SETTER: it recomputes opts.cellHeight as
cellWidth + (marginTop+marginBottom - marginLeft-marginRight)
whenever called, and regenerates the per-y/per-h CSS rules
with the new value. My updateContainerHeight() called it on
every load / capability flip / drag, so opts.cellHeight kept
drifting away from the configured 80 toward whatever the
current container width yielded. That's why bumping margins
never landed exactly where the math predicted — every margin
change shifted the cellWidth-derived cellHeight as a side
effect.
Fix: use getCellHeight(false). Same return value, no setter
side effect, no CSS-rule regeneration.
2. With the cellHeight finally stable at 80, the rows still felt
tight at the prior 18 top/bottom margin. Bumped to 24 each
(48px between rows) which lines up with what the user
sketched as "Better" in the dev-tools screenshot (top: 990
for the y=12 tile = 12*80 + 24/2 + 24/2 in the new math).
Horizontal margins stay at 12 each (24px between side-by-side)
because that gap already looked right.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#103): remove height:100% from inner card — was eating the row gap
Diagnosed via Chrome MCP direct DOM inspection. Three rules were
fighting over the inner card's size:
GridStack vendor CSS → position: absolute; top: 24px; bottom: 24px
My rule on #charts-grid → height: 100%
CSS spec when over-constrained → height wins
Resolved: inner ended up 320px tall (matching outer's full height)
anchored at top:24, so it extended 24px BELOW the outer's bottom —
exactly where the next row's outer begins. Visual result: zero gap
between adjacent rows' inner cards, no matter what margin value I
set on the GridStack init. That's why margins 18, 24, 36 all looked
identical — the cells were always butting up against each other.
Fix: drop the `height: 100%`. GridStack's inline top + bottom is
sufficient to size the absolute-positioned inner content (inner
height becomes outer_height − marginTop − marginBottom = 272px for
gs-h=4, leaving a 48px gap between rows that matches the configured
margins).
Verified via Chrome MCP after fix:
inner height: 272 (was 320, was overrunning by 24)
visible gap between row 0 inner bottom and row 4 inner top: 48px
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#103): match chart-grid spacing to the KPI band (12px both axes)
After the previous height:100% fix exposed the real margin
spacing, 48px vertical felt too generous compared to the KPI
band's tight 12px gap-3 grid directly above. Drop GridStack
margins to 6 on every side → 12px gap on both axes, matching
the KPI band exactly so the whole metrics page reads as one
consistently-spaced surface.
Verified via Chrome MCP after fix:
horizontal gap between adjacent inner cards: 12px
vertical gap between adjacent inner cards: 12px
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(#103): align chart grid with KPI band (top gap + outer edges)
The KPI band has no outer inset on its cards (Tailwind grid + gap-3),
so its leftmost card sits flush with the page-content edge. The
chart grid below was 6px inset on left/right and 22px below the KPI
band (16px from KPI's mb-4 + 6px GridStack first-row inset). The
result was a 6px shift right + a too-large vertical gap that broke
the consistent 12px rhythm.
Fix: pull #charts-grid outward with negative margins that absorb
GridStack's outer inset:
margin-left: -6 → first column visible-left aligns with KPI
margin-right: -6 → last column visible-right aligns with KPI
margin-top: -10 → 16 (KPI mb-4) - 10 + 6 (gs marginTop) = 12px gap
Verified via Chrome MCP:
kpiContainer.left = chart first-inner.left (280 = 280)
kpiContainer.right = chart last-inner.right (1428.5 = 1428.5)
topGap = 12
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(#103): standardise gear edit/settings affordance on a cog icon
Replace the Home and Metrics gears' Edit toggles with an icon-only
cog button anchored at the top-right of each page's header. Same
SVG path as the main sidebar Settings cog so the visual vocabulary
stays consistent and a future contributor reading either gear sees
the same affordance.
Shared styling in static/css/components/buttons.css under the
.gear-cog-btn class — aria-pressed="true" tints the button blue so
the edit-mode state is glanceable. !important is needed because the
buttons carry Tailwind dark:bg-slate-* utility classes that win the
cascade under the Tailwind v4 Play CDN (issue #99) regardless of
selector specificity or source order; flagging the pressed-state
palette as !important is the lowest-friction way to make it
deterministic without stripping the utility classes off every cog.
Verified via Chrome MCP direct DOM inspection:
- Home cog rendered at top:10, right:1436 (24px from the 1460px
viewport — upper-right corner as requested)
- Click flips aria-pressed false ↔ true
- Pressed state's background is rgba(30,58,138,0.4), color is
rgb(147,197,253), border is rgba(96,165,250,0.5) — the dark-mode
blue palette, overriding the slate dark:* utilities as intended
Home gear: moved cog AFTER the "+ Add tile" button so it lands at
the actual far right of the header row.
JS updates set aria-pressed on click in both home.js and
metrics-layout.js (kept the legacy label-toggle code path in home.js
in case a future variant re-introduces text labels).
Future gears can adopt the cog affordance by:
1. Adding a `<button class="gear-cog-btn ..." aria-pressed="false">`
with the canonical SVG.
2. Toggling aria-pressed from their JS edit-mode handler.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Enhance accessibility and user experience across modals and dialogs
- Added ARIA roles and labels to modals in haproxy_config templates for improved screen reader support.
- Included aria-label attributes on buttons to provide context for actions (e.g., close, cancel).
- Implemented live value displays next to chart titles in metrics page for better visibility of current data.
- Updated keydown event handlers to prevent default actions on Escape key to avoid unintended navigation or dialog closures.
- Improved handling of focusable elements and overlays to ensure a smoother user experience when interacting with modals.
* feat: implement InfoTooltip component and integrate into metrics layout
* fix(#103): address Copilot review findings on PR #104 (round 2)
Addresses the unresolved threads on PR #104 raised across the recent
review passes. Each change is one-to-one with a finding:
- metrics.templ: in-templ fullscreen Esc handler now preventDefaults
so it doesn't fall through to the global Esc → history.back().
- metrics-layout.js patchLayout(): check `res.ok` and console.warn on
non-2xx so 401/403/413/500 saves no longer fail silently.
- metrics-layout.js applyCapabilityHiding(): switch the capability
marker from `.hidden` to a dedicated `data-cap-hidden` attribute so
fullscreen-driven `.hidden`s on neighbouring cards can't get
mis-interpreted as capability hides. applyCapabilities() in the
templ sets the new attribute; per-source templ cards carry the
initial `data-cap-hidden="true"` so the marker is correct before
the first applyCapabilities() pass runs.
- metrics-layout.js: snapshot the (x,y,w,h) of each tile we remove
from the engine into `capHiddenSnapshots` and merge those entries
into `captureLayout()`, so a tile's last-known position survives
a capability flip → re-flip cycle.
- metrics-layout.js loadSavedLayout(): pass `addRemove:false` to
`gs.load()` — without it GridStack yanks any DOM tile whose gs-id
isn't in the loaded blob, which would permanently strip the
capability-hidden DOM nodes we still need to re-attach later.
- metrics-layout.js: fix updateContainerHeight() comment to describe
what the code actually does (no margin term).
- metrics.templ: rewrite the "waits on DOMContentLoaded" script-tag
comment to match the actual `defer` execution semantics.
- api_metrics_layout.go: normalise omitted `w`/`h` (GridStack drops
them when they equal the default of 1) before the positive check,
so resizing a tile to 1×N or N×1 no longer fails validation. Plus
add column-count validation (x < 12, x+w ≤ 12) so a misbehaving
client can't persist tiles that will silently clamp on next
render. Tests cover both new behaviours plus a regression for the
omitted-w/h case.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0384a1e commit 48f6c7a
24 files changed
Lines changed: 1883 additions & 134 deletions
File tree
- gearbox
- cmd/server
- internal
- framework
- database
- handler
- templates
- components
- layouts
- pages
- haproxy_config
- gears/home
- static
- css/components
- js
- charts
- common
- gears
- haproxy_config
- os-updates
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
175 | 175 | | |
176 | 176 | | |
177 | 177 | | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
178 | 213 | | |
179 | 214 | | |
180 | 215 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | | - | |
19 | | - | |
20 | 18 | | |
| 19 | + | |
21 | 20 | | |
| 21 | + | |
22 | 22 | | |
23 | 23 | | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
24 | 27 | | |
25 | 28 | | |
26 | 29 | | |
27 | 30 | | |
28 | | - | |
29 | | - | |
30 | | - | |
31 | 31 | | |
32 | 32 | | |
33 | 33 | | |
| |||
383 | 383 | | |
384 | 384 | | |
385 | 385 | | |
386 | | - | |
387 | | - | |
388 | | - | |
389 | | - | |
390 | | - | |
391 | | - | |
392 | | - | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
393 | 393 | | |
394 | 394 | | |
395 | 395 | | |
| |||
540 | 540 | | |
541 | 541 | | |
542 | 542 | | |
543 | | - | |
| 543 | + | |
544 | 544 | | |
545 | 545 | | |
546 | 546 | | |
| |||
704 | 704 | | |
705 | 705 | | |
706 | 706 | | |
| 707 | + | |
| 708 | + | |
| 709 | + | |
| 710 | + | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
| 714 | + | |
707 | 715 | | |
708 | 716 | | |
709 | 717 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
102 | 102 | | |
103 | 103 | | |
104 | 104 | | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
105 | 111 | | |
106 | 112 | | |
107 | 113 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
0 commit comments