millas 0.2.12-beta-1 → 0.2.13

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 (120) hide show
  1. package/package.json +3 -2
  2. package/src/admin/ActivityLog.js +153 -52
  3. package/src/admin/Admin.js +516 -199
  4. package/src/admin/AdminAuth.js +213 -98
  5. package/src/admin/FormGenerator.js +372 -0
  6. package/src/admin/HookRegistry.js +256 -0
  7. package/src/admin/QueryEngine.js +263 -0
  8. package/src/admin/ViewContext.js +318 -0
  9. package/src/admin/WidgetRegistry.js +406 -0
  10. package/src/admin/index.js +17 -0
  11. package/src/admin/resources/AdminResource.js +393 -97
  12. package/src/admin/static/admin.css +1422 -0
  13. package/src/admin/static/date-picker.css +157 -0
  14. package/src/admin/static/date-picker.js +316 -0
  15. package/src/admin/static/json-editor.css +649 -0
  16. package/src/admin/static/json-editor.js +1429 -0
  17. package/src/admin/static/ui.js +1044 -0
  18. package/src/admin/views/layouts/base.njk +87 -1046
  19. package/src/admin/views/pages/detail.njk +56 -21
  20. package/src/admin/views/pages/error.njk +65 -0
  21. package/src/admin/views/pages/form.njk +47 -599
  22. package/src/admin/views/pages/list.njk +270 -62
  23. package/src/admin/views/partials/form-field.njk +53 -0
  24. package/src/admin/views/partials/form-footer.njk +28 -0
  25. package/src/admin/views/partials/form-readonly.njk +114 -0
  26. package/src/admin/views/partials/form-scripts.njk +480 -0
  27. package/src/admin/views/partials/form-widget.njk +297 -0
  28. package/src/admin/views/partials/icons.njk +64 -0
  29. package/src/admin/views/partials/json-dialog.njk +80 -0
  30. package/src/admin/views/partials/json-editor.njk +37 -0
  31. package/src/ai/AIManager.js +954 -0
  32. package/src/ai/AITokenBudget.js +250 -0
  33. package/src/ai/PromptGuard.js +216 -0
  34. package/src/ai/agents.js +218 -0
  35. package/src/ai/conversation.js +213 -0
  36. package/src/ai/drivers.js +734 -0
  37. package/src/ai/files.js +249 -0
  38. package/src/ai/media.js +303 -0
  39. package/src/ai/pricing.js +152 -0
  40. package/src/ai/provider_tools.js +114 -0
  41. package/src/ai/types.js +356 -0
  42. package/src/auth/Auth.js +18 -2
  43. package/src/auth/AuthUser.js +65 -44
  44. package/src/cli.js +3 -1
  45. package/src/commands/createsuperuser.js +267 -0
  46. package/src/commands/lang.js +589 -0
  47. package/src/commands/migrate.js +154 -81
  48. package/src/commands/serve.js +3 -4
  49. package/src/container/AppInitializer.js +101 -20
  50. package/src/container/Application.js +31 -1
  51. package/src/container/MillasApp.js +10 -3
  52. package/src/container/MillasConfig.js +35 -6
  53. package/src/core/admin.js +5 -0
  54. package/src/core/db.js +2 -1
  55. package/src/core/foundation.js +2 -10
  56. package/src/core/lang.js +1 -0
  57. package/src/errors/HttpError.js +32 -16
  58. package/src/facades/AI.js +411 -0
  59. package/src/facades/Hash.js +67 -0
  60. package/src/facades/Process.js +144 -0
  61. package/src/hashing/Hash.js +262 -0
  62. package/src/http/HtmlEscape.js +162 -0
  63. package/src/http/MillasRequest.js +63 -7
  64. package/src/http/MillasResponse.js +70 -4
  65. package/src/http/ResponseDispatcher.js +21 -27
  66. package/src/http/SafeFilePath.js +195 -0
  67. package/src/http/SafeRedirect.js +62 -0
  68. package/src/http/SecurityBootstrap.js +70 -0
  69. package/src/http/helpers.js +40 -125
  70. package/src/http/index.js +10 -1
  71. package/src/http/middleware/CsrfMiddleware.js +258 -0
  72. package/src/http/middleware/RateLimiter.js +314 -0
  73. package/src/http/middleware/SecurityHeaders.js +281 -0
  74. package/src/i18n/I18nServiceProvider.js +91 -0
  75. package/src/i18n/Translator.js +643 -0
  76. package/src/i18n/defaults.js +122 -0
  77. package/src/i18n/index.js +164 -0
  78. package/src/i18n/locales/en.js +55 -0
  79. package/src/i18n/locales/sw.js +48 -0
  80. package/src/logger/LogRedactor.js +247 -0
  81. package/src/logger/Logger.js +1 -1
  82. package/src/logger/formatters/JsonFormatter.js +11 -4
  83. package/src/logger/formatters/PrettyFormatter.js +103 -65
  84. package/src/logger/formatters/SimpleFormatter.js +14 -3
  85. package/src/middleware/ThrottleMiddleware.js +27 -4
  86. package/src/migrations/system/0001_users.js +21 -0
  87. package/src/migrations/system/0002_admin_log.js +25 -0
  88. package/src/migrations/system/0003_sessions.js +23 -0
  89. package/src/orm/fields/index.js +210 -188
  90. package/src/orm/migration/DefaultValueParser.js +325 -0
  91. package/src/orm/migration/InteractiveResolver.js +191 -0
  92. package/src/orm/migration/Makemigrations.js +312 -0
  93. package/src/orm/migration/MigrationGraph.js +227 -0
  94. package/src/orm/migration/MigrationRunner.js +202 -108
  95. package/src/orm/migration/MigrationWriter.js +463 -0
  96. package/src/orm/migration/ModelInspector.js +143 -74
  97. package/src/orm/migration/ModelScanner.js +225 -0
  98. package/src/orm/migration/ProjectState.js +213 -0
  99. package/src/orm/migration/RenameDetector.js +175 -0
  100. package/src/orm/migration/SchemaBuilder.js +8 -81
  101. package/src/orm/migration/operations/base.js +57 -0
  102. package/src/orm/migration/operations/column.js +191 -0
  103. package/src/orm/migration/operations/fields.js +252 -0
  104. package/src/orm/migration/operations/index.js +55 -0
  105. package/src/orm/migration/operations/models.js +152 -0
  106. package/src/orm/migration/operations/registry.js +131 -0
  107. package/src/orm/migration/operations/special.js +51 -0
  108. package/src/orm/migration/utils.js +208 -0
  109. package/src/orm/model/Model.js +81 -13
  110. package/src/process/Process.js +333 -0
  111. package/src/providers/AdminServiceProvider.js +66 -9
  112. package/src/providers/AuthServiceProvider.js +40 -5
  113. package/src/providers/CacheStorageServiceProvider.js +2 -2
  114. package/src/providers/DatabaseServiceProvider.js +3 -2
  115. package/src/providers/LogServiceProvider.js +4 -1
  116. package/src/providers/MailServiceProvider.js +1 -1
  117. package/src/providers/QueueServiceProvider.js +1 -1
  118. package/src/router/MiddlewareRegistry.js +27 -2
  119. package/src/scaffold/templates.js +80 -21
  120. package/src/validation/Validator.js +348 -607
