mbeditor 0.12.8 → 0.13.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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +207 -0
  3. data/app/assets/javascripts/mbeditor/application.js +7 -1
  4. data/app/assets/javascripts/mbeditor/collaboration_service.js +31 -0
  5. data/app/assets/javascripts/mbeditor/color_provider.js +5 -0
  6. data/app/assets/javascripts/mbeditor/components/CollapsibleSection.js +3 -1
  7. data/app/assets/javascripts/mbeditor/components/EditorPanel.js +135 -40
  8. data/app/assets/javascripts/mbeditor/components/FileTree.js +6 -1
  9. data/app/assets/javascripts/mbeditor/components/GitPanel.js +10 -1
  10. data/app/assets/javascripts/mbeditor/components/ImportConflictModal.js +15 -8
  11. data/app/assets/javascripts/mbeditor/components/ImportDialog.js +216 -0
  12. data/app/assets/javascripts/mbeditor/components/MbeditorApp.js +629 -137
  13. data/app/assets/javascripts/mbeditor/components/ModelGraph.js +25 -6
  14. data/app/assets/javascripts/mbeditor/components/ProblemsPanel.js +218 -13
  15. data/app/assets/javascripts/mbeditor/components/QuickOpenDialog.js +39 -33
  16. data/app/assets/javascripts/mbeditor/components/TabBar.js +11 -2
  17. data/app/assets/javascripts/mbeditor/components/TestRunPanel.js +312 -0
  18. data/app/assets/javascripts/mbeditor/editor_plugins.js +444 -116
  19. data/app/assets/javascripts/mbeditor/file_import.js +78 -0
  20. data/app/assets/javascripts/mbeditor/file_service.js +122 -18
  21. data/app/assets/javascripts/mbeditor/git_service.js +130 -8
  22. data/app/assets/javascripts/mbeditor/history_service.js +33 -38
  23. data/app/assets/javascripts/mbeditor/log_service.js +4 -0
  24. data/app/assets/javascripts/mbeditor/search_service.js +30 -2
  25. data/app/assets/javascripts/mbeditor/tab_manager.js +59 -6
  26. data/app/assets/javascripts/mbeditor/websocket_service.js +88 -4
  27. data/app/assets/stylesheets/mbeditor/editor.css +302 -55
  28. data/app/channels/mbeditor/channel_authentication.rb +7 -0
  29. data/app/channels/mbeditor/editor_channel.rb +6 -3
  30. data/app/controllers/mbeditor/application_controller.rb +5 -0
  31. data/app/controllers/mbeditor/editors_controller.rb +173 -29
  32. data/app/controllers/mbeditor/git_controller.rb +3 -3
  33. data/app/controllers/mbeditor/logs_controller.rb +3 -1
  34. data/app/services/mbeditor/collaboration_doc_store.rb +7 -0
  35. data/app/services/mbeditor/editor_state_service.rb +36 -26
  36. data/app/services/mbeditor/exclusion_matcher.rb +12 -10
  37. data/app/services/mbeditor/file_operation_service.rb +38 -3
  38. data/app/services/mbeditor/file_tree_service.rb +30 -2
  39. data/app/services/mbeditor/git_combined_diff_service.rb +13 -3
  40. data/app/services/mbeditor/git_commit_detail_service.rb +20 -16
  41. data/app/services/mbeditor/git_diff_service.rb +5 -1
  42. data/app/services/mbeditor/git_info_service.rb +20 -8
  43. data/app/services/mbeditor/git_line_diff_service.rb +6 -2
  44. data/app/services/mbeditor/git_service.rb +65 -15
  45. data/app/services/mbeditor/js_definition_service.rb +3 -1
  46. data/app/services/mbeditor/js_globals_service.rb +7 -4
  47. data/app/services/mbeditor/js_members_service.rb +3 -2
  48. data/app/services/mbeditor/js_program_service.rb +15 -5
  49. data/app/services/mbeditor/js_syntax_check_service.rb +15 -4
  50. data/app/services/mbeditor/process_runner.rb +42 -12
  51. data/app/services/mbeditor/ri_definition_service.rb +8 -1
  52. data/app/services/mbeditor/route_service.rb +45 -8
  53. data/app/services/mbeditor/rubocop_run_service.rb +86 -0
  54. data/app/services/mbeditor/ruby_definition_service.rb +23 -4
  55. data/app/services/mbeditor/schema_service.rb +65 -64
  56. data/app/services/mbeditor/search_replace_service.rb +75 -10
  57. data/app/services/mbeditor/test_runner_service.rb +98 -10
  58. data/lib/mbeditor/cable_log_filter.rb +8 -2
  59. data/lib/mbeditor/configuration.rb +12 -1
  60. data/lib/mbeditor/editor_bootstrap.rb +18 -12
  61. data/lib/mbeditor/engine.rb +22 -3
  62. data/lib/mbeditor/exception_log.rb +2 -2
  63. data/lib/mbeditor/pending_migrations.rb +33 -0
  64. data/lib/mbeditor/rack/handle_pending_migrations.rb +25 -15
  65. data/lib/mbeditor/rack/pending_migration_bypass.rb +104 -0
  66. data/lib/mbeditor/rack/silence_ping_request.rb +10 -2
  67. data/lib/mbeditor/route_map.rb +2 -0
  68. data/lib/mbeditor/ruby_lsp_client.rb +71 -12
  69. data/lib/mbeditor/version.rb +1 -1
  70. metadata +7 -2
