tempest-express-sdk 0.28.0 → 0.30.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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { z, looseBoolean, toDict, PasswordUtils } from './chunk-3NS5KVHT.js';
2
- export { PasswordUtils, VERSION, baseAppSettingsSchema, baseAppSettingsShape, baseResponseSchema, centsField, corsSettingsShape, databaseSettingsShape, looseBoolean as envBoolean, hexColorField, latitudeField, loadSettings, longitudeField, looseBoolean, nonEmptyStrField, nonNegativeFloatField, nonNegativeIntField, percentField, portField, positiveFloatField, positiveIntField, priceField, ratingField, ratioField, serverSettingsShape, slugField, toDict, z } from './chunk-3NS5KVHT.js';
1
+ import { z, looseBoolean, toDict, PasswordUtils } from './chunk-DK46733U.js';
2
+ export { PasswordUtils, VERSION, baseAppSettingsSchema, baseAppSettingsShape, baseResponseSchema, centsField, corsSettingsShape, databaseSettingsShape, looseBoolean as envBoolean, hexColorField, latitudeField, loadSettings, longitudeField, looseBoolean, nonEmptyStrField, nonNegativeFloatField, nonNegativeIntField, percentField, portField, positiveFloatField, positiveIntField, priceField, ratingField, ratioField, serverSettingsShape, slugField, toDict, z } from './chunk-DK46733U.js';
3
3
  import { AsyncLocalStorage } from 'async_hooks';
4
4
  import { Model, column, sql, BaseRepository, RecordNotFound, detectDialect, columnsOf, NodeSqliteDriver, AsyncEngine, or, and } from 'tempest-db-js';
5
5
  export { AsyncEngine, AsyncResult, AsyncSession, BaseRepository, Column, DeleteBuilder, InsertBuilder, Model, NoResultError, NodeSqliteDriver, PostgresDialect, RecordNotFound, SelectBuilder, SqliteDialect, SyncEngine, SyncSession, UpdateBuilder, and, belongsTo, column, columnsOf, createEngine, createSyncEngine, del, detectDialect, getDialect, hasMany, insert, join, loadRelations, not, or, parseDatabaseUrl, select, sql, update } from 'tempest-db-js';
@@ -8546,6 +8546,7 @@ var TaskManager = class {
8546
8546
  broker;
8547
8547
  queue;
8548
8548
  handlers = /* @__PURE__ */ new Map();
8549
+ metadata = /* @__PURE__ */ new Map();
8549
8550
  unsubscribe = null;
8550
8551
  /**
8551
8552
  * @param options - Broker and queue name.
@@ -8559,9 +8560,32 @@ var TaskManager = class {
8559
8560
  *
8560
8561
  * @param name - The task name.
8561
8562
  * @param handler - The handler invoked with the task payload.
8563
+ * @param options - Description and declared schedule, surfaced by
8564
+ * {@link TaskManager.inventory} and by the admin panel's tasks page.
8562
8565
  */
8563
- register(name, handler) {
8566
+ register(name, handler, options = {}) {
8564
8567
  this.handlers.set(name, handler);
8568
+ this.metadata.set(name, options);
8569
+ }
8570
+ /**
8571
+ * Return what this process would run, ordered by name.
8572
+ *
8573
+ * This is the **declared** side of background work — the handlers this
8574
+ * process knows about — not queue state. A broker's pending depth is not
8575
+ * something the manager can see, and a screen that implied otherwise would
8576
+ * be worse than one that says nothing.
8577
+ *
8578
+ * @returns One entry per registered task.
8579
+ */
8580
+ inventory() {
8581
+ return [...this.handlers.keys()].sort((left, right) => left.localeCompare(right)).map((name) => {
8582
+ const meta = this.metadata.get(name) ?? {};
8583
+ return {
8584
+ name,
8585
+ description: meta.description ?? null,
8586
+ schedule: meta.schedule ?? null
8587
+ };
8588
+ });
8565
8589
  }
8566
8590
  /**
8567
8591
  * Enqueue a task by name.
@@ -8595,6 +8619,180 @@ var TaskManager = class {
8595
8619
  this.unsubscribe = null;
8596
8620
  }
8597
8621
  };
8622
+ var JobStatus = {
8623
+ /** Written, not started. */
8624
+ QUEUED: "queued",
8625
+ /** A worker picked it up. */
8626
+ RUNNING: "running",
8627
+ /** Finished cleanly. */
8628
+ SUCCEEDED: "succeeded",
8629
+ /** Finished with an error. */
8630
+ FAILED: "failed",
8631
+ /** An operator stopped it before it finished. */
8632
+ CANCELLED: "cancelled"
8633
+ };
8634
+ var TERMINAL = [
8635
+ JobStatus.SUCCEEDED,
8636
+ JobStatus.FAILED,
8637
+ JobStatus.CANCELLED
8638
+ ];
8639
+ var BaseJobModel = class extends BaseModel {
8640
+ /** The task name this run belongs to. */
8641
+ name = column.varchar(128).notNull();
8642
+ /** Lifecycle state — a {@link JobStatus} value. */
8643
+ status = column.varchar(16).notNull().default(JobStatus.QUEUED);
8644
+ /** The payload the run was started with. */
8645
+ payload = column.json();
8646
+ /** Whatever the run produced, for an operator to read afterwards. */
8647
+ result = column.json();
8648
+ /** The failure message, when the run failed. */
8649
+ error = column.text();
8650
+ /** How many times the run has been attempted. */
8651
+ attempts = column.integer().notNull().default(0);
8652
+ /** When a worker picked it up. */
8653
+ startedAt = column.datetime();
8654
+ /** When it reached a terminal state. */
8655
+ finishedAt = column.datetime();
8656
+ };
8657
+ var JobStore = class {
8658
+ /**
8659
+ * @param model - The concrete {@link BaseJobModel} subclass.
8660
+ * @param session - The session rows are written on.
8661
+ */
8662
+ constructor(model, session) {
8663
+ this.model = model;
8664
+ this.repository = new BaseRepository(model, session);
8665
+ }
8666
+ model;
8667
+ repository;
8668
+ /**
8669
+ * Record a job about to run.
8670
+ *
8671
+ * @param name - The task name.
8672
+ * @param payload - The payload the run was started with.
8673
+ * @returns The created row.
8674
+ */
8675
+ async enqueue(name, payload = {}) {
8676
+ return await this.repository.create({
8677
+ name,
8678
+ status: JobStatus.QUEUED,
8679
+ payload,
8680
+ result: null,
8681
+ error: null,
8682
+ attempts: 0,
8683
+ startedAt: null,
8684
+ finishedAt: null
8685
+ });
8686
+ }
8687
+ /**
8688
+ * Mark a job as picked up, counting the attempt.
8689
+ *
8690
+ * @param id - The job id.
8691
+ * @param attempt - Which attempt this is. Default `1`.
8692
+ * @returns How many rows changed.
8693
+ */
8694
+ async start(id, attempt = 1) {
8695
+ return await this.repository.update(
8696
+ { id },
8697
+ {
8698
+ status: JobStatus.RUNNING,
8699
+ startedAt: /* @__PURE__ */ new Date(),
8700
+ attempts: attempt
8701
+ }
8702
+ );
8703
+ }
8704
+ /**
8705
+ * Mark a job as finished cleanly.
8706
+ *
8707
+ * @param id - The job id.
8708
+ * @param result - Whatever the run produced.
8709
+ * @returns How many rows changed.
8710
+ */
8711
+ async succeed(id, result = {}) {
8712
+ return await this.repository.update(
8713
+ { id },
8714
+ {
8715
+ status: JobStatus.SUCCEEDED,
8716
+ result,
8717
+ error: null,
8718
+ finishedAt: /* @__PURE__ */ new Date()
8719
+ }
8720
+ );
8721
+ }
8722
+ /**
8723
+ * Mark a job as failed.
8724
+ *
8725
+ * @param id - The job id.
8726
+ * @param error - The failure, as an `Error` or a message.
8727
+ * @returns How many rows changed.
8728
+ */
8729
+ async fail(id, error) {
8730
+ return await this.repository.update(
8731
+ { id },
8732
+ {
8733
+ status: JobStatus.FAILED,
8734
+ error: error instanceof Error ? error.message : String(error),
8735
+ finishedAt: /* @__PURE__ */ new Date()
8736
+ }
8737
+ );
8738
+ }
8739
+ /**
8740
+ * Ask a job to stop.
8741
+ *
8742
+ * A job already in a terminal state is left alone and reported as not
8743
+ * cancelled, so an operator clicking cancel on a run that just finished sees
8744
+ * the truth rather than a row rewritten under them.
8745
+ *
8746
+ * @param id - The job id.
8747
+ * @returns Whether the row moved to `cancelled`.
8748
+ */
8749
+ async cancel(id) {
8750
+ const row = await this.repository.getByIdOrNull(id);
8751
+ if (row === null || TERMINAL.includes(String(row.status))) return false;
8752
+ const changed = await this.repository.update(
8753
+ { id },
8754
+ {
8755
+ status: JobStatus.CANCELLED,
8756
+ finishedAt: /* @__PURE__ */ new Date()
8757
+ }
8758
+ );
8759
+ return changed > 0;
8760
+ }
8761
+ /**
8762
+ * Read one job row.
8763
+ *
8764
+ * @param id - The job id.
8765
+ * @returns The row, or `null`.
8766
+ */
8767
+ async get(id) {
8768
+ return await this.repository.getByIdOrNull(id);
8769
+ }
8770
+ /**
8771
+ * Read a page of jobs, newest first.
8772
+ *
8773
+ * @param filter - Page, size and optional `name` / `status` filters.
8774
+ * @returns The page plus its metadata.
8775
+ */
8776
+ async list(filter = {}) {
8777
+ const filters = {};
8778
+ if (filter.name !== void 0 && filter.name !== "") filters.name = filter.name;
8779
+ if (filter.status !== void 0) filters.status = filter.status;
8780
+ const result = await this.repository.paginate({
8781
+ page: filter.page ?? 1,
8782
+ pageSize: filter.pageSize ?? 25,
8783
+ orderBy: "createdAt",
8784
+ ascending: false,
8785
+ ...Object.keys(filters).length === 0 ? {} : { filters }
8786
+ });
8787
+ return {
8788
+ items: result.items,
8789
+ total: result.total,
8790
+ page: result.page,
8791
+ pageSize: result.pageSize,
8792
+ pages: result.pages
8793
+ };
8794
+ }
8795
+ };
8598
8796
 
8599
8797
  // src/flags/backends.ts
8600
8798
  function coerceFlag(value) {
@@ -9334,6 +9532,199 @@ function partitionTotal(partition) {
9334
9532
  return partition.segments.reduce((total, segment) => total + segment.value, 0);
9335
9533
  }
9336
9534
 
9535
+ // src/admin/logs.ts
9536
+ var LIFTED = /* @__PURE__ */ new Set(["level", "logger", "message", "timestamp", "stack"]);
9537
+ function toLogEntry(raw) {
9538
+ const context = {};
9539
+ for (const [key, value] of Object.entries(raw)) {
9540
+ if (LIFTED.has(key) || value === null || value === void 0) continue;
9541
+ context[key] = value;
9542
+ }
9543
+ return {
9544
+ level: typeof raw.level === "string" ? raw.level : "info",
9545
+ logger: typeof raw.logger === "string" ? raw.logger : "",
9546
+ message: typeof raw.message === "string" ? raw.message : "",
9547
+ timestamp: typeof raw.timestamp === "string" ? raw.timestamp : "",
9548
+ stack: typeof raw.stack === "string" ? raw.stack : null,
9549
+ context,
9550
+ raw
9551
+ };
9552
+ }
9553
+ function filterLogEntries(entries, term) {
9554
+ const needle = term.trim().toLowerCase();
9555
+ if (needle === "") return entries;
9556
+ return entries.filter(
9557
+ (entry) => `${entry.message} ${entry.logger} ${entry.stack ?? ""}`.toLowerCase().includes(needle)
9558
+ );
9559
+ }
9560
+ function renderLogEntriesMarkdown(entries, options) {
9561
+ const lines = [
9562
+ "# Application logs",
9563
+ "",
9564
+ `- **Source:** \`${options.source}\``,
9565
+ `- **Search:** ${options.query === "" ? "_none_" : `\`${options.query}\``}`,
9566
+ `- **Exported:** ${entries.length} of ${options.total} matching record(s)`,
9567
+ ""
9568
+ ];
9569
+ if (entries.length < options.total) {
9570
+ lines.push(
9571
+ `> Truncated: the export is capped, so ${options.total - entries.length} older matching record(s) are not included.`,
9572
+ ""
9573
+ );
9574
+ }
9575
+ for (const entry of entries) {
9576
+ lines.push(
9577
+ `## ${entry.level.toUpperCase()} \u2014 ${entry.message || "(no message)"}`,
9578
+ "",
9579
+ `- **When:** ${entry.timestamp || "unknown"}`,
9580
+ `- **Logger:** ${entry.logger || "unknown"}`
9581
+ );
9582
+ for (const [key, value] of Object.entries(entry.context)) {
9583
+ lines.push(
9584
+ `- **${key}:** ${typeof value === "string" ? value : JSON.stringify(value)}`
9585
+ );
9586
+ }
9587
+ lines.push("");
9588
+ if (entry.stack !== null) {
9589
+ lines.push("```text", entry.stack, "```", "");
9590
+ }
9591
+ }
9592
+ return lines.join("\n");
9593
+ }
9594
+ function renderLogEntriesJson(entries) {
9595
+ return JSON.stringify(
9596
+ entries.map((entry) => entry.raw),
9597
+ null,
9598
+ 2
9599
+ );
9600
+ }
9601
+
9602
+ // src/admin/sqlConsole.ts
9603
+ var SqlCapability = {
9604
+ /** `SELECT`, `WITH … SELECT`, `EXPLAIN`, `SHOW`. */
9605
+ READ: "read",
9606
+ /** Adds rows. */
9607
+ INSERT: "insert",
9608
+ /** Changes rows. */
9609
+ UPDATE: "update",
9610
+ /** Removes rows. */
9611
+ DELETE: "delete",
9612
+ /** `CREATE` / `ALTER` / `COMMENT`. */
9613
+ DDL: "ddl",
9614
+ /** `DROP` and `TRUNCATE`: irreversible structure loss. */
9615
+ DROP: "drop",
9616
+ /**
9617
+ * `GRANT` / `REVOKE` / `SET`, and anything the analyser cannot classify.
9618
+ * Unknown statements land here on purpose, so a construct nobody anticipated
9619
+ * needs the most privileged capability rather than the least.
9620
+ */
9621
+ ADMIN: "admin"
9622
+ };
9623
+ var STATEMENT_CAPABILITIES = {
9624
+ select: SqlCapability.READ,
9625
+ with: SqlCapability.READ,
9626
+ explain: SqlCapability.READ,
9627
+ show: SqlCapability.READ,
9628
+ desc: SqlCapability.READ,
9629
+ describe: SqlCapability.READ,
9630
+ insert: SqlCapability.INSERT,
9631
+ replace: SqlCapability.INSERT,
9632
+ update: SqlCapability.UPDATE,
9633
+ delete: SqlCapability.DELETE,
9634
+ create: SqlCapability.DDL,
9635
+ alter: SqlCapability.DDL,
9636
+ comment: SqlCapability.DDL,
9637
+ rename: SqlCapability.DDL,
9638
+ drop: SqlCapability.DROP,
9639
+ truncate: SqlCapability.DROP
9640
+ };
9641
+ var PARSER_HINT = "The admin SQL console needs the optional peer `node-sql-parser`. Install it with: npm install node-sql-parser";
9642
+ async function loadSqlParser() {
9643
+ try {
9644
+ const module = await import('node-sql-parser');
9645
+ const Parser = module.Parser ?? module.default?.Parser;
9646
+ if (Parser === void 0) throw new Error(PARSER_HINT);
9647
+ return new Parser();
9648
+ } catch {
9649
+ throw new Error(PARSER_HINT);
9650
+ }
9651
+ }
9652
+ function analyzeSql(sql5, dialect, parser) {
9653
+ const capabilities = /* @__PURE__ */ new Set();
9654
+ const tables = /* @__PURE__ */ new Set();
9655
+ let statements = 0;
9656
+ let parsed = true;
9657
+ let unscopedWrite = false;
9658
+ try {
9659
+ const ast = parser.astify(sql5, { database: dialect });
9660
+ const list = Array.isArray(ast) ? ast : [ast];
9661
+ statements = list.length;
9662
+ for (const statement of list) {
9663
+ const type = String(statement.type ?? "").toLowerCase();
9664
+ capabilities.add(STATEMENT_CAPABILITIES[type] ?? SqlCapability.ADMIN);
9665
+ if ((type === "update" || type === "delete") && !statement.where) {
9666
+ unscopedWrite = true;
9667
+ }
9668
+ }
9669
+ } catch {
9670
+ parsed = false;
9671
+ statements = 1;
9672
+ capabilities.add(SqlCapability.ADMIN);
9673
+ }
9674
+ try {
9675
+ for (const entry of parser.tableList(sql5, { database: dialect })) {
9676
+ const name = entry.split("::").pop();
9677
+ if (name !== void 0 && name !== "null") tables.add(name.toLowerCase());
9678
+ }
9679
+ } catch {
9680
+ }
9681
+ return {
9682
+ statements,
9683
+ capabilities: [...capabilities],
9684
+ tables: [...tables],
9685
+ parsed,
9686
+ unscopedWrite
9687
+ };
9688
+ }
9689
+ function checkSqlPolicy(analysis, policy) {
9690
+ const granted = new Set(policy.capabilities ?? [SqlCapability.READ]);
9691
+ for (const capability of analysis.capabilities) {
9692
+ if (!granted.has(capability)) {
9693
+ return {
9694
+ allowed: false,
9695
+ reason: analysis.parsed ? `This console may not run ${capability} statements.` : "The statement could not be parsed, so it needs the admin capability."
9696
+ };
9697
+ }
9698
+ }
9699
+ if ((policy.requireWhereOnWrites ?? true) && analysis.unscopedWrite) {
9700
+ return {
9701
+ allowed: false,
9702
+ reason: "An UPDATE or DELETE without a WHERE clause is refused."
9703
+ };
9704
+ }
9705
+ const denied = new Set((policy.denyTables ?? []).map((name) => name.toLowerCase()));
9706
+ for (const table of analysis.tables) {
9707
+ if (denied.has(table)) {
9708
+ return { allowed: false, reason: `Table "${table}" is not available here.` };
9709
+ }
9710
+ }
9711
+ if (policy.allowTables !== void 0) {
9712
+ const allowed = new Set(policy.allowTables.map((name) => name.toLowerCase()));
9713
+ if (analysis.tables.length === 0) {
9714
+ return {
9715
+ allowed: false,
9716
+ reason: "This console only runs statements naming an allowed table."
9717
+ };
9718
+ }
9719
+ for (const table of analysis.tables) {
9720
+ if (!allowed.has(table)) {
9721
+ return { allowed: false, reason: `Table "${table}" is not on the allow list.` };
9722
+ }
9723
+ }
9724
+ }
9725
+ return { allowed: true, reason: null };
9726
+ }
9727
+
9337
9728
  // src/admin/inlines.ts