@@ -99,8 +99,11 @@
99
99
  <div class="detail-label">{{ field.label }}</div>
100
100
  <div class="detail-value">
101
101
  {% set val = record[field.name] %}
102
- {% include "partials/detail-value.njk" ignore missing %}
103
- {{ val | adminCell(field) | safe if val is not none else '<span class="cell-muted">—</span>' }}
102
+ {% if val is not none %}
103
+ {{ val | adminCell(field) | safe }}
104
+ {% else %}
105
+ <span class="cell-muted">—</span>
106
+ {% endif %}
104
107
  </div>
105
108
  </div>
106
109
  {% endif %}
@@ -125,7 +128,11 @@
125
128
  <div class="detail-label">{{ field.label }}</div>
126
129
  <div class="detail-value">
127
130
  {% set val = record[field.name] %}
128
- {{ val | adminDetail(field) | safe }}
131
+ {% if val is not none %}
132
+ {{ val | adminDetail(field) | safe }}
133
+ {% else %}
134
+ <span class="cell-muted">—</span>
135
+ {% endif %}
129
136
  </div>
130
137
  </div>
131
138
  {% endif %}
@@ -161,6 +168,7 @@
161
168
  </a>
162
169
  {% elif ra.action %}
163
170
  <form method="POST" action="{{ adminPrefix }}/{{ resource.slug }}/{{ record.id }}/action/{{ ra.action }}" style="display:inline">
171
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
164
172
  <button type="submit" class="btn btn-ghost btn-sm">