@@ -8,10 +8,86 @@
8
8
  box-shadow: none !important;
9
9
  }
10
10
 
11
- /* Prevent Pico CSS from applying its primary-colour focus/active state to IDE buttons.
12
- Pico sets --pico-background-color and --pico-border-color to its primary blue on
13
- button:focus, which bleeds through as a blue tint on any button not explicitly
14
- overriding its background with a literal value. */
11
+ /* ── Pico containment ──────────────────────────────────────────────────────
12
+ *
13
+ * The host app's stylesheet loads alongside this one, and when it is Pico (the
14
+ * dummy app's is) it styles bare element and ARIA selectors — `button`,
15
+ * `[role=button]`, `[role=group]`, `[disabled]` — every one of which matches
16
+ * this UI's markup. Two shipped bugs came from exactly that: `[role=button]`
17
+ * painted the tab close buttons as filled primary squares, and
18
+ * `[disabled] { pointer-events: none }` stopped disabled toolbar buttons from
19
+ * receiving hover at all, so their tooltips never appeared.
20
+ *
21
+ * The technique is to zero Pico's own custom properties: every Pico button
22
+ * declaration reads from them, so neutralising the variables disarms the whole
23
+ * ruleset at once, which beats fighting each declaration on specificity. Only
24
+ * the few properties Pico hard-codes need explicit overrides.
25
+ *
26
+ * Two things make this safe to widen, and both depend on where the block sits:
27
+ *
28
+ * - The root is wrapped in :where(), so these rules carry exactly Pico's own
29
+ * specificity, no more. They win over Pico only because the whole of
30
+ * editor.css is bundled after the whole of Pico.
31
+ * - The block lives at the TOP of this file, so every component rule below —
32
+ * .statusbar-btn, .project-actions, .tab-close — is later at equal or
33
+ * greater specificity and still wins.
34
+ *
35
+ * That combination is what makes this a floor rather than a new source of
36
+ * truth: it can only remove Pico's opinions, never override this file's own.
37
+ * Moving it further down would silently invert that.
38
+ *
39
+ * Deliberately NOT reset: Pico's `*` box-sizing and background-repeat rules
40
+ * (this UI assumes border-box, which is what Pico sets), and `position` on
41
+ * [role=group] (Pico's `relative` is the safer value for any absolutely
42
+ * positioned descendant, and nothing here needs it to be static).
43
+ *
44
+ * Monaco is listed separately because its widgets portal out of the shell to
45
+ * document.body (.context-view, .action-widget), so a #mbeditor-root scope
46
+ * would miss them. Monaco renders its own actions as <a role="button">.
47
+ */
48
+ :where(#mbeditor-root) :is(button, [type="button"], [type="submit"], [type="reset"], [role="button"], [role="group"]),
49
+ .monaco-hover [role=button],
50
+ .action-widget [role=button],
51
+ .context-view [role=button],
52
+ .monaco-editor [role=button],
53
+ .monaco-diff-editor [role=button] {
54
+ --pico-background-color: transparent;
55
+ --pico-border-color: transparent;
56
+ --pico-color: inherit;
57
+ --pico-box-shadow: none;
58
+ --pico-form-element-spacing-vertical: 0;
59
+ --pico-form-element-spacing-horizontal: 0;
60
+ }
61
+
62
+ /* Properties Pico hard-codes, which the variable reset above cannot reach. */
63
+
64
+ /* Pico's `[disabled] { pointer-events: none }` means a disabled control never
65
+ * receives hover, so neither a title attribute nor a CSS hover tooltip can
66
+ * fire on it — and a disabled button is exactly when a user most wants to be
67
+ * told what it is. */
68
+ :where(#mbeditor-root) [disabled],
69
+ :where(#mbeditor-root) [aria-disabled="true"] {
70
+ pointer-events: auto;
71
+ }
72
+
73
+ /* Pico lays [role=group] out as a full-width bordered input group. It is used
74
+ * here only to label toolbars for screen readers. */
75
+ :where(#mbeditor-root) [role="group"] {
76
+ display: inline-flex;
77
+ width: auto;
78
+ margin-bottom: 0;
79
+ }
80
+
81
+ /* Pico gives every button a bottom margin to space form controls. */
82
+ :where(#mbeditor-root) :is(button, [type="button"], [type="submit"], [type="reset"]) {
83
+ margin: 0;
84
+ }
85
+
86
+ /* Pico's focus/active state sets its primary blue on --pico-background-color
87
+ and --pico-border-color, which bleeds through as a blue tint on any button
88
+ not overriding its background with a literal value. Kept separate from the
89
+ block above: this one is intentionally more specific than a component rule,
90
+ because no component should be able to opt back into a Pico focus colour. */
15
91
  #mbeditor-root button:not(.pico-btn):focus,
16
92
  #mbeditor-root button:not(.pico-btn):focus-visible,
17
93
  #mbeditor-root button:not(.pico-btn):active {
@@ -129,57 +205,66 @@ html, body, #mbeditor-root {
129
205
 
130
206
  /* ── Activity Bar (VSCode-style, always visible) ───────── */
131
207
  .ide-activity-bar {
132
- width: 48px;
133
- min-width: 48px;
208
+ width: 60px;
209
+ min-width: 60px;
134
210
  flex-shrink: 0;
135
211
  background: var(--ide-panel-bg);
136
212
  border-right: 1px solid var(--ide-border);
137
213
  display: flex;
138
214
  flex-direction: column;
139
- align-items: center;
140
- padding: 4px 0;
215
+ /* stretch, not center: the buttons fill the bar edge to edge so the active
216
+ indicator can sit flush against the left of the screen. Centering them
217
+ left a gutter the indicator was inset by. */
218
+ align-items: stretch;
219
+ padding: 0;
141
220
  z-index: 10;
142
221
  }
143
222
 
144
223
  .ide-activity-bar-top {
145
224
  display: flex;
146
225
  flex-direction: column;
147
- align-items: center;
148
- gap: 2px;
226
+ align-items: stretch;
227
+ gap: 0;
149
228
  flex: 1;
150
229
  }
151
230
 
152
231
  .ide-activity-bar-bottom {
153
232
  display: flex;
154
233
  flex-direction: column;
155
- align-items: center;
156
- gap: 2px;
157
- padding-bottom: 4px;
234
+ align-items: stretch;
235
+ gap: 0;
158
236
  }
159
237
 
160
- .ide-activity-btn {
161
- width: 36px;
162
- height: 36px;
238
+ /* Scoped through the bar so the glyph size outranks the global
239
+ `button:not(.pico-btn) { font-size: 12px }` reset, which is more specific
240
+ than a bare class and was silently winning. */
241
+ .ide-activity-bar .ide-activity-btn {
242
+ width: 100%;
243
+ height: 60px;
163
244
  background: transparent;
164
245
  border: none;
246
+ /* Always reserve the indicator's width so the glyph does not shift sideways
247
+ by a pixel the moment a panel is opened. */
248
+ border-left: 2px solid transparent;
165
249
  border-radius: 0;
166
250
  color: var(--ide-text-muted);
167
251
  display: flex;
168
252
  align-items: center;
169
253
  justify-content: center;
170
254
  cursor: pointer;
171
- font-size: 15px;
172
- transition: background 0.12s ease, color 0.12s ease;
255
+ font-size: 25px;
256
+ transition: color 0.12s ease;
173
257
  }
174
258
 
175
- .ide-activity-btn:hover {
176
- background: var(--ide-hover-bg);
259
+ /* The glyph carries the state, not a filled box behind it. */
260
+ .ide-activity-bar .ide-activity-btn:hover {
261
+ background: transparent;
177
262
  color: var(--ide-text);
178
263
  }
179
264
 
180
- .ide-activity-btn.active {
265
+ .ide-activity-bar .ide-activity-btn.active {
181
266
  color: var(--ide-accent-fg);
182
- border-left: 2px solid var(--ide-accent-fg);
267
+ border-left-color: var(--ide-accent-fg);
183
268
  }
184
269
 
185
270
  .sidebar-panel-title {
@@ -300,25 +385,25 @@ html, body, #mbeditor-root {
300
385
  border: 1px solid var(--ide-accent);
301
386
  border-radius: 3px;
302
387
  color: var(--ide-text);
303
- font-size: 12px;
388
+ font-size: 14px;
304
389
  font-family: inherit;
305
390
  padding: 1px 4px;
306
391
  outline: none;
307
- height: 18px;
308
- line-height: 18px;
392
+ height: 21px;
393
+ line-height: 21px;
309
394
  }
310
395
 
311
396
  .tree-item-icon {
312
- width: 16px;
313
- margin-right: 5px;
314
- font-size: 12px;
397
+ width: 20px;
398
+ margin-right: 6px;
399
+ font-size: 14px;
315
400
  flex-shrink: 0;
316
401
  text-align: center;
317
402
  }
318
403
 
319
404
  .tree-folder-icon { color: #dcb67a; }
320
405
  .tree-file-icon { color: #78b4e0; }
321
- .tree-item-name { flex: 1; overflow: hidden; text-overflow: ellipsis; font-size: 12px; }
406
+ .tree-item-name { flex: 1; overflow: hidden; text-overflow: ellipsis; font-size: 14px; }
322
407
 
323
408
  /* ── Custom File Icons ───────────────────────────────────── */
324
409
  .ruby-icon { color: #f26262 !important; }
@@ -1412,7 +1497,90 @@ html, body, #mbeditor-root {
1412
1497
  cursor: pointer;
1413
1498
  }
1414
1499
  .import-conflict-btn:hover { background: var(--ide-hover-bg, #4a4a4a); }
1500
+ .import-conflict-btn:disabled { opacity: 0.45; cursor: default; }
1415
1501
  .import-conflict-btn-danger { border-color: var(--ide-error, #f48771); color: var(--ide-error, #f48771); }
1502
+ .import-conflict-btn-primary {
1503
+ background: var(--ide-accent, #0e639c);
1504
+ border-color: var(--ide-accent, #0e639c);
1505
+ color: #fff;
1506
+ }
1507
+ .import-conflict-btn-primary:hover:not(:disabled) { background: var(--ide-accent-hover, #1177bb); }
1508
+
1509
+ /* Upload dialog — the pointed-at half of drag-and-drop import */
1510
+ .import-dialog { min-width: 460px; max-width: 620px; }
1511
+ .import-dialog .schema-modal-body { padding: 12px 16px; }
1512
+ .import-pick-row { display: flex; gap: 8px; }
1513
+ .import-pick-btn {
1514
+ background: var(--ide-input-bg, #3c3c3c);
1515
+ color: var(--ide-text, #d4d4d4);
1516
+ border: 1px solid var(--ide-border, #3c3c3c);
1517
+ border-radius: 3px;
1518
+ padding: 6px 12px;
1519
+ font-size: 12px;
1520
+ cursor: pointer;
1521
+ }
1522
+ .import-pick-btn:hover { background: var(--ide-hover-bg, #4a4a4a); }
1523
+ .import-pick-btn i { margin-right: 6px; opacity: 0.75; }
1524
+ .import-dialog-empty,
1525
+ .import-dialog-warning {
1526
+ margin-top: 12px;
1527
+ font-size: 12px;
1528
+ color: var(--ide-text-muted, #888);
1529
+ }
1530
+ .import-dialog-warning { color: var(--ide-warning, #cca700); }
1531
+ .import-dialog-section-title {
1532
+ margin: 16px 0 6px;
1533
+ font-size: 10px;
1534
+ font-weight: 600;
1535
+ text-transform: uppercase;
1536
+ letter-spacing: 0.06em;
1537
+ color: var(--ide-text-muted, #888);
1538
+ }
1539
+ .import-dest-options { display: flex; flex-direction: column; gap: 2px; }
1540
+ .import-dest-option {
1541
+ display: flex;
1542
+ align-items: center;
1543
+ gap: 8px;
1544
+ width: 100%;
1545
+ text-align: left;
1546
+ background: none;
1547
+ border: none;
1548
+ border-radius: 3px;
1549
+ padding: 4px 6px;
1550
+ color: var(--ide-text, #d4d4d4);
1551
+ font-size: 12px;
1552
+ cursor: pointer;
1553
+ }
1554
+ .import-dest-option:hover { background: var(--ide-hover, rgba(255,255,255,0.06)); }
1555
+ .import-dest-option.active { background: var(--ide-selection-bg, rgba(14,99,156,0.35)); }
1556
+ .import-dest-radio { opacity: 0.7; font-size: 11px; }
1557
+ .import-dest-label {
1558
+ font-family: var(--ide-mono-font, monospace);
1559
+ overflow: hidden;
1560
+ text-overflow: ellipsis;
1561
+ white-space: nowrap;
1562
+ }
1563
+ .import-dest-hint { color: var(--ide-text-muted, #888); font-size: 11px; margin-left: auto; flex-shrink: 0; }
1564
+ .import-dest-input {
1565
+ width: 100%;
1566
+ margin-top: 8px;
1567
+ box-sizing: border-box;
1568
+ background: var(--ide-input-bg, #3c3c3c);
1569
+ color: var(--ide-text, #d4d4d4);
1570
+ border: 1px solid var(--ide-border, #3c3c3c);
1571
+ border-radius: 3px;
1572
+ padding: 5px 8px;
1573
+ font-family: var(--ide-mono-font, monospace);
1574
+ font-size: 12px;
1575
+ }
1576
+ .import-dest-input:focus { outline: 1px solid var(--ide-accent, #0e639c); }
1577
+ .import-target-replaces {
1578
+ margin-left: 8px;
1579
+ font-size: 10px;
1580
+ text-transform: uppercase;
1581
+ letter-spacing: 0.06em;
1582
+ color: var(--ide-warning, #cca700);
1583
+ }
1416
1584
  .schema-table {
1417
1585
  width: 100%;
1418
1586
  border-collapse: collapse;
@@ -1969,12 +2137,20 @@ html, body, #mbeditor-root {
1969
2137
  flex-shrink: 0;
1970
2138
  }
1971
2139
 
2140
+ /* The title yields the header to the action buttons rather than wrapping.
2141
+ *
2142
+ * It used to break-word onto three lines ("SAMPLE_ / WORKSPA / CE") because a
2143
+ * long workspace name plus six buttons genuinely does not fit a 300px sidebar.
2144
+ * flex-shrink against a `flex-shrink: 0` action group means the buttons always
2145
+ * win: full title, then ellipsis, then gone. No JS measuring, and it adapts on
2146
+ * its own to sections with fewer buttons — the Open Editors header keeps its
2147
+ * title at the same width where Explorer has already dropped it. */
1972
2148
  .collapsible-title {
1973
- flex: 1;
2149
+ flex: 1 1 auto;
1974
2150
  min-width: 0;
1975
- word-wrap: break-word;
1976
- overflow-wrap: break-word;
1977
- word-break: break-word;
2151
+ white-space: nowrap;
2152
+ overflow: hidden;
2153
+ text-overflow: ellipsis;
1978
2154
  }
1979
2155
 
1980
2156
  .collapsible-actions {
@@ -1991,6 +2167,45 @@ html, body, #mbeditor-root {
1991
2167
  gap: 0;
1992
2168
  }
1993
2169
 
2170
+ /* Tooltip for the icon-only sidebar action buttons. See SidebarActionButton
2171
+ * for why this is not a title attribute. Anchored to the button's right edge
2172
+ * because .ide-sidebar-scrollable is overflow-x: hidden — a left-anchored or
2173
+ * centred tooltip on the rightmost button would be clipped by the sidebar. */
2174
+ .sb-tip {
2175
+ position: relative;
2176
+ display: inline-flex;
2177
+ }
2178
+ .sb-tip::after {
2179
+ content: attr(data-tip);
2180
+ position: absolute;
2181
+ top: calc(100% + 4px);
2182
+ right: 0;
2183
+ z-index: 4000;
2184
+ padding: 4px 7px;
2185
+ background: var(--ide-panel-bg, #252526);
2186
+ border: 1px solid var(--ide-border, #3c3c3c);
2187
+ border-radius: 4px;
2188
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
2189
+ color: var(--ide-text, #d4d4d4);
2190
+ /* The section header sets uppercase, 600 weight and letter-spacing; a
2191
+ tooltip inheriting those reads as another heading. */
2192
+ font-size: 11px;
2193
+ font-weight: 400;
2194
+ letter-spacing: 0;
2195
+ text-transform: none;
2196
+ white-space: nowrap;
2197
+ opacity: 0;
2198
+ pointer-events: none;
2199
+ transition: opacity 0.08s ease 0.3s;
2200
+ }
2201
+ .sb-tip:hover::after,
2202
+ .sb-tip:focus-within::after {
2203
+ opacity: 1;
2204
+ }
2205
+ @media (prefers-reduced-motion: reduce) {
2206
+ .sb-tip::after { transition: none; }
2207
+ }
2208
+
1994
2209
  /* Open-editors panel: height is controlled by the --open-editors-height CSS variable
1995
2210
  set inline from React state (default 140px ≈ 5-6 items), drag-resizable via the
1996
2211
  .open-editors-resize-handle strip below. */
@@ -2652,26 +2867,8 @@ button:not(.pico-btn) {
2652
2867
  font-size: 12px;
2653
2868
  }
2654
2869
 
2655
- /* Monaco widget isolationPico's [role=button] selector matches Monaco's
2656
- * internal action elements (View Problem, Quick Fix…, lightbulb actions)
2657
- * because Monaco renders them as <a role="button">. Without this reset, Pico
2658
- * applies its primary-colour background, white text, and large form-element
2659
- * padding, making those buttons huge and unreadable on certain themes.
2660
- * Resetting the Pico CSS variables here is lower-impact than fighting
2661
- * per-property specificity battles with Monaco's --vscode-* rules.
2662
- */
2663
- .monaco-hover [role=button],
2664
- .action-widget [role=button],
2665
- .context-view [role=button],
2666
- .monaco-editor [role=button],
2667
- .monaco-diff-editor [role=button] {
2668
- --pico-background-color: transparent;
2669
- --pico-border-color: transparent;
2670
- --pico-color: inherit;
2671
- --pico-box-shadow: none;
2672
- --pico-form-element-spacing-vertical: 0;
2673
- --pico-form-element-spacing-horizontal: 0;
2674
- }
2870
+ /* Pico containment lives at the top of this file see the block there. It has
2871
+ * to sit above every component rule to stay a floor rather than an override. */
2675
2872
 
2676
2873
  /* Inputs and selects — keep compact but styled */
2677
2874
  input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
@@ -2998,9 +3195,41 @@ button:not(.pico-btn) { margin-bottom: 0; }
2998
3195
  color: var(--ide-fg-muted, #ccc);
2999
3196
  }
3000
3197
  .ide-problems-title { font-weight: 600; }
3001
- .ide-problems-summary { color: var(--ide-text-muted, #858585); font-size: 11px; }
3002
- .ide-problems-filter {
3198
+ /* Severity toggles. Pushed to the right of the summary; the text filter that
3199
+ follows keeps its own left margin off `auto` so the two sit together. */
3200
+ .ide-problems-severity-filter {
3003
3201
  margin-left: auto;
3202
+ display: flex;
3203
+ gap: 4px;
3204
+ }
3205
+ .ide-problems-sev-chip {
3206
+ display: inline-flex;
3207
+ align-items: center;
3208
+ gap: 4px;
3209
+ background: transparent;
3210
+ border: 1px solid transparent;
3211
+ border-radius: 3px;
3212
+ padding: 1px 6px;
3213
+ font-size: 11px;
3214
+ font-variant-numeric: tabular-nums;
3215
+ cursor: pointer;
3216
+ /* Off is legible but clearly recessive — a disabled-looking chip reads as
3217
+ "broken" rather than "toggled off". */
3218
+ color: var(--ide-text-muted, #858585);
3219
+ opacity: 0.55;
3220
+ }
3221
+ .ide-problems-sev-chip:hover { opacity: 0.85; }
3222
+ .ide-problems-sev-chip.is-on {
3223
+ opacity: 1;
3224
+ border-color: var(--ide-border, #333);
3225
+ background: var(--ide-input-bg, #2d2d2d);
3226
+ }
3227
+ .ide-problems-sev-chip.is-on.ide-problems-sev-error { color: var(--ide-error, #f14c4c); }
3228
+ .ide-problems-sev-chip.is-on.ide-problems-sev-warning { color: var(--ide-warning, #cca700); }
3229
+ .ide-problems-sev-chip.is-on.ide-problems-sev-info { color: var(--ide-info, #3794ff); }
3230
+
3231
+ .ide-problems-filter {
3232
+ margin-left: 8px;
3004
3233
  background: var(--ide-input-bg, #2d2d2d);
3005
3234
  color: var(--ide-fg, #eee);
3006
3235
  border: 1px solid var(--ide-border, #333);
@@ -3017,6 +3246,24 @@ button:not(.pico-btn) { margin-bottom: 0; }
3017
3246
  padding: 2px 6px;
3018
3247
  }
3019
3248
  .ide-problems-btn:hover { color: var(--ide-fg, #fff); }
3249
+ .ide-problems-btn:disabled { opacity: 0.4; cursor: default; }
3250
+ .ide-problems-btn:disabled:hover { color: var(--ide-fg-muted, #ccc); }
3251
+ .ide-problems-actions { display: flex; gap: 2px; }
3252
+ .ide-problems-actions .ide-problems-btn { white-space: nowrap; }
3253
+ .ide-problems-btn.is-on { color: var(--ide-accent, #0a84ff); }
3254
+ .ide-testrun-stat { font-size: 11px; color: var(--ide-text-muted, #858585); }
3255
+ .ide-testrun-pass { color: #4ec9b0; }
3256
+ .ide-testrun-fail { color: #f14c4c; }
3257
+ .ide-testrun-raw {
3258
+ margin: 0;
3259
+ padding: 8px 14px;
3260
+ font-family: var(--ide-mono, monospace);
3261
+ font-size: 11px;
3262
+ line-height: 1.5;
3263
+ white-space: pre;
3264
+ overflow-x: auto;
3265
+ color: var(--ide-fg, #eee);
3266
+ }
3020
3267
  .ide-problems-body { flex: 1; overflow: auto; padding: 4px 0; font-size: 12px; }
3021
3268
  .ide-problems-empty {
3022
3269
  padding: 10px 14px;
@@ -19,6 +19,13 @@ module Mbeditor
19
19
  # True when the connection is allowed (or no hook is configured); otherwise
20
20
  # rejects the subscription and returns false.
21
21
  def mbeditor_authenticated?
22
+ # The same gate the controllers apply. Without it the cable channels were
23
+ # reachable in any environment, hook or no hook.
24
+ unless Array(Mbeditor.configuration.allowed_environments).map(&:to_sym).include?(Rails.env.to_sym)
25
+ mbeditor_reject_subscription
26
+ return false
27
+ end
28
+
22
29
  hook = mbeditor_auth_hook
23
30
  return true unless hook
24
31
 
@@ -50,8 +50,10 @@ module Mbeditor
50
50
  def save_state(data)
51
51
  state = data["state"] || data
52
52
  EditorStateService.new(workspace_root).write_state(state)
53
- rescue StandardError
54
- # Never let a state-save failure crash the WebSocket connection
53
+ rescue StandardError => e
54
+ # Never let a state-save failure crash the WebSocket connection — but a
55
+ # silently dropped save looked exactly like a working one.
56
+ Rails.logger.warn("[mbeditor] EditorChannel#save_state failed: #{e.class}: #{e.message}")
55
57
  end
56
58
 
57
59
  def save_branch_state(data)
@@ -62,8 +64,9 @@ module Mbeditor
62
64
  # A misbehaving client sent a malformed branch name. Don't crash the
63
65
  # connection, but log it so the misconfiguration is observable.
64
66
  Rails.logger.warn("[mbeditor] EditorChannel#save_branch_state: rejected invalid branch name #{branch.inspect}")
65
- rescue StandardError
67
+ rescue StandardError => e
66
68
  # Never let a state-save failure crash the WebSocket connection
69
+ Rails.logger.warn("[mbeditor] EditorChannel#save_branch_state failed: #{e.class}: #{e.message}")
67
70
  end
68
71
 
69
72
  def start_log_tail(data)
@@ -6,6 +6,11 @@ require "pathname"
6
6
  module Mbeditor
7
7
  class ApplicationController < ActionController::Base
8
8
  protect_from_forgery with: :exception
9
+ # Before the auth hook, and on every controller: a disallowed environment is
10
+ # "this engine is not here", so it must 404 without running the host app's
11
+ # authenticate_with proc. Declaring it per-controller meant LogsController
12
+ # missed it entirely and served the environment's log file anywhere.
13
+ before_action :ensure_allowed_environment!
9
14
  before_action :run_authentication
10
15
 
11
16
  private