alchemy-chimera 1.3.0-alpha.4 → 1.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## 1.3.0 (2026-01-21)
2
+
3
+ * Add `addRowAction()` to ChimeraConfig for model-defined row actions
4
+ * Fix WebSocket compatibility in ChimeraController (skip theme/beforeAction for WS connections)
5
+ * Add live task monitoring UI with WebSocket support
6
+ * Add "Monitor" button to TaskHistory index and edit pages
7
+ * Add widgets to dashboard
8
+ * Move `setToolbarManagerVariable()` call to end of actions (after document context setup) to ensure proper serialization of document watcher state
9
+ * Fix `setToolbarManagerVariable()` to clear document watcher synchronously for non-document actions, preventing avatar from persisting on list pages
10
+
1
11
  ## 1.3.0-alpha.4 (2025-07-10)
2
12
 
3
13
  * Always set a new toolbar_manager received from the server
package/CLAUDE.md ADDED
@@ -0,0 +1,297 @@
1
+ # Alchemy Chimera Development Guide
2
+
3
+ ## Overview
4
+
5
+ Chimera is the admin/CMS interface plugin for AlchemyMVC. It provides a complete backend administration interface for managing application data models with CRUD operations, live task monitoring, and system settings management.
6
+
7
+ ## Commands
8
+ - Run tests: `npm test`
9
+
10
+ ## Dependencies
11
+ - alchemymvc (>=1.4.0-alpha)
12
+ - alchemy-acl (~0.9.0-alpha) - Access control
13
+ - alchemy-form (~0.3.0-alpha) - Form handling
14
+ - alchemy-widget (~0.3.0-alpha) - Widget system
15
+ - alchemy-styleboost (>=0.5.0-alpha) - SCSS framework
16
+
17
+ ## Directory Structure
18
+
19
+ ```
20
+ controller/
21
+ ├── 00-chimera_controller.js # Base controller
22
+ ├── chimera_editor_controller.js # CRUD interface
23
+ ├── chimera_settings_controller.js # Settings management
24
+ ├── chimera_static_controller.js # Dashboard/sidebar
25
+ └── system_task_controller.js # Task execution/monitoring
26
+
27
+ lib/
28
+ └── chimera_config.js # Model configuration class
29
+
30
+ model/
31
+ └── model.js # Adds chimera property to models
32
+
33
+ element/
34
+ ├── chimera_run_task_button.js # Task execution button
35
+ └── chimera_task_monitor.js # Live task progress UI
36
+
37
+ view/
38
+ ├── chimera/
39
+ │ ├── dashboard.hwk
40
+ │ ├── sidebar.hwk
41
+ │ ├── editor/ # CRUD views
42
+ │ └── settings/
43
+ └── layouts/
44
+ └── chimera_*.hwk
45
+ ```
46
+
47
+ ## Routes
48
+
49
+ All routes require `'chimera'` permission and are prefixed with `/chimera`.
50
+
51
+ | Route | Method | Handler | Description |
52
+ |-------|--------|---------|-------------|
53
+ | `/` | GET | `Chimera.Static#dashboard` | Dashboard |
54
+ | `/settings` | GET/POST | `Chimera.Settings#editor` | System settings |
55
+ | `/editor/{model}/index` | GET | `Chimera.Editor#index` | List records |
56
+ | `/editor/{model}/add` | GET/POST | `Chimera.Editor#add` | Create record |
57
+ | `/editor/{model}/edit/{pk}` | GET/POST | `Chimera.Editor#edit` | Edit record |
58
+ | `/editor/{model}/trash/{pk}` | GET/POST | `Chimera.Editor#trash` | Delete record |
59
+ | `/editor/{model}/preview/{pk}` | GET | `Chimera.Editor#preview` | Preview record |
60
+ | `/task-monitor/{task_history_id}` | GET | `Chimera.Editor#taskMonitor` | Task monitor page |
61
+
62
+ ### API Routes
63
+ | Route | Method | Handler |
64
+ |-------|--------|---------|
65
+ | `/api/editor/{model}/records` | POST | Fetch paginated records |
66
+ | `/api/content/sidebar` | GET | Dynamic sidebar content |
67
+ | `/api/system/task/{id}/run` | POST | Execute system task |
68
+
69
+ ### WebSocket Linkups
70
+ - `chimera@taskmonitor` - Live task monitoring
71
+
72
+ ## Model Configuration
73
+
74
+ Every model has a static `chimera` property (ChimeraConfig instance):
75
+
76
+ ### Field Sets
77
+
78
+ Define which fields appear in different contexts:
79
+
80
+ ```javascript
81
+ // In model constitute() or after model definition
82
+ const MyModel = Model.getClass('MyModel');
83
+
84
+ // Edit form fields
85
+ MyModel.chimera.getFieldSet('edit')
86
+ .add('title', {group: 'main'})
87
+ .add('body', {group: 'content'})
88
+ .add('status', {group: 'settings'});
89
+
90
+ // List/table fields
91
+ MyModel.chimera.getFieldSet('list')
92
+ .add('title')
93
+ .add('status')
94
+ .add('created');
95
+ ```
96
+
97
+ ### Field Options
98
+ ```javascript
99
+ {
100
+ title: 'Field Title', // Display name
101
+ purpose: 'default', // Field purpose for form
102
+ mode: 'default', // Edit mode
103
+ readonly: false, // Read-only field
104
+ view: 'default', // View template
105
+ wrapper: 'default', // Wrapper template
106
+ group: 'main', // Tab group (multiple = tabs)
107
+ widget_settings: {} // Widget-specific config
108
+ }
109
+ ```
110
+
111
+ ### Row Actions
112
+
113
+ Add custom actions to record rows:
114
+
115
+ ```javascript
116
+ MyModel.chimera.addRowAction({
117
+ name: 'publish',
118
+ icon: 'share',
119
+ title: 'Publish',
120
+ placement: ['row', 'context'], // Where to show
121
+ route: 'PublishController#publish',
122
+ route_params: {
123
+ pk: '$pk', // Auto-substituted with record primary key
124
+ type: 'article'
125
+ }
126
+ });
127
+ ```
128
+
129
+ Built-in actions: `edit`, `trash`
130
+
131
+ ### Record Preview
132
+
133
+ ```javascript
134
+ MyModel.chimera.setRecordPreview('MyController#preview');
135
+ ```
136
+
137
+ ## Widget Generation
138
+
139
+ Chimera auto-generates UI based on field sets:
140
+
141
+ ### Edit Action
142
+ - Single group → Flat form
143
+ - Multiple groups → Tab navigation
144
+ - Includes save button with states (default, busy, done, error)
145
+
146
+ ### List Action
147
+ - `<alchemy-table>` with pagination
148
+ - Filter row for searching
149
+ - Sortable columns
150
+ - Configurable page size
151
+
152
+ ## Task Monitoring
153
+
154
+ ### Run Task Button
155
+ ```html
156
+ <chimera-run-task-button task-id="{{task.$pk}}">
157
+ Run Task
158
+ </chimera-run-task-button>
159
+ ```
160
+
161
+ ### Task Monitor Element
162
+ ```html
163
+ <chimera-task-monitor task-history-id="{{history.$pk}}">
164
+ </chimera-task-monitor>
165
+ ```
166
+
167
+ Features:
168
+ - Real-time progress via WebSocket
169
+ - Log output with timestamps
170
+ - Controls: Stop, Pause, Resume
171
+ - Status display with elapsed time
172
+ - Error handling with fallback to database state
173
+
174
+ ### Linkup Events
175
+ | From Server | Description |
176
+ |-------------|-------------|
177
+ | `state` | Initial state |
178
+ | `progress` | Percentage updates |
179
+ | `report` | Log entries |
180
+ | `complete` | Task finished |
181
+ | `paused`, `resumed`, `stopped` | Control confirmations |
182
+ | `error` | Error occurred |
183
+
184
+ | To Server | Description |
185
+ |-----------|-------------|
186
+ | `stop` | Stop running task |
187
+ | `pause` | Pause task |
188
+ | `resume` | Resume paused task |
189
+
190
+ ## Records API
191
+
192
+ **POST `/chimera/api/editor/{model}/records`**
193
+
194
+ Request:
195
+ ```javascript
196
+ {
197
+ page_size: 20,
198
+ page: 1,
199
+ fields: ['title', 'status'],
200
+ sort: {field: 'created', dir: 'desc'},
201
+ filters: {status: 'published'}
202
+ }
203
+ ```
204
+
205
+ Response:
206
+ ```javascript
207
+ {
208
+ records: [{
209
+ // Record data
210
+ $hold: {
211
+ actions: [{name: 'edit', icon, url, placement}]
212
+ }
213
+ }]
214
+ }
215
+ ```
216
+
217
+ ## Sidebar Configuration
218
+
219
+ Auto-generated from models with Chimera configuration, or customize:
220
+
221
+ ```javascript
222
+ alchemy.plugins.chimera.sidebar_menu = [
223
+ {model: 'Article'}, // Auto-generate link
224
+ {model: 'User'},
225
+ {href: '/custom', title: 'Custom Page'}
226
+ ];
227
+ ```
228
+
229
+ ## Plugin Configuration
230
+
231
+ ```javascript
232
+ alchemy.plugins.chimera = {
233
+ theme: 'custom-theme', // Custom theme name
234
+ title: 'My Admin', // Window title prefix
235
+ sidebar_menu: [...] // Custom sidebar (optional)
236
+ };
237
+ ```
238
+
239
+ ## Controllers
240
+
241
+ ### ChimeraController (Base)
242
+ - Sets theme on HTTP requests
243
+ - Manages toolbar for document editing
244
+ - Provides `setTitle(title)` method
245
+
246
+ ### ChimeraEditor
247
+ - `index` - Record list with table
248
+ - `add` - Create form
249
+ - `edit` - Edit form with document watching
250
+ - `trash` - Delete confirmation
251
+ - `preview` - Delegates to model's preview action
252
+ - `records` - API for fetching records
253
+
254
+ ### ChimeraSettings
255
+ - `editor` - System settings form
256
+
257
+ ### SystemTask
258
+ - `run` - Execute task manually
259
+ - `monitor` - WebSocket handler for live updates
260
+
261
+ ## View Templates
262
+
263
+ Templates use Hawkejs (`.hwk` files):
264
+
265
+ - `chimera/dashboard.hwk` - Main dashboard
266
+ - `chimera/sidebar.hwk` - Navigation
267
+ - `chimera/editor/index.hwk` - Record list
268
+ - `chimera/editor/add.hwk` - Create form
269
+ - `chimera/editor/edit.hwk` - Edit form
270
+ - `chimera/editor/trash.hwk` - Delete confirmation
271
+ - `chimera/editor/task_monitor.hwk` - Task progress
272
+ - `chimera/settings/editor.hwk` - Settings form
273
+
274
+ ## Utility
275
+
276
+ ```javascript
277
+ // Convert model name to URL segment
278
+ Plugin.modelNameToUrl('MyModel'); // 'my_model'
279
+ ```
280
+
281
+ ## Gotchas
282
+
283
+ 1. **Permission required:** All Chimera routes require `'chimera'` permission
284
+
285
+ 2. **Field sets:** Models need `getFieldSet('edit')` configured to appear in admin
286
+
287
+ 3. **has_configuration:** Models only show in sidebar if `chimera.has_configuration` is true
288
+
289
+ 4. **Group tabs:** Multiple groups in edit fieldset automatically creates tabbed interface
290
+
291
+ 5. **WebSocket skip:** Theme/beforeAction skipped for WebSocket connections
292
+
293
+ 6. **Document watching:** Edit page uses toolbar manager for tracking document changes
294
+
295
+ 7. **Private fields:** Call `record.keepPrivateFields()` to preserve sensitive data in responses
296
+
297
+ 8. **Row action $pk:** Use `$pk` in route_params to substitute record's primary key
@@ -614,6 +614,56 @@ al-field[mode="inline"] {
614
614
  display: flex;
615
615
  gap: 1rem;
616
616
  }