165
173
  <span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-{{ ra.icon or 'check' }}"/></svg></span>
166
174
  {{ ra.label }}
@@ -179,6 +187,8 @@
179
187
  {% if inlines | length %}
180
188
  <div style="max-width:860px;margin-top:24px">
181
189
  {% for inline in inlines %}
190
+ {% set inlineIndex = loop.index0 %}
191
+ {% set inlineBase = adminPrefix + '/' + resource.slug + '/' + record.id + '/inline/' + inlineIndex %}
182
192
  <div class="card mb-5">
183
193
  <div class="card-header">
184
194
  <span class="card-title">
@@ -191,14 +201,40 @@
191
201
  {% endif %}
192
202
  </span>
193
203
  {% if inline.canCreate %}
194
- <a href="{{ adminPrefix }}/{{ inline.label | lower | replace(' ', '-') }}/create?{{ inline.foreignKey }}={{ record.id }}"
195
- class="btn btn-ghost btn-sm">
204
+ <button class="btn btn-ghost btn-sm" onclick="document.getElementById('inline-form-{{ inlineIndex }}').style.display='block';this.style.display='none'">
196
205
  <span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-plus"/></svg></span>
197
- Add {{ inline.label | replace('s','') if inline.label | last == 's' else inline.label }}
198
- </a>
206
+ Add
207
+ </button>
199
208
  {% endif %}
200
209
  </div>
201
210
 
211
+ {# ── Inline quick-add form ── #}
212
+ {% if inline.canCreate %}
213
+ <div id="inline-form-{{ inlineIndex }}" style="display:none;padding:12px 16px;border-bottom:1px solid var(--border);background:var(--surface)">
214
+ <form method="POST" action="{{ inlineBase }}">
215
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
216
+ <div style="display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end">
217
+ {% for field in inline.fields %}
218
+ {% if field.name != inline.foreignKey %}
219
+ <div style="flex:1;min-width:140px">
220
+ <label class="form-label" style="font-size:11px">{{ field.label }}</label>
221
+ {% if field.type == 'boolean' %}
222
+ <input type="checkbox" name="{{ field.name }}" value="1">
223
+ {% else %}
224
+ <input type="{{ field.type or 'text' }}" name="{{ field.name }}" class="form-control form-control-sm" placeholder="{{ field.label }}">
225
+ {% endif %}
226
+ </div>
227
+ {% endif %}
228
+ {% endfor %}
229
+ <div style="display:flex;gap:6px">
230
+ <button type="submit" class="btn btn-primary btn-sm">Save</button>
231
+ <button type="button" class="btn btn-ghost btn-sm" onclick="document.getElementById('inline-form-{{ inlineIndex }}').style.display='none';this.closest('.card').querySelector('.btn-ghost').style.display=''">Cancel</button>
232
+ </div>
233
+ </div>
234
+ </form>
235
+ </div>
236
+ {% endif %}
237
+
202
238
  {% if inline.rows | length %}
203
239
  <div class="table-wrap">
204
240
  <table>
@@ -217,16 +253,22 @@
217
253
  <tr>
218
254
  {% for field in inline.fields %}
219
255
  <td {% if loop.first %}class="td-primary"{% endif %}>
220
- {{ row[field.name] | adminCell(field) | safe if row[field.name] is not none else '<span class="cell-muted">—</span>' }}
256
+ {% if row[field.name] is not none %}
257
+ {{ row[field.name] | adminCell(field) | safe }}
258
+ {% else %}
259
+ <span class="cell-muted">—</span>
260
+ {% endif %}
221
261
  </td>
222
262
  {% endfor %}
223
263
  {% if inline.canDelete %}
224
264
  <td class="col-actions" style="text-align:right">
225
- <button
226
- onclick="confirmDelete('{{ adminPrefix }}/{{ inline.label | lower | replace(' ','-') }}/{{ row.id }}/delete','{{ inline.label | replace('s','') if inline.label | last == 's' else inline.label }} #{{ row.id }}')"
227
- class="btn btn-danger btn-xs">
228
- <span class="icon icon-12"><svg viewBox="0 0 24 24"><use href="#ic-trash"/></svg></span>
229
- </button>
265
+ <form method="POST" action="{{ inlineBase }}/{{ row.id }}/delete" style="display:inline">
266
+ <input type="hidden" name="_csrf" value="{{ csrfToken }}">
267
+ <button type="submit" class="btn btn-danger btn-xs"
268
+ onclick="return confirm('Delete this record?')">
269
+ <span class="icon icon-12"><svg viewBox="0 0 24 24"><use href="#ic-trash"/></svg></span>
270
+ </button>
271
+ </form>
230
272
  </td>
231
273
  {% endif %}
232
274
  </tr>
@@ -240,13 +282,6 @@
240
282
  <span class="icon icon-16"><svg viewBox="0 0 24 24"><use href="#ic-table"/></svg></span>
241
283
  </div>
242
284
  <div class="empty-title" style="font-size:13px">No {{ inline.label | lower }} yet</div>
243
- {% if inline.canCreate %}
244
- <a href="{{ adminPrefix }}/{{ inline.label | lower | replace(' ', '-') }}/create?{{ inline.foreignKey }}={{ record.id }}"
245
- class="btn btn-ghost btn-sm" style="margin-top:10px">
246
- <span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-plus"/></svg></span>
247
- Add first
248
- </a>
249
- {% endif %}
250
285
  </div>
251
286
  {% endif %}
252
287
  </div>
@@ -319,4 +354,4 @@
319
354
  document.querySelectorAll('.tab-panel').forEach((p, i) => p.classList.toggle('active', i === idx));
320
355
  }