9338
9729
  function adminInline(options) {
9339
9730
  const slug = options.model.tablename;
@@ -9474,8 +9865,8 @@ function humanizeField(name) {
9474
9865
  function adminColumns(model) {
9475
9866
  return columnsOf(model);
9476
9867
  }
9477
- function widgetForColumn(column6) {
9478
- const { kind, meta } = column6.type;
9868
+ function widgetForColumn(column7) {
9869
+ const { kind, meta } = column7.type;
9479
9870
  const plain = (widget) => ({
9480
9871
  widget,
9481
9872
  step: null,
@@ -9519,11 +9910,11 @@ function widgetForColumn(column6) {
9519
9910
  return plain("text");
9520
9911
  }
9521
9912
  }
9522
- function isColumnOptional(column6) {
9523
- return !column6.flags.notNull || column6.flags.hasDefault || column6.flags.primaryKey;
9913
+ function isColumnOptional(column7) {
9914
+ return !column7.flags.notNull || column7.flags.hasDefault || column7.flags.primaryKey;
9524
9915
  }
9525
- function filterForColumn(column6) {
9526
- const { kind, meta } = column6.type;
9916
+ function filterForColumn(column7) {
9917
+ const { kind, meta } = column7.type;
9527
9918
  if (kind === "boolean") {
9528
9919
  return {
9529
9920
  kind: "select",
@@ -9547,11 +9938,11 @@ function filterForColumn(column6) {
9547
9938
  }
9548
9939
  return { kind: "text", options: [] };
9549
9940
  }
9550
- function foreignKeyTable(column6) {
9551
- return column6.reference?.table ?? null;
9941
+ function foreignKeyTable(column7) {
9942
+ return column7.reference?.table ?? null;
9552
9943
  }
9553
- function isSearchableColumn(column6) {
9554
- const { kind } = column6.type;
9944
+ function isSearchableColumn(column7) {
9945
+ const { kind } = column7.type;
9555
9946
  return kind === "varchar" || kind === "text" || kind === "char";
9556
9947
  }
9557
9948
 
@@ -9833,8 +10224,8 @@ function formatFieldValue(widget, value) {
9833
10224
  }
9834
10225
  return String(value);
9835
10226
  }
9836
- function literalDefault(column6) {
9837
- const fallback = column6.defaultValue;
10227
+ function literalDefault(column7) {
10228
+ const fallback = column7.defaultValue;
9838
10229
  if (fallback === null || fallback.kind !== "literal") return void 0;
9839
10230
  return fallback.value;
9840
10231
  }
@@ -9847,19 +10238,19 @@ function buildFormFields(admin, options = {}) {
9847
10238
  const autocompleteLabels = options.autocompleteLabels ?? {};
9848
10239
  const uploads = new Set(admin.uploadFields);
9849
10240
  return admin.editableFieldNames().flatMap((name) => {
9850
- const column6 = columns[name];
9851
- if (column6 === void 0) return [];
10241
+ const column7 = columns[name];
10242
+ if (column7 === void 0) return [];
9852
10243
  const related = foreignKeys[name];
9853
10244
  const autocompleteUrl = autocompleteUrls[name];
9854
- const spec = uploads.has(name) ? { widget: "file", step: null, options: [] } : autocompleteUrl !== void 0 ? { widget: "autocomplete", step: null, options: [] } : related === void 0 ? widgetForColumn(column6) : { widget: "select", step: null, options: related };
9855
- const raw = name in values ? values[name] : literalDefault(column6);
10245
+ const spec = uploads.has(name) ? { widget: "file", step: null, options: [] } : autocompleteUrl !== void 0 ? { widget: "autocomplete", step: null, options: [] } : related === void 0 ? widgetForColumn(column7) : { widget: "select", step: null, options: related };
10246
+ const raw = name in values ? values[name] : literalDefault(column7);
9856
10247
  return [
9857
10248
  {
9858
10249
  name,
9859
10250
  label: humanizeField(name),
9860
10251
  widget: spec.widget,
9861
10252
  value: spec.widget === "checkbox" ? "" : formatFieldValue(spec.widget, raw),
9862
- required: !isColumnOptional(column6),
10253
+ required: !isColumnOptional(column7),
9863
10254
  checked: spec.widget === "checkbox" && toBoolean(raw),
9864
10255
  step: spec.step,
9865
10256
  options: spec.options,
@@ -9876,8 +10267,8 @@ function toBoolean(value) {
9876
10267
  if (typeof value !== "string") return false;
9877
10268
  return ["true", "on", "yes", "1"].includes(value.trim().toLowerCase());
9878
10269
  }
9879
- function coerceValue(column6, widget, raw) {
9880
- const { kind, meta } = column6.type;
10270
+ function coerceValue(column7, widget, raw) {
10271
+ const { kind, meta } = column7.type;
9881
10272
  switch (widget) {
9882
10273
  case "number": {
9883
10274
  const parsed = Number(raw);
@@ -9921,10 +10312,10 @@ function parseFormBody(admin, body, options = {}) {
9921
10312
  const only = options.only === void 0 ? null : new Set(options.only);
9922
10313
  for (const name of admin.editableFieldNames()) {
9923
10314
  if (only !== null && !only.has(name)) continue;
9924
- const column6 = columns[name];
9925
- if (column6 === void 0) continue;
10315
+ const column7 = columns[name];
10316
+ if (column7 === void 0) continue;
9926
10317
  if (uploads.has(name)) continue;
9927
- const { widget } = widgetForColumn(column6);
10318
+ const { widget } = widgetForColumn(column7);
9928
10319
  if (widget === "checkbox") {
9929
10320
  data[name] = toBoolean(body[name]);
9930
10321
  continue;
@@ -9932,16 +10323,16 @@ function parseFormBody(admin, body, options = {}) {
9932
10323
  const submitted = body[name];
9933
10324
  const raw = typeof submitted === "string" ? submitted.trim() : "";
9934
10325
  if (raw === "") {
9935
- if (!isColumnOptional(column6)) {
10326
+ if (!isColumnOptional(column7)) {
9936
10327
  errors[name] = "This field is required.";
9937
10328
  continue;
9938
10329
  }
9939
- if (column6.flags.hasDefault && !(name in body)) continue;
10330
+ if (column7.flags.hasDefault && !(name in body)) continue;
9940
10331
  data[name] = null;
9941
10332
  continue;
9942
10333
  }
9943
10334
  try {
9944
- data[name] = coerceValue(column6, widget, raw);
10335
+ data[name] = coerceValue(column7, widget, raw);
9945
10336
  } catch (error) {
9946
10337
  errors[name] = error instanceof Error ? error.message : "Invalid value.";
9947
10338
  }
@@ -9959,9 +10350,9 @@ function foreignKeyFields(admin) {
9959
10350
  const columns = adminColumns(admin.model);
9960
10351
  const out = {};
9961
10352
  for (const name of admin.editableFieldNames()) {
9962
- const column6 = columns[name];
9963
- if (column6 === void 0) continue;
9964
- const table = foreignKeyTable(column6);
10353
+ const column7 = columns[name];
10354
+ if (column7 === void 0) continue;
10355
+ const table = foreignKeyTable(column7);
9965
10356
  if (table !== null) out[name] = table;
9966
10357
  }
9967
10358
  return out;
@@ -10347,7 +10738,14 @@ var AdminSite = class {
10347
10738
  };
10348
10739
 
10349
10740
  // src/admin/styles.ts
10350
- var ADMIN_CSS = `:root {
10741
+ var ADMIN_CSS_EXTRA = `
10742
+ .tempest-log-badge--succeeded { background: #dcfce7; color: #166534; }
10743
+ .tempest-log-badge--failed { background: #fee2e2; color: #991b1b; }
10744
+ .tempest-log-badge--running { background: #dbeafe; color: #1e40af; }
10745
+ .tempest-log-badge--queued { background: #f1f5f9; color: #475569; }
10746
+ .tempest-log-badge--cancelled { background: #fef3c7; color: #92400e; }
10747
+ `;
10748
+ var ADMIN_CSS_BASE = `:root {
10351
10749
  --tempest-bg: #0f172a;
10352
10750
  --tempest-bg-soft: #1e293b;
10353
10751
  --tempest-bg-row: #f8fafc;
@@ -11906,6 +12304,7 @@ a.tempest-admin-list__new:hover {
11906
12304
  }
11907
12305
  }
11908
12306
  `;
12307
+ var ADMIN_CSS = `${ADMIN_CSS_BASE}${ADMIN_CSS_EXTRA}`;
11909
12308
 
11910
12309
  // src/admin/theme.ts
11911
12310
  var FORBIDDEN_CHARS = ["<", ">", "{", "}", '"'];
@@ -11985,7 +12384,7 @@ function escapeHtml(value) {
11985
12384
  return String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
11986
12385
  }
11987
12386
  function renderLayout(context, title, body) {
11988
- const { site, theme, prefix, session, currentPath, navModels, messages } = context;
12387
+ const { site, theme, prefix, session, currentPath, navModels, navSystem, messages } = context;
11989
12388
  const authed = session !== null;
11990
12389
  const indexUrl = `${prefix}/`;
11991
12390
  const navLink = (url, label, active) => `<a href="${escapeHtml(url)}" class="tempest-admin-sidebar__link${active ? " tempest-admin-sidebar__link--active" : ""}">${escapeHtml(label)}</a>`;
@@ -11995,6 +12394,9 @@ function renderLayout(context, title, body) {
11995
12394
  ${navLink(indexUrl, "Dashboard", currentPath === indexUrl)}
11996
12395
  ${navModels.length > 0 ? `<span class="tempest-admin-sidebar__heading">Models</span>${navModels.map(
11997
12396
  (entry) => navLink(entry.url, entry.label, currentPath.startsWith(entry.url))
12397
+ ).join("")}` : ""}
12398
+ ${navSystem.length > 0 ? `<span class="tempest-admin-sidebar__heading">System</span>${navSystem.map(
12399
+ (entry) => navLink(entry.url, entry.label, currentPath.startsWith(entry.url))
11998
12400
  ).join("")}` : ""}
11999
12401
  </nav>
12000
12402
  </aside>` : "";
@@ -12163,11 +12565,11 @@ function renderFilter(filter) {
12163
12565
  function renderListPage(context, view) {
12164
12566
  const bulk = view.bulkActions.length > 0 && context.session !== null;
12165
12567
  const checkColumn = bulk ? 1 : 0;
12166
- const headers = view.columns.map((column6) => {
12167
- const state = view.sort[column6];
12168
- if (state === void 0) return `<th>${escapeHtml(column6)}</th>`;
12568
+ const headers = view.columns.map((column7) => {
12569
+ const state = view.sort[column7];
12570
+ if (state === void 0) return `<th>${escapeHtml(column7)}</th>`;
12169
12571
  const arrow = state.active ? state.ascending ? "\u25B2" : "\u25BC" : "\u2195";
12170
- return `<th><a class="tempest-sort${state.active ? " tempest-sort--active" : ""}" href="${escapeHtml(state.url)}"><span>${escapeHtml(column6)}</span><span class="tempest-sort__arrow" aria-hidden="true">${arrow}</span></a></th>`;
12572
+ return `<th><a class="tempest-sort${state.active ? " tempest-sort--active" : ""}" href="${escapeHtml(state.url)}"><span>${escapeHtml(column7)}</span><span class="tempest-sort__arrow" aria-hidden="true">${arrow}</span></a></th>`;
12171
12573
  }).join("");
12172
12574
  const rows = view.rows.length > 0 ? view.rows.map((row) => {
12173
12575
  const check = bulk ? `<td class="tempest-admin-list__check"><input type="checkbox" name="ids" value="${escapeHtml(row.identity)}" data-row-check aria-label="Select row"></td>` : "";
@@ -12308,7 +12710,7 @@ function renderInline(inline, csrfToken, error) {
12308
12710
  if (inline.rows.length === 0) {
12309
12711
  return `<section class="tempest-admin-inline">${heading}<p><em>No related records.</em></p></section>`;
12310
12712
  }
12311
- const head2 = `<tr>${inline.columns.map((column6) => `<th>${escapeHtml(column6)}</th>`).join("")}<th></th></tr>`;
12713
+ const head2 = `<tr>${inline.columns.map((column7) => `<th>${escapeHtml(column7)}</th>`).join("")}<th></th></tr>`;
12312
12714
  const body = inline.rows.map(
12313
12715
  (row) => `<tr>${row.cells.map((cell) => `<td>${escapeHtml(cell)}</td>`).join("")}<td>${row.url === null ? "" : `<a href="${escapeHtml(row.url)}">View</a>`}</td></tr>`
12314
12716
  ).join("");
@@ -12323,7 +12725,7 @@ function renderInline(inline, csrfToken, error) {
12323
12725
  ${more}
12324
12726
  </section>`;
12325
12727
  }
12326
- const head = `<tr>${inline.columns.map((column6) => `<th>${escapeHtml(column6)}</th>`).join("")}${inline.canDelete ? "<th>Delete</th>" : ""}</tr>`;
12728
+ const head = `<tr>${inline.columns.map((column7) => `<th>${escapeHtml(column7)}</th>`).join("")}${inline.canDelete ? "<th>Delete</th>" : ""}</tr>`;
12327
12729
  const renderRow = (row, isNew) => `<tr${isNew ? ' class="tempest-admin-inline__new"' : ""}>${row.fields.map((field) => `<td>${renderInlineCell(field)}</td>`).join("")}${inline.canDelete ? `<td class="tempest-admin-inline__del">${isNew ? "" : `<input type="checkbox" name="row.${escapeHtml(row.key)}.__delete" value="true">`}</td>` : ""}</tr>`;
12328
12730
  const rows = inline.rows.map((row) => renderRow(row, false)).join("");
12329
12731
  const blank = inline.newRow === null ? "" : renderRow(inline.newRow, true);
@@ -12345,11 +12747,11 @@ function renderInline(inline, csrfToken, error) {
12345
12747
  ${more}
12346
12748
  </section>`;
12347
12749
  }
12348
- function renderAuditPanel(audit) {
12349
- const rows = audit.fields.map(
12750
+ function renderAuditPanel(audit2) {
12751
+ const rows = audit2.fields.map(
12350
12752
  (field) => `<dt>${escapeHtml(field.label)}</dt><dd>${field.value === "" ? "<em>\u2014</em>" : escapeHtml(field.value)}</dd>`
12351
12753
  ).join("");
12352
- const history = audit.history.length > 0 ? `<ol class="tempest-admin-history">${audit.history.map((entry) => {
12754
+ const history = audit2.history.length > 0 ? `<ol class="tempest-admin-history">${audit2.history.map((entry) => {
12353
12755
  const changes = entry.changes.length > 0 ? `<table class="tempest-admin-history__changes"><thead><tr><th>Field</th><th>Before</th><th>After</th></tr></thead><tbody>${entry.changes.map(
12354
12756
  (change) => `<tr><td>${escapeHtml(change.field)}</td><td>${escapeHtml(change.before)}</td><td>${escapeHtml(change.after)}</td></tr>`
12355
12757
  ).join("")}</tbody></table>` : "<p><em>No field changes recorded.</em></p>";
@@ -12447,6 +12849,160 @@ function renderFormPage(context, view) {
12447
12849
  </section>`;
12448
12850
  return renderLayout(context, `${heading} \xB7 ${context.site.title}`, body);
12449
12851
  }
12852
+ function renderLogsPage(context, view) {
12853
+ const options = view.sources.map(
12854
+ (source) => `<option value="${escapeHtml(source.value)}"${source.selected ? " selected" : ""}>${escapeHtml(source.label)}</option>`
12855
+ ).join("");
12856
+ const rows = view.rows.length > 0 ? view.rows.map((row) => {
12857
+ const context_ = row.context.map(
12858
+ (entry) => `<span class="tempest-admin-logs__meta"><b>${escapeHtml(entry.key)}</b>: ${escapeHtml(entry.value)}</span>`
12859
+ ).join(" ");
12860
+ const message = row.stack === null ? `${escapeHtml(row.message)}${context_ === "" ? "" : `<br>${context_}`}` : `<details><summary>${escapeHtml(row.message)}</summary>${context_ === "" ? "" : `<p>${context_}</p>`}<pre>${escapeHtml(row.stack)}</pre></details>`;
12861
+ return `<tr>
12862
+ <td data-label="Level"><span class="tempest-log-badge tempest-log-badge--${escapeHtml(row.level)}">${escapeHtml(row.level)}</span></td>
12863
+ <td data-label="When">${escapeHtml(row.timestamp)}</td>
12864
+ <td data-label="Logger">${escapeHtml(row.logger)}</td>
12865
+ <td data-label="Message">${message}</td>
12866
+ </tr>`;
12867
+ }).join("") : `<tr><td colspan="4">No log records yet. Point <code>configureFileLogging</code> at the same directory to populate this page.</td></tr>`;
12868
+ const body = `<section class="tempest-admin-logs">
12869
+ <header class="tempest-admin-list__header">
12870
+ <h1>Logs</h1>
12871
+ <p>${escapeHtml(view.total)} record${view.total === 1 ? "" : "s"}.</p>
12872
+ </header>
12873
+ <div class="tempest-admin-list__toolbar">
12874
+ <form method="get" class="tempest-admin-list__filters">
12875
+ <label><span>Source</span><select name="source">${options}</select></label>
12876
+ <input type="search" name="q" value="${escapeHtml(view.query)}" placeholder="Search\u2026" aria-label="Search">
12877
+ <button type="submit">Apply</button>
12878
+ </form>
12879
+ <div class="tempest-admin-list__actions">
12880
+ <a href="${escapeHtml(view.exportMarkdownUrl)}">Export Markdown</a>
12881
+ <a href="${escapeHtml(view.exportJsonUrl)}">Export JSON</a>
12882
+ </div>
12883
+ </div>
12884
+ <div class="tempest-admin-table-wrap">
12885
+ <table class="tempest-admin-list__table">
12886
+ <thead><tr><th>Level</th><th>When</th><th>Logger</th><th>Message</th></tr></thead>
12887
+ <tbody>${rows}</tbody>
12888
+ </table>
12889
+ </div>
12890
+ ${view.pages > 1 ? `<nav class="tempest-admin-list__pagination" aria-label="Pagination">
12891
+ ${view.prevUrl !== null ? `<a href="${escapeHtml(view.prevUrl)}">\u2190 Prev</a>` : ""}
12892
+ <span>Page ${escapeHtml(view.page)} of ${escapeHtml(view.pages)}</span>
12893
+ ${view.nextUrl !== null ? `<a href="${escapeHtml(view.nextUrl)}">Next \u2192</a>` : ""}
12894
+ </nav>` : ""}
12895
+ <p class="tempest-admin-form__hint">Exports carry at most ${escapeHtml(view.exportMax)} records, newest first, honouring the filters above.</p>
12896
+ </section>`;
12897
+ return renderLayout(context, `Logs \xB7 ${context.site.title}`, body);
12898
+ }
12899
+ function renderTasksPage(context, view) {
12900
+ const declared = view.inventory === null ? "" : `<section class="tempest-admin-tasks__declared">
12901
+ <h2>Declared</h2>
12902
+ ${view.inventory.length === 0 ? "<p><em>No tasks registered in this process.</em></p>" : `<div class="tempest-admin-table-wrap"><table class="tempest-admin-list__table">
12903
+ <thead><tr><th>Task</th><th>Schedule</th><th>Description</th></tr></thead>
12904
+ <tbody>${view.inventory.map(
12905
+ (entry) => `<tr><td>${escapeHtml(entry.name)}</td><td>${escapeHtml(entry.schedule)}</td><td>${escapeHtml(entry.description)}</td></tr>`
12906
+ ).join("")}</tbody>
12907
+ </table></div>`}
12908
+ </section>`;
12909
+ const runs = view.runs === null ? "" : `<section class="tempest-admin-tasks__runs">
12910
+ <h2>Runs</h2>
12911
+ <div class="tempest-admin-list__toolbar">
12912
+ <form method="get" class="tempest-admin-list__filters">
12913
+ <label><span>Status</span><select name="status">${view.runs.statuses.map(
12914
+ (status) => `<option value="${escapeHtml(status.value)}"${status.selected ? " selected" : ""}>${escapeHtml(status.label)}</option>`
12915
+ ).join("")}</select></label>
12916
+ <input type="search" name="task" value="${escapeHtml(view.runs.nameQuery)}" placeholder="Task name\u2026" aria-label="Task name">
12917
+ <button type="submit">Apply</button>
12918
+ </form>
12919
+ </div>
12920
+ <div class="tempest-admin-table-wrap"><table class="tempest-admin-list__table">
12921
+ <thead><tr><th>Task</th><th>Status</th><th>Started</th><th>Finished</th><th>Attempts</th><th></th></tr></thead>
12922
+ <tbody>${view.runs.rows.length === 0 ? '<tr><td colspan="6">No runs recorded yet.</td></tr>' : view.runs.rows.map(
12923
+ (row) => `<tr>
12924
+ <td>${escapeHtml(row.name)}</td>
12925
+ <td><span class="tempest-log-badge tempest-log-badge--${escapeHtml(row.status)}">${escapeHtml(row.status)}</span></td>
12926
+ <td>${escapeHtml(row.startedAt)}</td>
12927
+ <td>${escapeHtml(row.finishedAt)}</td>
12928
+ <td>${escapeHtml(row.attempts)}</td>
12929
+ <td><a href="${escapeHtml(row.url)}">View</a></td>
12930
+ </tr>`
12931
+ ).join("")}</tbody>
12932
+ </table></div>
12933
+ ${view.runs.pages > 1 ? `<nav class="tempest-admin-list__pagination" aria-label="Pagination">
12934
+ ${view.runs.prevUrl !== null ? `<a href="${escapeHtml(view.runs.prevUrl)}">\u2190 Prev</a>` : ""}
12935
+ <span>Page ${escapeHtml(view.runs.page)} of ${escapeHtml(view.runs.pages)}</span>
12936
+ ${view.runs.nextUrl !== null ? `<a href="${escapeHtml(view.runs.nextUrl)}">Next \u2192</a>` : ""}
12937
+ </nav>` : ""}
12938
+ </section>`;
12939
+ const body = `<section class="tempest-admin-tasks">
12940
+ <header class="tempest-admin-list__header">
12941
+ <h1>Tasks</h1>
12942
+ <p>What this process declares, and what its workers recorded.</p>
12943
+ </header>
12944
+ ${declared}
12945
+ ${runs}
12946
+ <p class="tempest-admin-form__hint">Live queue depth is not shown: a broker does not expose it, and a number that looked like one would be worse than none.</p>
12947
+ </section>`;
12948
+ return renderLayout(context, `Tasks \xB7 ${context.site.title}`, body);
12949
+ }
12950
+ function renderTaskDetailPage(context, view) {
12951
+ if (context.session === null) throw new Error("The task detail requires a session");
12952
+ const block = (label, content) => content === null ? "" : `<h2>${escapeHtml(label)}</h2><pre class="tempest-admin-detail__json">${escapeHtml(content)}</pre>`;
12953
+ const body = `<section class="tempest-admin-detail">
12954
+ <header class="tempest-admin-detail__header">
12955
+ <h1>${escapeHtml(view.name)} \xB7 <span class="tempest-log-badge tempest-log-badge--${escapeHtml(view.status)}">${escapeHtml(view.status)}</span></h1>
12956
+ <div class="tempest-admin-detail__actions">
12957
+ <a href="${escapeHtml(view.backUrl)}">\u2190 Back to tasks</a>
12958
+ ${view.cancelUrl !== null ? `<form method="post" action="${escapeHtml(view.cancelUrl)}" class="tempest-admin-detail__delete" onsubmit="return confirm('Ask this run to stop?');">
12959
+ <input type="hidden" name="csrf_token" value="${escapeHtml(context.session.csrfToken)}">
12960
+ <button type="submit" class="tempest-admin-btn--danger">Cancel</button>
12961
+ </form>` : ""}
12962
+ </div>
12963
+ </header>
12964
+ <dl class="tempest-admin-detail__fields">${view.fields.map(
12965
+ (field) => `<dt>${escapeHtml(field.label)}</dt><dd>${field.value === "" ? "<em>\u2014</em>" : escapeHtml(field.value)}</dd>`
12966
+ ).join("")}</dl>
12967
+ ${view.error === null ? "" : `<p class="tempest-admin-form__error">${escapeHtml(view.error)}</p>`}
12968
+ ${block("Payload", view.payload)}
12969
+ ${block("Result", view.result)}
12970
+ </section>`;
12971
+ return renderLayout(context, `${view.name} \xB7 ${context.site.title}`, body);
12972
+ }
12973
+ function renderSqlPage(context, view) {
12974
+ if (context.session === null) throw new Error("The SQL console requires a session");
12975
+ const result = view.rowCount === null ? "" : `<p class="tempest-admin-form__hint">${escapeHtml(view.rowCount)} row${view.rowCount === 1 ? "" : "s"}${view.durationMs === null ? "" : ` in ${escapeHtml(view.durationMs)} ms`}${view.truncated ? " (truncated by the row cap)" : ""}.</p>
12976
+ <div class="tempest-admin-table-wrap">
12977
+ <table class="tempest-admin-list__table">
12978
+ <thead><tr>${view.columns.map((column7) => `<th>${escapeHtml(column7)}</th>`).join("")}</tr></thead>
12979
+ <tbody>${view.rows.map(
12980
+ (row) => `<tr>${row.map((cell) => `<td>${escapeHtml(cell)}</td>`).join("")}</tr>`
12981
+ ).join("")}</tbody>
12982
+ </table>
12983
+ </div>`;
12984
+ const body = `<section class="tempest-admin-sql">
12985
+ <header class="tempest-admin-list__header">
12986
+ <h1>SQL console</h1>
12987
+ <p>Capabilities: <code>${escapeHtml(view.capabilities.join(", "))}</code></p>
12988
+ </header>
12989
+ ${view.error !== null ? `<p class="tempest-admin-form__error">${escapeHtml(view.error)}</p>` : ""}
12990
+ <form method="post" action="${escapeHtml(`${context.prefix}/sql`)}" class="tempest-admin-form__form">
12991
+ <input type="hidden" name="csrf_token" value="${escapeHtml(context.session.csrfToken)}">
12992
+ <div class="tempest-admin-form__field">
12993
+ <label>
12994
+ <span>Statement</span>
12995
+ <textarea name="sql" rows="6" spellcheck="false" required>${escapeHtml(view.sql)}</textarea>
12996
+ </label>
12997
+ </div>
12998
+ <div class="tempest-admin-form__actions">
12999
+ <button type="submit">Run</button>
13000
+ </div>
13001
+ </form>
13002
+ ${result}
13003
+ </section>`;
13004
+ return renderLayout(context, `SQL console \xB7 ${context.site.title}`, body);
13005
+ }
12450
13006
  function renderImportPage(context, view) {
12451
13007
  if (context.session === null) throw new Error("The import page requires a session");
12452
13008
  const summary = view.created === null ? "" : `<p class="tempest-admin-import__summary">Created ${escapeHtml(view.created)} record${view.created === 1 ? "" : "s"}.</p>`;
@@ -12532,7 +13088,75 @@ var AUTOCOMPLETE_SCRIPT = `<script>
12532
13088
  });
12533
13089
  })();
12534
13090
  </script>`;
13091
+ function filesFor(dir, source) {
13092
+ if (source === "500") return [join(dir, HTTP_500_LOG_FILE)];
13093
+ if (source === "all") return Object.values(LEVEL_LOG_FILES).map((f) => join(dir, f));
13094
+ return [join(dir, LEVEL_LOG_FILES[source])];
13095
+ }
13096
+ async function readLogEntries(dir, source) {
13097
+ return await readEntries(filesFor(dir, source));
13098
+ }
13099
+ async function readEntries(files) {
13100
+ const entries = [];
13101
+ for (const file of files) {
13102
+ let content;
13103
+ try {
13104
+ content = await readFile(file, "utf8");
13105
+ } catch {
13106
+ continue;
13107
+ }
13108
+ for (const line of content.split("\n")) {
13109
+ if (!line.trim()) continue;
13110
+ try {
13111
+ entries.push(JSON.parse(line));
13112
+ } catch {
13113
+ }
13114
+ }
13115
+ }
13116
+ return entries;
13117
+ }
13118
+ function makeLogsRouter(options) {
13119
+ const router = Router();
13120
+ const path = options.path ?? "/logs";
13121
+ const guards = options.guards ?? [];
13122
+ const handler = (req, res, next) => {
13123
+ const source = req.query.source ?? "all";
13124
+ const validSources = ["all", "debug", "info", "warning", "error", "500"];
13125
+ if (!validSources.includes(source)) {
13126
+ res.status(422).json({ detail: "Invalid log source", code: "VALIDATION_ERROR", details: {} });
13127
+ return;
13128
+ }
13129
+ const page2 = Math.max(1, Number(req.query.page) || 1);
13130
+ const pageSize = Math.min(500, Math.max(1, Number(req.query.pageSize) || 50));
13131
+ readEntries(filesFor(options.dir, source)).then((entries) => {
13132
+ entries.reverse();
13133
+ const total = entries.length;
13134
+ const start = (page2 - 1) * pageSize;
13135
+ res.json({
13136
+ items: entries.slice(start, start + pageSize),
13137
+ total,
13138
+ page: page2,
13139
+ pageSize,
13140
+ pages: Math.ceil(total / pageSize)
13141
+ });
13142
+ }).catch(next);
13143
+ };
13144
+ router.get(path, ...guards, handler);
13145
+ return router;
13146
+ }
12535
13147
  var logger2 = new JSONLogger("tempest_express_sdk.admin.router");
13148
+ var JOB_STATUSES = [
13149
+ "queued",
13150
+ "running",
13151
+ "succeeded",
13152
+ "failed",
13153
+ "cancelled"
13154
+ ];
13155
+ var TERMINAL_JOB_STATUSES = ["succeeded", "failed", "cancelled"];
13156
+ var TASK_PAGE_SIZE = 25;
13157
+ var LOG_SOURCES = ["all", "debug", "info", "warning", "error", "500"];
13158
+ var LOG_PAGE_SIZE = 50;
13159
+ var LOG_EXPORT_MAX = 500;
12536
13160
  var INLINE_ROW_LIMIT = 50;
12537
13161
  var AUTOCOMPLETE_LIMIT = 20;
12538
13162
  var AUDIT_HISTORY_LIMIT = 50;
@@ -12564,6 +13188,16 @@ function makeAdminRouter(site, options) {
12564
13188
  const theme = resolveAdminTheme(site.theme);
12565
13189
  const showMetrics = options.showMetrics ?? true;
12566
13190
  const exportMaxRows = options.exportMaxRows ?? 5e3;
13191
+ const systemNav = [];
13192
+ if (options.logDir !== void 0) {
13193
+ systemNav.push({ label: "Logs", url: `${prefix}/logs` });
13194
+ }
13195
+ if (options.sqlConsole !== void 0) {
13196
+ systemNav.push({ label: "SQL console", url: `${prefix}/sql` });
13197
+ }
13198
+ if (options.tasks !== void 0) {
13199
+ systemNav.push({ label: "Tasks", url: `${prefix}/tasks` });
13200
+ }
12567
13201
  const maxUploadBytes = options.maxUploadBytes ?? 10 * 1024 * 1024;
12568
13202
  const sessions = new AdminSessionStore({
12569
13203
  secret: options.secretKey,
@@ -12585,6 +13219,7 @@ function makeAdminRouter(site, options) {
12585
13219
  label: admin.verboseNamePlural(),
12586
13220
  url: `${prefix}/m/${admin.slug()}`
12587
13221
  })),
13222
+ navSystem: systemNav,
12588
13223
  messages: flashFor(req)
12589
13224
  });
12590
13225
  const allows = async (principal, admin, action) => {
@@ -12795,6 +13430,296 @@ function makeAdminRouter(site, options) {
12795
13430
  );
12796
13431
  })
12797
13432
  );
13433
+ if (options.logDir !== void 0) {
13434
+ const logDir = options.logDir;
13435
+ router.get(
13436
+ `${prefix}/logs`,
13437
+ guarded(async (req, res) => {
13438
+ const state = await authenticate(req, res);
13439
+ if (state === null) return;
13440
+ const { entries, source, search } = await readLogs(req, logDir);
13441
+ const page2 = Math.max(1, Number.parseInt(queryString(req.query.page), 10) || 1);
13442
+ const start = (page2 - 1) * LOG_PAGE_SIZE;
13443
+ const pages = Math.max(1, Math.ceil(entries.length / LOG_PAGE_SIZE));
13444
+ const exportUrl = (format) => `${prefix}/logs/export?${buildQuery({ source, q: search, format })}`;
13445
+ const pageUrl = (target) => `?${buildQuery({ source, q: search, page: target })}`;
13446
+ html(
13447
+ res,
13448
+ renderLogsPage(context(req, state.session, state.visible), {
13449
+ sources: LOG_SOURCES.map((value) => ({
13450
+ value,
13451
+ label: value === "500" ? "HTTP 500" : humanizeField(value),
13452
+ selected: value === source
13453
+ })),
13454
+ query: search,
13455
+ rows: entries.slice(start, start + LOG_PAGE_SIZE).map((entry) => ({
13456
+ level: entry.level,
13457
+ timestamp: entry.timestamp,
13458
+ logger: entry.logger,
13459
+ message: entry.message,
13460
+ stack: entry.stack,
13461
+ context: Object.entries(entry.context).map(([key, value]) => ({
13462
+ key,
13463
+ value: typeof value === "string" ? value : JSON.stringify(value)
13464
+ }))
13465
+ })),
13466
+ total: entries.length,
13467
+ page: page2,
13468
+ pages,
13469
+ prevUrl: page2 > 1 ? pageUrl(page2 - 1) : null,
13470
+ nextUrl: page2 < pages ? pageUrl(page2 + 1) : null,
13471
+ exportMarkdownUrl: exportUrl("md"),
13472
+ exportJsonUrl: exportUrl("json"),
13473
+ exportMax: LOG_EXPORT_MAX
13474
+ })
13475
+ );
13476
+ })
13477
+ );
13478
+ router.get(
13479
+ `${prefix}/logs/export`,
13480
+ guarded(async (req, res) => {
13481
+ const state = await authenticate(req, res);
13482
+ if (state === null) return;
13483
+ const format = queryString(req.query.format) === "json" ? "json" : "md";
13484
+ const { entries, source, search } = await readLogs(req, logDir);
13485
+ const window = entries.slice(0, LOG_EXPORT_MAX);
13486
+ const payload = format === "json" ? renderLogEntriesJson(window) : renderLogEntriesMarkdown(window, {
13487
+ source,
13488
+ query: search,
13489
+ total: entries.length
13490
+ });
13491
+ res.status(200).type(format === "json" ? "application/json" : "text/markdown; charset=utf-8").set("content-disposition", `attachment; filename="logs.${format}"`).send(payload);
13492
+ })
13493
+ );
13494
+ }
13495
+ if (options.sqlConsole !== void 0) {
13496
+ const console_ = options.sqlConsole;
13497
+ const policy = console_.policy ?? {};
13498
+ const dialect = console_.dialect ?? "postgresql";
13499
+ const capabilities = policy.capabilities ?? [SqlCapability.READ];
13500
+ const maxRows = policy.maxRows ?? 200;
13501
+ router.get(
13502
+ `${prefix}/sql`,
13503
+ guarded(async (req, res) => {
13504
+ const state = await authenticate(req, res);
13505
+ if (state === null) return;
13506
+ html(
13507
+ res,
13508
+ renderSqlPage(context(req, state.session, state.visible), {
13509
+ sql: "",
13510
+ capabilities: [...capabilities],
13511
+ error: null,
13512
+ columns: [],
13513
+ rows: [],
13514
+ rowCount: null,
13515
+ truncated: false,
13516
+ durationMs: null
13517
+ })
13518
+ );
13519
+ })
13520
+ );
13521
+ router.post(
13522
+ `${prefix}/sql`,
13523
+ guarded(async (req, res) => {
13524
+ const state = await authenticate(req, res);
13525
+ if (state === null) return;
13526
+ if (!checkCsrf(req, res, state)) return;
13527
+ const body = req.body;
13528
+ const sql5 = typeof body.sql === "string" ? body.sql : "";
13529
+ const principal = backend.displayName(state.principal);
13530
+ const render = (view) => {
13531
+ html(
13532
+ res,
13533
+ renderSqlPage(context(req, state.session, state.visible), {
13534
+ sql: sql5,
13535
+ capabilities: [...capabilities],
13536
+ ...view
13537
+ }),
13538
+ view.error === null ? 200 : 400
13539
+ );
13540
+ };
13541
+ const parser = await loadSqlParser();
13542
+ const analysis = analyzeSql(sql5, dialect, parser);
13543
+ const verdict = checkSqlPolicy(analysis, policy);
13544
+ if (!verdict.allowed) {
13545
+ await audit(console_.onAudit, {
13546
+ sql: sql5,
13547
+ principal,
13548
+ allowed: false,
13549
+ reason: verdict.reason,
13550
+ analysis,
13551
+ durationMs: null,
13552
+ rowCount: null
13553
+ });
13554
+ render({
13555
+ error: verdict.reason,
13556
+ columns: [],
13557
+ rows: [],
13558
+ rowCount: null,
13559
+ truncated: false,
13560
+ durationMs: null
13561
+ });
13562
+ return;
13563
+ }
13564
+ const started = Date.now();
13565
+ let rows;
13566
+ try {
13567
+ rows = console_.run === void 0 ? await state.dbSession.raw(sql5).all() : await console_.run(sql5, state.dbSession);
13568
+ } catch (error) {
13569
+ const message = error instanceof Error ? error.message : String(error);
13570
+ await audit(console_.onAudit, {
13571
+ sql: sql5,
13572
+ principal,
13573
+ allowed: true,
13574
+ reason: message,
13575
+ analysis,
13576
+ durationMs: Date.now() - started,
13577
+ rowCount: null
13578
+ });
13579
+ render({
13580
+ error: message,
13581
+ columns: [],
13582
+ rows: [],
13583
+ rowCount: null,
13584
+ truncated: false,
13585
+ durationMs: null
13586
+ });
13587
+ return;
13588
+ }
13589
+ const durationMs = Date.now() - started;
13590
+ await audit(console_.onAudit, {
13591
+ sql: sql5,
13592
+ principal,
13593
+ allowed: true,
13594
+ reason: null,
13595
+ analysis,
13596
+ durationMs,
13597
+ rowCount: rows.length
13598
+ });
13599
+ const window = rows.slice(0, maxRows);
13600
+ const columns = window.length === 0 ? [] : Object.keys(window[0] ?? {});
13601
+ render({
13602
+ error: null,
13603
+ columns,
13604
+ rows: window.map(
13605
+ (row) => columns.map((column7) => formatCellValue(row[column7]))
13606
+ ),
13607
+ rowCount: rows.length,
13608
+ truncated: rows.length > window.length,
13609
+ durationMs
13610
+ });
13611
+ })
13612
+ );
13613
+ }
13614
+ if (options.tasks !== void 0) {
13615
+ const tasks = options.tasks;
13616
+ router.get(
13617
+ `${prefix}/tasks`,
13618
+ guarded(async (req, res) => {
13619
+ const state = await authenticate(req, res);
13620
+ if (state === null) return;
13621
+ const inventory = tasks.manager === void 0 ? null : tasks.manager.inventory().map((entry) => ({
13622
+ name: entry.name,
13623
+ description: entry.description ?? "",
13624
+ schedule: entry.schedule ?? "\u2014"
13625
+ }));
13626
+ let runs = null;
13627
+ if (tasks.jobs !== void 0) {
13628
+ const store = tasks.jobs(state.dbSession);
13629
+ const page2 = Math.max(1, Number.parseInt(queryString(req.query.page), 10) || 1);
13630
+ const status = queryString(req.query.status);
13631
+ const name = queryString(req.query.task);
13632
+ const result = await store.list({
13633
+ page: page2,
13634
+ pageSize: TASK_PAGE_SIZE,
13635
+ ...name === "" ? {} : { name },
13636
+ ...JOB_STATUSES.includes(status) ? { status } : {}
13637
+ });
13638
+ const pageUrl = (target) => `?${buildQuery({ status, task: name, page: target })}`;
13639
+ runs = {
13640
+ rows: result.items.map((row) => ({
13641
+ id: String(row.id),
13642
+ name: String(row.name ?? ""),
13643
+ status: String(row.status ?? ""),
13644
+ startedAt: formatCellValue(row.startedAt),
13645
+ finishedAt: formatCellValue(row.finishedAt),
13646
+ attempts: formatCellValue(row.attempts),
13647
+ url: `${prefix}/tasks/${String(row.id)}`
13648
+ })),
13649
+ total: result.total,
13650
+ page: result.page,
13651
+ pages: result.pages,
13652
+ prevUrl: result.page > 1 ? pageUrl(result.page - 1) : null,
13653
+ nextUrl: result.page < result.pages ? pageUrl(result.page + 1) : null,
13654
+ statuses: ["", ...JOB_STATUSES].map((value) => ({
13655
+ value,
13656
+ label: value === "" ? "\u2014 any \u2014" : humanizeField(value),
13657
+ selected: value === status
13658
+ })),
13659
+ nameQuery: name
13660
+ };
13661
+ }
13662
+ html(
13663
+ res,
13664
+ renderTasksPage(context(req, state.session, state.visible), {
13665
+ inventory,
13666
+ runs
13667
+ })
13668
+ );
13669
+ })
13670
+ );
13671
+ if (tasks.jobs !== void 0) {
13672
+ const jobs = tasks.jobs;
13673
+ router.get(
13674
+ `${prefix}/tasks/:id`,
13675
+ guarded(async (req, res) => {
13676
+ const state = await authenticate(req, res);
13677
+ if (state === null) return;
13678
+ const row = await jobs(state.dbSession).get(String(req.params.id));
13679
+ if (row === null) {
13680
+ html(res, renderNotFound(context(req, state.session, state.visible)), 404);
13681
+ return;
13682
+ }
13683
+ const status = String(row.status ?? "");
13684
+ html(
13685
+ res,
13686
+ renderTaskDetailPage(context(req, state.session, state.visible), {
13687
+ id: String(row.id),
13688
+ name: String(row.name ?? ""),
13689
+ status,
13690
+ fields: [
13691
+ { label: "Created At", value: formatCellValue(row.createdAt) },
13692
+ { label: "Started At", value: formatCellValue(row.startedAt) },
13693
+ { label: "Finished At", value: formatCellValue(row.finishedAt) },
13694
+ { label: "Attempts", value: formatCellValue(row.attempts) }
13695
+ ],
13696
+ payload: jsonBlock(row.payload),
13697
+ result: jsonBlock(row.result),
13698
+ error: typeof row.error === "string" && row.error !== "" ? row.error : null,
13699
+ backUrl: `${prefix}/tasks`,
13700
+ cancelUrl: TERMINAL_JOB_STATUSES.includes(status) ? null : `${prefix}/tasks/${String(row.id)}/cancel`
13701
+ })
13702
+ );
13703
+ })
13704
+ );
13705
+ router.post(
13706
+ `${prefix}/tasks/:id/cancel`,
13707
+ guarded(async (req, res) => {
13708
+ const state = await authenticate(req, res);
13709
+ if (state === null) return;
13710
+ if (!checkCsrf(req, res, state)) return;
13711
+ const id = String(req.params.id);
13712
+ const cancelled = await jobs(state.dbSession).cancel(id);
13713
+ res.redirect(
13714
+ `${prefix}/tasks/${id}?${buildQuery({
13715
+ flash: cancelled ? "The run was asked to stop." : "That run had already finished.",
13716
+ level: cancelled ? "success" : "warning"
13717
+ })}`
13718
+ );
13719
+ })
13720
+ );
13721
+ }
13722
+ }
12798
13723
  router.get(
12799
13724
  `${prefix}/m/:slug`,
12800
13725
  guarded(async (req, res) => {
@@ -13105,8 +14030,8 @@ function makeAdminRouter(site, options) {
13105
14030
  res.status(404).json({ options: [] });
13106
14031
  return;
13107
14032
  }
13108
- const column6 = adminColumns(admin.model)[field];
13109
- const table = column6 === void 0 ? null : foreignKeyTable(column6);
14033
+ const column7 = adminColumns(admin.model)[field];
14034
+ const table = column7 === void 0 ? null : foreignKeyTable(column7);
13110
14035
  const referenced = table === null ? null : site.get(table);
13111
14036
  if (referenced === null) {
13112
14037
  res.json({ options: [] });
@@ -13415,7 +14340,7 @@ function makeAdminRouter(site, options) {
13415
14340
  formAction: "",
13416
14341
  rows: visible.map((child) => ({
13417
14342
  key: String(child[childAdmin?.identityField ?? "id"]),
13418
- cells: columns.map((column6) => formatCellValue(child[column6])),
14343
+ cells: columns.map((column7) => formatCellValue(child[column7])),
13419
14344
  fields: [],
13420
14345
  url: childAdmin === null ? null : `${prefix}/m/${inline.slug}/${String(child[childAdmin.identityField])}`
13421
14346
  })),
@@ -13512,6 +14437,15 @@ function makeAdminRouter(site, options) {
13512
14437
  const row = await admin.repository(dbSession).first({ [admin.identityField]: identity });
13513
14438
  return row ?? null;
13514
14439
  }
14440
+ async function readLogs(req, dir) {
14441
+ const requested = queryString(req.query.source);
14442
+ const source = LOG_SOURCES.includes(requested) ? requested : "all";
14443
+ const search = queryString(req.query.q);
14444
+ const raw = await readLogEntries(dir, source);
14445
+ const entries = filterLogEntries(raw.map(toLogEntry), search);
14446
+ entries.reverse();
14447
+ return { entries, source, search };
14448
+ }
13515
14449
  function renderImport(req, state, admin, error, created, rowErrors) {
13516
14450
  return renderImportPage(context(req, state.session, state.visible), {
13517
14451
  title: admin.verboseNamePlural(),
@@ -13538,8 +14472,8 @@ function makeAdminRouter(site, options) {
13538
14472
  for (const field of admin.uploadFields) {
13539
14473
  const file = files.find((candidate) => candidate.field === field);
13540
14474
  if (file === void 0) {
13541
- const column6 = columns[field];
13542
- if (creating && column6 !== void 0 && !isColumnOptional(column6)) {
14475
+ const column7 = columns[field];
14476
+ if (creating && column7 !== void 0 && !isColumnOptional(column7)) {
13543
14477
  errors[field] = "This field is required.";
13544
14478
  }
13545
14479
  continue;
@@ -13571,14 +14505,14 @@ function makeAdminRouter(site, options) {
13571
14505
  });
13572
14506
  const displayed = admin.listDisplayNames();
13573
14507
  const sort = {};
13574
- for (const column6 of displayed) {
13575
- if (!(column6 in columns)) continue;
13576
- const active = (query.sortColumn ?? admin.orderKey) === column6;
14508
+ for (const column7 of displayed) {
14509
+ if (!(column7 in columns)) continue;
14510
+ const active = (query.sortColumn ?? admin.orderKey) === column7;
13577
14511
  const nextAscending = active ? !query.ascending : true;
13578
- sort[column6] = {
14512
+ sort[column7] = {
13579
14513
  url: `?${buildQuery({
13580
14514
  ...query.baseQuery,
13581
- sort: column6,
14515
+ sort: column7,
13582
14516
  dir: nextAscending ? "asc" : "desc"
13583
14517
  })}`,
13584
14518
  active,
@@ -13599,7 +14533,7 @@ function makeAdminRouter(site, options) {
13599
14533
  const identity = String(row[admin.identityField]);
13600
14534
  return {
13601
14535
  identity,
13602
- cells: displayed.map((column6) => formatCellValue(row[column6])),
14536
+ cells: displayed.map((column7) => formatCellValue(row[column7])),
13603
14537
  url: `${prefix}/m/${admin.slug()}/${identity}`
13604
14538
  };
13605
14539
  }),
@@ -13645,9 +14579,9 @@ function makeAdminRouter(site, options) {
13645
14579
  }
13646
14580
  const filterViews = [];
13647
14581
  for (const field of admin.listFilter) {
13648
- const column6 = columns[field];
13649
- if (column6 === void 0) continue;
13650
- const spec = filterForColumn(column6);
14582
+ const column7 = columns[field];
14583
+ if (column7 === void 0) continue;
14584
+ const spec = filterForColumn(column7);
13651
14585
  if (spec.kind === "daterange") {
13652
14586
  const from = queryString(req.query[`filter_${field}_from`]);
13653
14587
  const to = queryString(req.query[`filter_${field}_to`]);
@@ -13666,12 +14600,12 @@ function makeAdminRouter(site, options) {
13666
14600
  });
13667
14601
  continue;
13668
14602
  }
13669
- const related = await relatedOptions(column6, dbSession);
14603
+ const related = await relatedOptions(column7, dbSession);
13670
14604
  const options2 = related ?? spec.options;
13671
14605
  const value = queryString(req.query[`filter_${field}`]);
13672
14606
  if (value !== "") {
13673
14607
  conditions.push({
13674
- [field]: column6.type.kind === "boolean" ? value === "true" : value
14608
+ [field]: column7.type.kind === "boolean" ? value === "true" : value
13675
14609
  });
13676
14610
  }
13677
14611
  filterViews.push({
@@ -13689,8 +14623,8 @@ function makeAdminRouter(site, options) {
13689
14623
  });
13690
14624
  }
13691
14625
  const searchable = admin.searchFields.filter((field) => {
13692
- const column6 = columns[field];
13693
- return column6 !== void 0 && isSearchableColumn(column6);
14626
+ const column7 = columns[field];
14627
+ return column7 !== void 0 && isSearchableColumn(column7);
13694
14628
  });
13695
14629
  if (search !== "" && searchable.length > 0) {
13696
14630
  conditions.push(
@@ -13723,8 +14657,8 @@ function makeAdminRouter(site, options) {
13723
14657
  lens: lens?.slug ?? ""
13724
14658
  };
13725
14659
  }
13726
- async function relatedOptions(column6, dbSession) {
13727
- const table = foreignKeyTable(column6);
14660
+ async function relatedOptions(column7, dbSession) {
14661
+ const table = foreignKeyTable(column7);
13728
14662
  if (table === null) return null;
13729
14663
  const referenced = site.get(table);
13730
14664
  if (referenced === null) return null;
@@ -13739,9 +14673,9 @@ function makeAdminRouter(site, options) {
13739
14673
  const options2 = {};
13740
14674
  for (const field of Object.keys(foreignKeyFields(admin))) {
13741
14675
  if (admin.autocompleteFields.includes(field)) continue;
13742
- const column6 = columns[field];
13743
- if (column6 === void 0) continue;
13744
- const related = await relatedOptions(column6, dbSession);
14676
+ const column7 = columns[field];
14677
+ if (column7 === void 0) continue;
14678
+ const related = await relatedOptions(column7, dbSession);
13745
14679
  if (related !== null) options2[field] = related;
13746
14680
  }
13747
14681
  return options2;
@@ -13760,8 +14694,8 @@ function makeAdminRouter(site, options) {
13760
14694
  for (const field of admin.autocompleteFields) {
13761
14695
  const value = row[field];
13762
14696
  if (value === null || value === void 0 || value === "") continue;
13763
- const column6 = columns[field];
13764
- const table = column6 === void 0 ? null : foreignKeyTable(column6);
14697
+ const column7 = columns[field];
14698
+ const table = column7 === void 0 ? null : foreignKeyTable(column7);
13765
14699
  const referenced = table === null ? null : site.get(table);
13766
14700
  if (referenced === null) continue;
13767
14701
  const related = await referenced.repository(dbSession).first({ [referenced.identityField]: value });
@@ -13800,6 +14734,21 @@ function inlineFieldNames(childAdmin, inline) {
13800
14734
  function inlineFields(childAdmin, names, key, values, errors) {
13801
14735
  return buildFormFields(childAdmin, { values, errors }).filter((field) => names.includes(field.name)).map((field) => ({ ...field, name: `row.${key}.${field.name}` }));
13802
14736
  }
14737
+ function jsonBlock(value) {
14738
+ if (value === null || value === void 0) return null;
14739
+ if (typeof value === "object" && Object.keys(value).length === 0) return null;
14740
+ return JSON.stringify(value, null, 2);
14741
+ }
14742
+ async function audit(hook, entry) {
14743
+ if (hook === void 0) return;
14744
+ try {
14745
+ await hook(entry);
14746
+ } catch (error) {
14747
+ logger2.error("Admin SQL audit hook failed", {
14748
+ error: error instanceof Error ? error.message : String(error)
14749
+ });
14750
+ }
14751
+ }
13803
14752
  function flagAllows(admin, action) {
13804
14753
  if (action === AdminPermission.CREATE) return admin.canCreate;
13805
14754
  if (action === AdminPermission.EDIT) return admin.canEdit;
@@ -13953,7 +14902,7 @@ function csvField(value) {
13953
14902
  function toCsv(columns, rows) {
13954
14903
  const lines = [columns.map(csvField).join(",")];
13955
14904
  for (const row of rows) {
13956
- lines.push(columns.map((column6) => csvField(exportValue(row[column6]))).join(","));
14905
+ lines.push(columns.map((column7) => csvField(exportValue(row[column7]))).join(","));
13957
14906
  }
13958
14907
  return `${lines.join("\r\n")}\r
13959
14908
  `;
@@ -13961,7 +14910,7 @@ function toCsv(columns, rows) {
13961
14910
  function toJson(columns, rows) {
13962
14911
  return JSON.stringify(
13963
14912
  rows.map(
13964
- (row) => Object.fromEntries(columns.map((column6) => [column6, exportValue(row[column6])]))
14913
+ (row) => Object.fromEntries(columns.map((column7) => [column7, exportValue(row[column7])]))
13965
14914
  ),
13966
14915
  null,
13967
14916
  2
@@ -15886,59 +16835,6 @@ function makeToolSpecRouter(spec, options = {}) {
15886
16835
  });
15887
16836
  return router;
15888
16837
  }
15889
- function filesFor(dir, source) {
15890
- if (source === "500") return [join(dir, HTTP_500_LOG_FILE)];
15891
- if (source === "all") return Object.values(LEVEL_LOG_FILES).map((f) => join(dir, f));
15892
- return [join(dir, LEVEL_LOG_FILES[source])];
15893
- }
15894
- async function readEntries(files) {
15895
- const entries = [];
15896
- for (const file of files) {
15897
- let content;
15898
- try {
15899
- content = await readFile(file, "utf8");
15900
- } catch {
15901
- continue;
15902
- }
15903
- for (const line of content.split("\n")) {
15904
- if (!line.trim()) continue;
15905
- try {
15906
- entries.push(JSON.parse(line));
15907
- } catch {
15908
- }
15909
- }
15910
- }
15911
- return entries;
15912
- }
15913
- function makeLogsRouter(options) {
15914
- const router = Router();
15915
- const path = options.path ?? "/logs";
15916
- const guards = options.guards ?? [];
15917
- const handler = (req, res, next) => {
15918
- const source = req.query.source ?? "all";
15919
- const validSources = ["all", "debug", "info", "warning", "error", "500"];
15920
- if (!validSources.includes(source)) {
15921
- res.status(422).json({ detail: "Invalid log source", code: "VALIDATION_ERROR", details: {} });
15922
- return;
15923
- }
15924
- const page2 = Math.max(1, Number(req.query.page) || 1);
15925
- const pageSize = Math.min(500, Math.max(1, Number(req.query.pageSize) || 50));
15926
- readEntries(filesFor(options.dir, source)).then((entries) => {
15927
- entries.reverse();
15928
- const total = entries.length;
15929
- const start = (page2 - 1) * pageSize;
15930
- res.json({
15931
- items: entries.slice(start, start + pageSize),
15932
- total,
15933
- page: page2,
15934
- pageSize,
15935
- pages: Math.ceil(total / pageSize)
15936
- });
15937
- }).catch(next);
15938
- };
15939
- router.get(path, ...guards, handler);
15940
- return router;
15941
- }
15942
16838
  function createTestDatabase(models) {
15943
16839
  const driver = NodeSqliteDriver.open(":memory:");
15944
16840
  for (const model of models) {
@@ -15970,6 +16866,6 @@ async function withTestDatabase(models, fn) {
15970
16866
  }
15971
16867
  }
15972
16868
 
15973
- export { ADMIN_CSS, ActivationService, AdminJsonSite, AdminModel, AdminPermission, AdminSessionStore, AdminSite, AppException, AttemptThrottle, AuditAction, BaseAuditLogModel, BaseController, BaseModel, BaseOAuthClient, BaseOutboxModel, BaseService, BaseUserModel, BaseUserRefreshTokenModel, BaseUserTokenModel, CEP_PATTERN, CNPJ_PATTERN, CPF_PATTERN, CSRF_COOKIE_NAME, CSRF_HEADER_NAME, CircuitOpenError, CompositeFeatureFlagBackend, ConflictException, DEFAULT_DOCS_FAVICON, DEFAULT_LOCALE, EmailProvider, EmailUtils, EnvFeatureFlagBackend, EventStream, ExpiredTokenException, FeatureFlags, ForbiddenException, GitHubOAuthClient, GoogleOAuthClient, GracefulShutdown, HTTPClient, HTTP_500_LOG_FILE, HTTP_500_MARKER, HttpMetrics, IDEMPOTENCY_HEADER, InvalidTokenException, JSONLogger, JWTUtils, LEVEL_LOG_FILES, LocalUploadStorage, MemoryBroker, MemoryCacheManager, MemoryFeatureFlagBackend, MemoryIdempotencyStore, MemoryRateLimitStore, MemorySessionStore, MemoryThrottleBackend, MessageCatalog, MessagingHub, MetricsUtils, MfaService, MultipartLimitError, NotFoundException, OAuthError, OIDCProvider, OutboxRelay, OutboxStatus, PHONE_BR_PATTERN, PasswordResetService, REDOC_CDN_URL, REQUEST_ID_HEADER, RabbitBroker, RedisCacheManager, RedisIdempotencyStore, RedisRateLimitStore, RedisSSEBroker, RedisSessionStore, Region, RetryPolicy, S3UploadStorage, SSEBroker, ServerSentEvent, SessionService, TOTPHelper, TaskManager, TelegramProvider, TenantScopedRepository, TooManyRequestsException, TwilioSmsProvider, UF, UnauthorizedException, UserAuthService, UserModelAuthBackend, UserTokenPurpose, ValidationException, WebPushDispatcher, WebPushError, WebPushGoneError, WebSocketHub, WebhookSignatureVerifier, WhatsAppProvider, activationSchema, addLogSink, adminAction, adminColumns, adminInline, adminLens, adminThemeCss, attachWebSocketHub, authResponseSchema, authSettingsShape, backupDatabase, bearerToken, bodySizeLimitMiddleware, broadcastText, buildContentDisposition, buildFormFields, buildPaginationLinkHeader, cached2 as cached, cepField, citiesByUf, cnpjField, coerceFlag, configureFileLogging, configureLogging, cpfField, cpfOrCnpjField, createApp, createOpenApiRegistry, createTestDatabase, createdByColumn, csrfMiddleware, csrfTokenMatches, cursorPaginationFilterSchema, cursorPaginationSchema, decodeCursor, defaultMessageCatalog, defineEnum, deletedAtColumn, diffSnapshots, emailSettingsShape, encodeCursor, envList, escapeHtml, filterForColumn, foreignKeyFields, foreignKeyLabel, foreignKeyTable, formatCellValue, formatFieldValue, generateCsrfToken, generateOAuthState, generateOpaqueToken, generateOpenApiDocument, getAuth, getClientIp, getConditions, getPaginationConditions, getRequestId, getState, groupInlineSubmission, hashOpaqueToken, humanizeField, idempotencyMiddleware, inboundMessageSchema, isColumnOptional, isMultipart, isSearchableColumn, isValidCep, isValidCity, isValidCnpj, isValidCpf, isValidCpfCnpj, isValidPhoneBr, isValidUf, jwtSettingsShape, keyByHeader, keyByIp, keyByJwtClaim, keyByJwtSubject, listStates, logEntrySchema, logSettingsShape, loginSchema, makeAdminJsonRouter, makeAdminRouter, makeAppExceptionHandler, makeAuthRouter, makeFlagGuard, makeHealthRouter, makeJwtAuthMiddleware, makeLogsRouter, makeMetricsRouter, makeSessionMiddleware, makeToolSpecRouter, makeTwilioWebhookRouter, makeUnhandledExceptionHandler, makeWhatsAppWebhookRouter, metricCard, mfaChallengeSchema, mfaCodeSchema, mfaEnrollResponseSchema, minioSettingsShape, modifyDict, mountOpenApiJson, mountRedoc, mountSwaggerUi, normalizeCep, normalizeCnpj, normalizeCpf, normalizeCpfCnpj, normalizePhoneBr, normalizeUf, notFoundHandler, onlyDigits, paginationFilterSchema, paginationSchema, parseAcceptLanguage, parseCookies, parseCsv, parseFormBody, parseMultipart, partitionTotal, passwordResetConfirmSchema, passwordResetRequestSchema, phoneBrField, prometheusMiddleware, rabbitmqSettingsShape, rateLimitMiddleware, redisSettingsShape, refreshSchema, registerExceptionHandlers, renderAuthResultPage, renderDashboardPage, renderDetailPage, renderFormPage, renderImportPage, renderLayout, renderListPage, renderLoginPage, renderMfaPage, renderPasswordResetFormPage, requestIdMiddleware, requestTracingMiddleware, requireRoles, resolveAdminTheme, resolveDownloadPath, resolveRedocBundle, runServer, runWithRequestContext, sendBytesDownload, sendFileDownload, sessionCookie, sessionSettingsShape, setRequestId, signupSchema, snapshot, sseResponse, statesByRegion, syncFilterSchema, syncPaginationSchema, tableNameFor, toUtc, tokenFromUrl, tokenPairSchema, tokenSettingsShape, trendDirection, trendPercent, ufField, updatedByColumn, uploadSettingsShape, userPublicSchema, utcnow, validateTwilioSignature, verifyOpaqueToken, webPushKeysSchema, webPushPayloadSchema, webPushSettingsShape, webPushSubscriptionSchema, webSocketSettingsShape, widgetForColumn, withTestDatabase, wrapWithSlowQueryLog, wsEnvelopeSchema };
16869
+ export { ADMIN_CSS, ActivationService, AdminJsonSite, AdminModel, AdminPermission, AdminSessionStore, AdminSite, AppException, AttemptThrottle, AuditAction, BaseAuditLogModel, BaseController, BaseJobModel, BaseModel, BaseOAuthClient, BaseOutboxModel, BaseService, BaseUserModel, BaseUserRefreshTokenModel, BaseUserTokenModel, CEP_PATTERN, CNPJ_PATTERN, CPF_PATTERN, CSRF_COOKIE_NAME, CSRF_HEADER_NAME, CircuitOpenError, CompositeFeatureFlagBackend, ConflictException, DEFAULT_DOCS_FAVICON, DEFAULT_LOCALE, EmailProvider, EmailUtils, EnvFeatureFlagBackend, EventStream, ExpiredTokenException, FeatureFlags, ForbiddenException, GitHubOAuthClient, GoogleOAuthClient, GracefulShutdown, HTTPClient, HTTP_500_LOG_FILE, HTTP_500_MARKER, HttpMetrics, IDEMPOTENCY_HEADER, InvalidTokenException, JSONLogger, JWTUtils, JobStatus, JobStore, LEVEL_LOG_FILES, LocalUploadStorage, MemoryBroker, MemoryCacheManager, MemoryFeatureFlagBackend, MemoryIdempotencyStore, MemoryRateLimitStore, MemorySessionStore, MemoryThrottleBackend, MessageCatalog, MessagingHub, MetricsUtils, MfaService, MultipartLimitError, NotFoundException, OAuthError, OIDCProvider, OutboxRelay, OutboxStatus, PHONE_BR_PATTERN, PasswordResetService, REDOC_CDN_URL, REQUEST_ID_HEADER, RabbitBroker, RedisCacheManager, RedisIdempotencyStore, RedisRateLimitStore, RedisSSEBroker, RedisSessionStore, Region, RetryPolicy, S3UploadStorage, SSEBroker, ServerSentEvent, SessionService, SqlCapability, TOTPHelper, TaskManager, TelegramProvider, TenantScopedRepository, TooManyRequestsException, TwilioSmsProvider, UF, UnauthorizedException, UserAuthService, UserModelAuthBackend, UserTokenPurpose, ValidationException, WebPushDispatcher, WebPushError, WebPushGoneError, WebSocketHub, WebhookSignatureVerifier, WhatsAppProvider, activationSchema, addLogSink, adminAction, adminColumns, adminInline, adminLens, adminThemeCss, analyzeSql, attachWebSocketHub, authResponseSchema, authSettingsShape, backupDatabase, bearerToken, bodySizeLimitMiddleware, broadcastText, buildContentDisposition, buildFormFields, buildPaginationLinkHeader, cached2 as cached, cepField, checkSqlPolicy, citiesByUf, cnpjField, coerceFlag, configureFileLogging, configureLogging, cpfField, cpfOrCnpjField, createApp, createOpenApiRegistry, createTestDatabase, createdByColumn, csrfMiddleware, csrfTokenMatches, cursorPaginationFilterSchema, cursorPaginationSchema, decodeCursor, defaultMessageCatalog, defineEnum, deletedAtColumn, diffSnapshots, emailSettingsShape, encodeCursor, envList, escapeHtml, filterForColumn, filterLogEntries, foreignKeyFields, foreignKeyLabel, foreignKeyTable, formatCellValue, formatFieldValue, generateCsrfToken, generateOAuthState, generateOpaqueToken, generateOpenApiDocument, getAuth, getClientIp, getConditions, getPaginationConditions, getRequestId, getState, groupInlineSubmission, hashOpaqueToken, humanizeField, idempotencyMiddleware, inboundMessageSchema, isColumnOptional, isMultipart, isSearchableColumn, isValidCep, isValidCity, isValidCnpj, isValidCpf, isValidCpfCnpj, isValidPhoneBr, isValidUf, jwtSettingsShape, keyByHeader, keyByIp, keyByJwtClaim, keyByJwtSubject, listStates, loadSqlParser, logEntrySchema, logSettingsShape, loginSchema, makeAdminJsonRouter, makeAdminRouter, makeAppExceptionHandler, makeAuthRouter, makeFlagGuard, makeHealthRouter, makeJwtAuthMiddleware, makeLogsRouter, makeMetricsRouter, makeSessionMiddleware, makeToolSpecRouter, makeTwilioWebhookRouter, makeUnhandledExceptionHandler, makeWhatsAppWebhookRouter, metricCard, mfaChallengeSchema, mfaCodeSchema, mfaEnrollResponseSchema, minioSettingsShape, modifyDict, mountOpenApiJson, mountRedoc, mountSwaggerUi, normalizeCep, normalizeCnpj, normalizeCpf, normalizeCpfCnpj, normalizePhoneBr, normalizeUf, notFoundHandler, onlyDigits, paginationFilterSchema, paginationSchema, parseAcceptLanguage, parseCookies, parseCsv, parseFormBody, parseMultipart, partitionTotal, passwordResetConfirmSchema, passwordResetRequestSchema, phoneBrField, prometheusMiddleware, rabbitmqSettingsShape, rateLimitMiddleware, readLogEntries, redisSettingsShape, refreshSchema, registerExceptionHandlers, renderAuthResultPage, renderDashboardPage, renderDetailPage, renderFormPage, renderImportPage, renderLayout, renderListPage, renderLogEntriesJson, renderLogEntriesMarkdown, renderLoginPage, renderLogsPage, renderMfaPage, renderPasswordResetFormPage, renderSqlPage, renderTaskDetailPage, renderTasksPage, requestIdMiddleware, requestTracingMiddleware, requireRoles, resolveAdminTheme, resolveDownloadPath, resolveRedocBundle, runServer, runWithRequestContext, sendBytesDownload, sendFileDownload, sessionCookie, sessionSettingsShape, setRequestId, signupSchema, snapshot, sseResponse, statesByRegion, syncFilterSchema, syncPaginationSchema, tableNameFor, toLogEntry, toUtc, tokenFromUrl, tokenPairSchema, tokenSettingsShape, trendDirection, trendPercent, ufField, updatedByColumn, uploadSettingsShape, userPublicSchema, utcnow, validateTwilioSignature, verifyOpaqueToken, webPushKeysSchema, webPushPayloadSchema, webPushSettingsShape, webPushSubscriptionSchema, webSocketSettingsShape, widgetForColumn, withTestDatabase, wrapWithSlowQueryLog, wsEnvelopeSchema };
15974
16870
  //# sourceMappingURL=index.js.map
15975
16871
  //# sourceMappingURL=index.js.map