617
+
618
+ [data-he-slot="right"] {
619
+ display: flex;
620
+ gap: 1rem;
621
+ align-items: center;
622
+ }
623
+
624
+ // Button visibility based on toolbar state
625
+ &[state="editing"] {
626
+ .start-edit {
627
+ display: none;
628
+ }
629
+ }
630
+
631
+ &[state="default"],
632
+ &[state="ready"] {
633
+ .stop-and-save,
634
+ .stop-edit,
635
+ .save-all,
636
+ chimera-dashboard-button[action="reset"] {
637
+ display: none;
638
+ }
639
+ }
640
+
641
+ &[state="saving"],
642
+ &[state="saving-before-stop"] {
643
+ .start-edit,
644
+ .stop-edit {
645
+ display: none;
646
+ }
647
+ }
648
+
649
+ &[state="saving"] {
650
+ .stop-and-save {
651
+ display: none;
652
+ }
653
+ }
654
+
655
+ &[state="saving-before-stop"] {
656
+ .save-all {
657
+ display: none;
658
+ }
659
+ }
660
+
661
+ // Save button styling
662
+ .stop-and-save,
663
+ .save-all {
664
+ --al-button-bg-color: green;
665
+ --al-button-bg-color-hover: rgb(55, 155, 55);
666
+ }
617
667
  }