321
356
  </script>
322
- {% endblock %}
357
+ {% endblock %}
@@ -0,0 +1,65 @@
1
+ {% extends "layouts/base.njk" %}
2
+
3
+ {% block title %}{{ errorTitle }}{% endblock %}
4
+
5
+ {% block topbar_title %}
6
+ <span style="color:var(--danger)">{{ errorTitle }}</span>
7
+ {% endblock %}
8
+ {% block sidebar %}
9
+ {% endblock %}
10
+
11
+ {% block content %}
12
+ <div style="max-width:640px;margin:40px auto;padding:0 8px">
13
+
14
+ {# ── Icon + heading ── #}
15
+ <div style="display:flex;align-items:center;gap:14px;margin-bottom:28px">
16
+ <div style="flex-shrink:0;width:48px;height:48px;background:var(--danger-bg);border:1px solid var(--danger-border);border-radius:var(--radius-lg);display:flex;align-items:center;justify-content:center">
17
+ {% if errorStatus == 404 %}
18
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="var(--danger)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
19
+ <circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
20
+ <line x1="11" y1="8" x2="11" y2="11"/><line x1="11" y1="14" x2="11.01" y2="14"/>
21
+ </svg>
22
+ {% else %}
23
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="var(--danger)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
24
+ <circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>
25
+ </svg>
26
+ {% endif %}
27
+ </div>
28
+ <div>
29
+ <div style="font-size:22px;font-weight:700;color:var(--danger);line-height:1.2">{{ errorStatus }}</div>
30
+ <div style="font-size:14px;color:var(--text-soft);margin-top:2px">{{ errorTitle }}</div>
31
+ </div>
32
+ </div>
33
+
34
+ {# ── Message card ── #}
35
+ <div class="card" style="margin-bottom:20px">
36
+ <div style="padding:20px 22px;display:flex;align-items:flex-start;gap:12px">
37
+ <svg style="flex-shrink:0;margin-top:1px;color:var(--text-muted)" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
38
+ <circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>
39
+ </svg>
40
+ <span style="font-size:14px;color:var(--text);line-height:1.6">{{ errorMsg }}</span>
41
+ </div>
42
+ </div>
43
+
44
+ {# ── Stack trace (dev only) ── #}
45
+ {% if errorStack %}
46
+ <details style="margin-bottom:20px">
47
+ <summary style="font-size:12.5px;color:var(--text-muted);cursor:pointer;user-select:none;margin-bottom:8px">Stack trace</summary>
48
+ <pre style="background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius);padding:16px 18px;font-size:11.5px;color:var(--text-soft);overflow-x:auto;line-height:1.65;margin:0;white-space:pre-wrap;word-break:break-word">{{ errorStack }}</pre>
49
+ </details>
50
+ {% endif %}
51
+
52
+ {# ── Actions ── #}
53
+ <div style="display:flex;align-items:center;gap:10px">
54
+ <a href="javascript:history.back()" class="btn btn-ghost btn-sm">
55
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
56
+ Go back
57
+ </a>
58
+ <a href="{{ adminPrefix }}/" class="btn btn-ghost btn-sm">
59
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
60
+ Dashboard
61
+ </a>
62
+ </div>
63
+
64
+ </div>
65
+ {% endblock %}