618
668
  }
619
669
 
@@ -670,4 +720,231 @@ al-table {
670
720
  background-color: #edeff5;
671
721
  }
672
722
  }
723
+ }
724
+
725
+ // Task Monitor Styles
726
+ chimera-task-monitor {
727
+ display: block;
728
+
729
+ .task-monitor-container {
730
+ background-color: white;
731
+ border: 1px solid var(--color-box-border, #dadee0);
732
+ border-radius: 4px;
733
+ padding: 1.5rem;
734
+ margin: 1rem;
735
+ }
736
+
737
+ .task-monitor-header {
738
+ display: flex;
739
+ justify-content: space-between;
740
+ align-items: flex-start;
741
+ margin-bottom: 1.5rem;
742
+ padding-bottom: 1rem;
743
+ border-bottom: 1px solid var(--color-box-border, #dadee0);
744
+ }
745
+
746
+ .task-info {
747
+ .task-title {
748
+ margin: 0 0 0.5rem 0;
749
+ font-size: 1.5rem;
750
+ color: var(--color-title, #475466);
751
+ }
752
+
753
+ .task-type {
754
+ font-size: 0.9rem;
755
+ color: #888;
756
+ font-family: monospace;
757
+ }
758
+ }
759
+
760
+ .task-meta {
761
+ display: flex;
762
+ gap: 2rem;
763
+ align-items: flex-start;
764
+
765
+ .meta-item {
766
+ display: flex;
767
+ flex-direction: column;
768
+ align-items: flex-end;
769
+
770
+ .meta-label {
771
+ font-size: 0.75rem;
772
+ color: #888;
773
+ text-transform: uppercase;
774
+ margin-bottom: 0.25rem;
775
+ }
776
+
777
+ .task-status {
778
+ font-weight: 600;
779
+ padding: 0.25rem 0.75rem;
780
+ border-radius: 4px;
781
+ font-size: 0.9rem;
782
+
783
+ &.status-running {
784
+ background-color: #cff4fc;
785
+ color: #055160;
786
+ }
787
+
788
+ &.status-paused {
789
+ background-color: #fff3cd;
790
+ color: #664d03;
791
+ }
792
+
793
+ &.status-completed {
794
+ background-color: #d1e7dd;
795
+ color: #0f5132;
796
+ }
797
+
798
+ &.status-error {
799
+ background-color: #f8d7da;
800
+ color: #842029;
801
+ }
802
+
803
+ &.status-pending {
804
+ background-color: #e2e3e5;
805
+ color: #41464b;
806
+ }
807
+ }
808
+
809
+ .elapsed-time,
810
+ .progress-text {
811
+ font-size: 1.1rem;
812
+ font-weight: 600;
813
+ font-family: monospace;
814
+ }
815
+ }
816
+ }
817
+
818
+ .task-error {
819
+ background-color: #f8d7da;
820
+ color: #842029;
821
+ border: 1px solid #f5c2c7;
822
+ padding: 1rem;
823
+ border-radius: 4px;
824
+ margin-bottom: 1rem;
825
+ }
826
+
827
+ .progress-container {
828
+ margin-bottom: 1.5rem;
829
+
830
+ .progress-track {
831
+ height: 8px;
832
+ background-color: #e9ecef;
833
+ border-radius: 4px;
834
+ overflow: hidden;
835
+
836
+ .progress-bar {
837
+ height: 100%;
838
+ background-color: var(--color-active, #3699FF);
839
+ border-radius: 4px;
840
+ transition: width 0.3s ease;
841
+
842
+ &.complete {
843
+ background-color: #198754;
844
+ }
845
+
846
+ &.error {
847
+ background-color: #dc3545;
848
+ }
849
+ }
850
+ }
851
+ }
852
+
853
+ .task-controls {
854
+ display: flex;
855
+ gap: 1rem;
856
+ margin-bottom: 1.5rem;
857
+
858
+ .btn-danger {
859
+ --context-color: #842029;
860
+ --context-background-color: #f8d7da;
861
+ --context-border-color: #f5c2c7;
862
+ @extend .danger;
863
+ }
864
+ }
865
+
866
+ .task-logs {
867
+ margin-bottom: 1.5rem;
868
+
869
+ .logs-title {
870
+ margin: 0 0 0.75rem 0;
871
+ font-size: 1rem;
872
+ color: var(--color-title, #475466);
873
+ }
874
+
875
+ .log-output {
876
+ background-color: #1e1e1e;
877
+ color: #d4d4d4;
878
+ border-radius: 4px;
879
+ padding: 1rem;
880
+ font-family: 'Consolas', 'Monaco', monospace;
881
+ font-size: 0.85rem;
882
+ line-height: 1.5;
883
+ min-height: 200px;
884
+ max-height: 400px;
885
+ overflow-y: auto;
886
+
887
+ .log-entry {
888
+ margin-bottom: 0.25rem;
889
+ display: flex;
890
+ gap: 1rem;
891
+
892
+ .log-timestamp {
893
+ color: #888;
894
+ flex-shrink: 0;
895
+ }
896
+
897
+ .log-content {
898
+ white-space: pre-wrap;
899
+ word-break: break-word;
900
+ }
901
+
902
+ &.log-success .log-content {
903
+ color: #4ec9b0;
904
+ }
905
+
906
+ &.log-error .log-content {
907
+ color: #f14c4c;
908
+ }
909
+
910
+ &.log-warning .log-content {
911
+ color: #cca700;
912
+ }
913
+
914
+ &.log-info .log-content {
915
+ color: #3dc9b0;
916
+ }
917
+
918
+ &.log-detail .log-content {
919
+ color: #9cdcfe;
920
+ }
921
+
922
+ &.log-stack .log-content {
923
+ font-size: 0.8rem;
924
+ color: #808080;
925
+ }
926
+ }
927
+ }
928
+ }
929
+
930
+ .task-timestamps {
931
+ display: flex;
932
+ gap: 2rem;
933
+ padding-top: 1rem;
934
+ border-top: 1px solid var(--color-box-border, #dadee0);
935
+ font-size: 0.9rem;
936
+
937
+ .timestamp-item {
938
+ display: flex;
939
+ gap: 0.5rem;
940
+
941
+ .timestamp-label {
942
+ color: #888;
943
+ }
944
+
945
+ .timestamp-value {
946
+ font-family: monospace;
947
+ }
948
+ }
949
+ }
673
950
  }
package/config/routes.js CHANGED
@@ -13,6 +13,22 @@ chimera_section.add({
13
13
  handler : 'Chimera.Static#dashboard',
14
14
  });
15
15
 
16
+ // Customize dashboard - create a user-specific copy
17
+ chimera_section.add({
18
+ name : 'Chimera.Static#customizeDashboard',
19
+ methods : 'post',
20
+ paths : '/api/dashboard/customize',
21
+ is_system_route : true,
22
+ });
23
+
24
+ // Reset dashboard - remove user-specific copy, revert to default
25
+ chimera_section.add({
26
+ name : 'Chimera.Static#resetDashboard',
27
+ methods : 'post',
28
+ paths : '/api/dashboard/reset',
29
+ is_system_route : true,
30
+ });
31
+
16
32
  // Settings editor
17
33
  chimera_section.add({
18
34
  name : 'Chimera.Settings#editor',
@@ -52,6 +68,14 @@ chimera_section.add({
52
68
  breadcrumb : 'chimera.editor.{model}.trash.{pk}'
53
69
  });
54
70
 
71
+ // Task monitor page - shows live progress of a running task
72
+ chimera_section.add({
73
+ name : 'Chimera.Editor#taskMonitor',
74
+ methods : ['get'],
75
+ paths : '/task-monitor/{task_history_id}',
76
+ breadcrumb : 'chimera.task-monitor'
77
+ });
78
+
55
79
  // Editor data action
56
80
  chimera_section.add({
57
81
  name : 'Chimera.Editor#records',
@@ -68,6 +92,20 @@ chimera_section.add({
68
92
  is_system_route : true,
69
93
  });
70
94
 
95
+ // System Task run action - starts a task manually
96
+ chimera_section.add({
97
+ name : 'Chimera.SystemTask#run',
98
+ paths : '/api/system/task/{[System.Task]_id}/run',
99
+ methods : ['post'],
100
+ is_system_route : true,
101
+ });
102
+
103
+ // Linkup route for live task monitoring via WebSocket
104
+ // Permission is inherited from chimera_section.requirePermission('chimera')
105
+ // Note: eventname is 'taskmonitor' (not 'chimera@taskmonitor') because the section prefix
106
+ // is handled by the router lookup, not stored in the key
107
+ chimera_section.linkup('Chimera.SystemTask#monitor', 'taskmonitor', 'Chimera.SystemTask#monitor');
108
+
71
109
  alchemy.sputnik.after('base_app', () => {
72
110
 
73
111
  let prefixes = Prefix.all();