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.cjs CHANGED
@@ -8673,6 +8673,7 @@ var TaskManager = class {
8673
8673
  broker;
8674
8674
  queue;
8675
8675
  handlers = /* @__PURE__ */ new Map();
8676
+ metadata = /* @__PURE__ */ new Map();
8676
8677
  unsubscribe = null;
8677
8678
  /**
8678
8679
  * @param options - Broker and queue name.
@@ -8686,9 +8687,32 @@ var TaskManager = class {
8686
8687
  *
8687
8688
  * @param name - The task name.
8688
8689
  * @param handler - The handler invoked with the task payload.
8690
+ * @param options - Description and declared schedule, surfaced by
8691
+ * {@link TaskManager.inventory} and by the admin panel's tasks page.
8689
8692
  */
8690
- register(name, handler) {
8693
+ register(name, handler, options = {}) {
8691
8694
  this.handlers.set(name, handler);
8695
+ this.metadata.set(name, options);
8696
+ }
8697
+ /**
8698
+ * Return what this process would run, ordered by name.
8699
+ *
8700
+ * This is the **declared** side of background work — the handlers this
8701
+ * process knows about — not queue state. A broker's pending depth is not
8702
+ * something the manager can see, and a screen that implied otherwise would
8703
+ * be worse than one that says nothing.
8704
+ *
8705
+ * @returns One entry per registered task.
8706
+ */
8707
+ inventory() {
8708
+ return [...this.handlers.keys()].sort((left, right) => left.localeCompare(right)).map((name) => {
8709
+ const meta = this.metadata.get(name) ?? {};
8710
+ return {
8711
+ name,
8712
+ description: meta.description ?? null,
8713
+ schedule: meta.schedule ?? null
8714
+ };
8715
+ });
8692
8716
  }
8693
8717
  /**
8694
8718
  * Enqueue a task by name.
@@ -8722,6 +8746,180 @@ var TaskManager = class {
8722
8746
  this.unsubscribe = null;
8723
8747
  }
8724
8748
  };
8749
+ var JobStatus = {
8750
+ /** Written, not started. */
8751
+ QUEUED: "queued",
8752
+ /** A worker picked it up. */
8753
+ RUNNING: "running",
8754
+ /** Finished cleanly. */
8755
+ SUCCEEDED: "succeeded",
8756
+ /** Finished with an error. */
8757
+ FAILED: "failed",
8758
+ /** An operator stopped it before it finished. */
8759
+ CANCELLED: "cancelled"
8760
+ };
8761
+ var TERMINAL = [
8762
+ JobStatus.SUCCEEDED,
8763
+ JobStatus.FAILED,
8764
+ JobStatus.CANCELLED
8765
+ ];
8766
+ var BaseJobModel = class extends BaseModel {
8767
+ /** The task name this run belongs to. */
8768
+ name = tempestDbJs.column.varchar(128).notNull();
8769
+ /** Lifecycle state — a {@link JobStatus} value. */
8770
+ status = tempestDbJs.column.varchar(16).notNull().default(JobStatus.QUEUED);
8771
+ /** The payload the run was started with. */
8772
+ payload = tempestDbJs.column.json();
8773
+ /** Whatever the run produced, for an operator to read afterwards. */
8774
+ result = tempestDbJs.column.json();
8775
+ /** The failure message, when the run failed. */
8776
+ error = tempestDbJs.column.text();
8777
+ /** How many times the run has been attempted. */
8778
+ attempts = tempestDbJs.column.integer().notNull().default(0);
8779
+ /** When a worker picked it up. */
8780
+ startedAt = tempestDbJs.column.datetime();
8781
+ /** When it reached a terminal state. */
8782
+ finishedAt = tempestDbJs.column.datetime();
8783
+ };
8784
+ var JobStore = class {
8785
+ /**
8786
+ * @param model - The concrete {@link BaseJobModel} subclass.
8787
+ * @param session - The session rows are written on.
8788
+ */
8789
+ constructor(model, session) {
8790
+ this.model = model;
8791
+ this.repository = new tempestDbJs.BaseRepository(model, session);
8792
+ }
8793
+ model;
8794
+ repository;
8795
+ /**
8796
+ * Record a job about to run.
8797
+ *
8798
+ * @param name - The task name.
8799
+ * @param payload - The payload the run was started with.
8800
+ * @returns The created row.
8801
+ */
8802
+ async enqueue(name, payload = {}) {
8803
+ return await this.repository.create({
8804
+ name,
8805
+ status: JobStatus.QUEUED,
8806
+ payload,
8807
+ result: null,
8808
+ error: null,
8809
+ attempts: 0,
8810
+ startedAt: null,
8811
+ finishedAt: null
8812
+ });
8813
+ }
8814
+ /**
8815
+ * Mark a job as picked up, counting the attempt.
8816
+ *
8817
+ * @param id - The job id.
8818
+ * @param attempt - Which attempt this is. Default `1`.
8819
+ * @returns How many rows changed.
8820
+ */
8821
+ async start(id, attempt = 1) {
8822
+ return await this.repository.update(
8823
+ { id },
8824
+ {
8825
+ status: JobStatus.RUNNING,
8826
+ startedAt: /* @__PURE__ */ new Date(),
8827
+ attempts: attempt
8828
+ }
8829
+ );
8830
+ }
8831
+ /**
8832
+ * Mark a job as finished cleanly.
8833
+ *
8834
+ * @param id - The job id.
8835
+ * @param result - Whatever the run produced.
8836
+ * @returns How many rows changed.
8837
+ */
8838
+ async succeed(id, result = {}) {
8839
+ return await this.repository.update(
8840
+ { id },
8841
+ {
8842
+ status: JobStatus.SUCCEEDED,
8843
+ result,
8844
+ error: null,
8845
+ finishedAt: /* @__PURE__ */ new Date()
8846
+ }
8847
+ );
8848
+ }
8849
+ /**
8850
+ * Mark a job as failed.
8851
+ *
8852
+ * @param id - The job id.
8853
+ * @param error - The failure, as an `Error` or a message.
8854
+ * @returns How many rows changed.
8855
+ */
8856
+ async fail(id, error) {
8857
+ return await this.repository.update(
8858
+ { id },
8859
+ {
8860
+ status: JobStatus.FAILED,
8861
+ error: error instanceof Error ? error.message : String(error),
8862
+ finishedAt: /* @__PURE__ */ new Date()
8863
+ }
8864
+ );
8865
+ }
8866
+ /**
8867
+ * Ask a job to stop.
8868
+ *
8869
+ * A job already in a terminal state is left alone and reported as not
8870
+ * cancelled, so an operator clicking cancel on a run that just finished sees
8871
+ * the truth rather than a row rewritten under them.
8872
+ *
8873
+ * @param id - The job id.
8874
+ * @returns Whether the row moved to `cancelled`.
8875
+ */
8876
+ async cancel(id) {
8877
+ const row = await this.repository.getByIdOrNull(id);
8878
+ if (row === null || TERMINAL.includes(String(row.status))) return false;
8879
+ const changed = await this.repository.update(
8880
+ { id },
8881
+ {
8882
+ status: JobStatus.CANCELLED,
8883
+ finishedAt: /* @__PURE__ */ new Date()
8884
+ }
8885
+ );
8886
+ return changed > 0;
8887
+ }
8888
+ /**
8889
+ * Read one job row.
8890
+ *
8891
+ * @param id - The job id.
8892
+ * @returns The row, or `null`.
8893
+ */
8894
+ async get(id) {
8895
+ return await this.repository.getByIdOrNull(id);
8896
+ }
8897
+ /**
8898
+ * Read a page of jobs, newest first.
8899
+ *
8900
+ * @param filter - Page, size and optional `name` / `status` filters.
8901
+ * @returns The page plus its metadata.
8902
+ */
8903
+ async list(filter = {}) {
8904
+ const filters = {};
8905
+ if (filter.name !== void 0 && filter.name !== "") filters.name = filter.name;
8906
+ if (filter.status !== void 0) filters.status = filter.status;
8907
+ const result = await this.repository.paginate({
8908
+ page: filter.page ?? 1,
8909
+ pageSize: filter.pageSize ?? 25,
8910
+ orderBy: "createdAt",
8911
+ ascending: false,
8912
+ ...Object.keys(filters).length === 0 ? {} : { filters }
8913
+ });
8914
+ return {
8915
+ items: result.items,
8916
+ total: result.total,
8917
+ page: result.page,
8918
+ pageSize: result.pageSize,
8919
+ pages: result.pages
8920
+ };
8921
+ }
8922
+ };
8725
8923
 
8726
8924
  // src/flags/backends.ts
8727
8925
  function coerceFlag(value) {
@@ -9461,6 +9659,199 @@ function partitionTotal(partition) {
9461
9659
  return partition.segments.reduce((total, segment) => total + segment.value, 0);
9462
9660
  }
9463
9661
 
9662
+ // src/admin/logs.ts
9663
+ var LIFTED = /* @__PURE__ */ new Set(["level", "logger", "message", "timestamp", "stack"]);
9664
+ function toLogEntry(raw) {
9665
+ const context = {};
9666
+ for (const [key, value] of Object.entries(raw)) {
9667
+ if (LIFTED.has(key) || value === null || value === void 0) continue;
9668
+ context[key] = value;
9669
+ }
9670
+ return {
9671
+ level: typeof raw.level === "string" ? raw.level : "info",
9672
+ logger: typeof raw.logger === "string" ? raw.logger : "",
9673
+ message: typeof raw.message === "string" ? raw.message : "",
9674
+ timestamp: typeof raw.timestamp === "string" ? raw.timestamp : "",
9675
+ stack: typeof raw.stack === "string" ? raw.stack : null,
9676
+ context,
9677
+ raw
9678
+ };
9679
+ }
9680
+ function filterLogEntries(entries, term) {
9681
+ const needle = term.trim().toLowerCase();
9682
+ if (needle === "") return entries;
9683
+ return entries.filter(
9684
+ (entry) => `${entry.message} ${entry.logger} ${entry.stack ?? ""}`.toLowerCase().includes(needle)
9685
+ );
9686
+ }
9687
+ function renderLogEntriesMarkdown(entries, options) {
9688
+ const lines = [
9689
+ "# Application logs",
9690
+ "",
9691
+ `- **Source:** \`${options.source}\``,
9692
+ `- **Search:** ${options.query === "" ? "_none_" : `\`${options.query}\``}`,
9693
+ `- **Exported:** ${entries.length} of ${options.total} matching record(s)`,
9694
+ ""
9695
+ ];
9696
+ if (entries.length < options.total) {
9697
+ lines.push(
9698
+ `> Truncated: the export is capped, so ${options.total - entries.length} older matching record(s) are not included.`,
9699
+ ""
9700
+ );
9701
+ }
9702
+ for (const entry of entries) {
9703
+ lines.push(
9704
+ `## ${entry.level.toUpperCase()} \u2014 ${entry.message || "(no message)"}`,
9705
+ "",
9706
+ `- **When:** ${entry.timestamp || "unknown"}`,
9707
+ `- **Logger:** ${entry.logger || "unknown"}`
9708
+ );
9709
+ for (const [key, value] of Object.entries(entry.context)) {
9710
+ lines.push(
9711
+ `- **${key}:** ${typeof value === "string" ? value : JSON.stringify(value)}`
9712
+ );
9713
+ }
9714
+ lines.push("");
9715
+ if (entry.stack !== null) {
9716
+ lines.push("```text", entry.stack, "```", "");
9717
+ }
9718
+ }
9719
+ return lines.join("\n");
9720
+ }
9721
+ function renderLogEntriesJson(entries) {
9722
+ return JSON.stringify(
9723
+ entries.map((entry) => entry.raw),
9724
+ null,
9725
+ 2
9726
+ );
9727
+ }
9728
+
9729
+ // src/admin/sqlConsole.ts
9730
+ var SqlCapability = {
9731
+ /** `SELECT`, `WITH … SELECT`, `EXPLAIN`, `SHOW`. */
9732
+ READ: "read",
9733
+ /** Adds rows. */
9734
+ INSERT: "insert",
9735
+ /** Changes rows. */
9736
+ UPDATE: "update",
9737
+ /** Removes rows. */
9738
+ DELETE: "delete",
9739
+ /** `CREATE` / `ALTER` / `COMMENT`. */
9740
+ DDL: "ddl",
9741
+ /** `DROP` and `TRUNCATE`: irreversible structure loss. */
9742
+ DROP: "drop",
9743
+ /**
9744
+ * `GRANT` / `REVOKE` / `SET`, and anything the analyser cannot classify.
9745
+ * Unknown statements land here on purpose, so a construct nobody anticipated
9746
+ * needs the most privileged capability rather than the least.
9747
+ */
9748
+ ADMIN: "admin"
9749
+ };
9750
+ var STATEMENT_CAPABILITIES = {
9751
+ select: SqlCapability.READ,
9752
+ with: SqlCapability.READ,
9753
+ explain: SqlCapability.READ,
9754
+ show: SqlCapability.READ,
9755
+ desc: SqlCapability.READ,
9756
+ describe: SqlCapability.READ,
9757
+ insert: SqlCapability.INSERT,
9758
+ replace: SqlCapability.INSERT,
9759
+ update: SqlCapability.UPDATE,
9760
+ delete: SqlCapability.DELETE,
9761
+ create: SqlCapability.DDL,
9762
+ alter: SqlCapability.DDL,
9763
+ comment: SqlCapability.DDL,
9764
+ rename: SqlCapability.DDL,
9765
+ drop: SqlCapability.DROP,
9766
+ truncate: SqlCapability.DROP
9767
+ };
9768
+ var PARSER_HINT = "The admin SQL console needs the optional peer `node-sql-parser`. Install it with: npm install node-sql-parser";
9769
+ async function loadSqlParser() {
9770
+ try {
9771
+ const module = await import('node-sql-parser');
9772
+ const Parser = module.Parser ?? module.default?.Parser;
9773
+ if (Parser === void 0) throw new Error(PARSER_HINT);
9774
+ return new Parser();
9775
+ } catch {
9776
+ throw new Error(PARSER_HINT);
9777
+ }
9778
+ }
9779
+ function analyzeSql(sql5, dialect, parser) {
9780
+ const capabilities = /* @__PURE__ */ new Set();
9781
+ const tables = /* @__PURE__ */ new Set();
9782
+ let statements = 0;
9783
+ let parsed = true;
9784
+ let unscopedWrite = false;
9785
+ try {
9786
+ const ast = parser.astify(sql5, { database: dialect });
9787
+ const list = Array.isArray(ast) ? ast : [ast];
9788
+ statements = list.length;
9789
+ for (const statement of list) {
9790
+ const type = String(statement.type ?? "").toLowerCase();
9791
+ capabilities.add(STATEMENT_CAPABILITIES[type] ?? SqlCapability.ADMIN);
9792
+ if ((type === "update" || type === "delete") && !statement.where) {
9793
+ unscopedWrite = true;
9794
+ }
9795
+ }
9796
+ } catch {
9797
+ parsed = false;
9798
+ statements = 1;
9799
+ capabilities.add(SqlCapability.ADMIN);
9800
+ }
9801
+ try {
9802
+ for (const entry of parser.tableList(sql5, { database: dialect })) {
9803
+ const name = entry.split("::").pop();
9804
+ if (name !== void 0 && name !== "null") tables.add(name.toLowerCase());
9805
+ }
9806
+ } catch {
9807
+ }
9808
+ return {
9809
+ statements,
9810
+ capabilities: [...capabilities],
9811
+ tables: [...tables],
9812
+ parsed,
9813
+ unscopedWrite
9814
+ };
9815
+ }
9816
+ function checkSqlPolicy(analysis, policy) {
9817
+ const granted = new Set(policy.capabilities ?? [SqlCapability.READ]);
9818
+ for (const capability of analysis.capabilities) {
9819
+ if (!granted.has(capability)) {
9820
+ return {
9821
+ allowed: false,
9822
+ reason: analysis.parsed ? `This console may not run ${capability} statements.` : "The statement could not be parsed, so it needs the admin capability."
9823
+ };
9824
+ }
9825
+ }
9826
+ if ((policy.requireWhereOnWrites ?? true) && analysis.unscopedWrite) {
9827
+ return {
9828
+ allowed: false,
9829
+ reason: "An UPDATE or DELETE without a WHERE clause is refused."
9830
+ };
9831
+ }
9832
+ const denied = new Set((policy.denyTables ?? []).map((name) => name.toLowerCase()));
9833
+ for (const table of analysis.tables) {
9834
+ if (denied.has(table)) {
9835
+ return { allowed: false, reason: `Table "${table}" is not available here.` };
9836
+ }
9837
+ }
9838
+ if (policy.allowTables !== void 0) {
9839
+ const allowed = new Set(policy.allowTables.map((name) => name.toLowerCase()));
9840
+ if (analysis.tables.length === 0) {
9841
+ return {
9842
+ allowed: false,
9843
+ reason: "This console only runs statements naming an allowed table."
9844
+ };
9845
+ }
9846
+ for (const table of analysis.tables) {
9847
+ if (!allowed.has(table)) {
9848
+ return { allowed: false, reason: `Table "${table}" is not on the allow list.` };
9849
+ }
9850
+ }
9851
+ }
9852
+ return { allowed: true, reason: null };
9853
+ }
9854
+
9464
9855
  // src/admin/inlines.ts
9465
9856
  function adminInline(options) {
9466
9857
  const slug = options.model.tablename;
@@ -9601,8 +9992,8 @@ function humanizeField(name) {
9601
9992
  function adminColumns(model) {
9602
9993
  return tempestDbJs.columnsOf(model);
9603
9994
  }
9604
- function widgetForColumn(column6) {
9605
- const { kind, meta } = column6.type;
9995
+ function widgetForColumn(column7) {
9996
+ const { kind, meta } = column7.type;
9606
9997
  const plain = (widget) => ({
9607
9998
  widget,
9608
9999
  step: null,
@@ -9646,11 +10037,11 @@ function widgetForColumn(column6) {
9646
10037
  return plain("text");
9647
10038
  }
9648
10039
  }
9649
- function isColumnOptional(column6) {
9650
- return !column6.flags.notNull || column6.flags.hasDefault || column6.flags.primaryKey;
10040
+ function isColumnOptional(column7) {
10041
+ return !column7.flags.notNull || column7.flags.hasDefault || column7.flags.primaryKey;
9651
10042
  }
9652
- function filterForColumn(column6) {
9653
- const { kind, meta } = column6.type;
10043
+ function filterForColumn(column7) {
10044
+ const { kind, meta } = column7.type;
9654
10045
  if (kind === "boolean") {
9655
10046
  return {
9656
10047
  kind: "select",
@@ -9674,11 +10065,11 @@ function filterForColumn(column6) {
9674
10065
  }
9675
10066
  return { kind: "text", options: [] };
9676
10067
  }
9677
- function foreignKeyTable(column6) {
9678
- return column6.reference?.table ?? null;
10068
+ function foreignKeyTable(column7) {
10069
+ return column7.reference?.table ?? null;
9679
10070
  }
9680
- function isSearchableColumn(column6) {
9681
- const { kind } = column6.type;
10071
+ function isSearchableColumn(column7) {
10072
+ const { kind } = column7.type;
9682
10073
  return kind === "varchar" || kind === "text" || kind === "char";
9683
10074
  }
9684
10075
 
@@ -9960,8 +10351,8 @@ function formatFieldValue(widget, value) {
9960
10351
  }
9961
10352
  return String(value);
9962
10353
  }
9963
- function literalDefault(column6) {
9964
- const fallback = column6.defaultValue;
10354
+ function literalDefault(column7) {
10355
+ const fallback = column7.defaultValue;
9965
10356
  if (fallback === null || fallback.kind !== "literal") return void 0;
9966
10357
  return fallback.value;
9967
10358
  }
@@ -9974,19 +10365,19 @@ function buildFormFields(admin, options = {}) {
9974
10365
  const autocompleteLabels = options.autocompleteLabels ?? {};
9975
10366
  const uploads = new Set(admin.uploadFields);
9976
10367
  return admin.editableFieldNames().flatMap((name) => {
9977
- const column6 = columns[name];
9978
- if (column6 === void 0) return [];
10368
+ const column7 = columns[name];
10369
+ if (column7 === void 0) return [];
9979
10370
  const related = foreignKeys[name];
9980
10371
  const autocompleteUrl = autocompleteUrls[name];
9981
- 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 };
9982
- const raw = name in values ? values[name] : literalDefault(column6);
10372
+ 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 };
10373
+ const raw = name in values ? values[name] : literalDefault(column7);
9983
10374
  return [
9984
10375
  {
9985
10376
  name,
9986
10377
  label: humanizeField(name),
9987
10378
  widget: spec.widget,
9988
10379
  value: spec.widget === "checkbox" ? "" : formatFieldValue(spec.widget, raw),
9989
- required: !isColumnOptional(column6),
10380
+ required: !isColumnOptional(column7),
9990
10381
  checked: spec.widget === "checkbox" && toBoolean(raw),
9991
10382
  step: spec.step,
9992
10383
  options: spec.options,
@@ -10003,8 +10394,8 @@ function toBoolean(value) {
10003
10394
  if (typeof value !== "string") return false;
10004
10395
  return ["true", "on", "yes", "1"].includes(value.trim().toLowerCase());
10005
10396
  }
10006
- function coerceValue(column6, widget, raw) {
10007
- const { kind, meta } = column6.type;
10397
+ function coerceValue(column7, widget, raw) {
10398
+ const { kind, meta } = column7.type;
10008
10399
  switch (widget) {
10009
10400
  case "number": {
10010
10401
  const parsed = Number(raw);
@@ -10048,10 +10439,10 @@ function parseFormBody(admin, body, options = {}) {
10048
10439
  const only = options.only === void 0 ? null : new Set(options.only);
10049
10440
  for (const name of admin.editableFieldNames()) {
10050
10441
  if (only !== null && !only.has(name)) continue;
10051
- const column6 = columns[name];
10052
- if (column6 === void 0) continue;
10442
+ const column7 = columns[name];
10443
+ if (column7 === void 0) continue;
10053
10444
  if (uploads.has(name)) continue;
10054
- const { widget } = widgetForColumn(column6);
10445
+ const { widget } = widgetForColumn(column7);
10055
10446
  if (widget === "checkbox") {
10056
10447
  data[name] = toBoolean(body[name]);
10057
10448
  continue;
@@ -10059,16 +10450,16 @@ function parseFormBody(admin, body, options = {}) {
10059
10450
  const submitted = body[name];
10060
10451
  const raw = typeof submitted === "string" ? submitted.trim() : "";
10061
10452
  if (raw === "") {
10062
- if (!isColumnOptional(column6)) {
10453
+ if (!isColumnOptional(column7)) {
10063
10454
  errors[name] = "This field is required.";
10064
10455
  continue;
10065
10456
  }
10066
- if (column6.flags.hasDefault && !(name in body)) continue;
10457
+ if (column7.flags.hasDefault && !(name in body)) continue;
10067
10458
  data[name] = null;
10068
10459
  continue;
10069
10460
  }
10070
10461
  try {
10071
- data[name] = coerceValue(column6, widget, raw);
10462
+ data[name] = coerceValue(column7, widget, raw);
10072
10463
  } catch (error) {
10073
10464
  errors[name] = error instanceof Error ? error.message : "Invalid value.";
10074
10465
  }
@@ -10086,9 +10477,9 @@ function foreignKeyFields(admin) {
10086
10477
  const columns = adminColumns(admin.model);
10087
10478
  const out = {};
10088
10479
  for (const name of admin.editableFieldNames()) {
10089
- const column6 = columns[name];
10090
- if (column6 === void 0) continue;
10091
- const table = foreignKeyTable(column6);
10480
+ const column7 = columns[name];
10481
+ if (column7 === void 0) continue;
10482
+ const table = foreignKeyTable(column7);
10092
10483
  if (table !== null) out[name] = table;
10093
10484
  }
10094
10485
  return out;
@@ -10474,7 +10865,14 @@ var AdminSite = class {
10474
10865
  };
10475
10866
 
10476
10867
  // src/admin/styles.ts
10477
- var ADMIN_CSS = `:root {
10868
+ var ADMIN_CSS_EXTRA = `
10869
+ .tempest-log-badge--succeeded { background: #dcfce7; color: #166534; }
10870
+ .tempest-log-badge--failed { background: #fee2e2; color: #991b1b; }
10871
+ .tempest-log-badge--running { background: #dbeafe; color: #1e40af; }
10872
+ .tempest-log-badge--queued { background: #f1f5f9; color: #475569; }
10873
+ .tempest-log-badge--cancelled { background: #fef3c7; color: #92400e; }
10874
+ `;
10875
+ var ADMIN_CSS_BASE = `:root {
10478
10876
  --tempest-bg: #0f172a;
10479
10877
  --tempest-bg-soft: #1e293b;
10480
10878
  --tempest-bg-row: #f8fafc;
@@ -12033,6 +12431,7 @@ a.tempest-admin-list__new:hover {
12033
12431
  }
12034
12432
  }
12035
12433
  `;
12434
+ var ADMIN_CSS = `${ADMIN_CSS_BASE}${ADMIN_CSS_EXTRA}`;
12036
12435
 
12037
12436
  // src/admin/theme.ts
12038
12437
  var FORBIDDEN_CHARS = ["<", ">", "{", "}", '"'];
@@ -12112,7 +12511,7 @@ function escapeHtml(value) {
12112
12511
  return String(value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
12113
12512
  }
12114
12513
  function renderLayout(context, title, body) {
12115
- const { site, theme, prefix, session, currentPath, navModels, messages } = context;
12514
+ const { site, theme, prefix, session, currentPath, navModels, navSystem, messages } = context;
12116
12515
  const authed = session !== null;
12117
12516
  const indexUrl = `${prefix}/`;
12118
12517
  const navLink = (url, label, active) => `<a href="${escapeHtml(url)}" class="tempest-admin-sidebar__link${active ? " tempest-admin-sidebar__link--active" : ""}">${escapeHtml(label)}</a>`;
@@ -12122,6 +12521,9 @@ function renderLayout(context, title, body) {
12122
12521
  ${navLink(indexUrl, "Dashboard", currentPath === indexUrl)}
12123
12522
  ${navModels.length > 0 ? `<span class="tempest-admin-sidebar__heading">Models</span>${navModels.map(
12124
12523
  (entry) => navLink(entry.url, entry.label, currentPath.startsWith(entry.url))
12524
+ ).join("")}` : ""}
12525
+ ${navSystem.length > 0 ? `<span class="tempest-admin-sidebar__heading">System</span>${navSystem.map(
12526
+ (entry) => navLink(entry.url, entry.label, currentPath.startsWith(entry.url))
12125
12527
  ).join("")}` : ""}
12126
12528
  </nav>
12127
12529
  </aside>` : "";
@@ -12290,11 +12692,11 @@ function renderFilter(filter) {
12290
12692
  function renderListPage(context, view) {
12291
12693
  const bulk = view.bulkActions.length > 0 && context.session !== null;
12292
12694
  const checkColumn = bulk ? 1 : 0;
12293
- const headers = view.columns.map((column6) => {
12294
- const state = view.sort[column6];
12295
- if (state === void 0) return `<th>${escapeHtml(column6)}</th>`;
12695
+ const headers = view.columns.map((column7) => {
12696
+ const state = view.sort[column7];
12697
+ if (state === void 0) return `<th>${escapeHtml(column7)}</th>`;
12296
12698
  const arrow = state.active ? state.ascending ? "\u25B2" : "\u25BC" : "\u2195";
12297
- 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>`;
12699
+ 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>`;
12298
12700
  }).join("");
12299
12701
  const rows = view.rows.length > 0 ? view.rows.map((row) => {
12300
12702
  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>` : "";
@@ -12435,7 +12837,7 @@ function renderInline(inline, csrfToken, error) {
12435
12837
  if (inline.rows.length === 0) {
12436
12838
  return `<section class="tempest-admin-inline">${heading}<p><em>No related records.</em></p></section>`;
12437
12839
  }
12438
- const head2 = `<tr>${inline.columns.map((column6) => `<th>${escapeHtml(column6)}</th>`).join("")}<th></th></tr>`;
12840
+ const head2 = `<tr>${inline.columns.map((column7) => `<th>${escapeHtml(column7)}</th>`).join("")}<th></th></tr>`;
12439
12841
  const body = inline.rows.map(
12440
12842
  (row) => `<tr>${row.cells.map((cell) => `<td>${escapeHtml(cell)}</td>`).join("")}<td>${row.url === null ? "" : `<a href="${escapeHtml(row.url)}">View</a>`}</td></tr>`
12441
12843
  ).join("");
@@ -12450,7 +12852,7 @@ function renderInline(inline, csrfToken, error) {
12450
12852
  ${more}
12451
12853
  </section>`;
12452
12854
  }
12453
- const head = `<tr>${inline.columns.map((column6) => `<th>${escapeHtml(column6)}</th>`).join("")}${inline.canDelete ? "<th>Delete</th>" : ""}</tr>`;
12855
+ const head = `<tr>${inline.columns.map((column7) => `<th>${escapeHtml(column7)}</th>`).join("")}${inline.canDelete ? "<th>Delete</th>" : ""}</tr>`;
12454
12856
  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>`;
12455
12857
  const rows = inline.rows.map((row) => renderRow(row, false)).join("");
12456
12858
  const blank = inline.newRow === null ? "" : renderRow(inline.newRow, true);
@@ -12472,11 +12874,11 @@ function renderInline(inline, csrfToken, error) {
12472
12874
  ${more}
12473
12875
  </section>`;
12474
12876
  }
12475
- function renderAuditPanel(audit) {
12476
- const rows = audit.fields.map(
12877
+ function renderAuditPanel(audit2) {
12878
+ const rows = audit2.fields.map(
12477
12879
  (field) => `<dt>${escapeHtml(field.label)}</dt><dd>${field.value === "" ? "<em>\u2014</em>" : escapeHtml(field.value)}</dd>`
12478
12880
  ).join("");
12479
- const history = audit.history.length > 0 ? `<ol class="tempest-admin-history">${audit.history.map((entry) => {
12881
+ const history = audit2.history.length > 0 ? `<ol class="tempest-admin-history">${audit2.history.map((entry) => {
12480
12882
  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(
12481
12883
  (change) => `<tr><td>${escapeHtml(change.field)}</td><td>${escapeHtml(change.before)}</td><td>${escapeHtml(change.after)}</td></tr>`
12482
12884
  ).join("")}</tbody></table>` : "<p><em>No field changes recorded.</em></p>";
@@ -12574,6 +12976,160 @@ function renderFormPage(context, view) {
12574
12976
  </section>`;
12575
12977
  return renderLayout(context, `${heading} \xB7 ${context.site.title}`, body);
12576
12978
  }
12979
+ function renderLogsPage(context, view) {
12980
+ const options = view.sources.map(
12981
+ (source) => `<option value="${escapeHtml(source.value)}"${source.selected ? " selected" : ""}>${escapeHtml(source.label)}</option>`
12982
+ ).join("");
12983
+ const rows = view.rows.length > 0 ? view.rows.map((row) => {
12984
+ const context_ = row.context.map(
12985
+ (entry) => `<span class="tempest-admin-logs__meta"><b>${escapeHtml(entry.key)}</b>: ${escapeHtml(entry.value)}</span>`
12986
+ ).join(" ");
12987
+ 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>`;
12988
+ return `<tr>
12989
+ <td data-label="Level"><span class="tempest-log-badge tempest-log-badge--${escapeHtml(row.level)}">${escapeHtml(row.level)}</span></td>
12990
+ <td data-label="When">${escapeHtml(row.timestamp)}</td>
12991
+ <td data-label="Logger">${escapeHtml(row.logger)}</td>
12992
+ <td data-label="Message">${message}</td>
12993
+ </tr>`;
12994
+ }).join("") : `<tr><td colspan="4">No log records yet. Point <code>configureFileLogging</code> at the same directory to populate this page.</td></tr>`;
12995
+ const body = `<section class="tempest-admin-logs">
12996
+ <header class="tempest-admin-list__header">
12997
+ <h1>Logs</h1>
12998
+ <p>${escapeHtml(view.total)} record${view.total === 1 ? "" : "s"}.</p>
12999
+ </header>
13000
+ <div class="tempest-admin-list__toolbar">
13001
+ <form method="get" class="tempest-admin-list__filters">
13002
+ <label><span>Source</span><select name="source">${options}</select></label>
13003
+ <input type="search" name="q" value="${escapeHtml(view.query)}" placeholder="Search\u2026" aria-label="Search">
13004
+ <button type="submit">Apply</button>
13005
+ </form>
13006
+ <div class="tempest-admin-list__actions">
13007
+ <a href="${escapeHtml(view.exportMarkdownUrl)}">Export Markdown</a>
13008
+ <a href="${escapeHtml(view.exportJsonUrl)}">Export JSON</a>
13009
+ </div>
13010
+ </div>
13011
+ <div class="tempest-admin-table-wrap">
13012
+ <table class="tempest-admin-list__table">
13013
+ <thead><tr><th>Level</th><th>When</th><th>Logger</th><th>Message</th></tr></thead>
13014
+ <tbody>${rows}</tbody>
13015
+ </table>
13016
+ </div>
13017
+ ${view.pages > 1 ? `<nav class="tempest-admin-list__pagination" aria-label="Pagination">
13018
+ ${view.prevUrl !== null ? `<a href="${escapeHtml(view.prevUrl)}">\u2190 Prev</a>` : ""}
13019
+ <span>Page ${escapeHtml(view.page)} of ${escapeHtml(view.pages)}</span>
13020
+ ${view.nextUrl !== null ? `<a href="${escapeHtml(view.nextUrl)}">Next \u2192</a>` : ""}
13021
+ </nav>` : ""}
13022
+ <p class="tempest-admin-form__hint">Exports carry at most ${escapeHtml(view.exportMax)} records, newest first, honouring the filters above.</p>
13023
+ </section>`;
13024
+ return renderLayout(context, `Logs \xB7 ${context.site.title}`, body);
13025
+ }
13026
+ function renderTasksPage(context, view) {
13027
+ const declared = view.inventory === null ? "" : `<section class="tempest-admin-tasks__declared">
13028
+ <h2>Declared</h2>
13029
+ ${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">
13030
+ <thead><tr><th>Task</th><th>Schedule</th><th>Description</th></tr></thead>
13031
+ <tbody>${view.inventory.map(
13032
+ (entry) => `<tr><td>${escapeHtml(entry.name)}</td><td>${escapeHtml(entry.schedule)}</td><td>${escapeHtml(entry.description)}</td></tr>`
13033
+ ).join("")}</tbody>
13034
+ </table></div>`}
13035
+ </section>`;
13036
+ const runs = view.runs === null ? "" : `<section class="tempest-admin-tasks__runs">
13037
+ <h2>Runs</h2>
13038
+ <div class="tempest-admin-list__toolbar">
13039
+ <form method="get" class="tempest-admin-list__filters">
13040
+ <label><span>Status</span><select name="status">${view.runs.statuses.map(
13041
+ (status) => `<option value="${escapeHtml(status.value)}"${status.selected ? " selected" : ""}>${escapeHtml(status.label)}</option>`
13042
+ ).join("")}</select></label>
13043
+ <input type="search" name="task" value="${escapeHtml(view.runs.nameQuery)}" placeholder="Task name\u2026" aria-label="Task name">
13044
+ <button type="submit">Apply</button>
13045
+ </form>
13046
+ </div>
13047
+ <div class="tempest-admin-table-wrap"><table class="tempest-admin-list__table">
13048
+ <thead><tr><th>Task</th><th>Status</th><th>Started</th><th>Finished</th><th>Attempts</th><th></th></tr></thead>
13049
+ <tbody>${view.runs.rows.length === 0 ? '<tr><td colspan="6">No runs recorded yet.</td></tr>' : view.runs.rows.map(
13050
+ (row) => `<tr>
13051
+ <td>${escapeHtml(row.name)}</td>
13052
+ <td><span class="tempest-log-badge tempest-log-badge--${escapeHtml(row.status)}">${escapeHtml(row.status)}</span></td>
13053
+ <td>${escapeHtml(row.startedAt)}</td>
13054
+ <td>${escapeHtml(row.finishedAt)}</td>
13055
+ <td>${escapeHtml(row.attempts)}</td>
13056
+ <td><a href="${escapeHtml(row.url)}">View</a></td>
13057
+ </tr>`
13058
+ ).join("")}</tbody>
13059
+ </table></div>
13060
+ ${view.runs.pages > 1 ? `<nav class="tempest-admin-list__pagination" aria-label="Pagination">
13061
+ ${view.runs.prevUrl !== null ? `<a href="${escapeHtml(view.runs.prevUrl)}">\u2190 Prev</a>` : ""}
13062
+ <span>Page ${escapeHtml(view.runs.page)} of ${escapeHtml(view.runs.pages)}</span>
13063
+ ${view.runs.nextUrl !== null ? `<a href="${escapeHtml(view.runs.nextUrl)}">Next \u2192</a>` : ""}
13064
+ </nav>` : ""}
13065
+ </section>`;
13066
+ const body = `<section class="tempest-admin-tasks">
13067
+ <header class="tempest-admin-list__header">
13068
+ <h1>Tasks</h1>
13069
+ <p>What this process declares, and what its workers recorded.</p>
13070
+ </header>
13071
+ ${declared}
13072
+ ${runs}
13073
+ <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>
13074
+ </section>`;
13075
+ return renderLayout(context, `Tasks \xB7 ${context.site.title}`, body);
13076
+ }
13077
+ function renderTaskDetailPage(context, view) {
13078
+ if (context.session === null) throw new Error("The task detail requires a session");
13079
+ const block = (label, content) => content === null ? "" : `<h2>${escapeHtml(label)}</h2><pre class="tempest-admin-detail__json">${escapeHtml(content)}</pre>`;
13080
+ const body = `<section class="tempest-admin-detail">
13081
+ <header class="tempest-admin-detail__header">
13082
+ <h1>${escapeHtml(view.name)} \xB7 <span class="tempest-log-badge tempest-log-badge--${escapeHtml(view.status)}">${escapeHtml(view.status)}</span></h1>
13083
+ <div class="tempest-admin-detail__actions">
13084
+ <a href="${escapeHtml(view.backUrl)}">\u2190 Back to tasks</a>
13085
+ ${view.cancelUrl !== null ? `<form method="post" action="${escapeHtml(view.cancelUrl)}" class="tempest-admin-detail__delete" onsubmit="return confirm('Ask this run to stop?');">
13086
+ <input type="hidden" name="csrf_token" value="${escapeHtml(context.session.csrfToken)}">
13087
+ <button type="submit" class="tempest-admin-btn--danger">Cancel</button>
13088
+ </form>` : ""}
13089
+ </div>
13090
+ </header>
13091
+ <dl class="tempest-admin-detail__fields">${view.fields.map(
13092
+ (field) => `<dt>${escapeHtml(field.label)}</dt><dd>${field.value === "" ? "<em>\u2014</em>" : escapeHtml(field.value)}</dd>`
13093
+ ).join("")}</dl>
13094
+ ${view.error === null ? "" : `<p class="tempest-admin-form__error">${escapeHtml(view.error)}</p>`}
13095
+ ${block("Payload", view.payload)}
13096
+ ${block("Result", view.result)}
13097
+ </section>`;
13098
+ return renderLayout(context, `${view.name} \xB7 ${context.site.title}`, body);
13099
+ }
13100
+ function renderSqlPage(context, view) {
13101
+ if (context.session === null) throw new Error("The SQL console requires a session");
13102
+ 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>
13103
+ <div class="tempest-admin-table-wrap">
13104
+ <table class="tempest-admin-list__table">
13105
+ <thead><tr>${view.columns.map((column7) => `<th>${escapeHtml(column7)}</th>`).join("")}</tr></thead>
13106
+ <tbody>${view.rows.map(
13107
+ (row) => `<tr>${row.map((cell) => `<td>${escapeHtml(cell)}</td>`).join("")}</tr>`
13108
+ ).join("")}</tbody>
13109
+ </table>
13110
+ </div>`;
13111
+ const body = `<section class="tempest-admin-sql">
13112
+ <header class="tempest-admin-list__header">
13113
+ <h1>SQL console</h1>
13114
+ <p>Capabilities: <code>${escapeHtml(view.capabilities.join(", "))}</code></p>
13115
+ </header>
13116
+ ${view.error !== null ? `<p class="tempest-admin-form__error">${escapeHtml(view.error)}</p>` : ""}
13117
+ <form method="post" action="${escapeHtml(`${context.prefix}/sql`)}" class="tempest-admin-form__form">
13118
+ <input type="hidden" name="csrf_token" value="${escapeHtml(context.session.csrfToken)}">
13119
+ <div class="tempest-admin-form__field">
13120
+ <label>
13121
+ <span>Statement</span>
13122
+ <textarea name="sql" rows="6" spellcheck="false" required>${escapeHtml(view.sql)}</textarea>
13123
+ </label>
13124
+ </div>
13125
+ <div class="tempest-admin-form__actions">
13126
+ <button type="submit">Run</button>
13127
+ </div>
13128
+ </form>
13129
+ ${result}
13130
+ </section>`;
13131
+ return renderLayout(context, `SQL console \xB7 ${context.site.title}`, body);
13132
+ }
12577
13133
  function renderImportPage(context, view) {
12578
13134
  if (context.session === null) throw new Error("The import page requires a session");
12579
13135
  const summary = view.created === null ? "" : `<p class="tempest-admin-import__summary">Created ${escapeHtml(view.created)} record${view.created === 1 ? "" : "s"}.</p>`;
@@ -12659,7 +13215,75 @@ var AUTOCOMPLETE_SCRIPT = `<script>
12659
13215
  });
12660
13216
  })();
12661
13217
  </script>`;
13218
+ function filesFor(dir, source) {
13219
+ if (source === "500") return [path.join(dir, HTTP_500_LOG_FILE)];
13220
+ if (source === "all") return Object.values(LEVEL_LOG_FILES).map((f) => path.join(dir, f));
13221
+ return [path.join(dir, LEVEL_LOG_FILES[source])];
13222
+ }
13223
+ async function readLogEntries(dir, source) {
13224
+ return await readEntries(filesFor(dir, source));
13225
+ }
13226
+ async function readEntries(files) {
13227
+ const entries = [];
13228
+ for (const file of files) {
13229
+ let content;
13230
+ try {
13231
+ content = await promises.readFile(file, "utf8");
13232
+ } catch {
13233
+ continue;
13234
+ }
13235
+ for (const line of content.split("\n")) {
13236
+ if (!line.trim()) continue;
13237
+ try {
13238
+ entries.push(JSON.parse(line));
13239
+ } catch {
13240
+ }
13241
+ }
13242
+ }
13243
+ return entries;
13244
+ }
13245
+ function makeLogsRouter(options) {
13246
+ const router = express3.Router();
13247
+ const path = options.path ?? "/logs";
13248
+ const guards = options.guards ?? [];
13249
+ const handler = (req, res, next) => {
13250
+ const source = req.query.source ?? "all";
13251
+ const validSources = ["all", "debug", "info", "warning", "error", "500"];
13252
+ if (!validSources.includes(source)) {
13253
+ res.status(422).json({ detail: "Invalid log source", code: "VALIDATION_ERROR", details: {} });
13254
+ return;
13255
+ }
13256
+ const page2 = Math.max(1, Number(req.query.page) || 1);
13257
+ const pageSize = Math.min(500, Math.max(1, Number(req.query.pageSize) || 50));
13258
+ readEntries(filesFor(options.dir, source)).then((entries) => {
13259
+ entries.reverse();
13260
+ const total = entries.length;
13261
+ const start = (page2 - 1) * pageSize;
13262
+ res.json({
13263
+ items: entries.slice(start, start + pageSize),
13264
+ total,
13265
+ page: page2,
13266
+ pageSize,
13267
+ pages: Math.ceil(total / pageSize)
13268
+ });
13269
+ }).catch(next);
13270
+ };
13271
+ router.get(path, ...guards, handler);
13272
+ return router;
13273
+ }
12662
13274
  var logger2 = new JSONLogger("tempest_express_sdk.admin.router");
13275
+ var JOB_STATUSES = [
13276
+ "queued",
13277
+ "running",
13278
+ "succeeded",
13279
+ "failed",
13280
+ "cancelled"
13281
+ ];
13282
+ var TERMINAL_JOB_STATUSES = ["succeeded", "failed", "cancelled"];
13283
+ var TASK_PAGE_SIZE = 25;
13284
+ var LOG_SOURCES = ["all", "debug", "info", "warning", "error", "500"];
13285
+ var LOG_PAGE_SIZE = 50;
13286
+ var LOG_EXPORT_MAX = 500;
12663
13287
  var INLINE_ROW_LIMIT = 50;
12664
13288
  var AUTOCOMPLETE_LIMIT = 20;
12665
13289
  var AUDIT_HISTORY_LIMIT = 50;
@@ -12691,6 +13315,16 @@ function makeAdminRouter(site, options) {
12691
13315
  const theme = resolveAdminTheme(site.theme);
12692
13316
  const showMetrics = options.showMetrics ?? true;
12693
13317
  const exportMaxRows = options.exportMaxRows ?? 5e3;
13318
+ const systemNav = [];
13319
+ if (options.logDir !== void 0) {
13320
+ systemNav.push({ label: "Logs", url: `${prefix}/logs` });
13321
+ }
13322
+ if (options.sqlConsole !== void 0) {
13323
+ systemNav.push({ label: "SQL console", url: `${prefix}/sql` });
13324
+ }
13325
+ if (options.tasks !== void 0) {
13326
+ systemNav.push({ label: "Tasks", url: `${prefix}/tasks` });
13327
+ }
12694
13328
  const maxUploadBytes = options.maxUploadBytes ?? 10 * 1024 * 1024;
12695
13329
  const sessions = new AdminSessionStore({
12696
13330
  secret: options.secretKey,
@@ -12712,6 +13346,7 @@ function makeAdminRouter(site, options) {
12712
13346
  label: admin.verboseNamePlural(),
12713
13347
  url: `${prefix}/m/${admin.slug()}`
12714
13348
  })),
13349
+ navSystem: systemNav,
12715
13350
  messages: flashFor(req)
12716
13351
  });
12717
13352
  const allows = async (principal, admin, action) => {
@@ -12922,6 +13557,296 @@ function makeAdminRouter(site, options) {
12922
13557
  );
12923
13558
  })
12924
13559
  );
13560
+ if (options.logDir !== void 0) {
13561
+ const logDir = options.logDir;
13562
+ router.get(
13563
+ `${prefix}/logs`,
13564
+ guarded(async (req, res) => {
13565
+ const state = await authenticate(req, res);
13566
+ if (state === null) return;
13567
+ const { entries, source, search } = await readLogs(req, logDir);
13568
+ const page2 = Math.max(1, Number.parseInt(queryString(req.query.page), 10) || 1);
13569
+ const start = (page2 - 1) * LOG_PAGE_SIZE;
13570
+ const pages = Math.max(1, Math.ceil(entries.length / LOG_PAGE_SIZE));
13571
+ const exportUrl = (format) => `${prefix}/logs/export?${buildQuery({ source, q: search, format })}`;
13572
+ const pageUrl = (target) => `?${buildQuery({ source, q: search, page: target })}`;
13573
+ html(
13574
+ res,
13575
+ renderLogsPage(context(req, state.session, state.visible), {
13576
+ sources: LOG_SOURCES.map((value) => ({
13577
+ value,
13578
+ label: value === "500" ? "HTTP 500" : humanizeField(value),
13579
+ selected: value === source
13580
+ })),
13581
+ query: search,
13582
+ rows: entries.slice(start, start + LOG_PAGE_SIZE).map((entry) => ({
13583
+ level: entry.level,
13584
+ timestamp: entry.timestamp,
13585
+ logger: entry.logger,
13586
+ message: entry.message,
13587
+ stack: entry.stack,
13588
+ context: Object.entries(entry.context).map(([key, value]) => ({
13589
+ key,
13590
+ value: typeof value === "string" ? value : JSON.stringify(value)
13591
+ }))
13592
+ })),
13593
+ total: entries.length,
13594
+ page: page2,
13595
+ pages,
13596
+ prevUrl: page2 > 1 ? pageUrl(page2 - 1) : null,
13597
+ nextUrl: page2 < pages ? pageUrl(page2 + 1) : null,
13598
+ exportMarkdownUrl: exportUrl("md"),
13599
+ exportJsonUrl: exportUrl("json"),
13600
+ exportMax: LOG_EXPORT_MAX
13601
+ })
13602
+ );
13603
+ })
13604
+ );
13605
+ router.get(
13606
+ `${prefix}/logs/export`,
13607
+ guarded(async (req, res) => {
13608
+ const state = await authenticate(req, res);
13609
+ if (state === null) return;
13610
+ const format = queryString(req.query.format) === "json" ? "json" : "md";
13611
+ const { entries, source, search } = await readLogs(req, logDir);
13612
+ const window = entries.slice(0, LOG_EXPORT_MAX);
13613
+ const payload = format === "json" ? renderLogEntriesJson(window) : renderLogEntriesMarkdown(window, {
13614
+ source,
13615
+ query: search,
13616
+ total: entries.length
13617
+ });
13618
+ res.status(200).type(format === "json" ? "application/json" : "text/markdown; charset=utf-8").set("content-disposition", `attachment; filename="logs.${format}"`).send(payload);
13619
+ })
13620
+ );
13621
+ }
13622
+ if (options.sqlConsole !== void 0) {
13623
+ const console_ = options.sqlConsole;
13624
+ const policy = console_.policy ?? {};
13625
+ const dialect = console_.dialect ?? "postgresql";
13626
+ const capabilities = policy.capabilities ?? [SqlCapability.READ];
13627
+ const maxRows = policy.maxRows ?? 200;
13628
+ router.get(
13629
+ `${prefix}/sql`,
13630
+ guarded(async (req, res) => {
13631
+ const state = await authenticate(req, res);
13632
+ if (state === null) return;
13633
+ html(
13634
+ res,
13635
+ renderSqlPage(context(req, state.session, state.visible), {
13636
+ sql: "",
13637
+ capabilities: [...capabilities],
13638
+ error: null,
13639
+ columns: [],
13640
+ rows: [],
13641
+ rowCount: null,
13642
+ truncated: false,
13643
+ durationMs: null
13644
+ })
13645
+ );
13646
+ })
13647
+ );
13648
+ router.post(
13649
+ `${prefix}/sql`,
13650
+ guarded(async (req, res) => {
13651
+ const state = await authenticate(req, res);
13652
+ if (state === null) return;
13653
+ if (!checkCsrf(req, res, state)) return;
13654
+ const body = req.body;
13655
+ const sql5 = typeof body.sql === "string" ? body.sql : "";
13656
+ const principal = backend.displayName(state.principal);
13657
+ const render = (view) => {
13658
+ html(
13659
+ res,
13660
+ renderSqlPage(context(req, state.session, state.visible), {
13661
+ sql: sql5,
13662
+ capabilities: [...capabilities],
13663
+ ...view
13664
+ }),
13665
+ view.error === null ? 200 : 400
13666
+ );
13667
+ };
13668
+ const parser = await loadSqlParser();
13669
+ const analysis = analyzeSql(sql5, dialect, parser);
13670
+ const verdict = checkSqlPolicy(analysis, policy);
13671
+ if (!verdict.allowed) {
13672
+ await audit(console_.onAudit, {
13673
+ sql: sql5,
13674
+ principal,
13675
+ allowed: false,
13676
+ reason: verdict.reason,
13677
+ analysis,
13678
+ durationMs: null,
13679
+ rowCount: null
13680
+ });
13681
+ render({
13682
+ error: verdict.reason,
13683
+ columns: [],
13684
+ rows: [],
13685
+ rowCount: null,
13686
+ truncated: false,
13687
+ durationMs: null
13688
+ });
13689
+ return;
13690
+ }
13691
+ const started = Date.now();
13692
+ let rows;
13693
+ try {
13694
+ rows = console_.run === void 0 ? await state.dbSession.raw(sql5).all() : await console_.run(sql5, state.dbSession);
13695
+ } catch (error) {
13696
+ const message = error instanceof Error ? error.message : String(error);
13697
+ await audit(console_.onAudit, {
13698
+ sql: sql5,
13699
+ principal,
13700
+ allowed: true,
13701
+ reason: message,
13702
+ analysis,
13703
+ durationMs: Date.now() - started,
13704
+ rowCount: null
13705
+ });
13706
+ render({
13707
+ error: message,
13708
+ columns: [],
13709
+ rows: [],
13710
+ rowCount: null,
13711
+ truncated: false,
13712
+ durationMs: null
13713
+ });
13714
+ return;
13715
+ }
13716
+ const durationMs = Date.now() - started;
13717
+ await audit(console_.onAudit, {
13718
+ sql: sql5,
13719
+ principal,
13720
+ allowed: true,
13721
+ reason: null,
13722
+ analysis,
13723
+ durationMs,
13724
+ rowCount: rows.length
13725
+ });
13726
+ const window = rows.slice(0, maxRows);
13727
+ const columns = window.length === 0 ? [] : Object.keys(window[0] ?? {});
13728
+ render({
13729
+ error: null,
13730
+ columns,
13731
+ rows: window.map(
13732
+ (row) => columns.map((column7) => formatCellValue(row[column7]))
13733
+ ),
13734
+ rowCount: rows.length,
13735
+ truncated: rows.length > window.length,
13736
+ durationMs
13737
+ });
13738
+ })
13739
+ );
13740
+ }
13741
+ if (options.tasks !== void 0) {
13742
+ const tasks = options.tasks;
13743
+ router.get(
13744
+ `${prefix}/tasks`,
13745
+ guarded(async (req, res) => {
13746
+ const state = await authenticate(req, res);
13747
+ if (state === null) return;
13748
+ const inventory = tasks.manager === void 0 ? null : tasks.manager.inventory().map((entry) => ({
13749
+ name: entry.name,
13750
+ description: entry.description ?? "",
13751
+ schedule: entry.schedule ?? "\u2014"
13752
+ }));
13753
+ let runs = null;
13754
+ if (tasks.jobs !== void 0) {
13755
+ const store = tasks.jobs(state.dbSession);
13756
+ const page2 = Math.max(1, Number.parseInt(queryString(req.query.page), 10) || 1);
13757
+ const status = queryString(req.query.status);
13758
+ const name = queryString(req.query.task);
13759
+ const result = await store.list({
13760
+ page: page2,
13761
+ pageSize: TASK_PAGE_SIZE,
13762
+ ...name === "" ? {} : { name },
13763
+ ...JOB_STATUSES.includes(status) ? { status } : {}
13764
+ });
13765
+ const pageUrl = (target) => `?${buildQuery({ status, task: name, page: target })}`;
13766
+ runs = {
13767
+ rows: result.items.map((row) => ({
13768
+ id: String(row.id),
13769
+ name: String(row.name ?? ""),
13770
+ status: String(row.status ?? ""),
13771
+ startedAt: formatCellValue(row.startedAt),
13772
+ finishedAt: formatCellValue(row.finishedAt),
13773
+ attempts: formatCellValue(row.attempts),
13774
+ url: `${prefix}/tasks/${String(row.id)}`
13775
+ })),
13776
+ total: result.total,
13777
+ page: result.page,
13778
+ pages: result.pages,
13779
+ prevUrl: result.page > 1 ? pageUrl(result.page - 1) : null,
13780
+ nextUrl: result.page < result.pages ? pageUrl(result.page + 1) : null,
13781
+ statuses: ["", ...JOB_STATUSES].map((value) => ({
13782
+ value,
13783
+ label: value === "" ? "\u2014 any \u2014" : humanizeField(value),
13784
+ selected: value === status
13785
+ })),
13786
+ nameQuery: name
13787
+ };
13788
+ }
13789
+ html(
13790
+ res,
13791
+ renderTasksPage(context(req, state.session, state.visible), {
13792
+ inventory,
13793
+ runs
13794
+ })
13795
+ );
13796
+ })
13797
+ );
13798
+ if (tasks.jobs !== void 0) {
13799
+ const jobs = tasks.jobs;
13800
+ router.get(
13801
+ `${prefix}/tasks/:id`,
13802
+ guarded(async (req, res) => {
13803
+ const state = await authenticate(req, res);
13804
+ if (state === null) return;
13805
+ const row = await jobs(state.dbSession).get(String(req.params.id));
13806
+ if (row === null) {
13807
+ html(res, renderNotFound(context(req, state.session, state.visible)), 404);
13808
+ return;
13809
+ }
13810
+ const status = String(row.status ?? "");
13811
+ html(
13812
+ res,
13813
+ renderTaskDetailPage(context(req, state.session, state.visible), {
13814
+ id: String(row.id),
13815
+ name: String(row.name ?? ""),
13816
+ status,
13817
+ fields: [
13818
+ { label: "Created At", value: formatCellValue(row.createdAt) },
13819
+ { label: "Started At", value: formatCellValue(row.startedAt) },
13820
+ { label: "Finished At", value: formatCellValue(row.finishedAt) },
13821
+ { label: "Attempts", value: formatCellValue(row.attempts) }
13822
+ ],
13823
+ payload: jsonBlock(row.payload),
13824
+ result: jsonBlock(row.result),
13825
+ error: typeof row.error === "string" && row.error !== "" ? row.error : null,
13826
+ backUrl: `${prefix}/tasks`,
13827
+ cancelUrl: TERMINAL_JOB_STATUSES.includes(status) ? null : `${prefix}/tasks/${String(row.id)}/cancel`
13828
+ })
13829
+ );
13830
+ })
13831
+ );
13832
+ router.post(
13833
+ `${prefix}/tasks/:id/cancel`,
13834
+ guarded(async (req, res) => {
13835
+ const state = await authenticate(req, res);
13836
+ if (state === null) return;
13837
+ if (!checkCsrf(req, res, state)) return;
13838
+ const id = String(req.params.id);
13839
+ const cancelled = await jobs(state.dbSession).cancel(id);
13840
+ res.redirect(
13841
+ `${prefix}/tasks/${id}?${buildQuery({
13842
+ flash: cancelled ? "The run was asked to stop." : "That run had already finished.",
13843
+ level: cancelled ? "success" : "warning"
13844
+ })}`
13845
+ );
13846
+ })
13847
+ );
13848
+ }
13849
+ }
12925
13850
  router.get(
12926
13851
  `${prefix}/m/:slug`,
12927
13852
  guarded(async (req, res) => {
@@ -13232,8 +14157,8 @@ function makeAdminRouter(site, options) {
13232
14157
  res.status(404).json({ options: [] });
13233
14158
  return;
13234
14159
  }
13235
- const column6 = adminColumns(admin.model)[field];
13236
- const table = column6 === void 0 ? null : foreignKeyTable(column6);
14160
+ const column7 = adminColumns(admin.model)[field];
14161
+ const table = column7 === void 0 ? null : foreignKeyTable(column7);
13237
14162
  const referenced = table === null ? null : site.get(table);
13238
14163
  if (referenced === null) {
13239
14164
  res.json({ options: [] });
@@ -13542,7 +14467,7 @@ function makeAdminRouter(site, options) {
13542
14467
  formAction: "",
13543
14468
  rows: visible.map((child) => ({
13544
14469
  key: String(child[childAdmin?.identityField ?? "id"]),
13545
- cells: columns.map((column6) => formatCellValue(child[column6])),
14470
+ cells: columns.map((column7) => formatCellValue(child[column7])),
13546
14471
  fields: [],
13547
14472
  url: childAdmin === null ? null : `${prefix}/m/${inline.slug}/${String(child[childAdmin.identityField])}`
13548
14473
  })),
@@ -13639,6 +14564,15 @@ function makeAdminRouter(site, options) {
13639
14564
  const row = await admin.repository(dbSession).first({ [admin.identityField]: identity });
13640
14565
  return row ?? null;
13641
14566
  }
14567
+ async function readLogs(req, dir) {
14568
+ const requested = queryString(req.query.source);
14569
+ const source = LOG_SOURCES.includes(requested) ? requested : "all";
14570
+ const search = queryString(req.query.q);
14571
+ const raw = await readLogEntries(dir, source);
14572
+ const entries = filterLogEntries(raw.map(toLogEntry), search);
14573
+ entries.reverse();
14574
+ return { entries, source, search };
14575
+ }
13642
14576
  function renderImport(req, state, admin, error, created, rowErrors) {
13643
14577
  return renderImportPage(context(req, state.session, state.visible), {
13644
14578
  title: admin.verboseNamePlural(),
@@ -13665,8 +14599,8 @@ function makeAdminRouter(site, options) {
13665
14599
  for (const field of admin.uploadFields) {
13666
14600
  const file = files.find((candidate) => candidate.field === field);
13667
14601
  if (file === void 0) {
13668
- const column6 = columns[field];
13669
- if (creating && column6 !== void 0 && !isColumnOptional(column6)) {
14602
+ const column7 = columns[field];
14603
+ if (creating && column7 !== void 0 && !isColumnOptional(column7)) {
13670
14604
  errors[field] = "This field is required.";
13671
14605
  }
13672
14606
  continue;
@@ -13698,14 +14632,14 @@ function makeAdminRouter(site, options) {
13698
14632
  });
13699
14633
  const displayed = admin.listDisplayNames();
13700
14634
  const sort = {};
13701
- for (const column6 of displayed) {
13702
- if (!(column6 in columns)) continue;
13703
- const active = (query.sortColumn ?? admin.orderKey) === column6;
14635
+ for (const column7 of displayed) {
14636
+ if (!(column7 in columns)) continue;
14637
+ const active = (query.sortColumn ?? admin.orderKey) === column7;
13704
14638
  const nextAscending = active ? !query.ascending : true;
13705
- sort[column6] = {
14639
+ sort[column7] = {
13706
14640
  url: `?${buildQuery({
13707
14641
  ...query.baseQuery,
13708
- sort: column6,
14642
+ sort: column7,
13709
14643
  dir: nextAscending ? "asc" : "desc"
13710
14644
  })}`,
13711
14645
  active,
@@ -13726,7 +14660,7 @@ function makeAdminRouter(site, options) {
13726
14660
  const identity = String(row[admin.identityField]);
13727
14661
  return {
13728
14662
  identity,
13729
- cells: displayed.map((column6) => formatCellValue(row[column6])),
14663
+ cells: displayed.map((column7) => formatCellValue(row[column7])),
13730
14664
  url: `${prefix}/m/${admin.slug()}/${identity}`
13731
14665
  };
13732
14666
  }),
@@ -13772,9 +14706,9 @@ function makeAdminRouter(site, options) {
13772
14706
  }
13773
14707
  const filterViews = [];
13774
14708
  for (const field of admin.listFilter) {
13775
- const column6 = columns[field];
13776
- if (column6 === void 0) continue;
13777
- const spec = filterForColumn(column6);
14709
+ const column7 = columns[field];
14710
+ if (column7 === void 0) continue;
14711
+ const spec = filterForColumn(column7);
13778
14712
  if (spec.kind === "daterange") {
13779
14713
  const from = queryString(req.query[`filter_${field}_from`]);
13780
14714
  const to = queryString(req.query[`filter_${field}_to`]);
@@ -13793,12 +14727,12 @@ function makeAdminRouter(site, options) {
13793
14727
  });
13794
14728
  continue;
13795
14729
  }
13796
- const related = await relatedOptions(column6, dbSession);
14730
+ const related = await relatedOptions(column7, dbSession);
13797
14731
  const options2 = related ?? spec.options;
13798
14732
  const value = queryString(req.query[`filter_${field}`]);
13799
14733
  if (value !== "") {
13800
14734
  conditions.push({
13801
- [field]: column6.type.kind === "boolean" ? value === "true" : value
14735
+ [field]: column7.type.kind === "boolean" ? value === "true" : value
13802
14736
  });
13803
14737
  }
13804
14738
  filterViews.push({
@@ -13816,8 +14750,8 @@ function makeAdminRouter(site, options) {
13816
14750
  });
13817
14751
  }
13818
14752
  const searchable = admin.searchFields.filter((field) => {
13819
- const column6 = columns[field];
13820
- return column6 !== void 0 && isSearchableColumn(column6);
14753
+ const column7 = columns[field];
14754
+ return column7 !== void 0 && isSearchableColumn(column7);
13821
14755
  });
13822
14756
  if (search !== "" && searchable.length > 0) {
13823
14757
  conditions.push(
@@ -13850,8 +14784,8 @@ function makeAdminRouter(site, options) {
13850
14784
  lens: lens?.slug ?? ""
13851
14785
  };
13852
14786
  }
13853
- async function relatedOptions(column6, dbSession) {
13854
- const table = foreignKeyTable(column6);
14787
+ async function relatedOptions(column7, dbSession) {
14788
+ const table = foreignKeyTable(column7);
13855
14789
  if (table === null) return null;
13856
14790
  const referenced = site.get(table);
13857
14791
  if (referenced === null) return null;
@@ -13866,9 +14800,9 @@ function makeAdminRouter(site, options) {
13866
14800
  const options2 = {};
13867
14801
  for (const field of Object.keys(foreignKeyFields(admin))) {
13868
14802
  if (admin.autocompleteFields.includes(field)) continue;
13869
- const column6 = columns[field];
13870
- if (column6 === void 0) continue;
13871
- const related = await relatedOptions(column6, dbSession);
14803
+ const column7 = columns[field];
14804
+ if (column7 === void 0) continue;
14805
+ const related = await relatedOptions(column7, dbSession);
13872
14806
  if (related !== null) options2[field] = related;
13873
14807
  }
13874
14808
  return options2;
@@ -13887,8 +14821,8 @@ function makeAdminRouter(site, options) {
13887
14821
  for (const field of admin.autocompleteFields) {
13888
14822
  const value = row[field];
13889
14823
  if (value === null || value === void 0 || value === "") continue;
13890
- const column6 = columns[field];
13891
- const table = column6 === void 0 ? null : foreignKeyTable(column6);
14824
+ const column7 = columns[field];
14825
+ const table = column7 === void 0 ? null : foreignKeyTable(column7);
13892
14826
  const referenced = table === null ? null : site.get(table);
13893
14827
  if (referenced === null) continue;
13894
14828
  const related = await referenced.repository(dbSession).first({ [referenced.identityField]: value });
@@ -13927,6 +14861,21 @@ function inlineFieldNames(childAdmin, inline) {
13927
14861
  function inlineFields(childAdmin, names, key, values, errors) {
13928
14862
  return buildFormFields(childAdmin, { values, errors }).filter((field) => names.includes(field.name)).map((field) => ({ ...field, name: `row.${key}.${field.name}` }));
13929
14863
  }
14864
+ function jsonBlock(value) {
14865
+ if (value === null || value === void 0) return null;
14866
+ if (typeof value === "object" && Object.keys(value).length === 0) return null;
14867
+ return JSON.stringify(value, null, 2);
14868
+ }
14869
+ async function audit(hook, entry) {
14870
+ if (hook === void 0) return;
14871
+ try {
14872
+ await hook(entry);
14873
+ } catch (error) {
14874
+ logger2.error("Admin SQL audit hook failed", {
14875
+ error: error instanceof Error ? error.message : String(error)
14876
+ });
14877
+ }
14878
+ }
13930
14879
  function flagAllows(admin, action) {
13931
14880
  if (action === AdminPermission.CREATE) return admin.canCreate;
13932
14881
  if (action === AdminPermission.EDIT) return admin.canEdit;
@@ -14080,7 +15029,7 @@ function csvField(value) {
14080
15029
  function toCsv(columns, rows) {
14081
15030
  const lines = [columns.map(csvField).join(",")];
14082
15031
  for (const row of rows) {
14083
- lines.push(columns.map((column6) => csvField(exportValue(row[column6]))).join(","));
15032
+ lines.push(columns.map((column7) => csvField(exportValue(row[column7]))).join(","));
14084
15033
  }
14085
15034
  return `${lines.join("\r\n")}\r
14086
15035
  `;
@@ -14088,7 +15037,7 @@ function toCsv(columns, rows) {
14088
15037
  function toJson(columns, rows) {
14089
15038
  return JSON.stringify(
14090
15039
  rows.map(
14091
- (row) => Object.fromEntries(columns.map((column6) => [column6, exportValue(row[column6])]))
15040
+ (row) => Object.fromEntries(columns.map((column7) => [column7, exportValue(row[column7])]))
14092
15041
  ),
14093
15042
  null,
14094
15043
  2
@@ -16013,59 +16962,6 @@ function makeToolSpecRouter(spec, options = {}) {
16013
16962
  });
16014
16963
  return router;
16015
16964
  }
16016
- function filesFor(dir, source) {
16017
- if (source === "500") return [path.join(dir, HTTP_500_LOG_FILE)];
16018
- if (source === "all") return Object.values(LEVEL_LOG_FILES).map((f) => path.join(dir, f));
16019
- return [path.join(dir, LEVEL_LOG_FILES[source])];
16020
- }
16021
- async function readEntries(files) {
16022
- const entries = [];
16023
- for (const file of files) {
16024
- let content;
16025
- try {
16026
- content = await promises.readFile(file, "utf8");
16027
- } catch {
16028
- continue;
16029
- }
16030
- for (const line of content.split("\n")) {
16031
- if (!line.trim()) continue;
16032
- try {
16033
- entries.push(JSON.parse(line));
16034
- } catch {
16035
- }
16036
- }
16037
- }
16038
- return entries;
16039
- }
16040
- function makeLogsRouter(options) {
16041
- const router = express3.Router();
16042
- const path = options.path ?? "/logs";
16043
- const guards = options.guards ?? [];
16044
- const handler = (req, res, next) => {
16045
- const source = req.query.source ?? "all";
16046
- const validSources = ["all", "debug", "info", "warning", "error", "500"];
16047
- if (!validSources.includes(source)) {
16048
- res.status(422).json({ detail: "Invalid log source", code: "VALIDATION_ERROR", details: {} });
16049
- return;
16050
- }
16051
- const page2 = Math.max(1, Number(req.query.page) || 1);
16052
- const pageSize = Math.min(500, Math.max(1, Number(req.query.pageSize) || 50));
16053
- readEntries(filesFor(options.dir, source)).then((entries) => {
16054
- entries.reverse();
16055
- const total = entries.length;
16056
- const start = (page2 - 1) * pageSize;
16057
- res.json({
16058
- items: entries.slice(start, start + pageSize),
16059
- total,
16060
- page: page2,
16061
- pageSize,
16062
- pages: Math.ceil(total / pageSize)
16063
- });
16064
- }).catch(next);
16065
- };
16066
- router.get(path, ...guards, handler);
16067
- return router;
16068
- }
16069
16965
  function createTestDatabase(models) {
16070
16966
  const driver = tempestDbJs.NodeSqliteDriver.open(":memory:");
16071
16967
  for (const model of models) {
@@ -16098,7 +16994,7 @@ async function withTestDatabase(models, fn) {
16098
16994
  }
16099
16995
 
16100
16996
  // src/version.ts
16101
- var VERSION = "0.28.0";
16997
+ var VERSION = "0.30.0";
16102
16998
 
16103
16999
  Object.defineProperty(exports, "OpenAPIRegistry", {
16104
17000
  enumerable: true,
@@ -16264,6 +17160,7 @@ exports.AttemptThrottle = AttemptThrottle;
16264
17160
  exports.AuditAction = AuditAction;
16265
17161
  exports.BaseAuditLogModel = BaseAuditLogModel;
16266
17162
  exports.BaseController = BaseController;
17163
+ exports.BaseJobModel = BaseJobModel;
16267
17164
  exports.BaseModel = BaseModel;
16268
17165
  exports.BaseOAuthClient = BaseOAuthClient;
16269
17166
  exports.BaseOutboxModel = BaseOutboxModel;
@@ -16299,6 +17196,8 @@ exports.IDEMPOTENCY_HEADER = IDEMPOTENCY_HEADER;
16299
17196
  exports.InvalidTokenException = InvalidTokenException;
16300
17197
  exports.JSONLogger = JSONLogger;
16301
17198
  exports.JWTUtils = JWTUtils;
17199
+ exports.JobStatus = JobStatus;
17200
+ exports.JobStore = JobStore;
16302
17201
  exports.LEVEL_LOG_FILES = LEVEL_LOG_FILES;
16303
17202
  exports.LocalUploadStorage = LocalUploadStorage;
16304
17203
  exports.MemoryBroker = MemoryBroker;
@@ -16335,6 +17234,7 @@ exports.S3UploadStorage = S3UploadStorage;
16335
17234
  exports.SSEBroker = SSEBroker;
16336
17235
  exports.ServerSentEvent = ServerSentEvent;
16337
17236
  exports.SessionService = SessionService;
17237
+ exports.SqlCapability = SqlCapability;
16338
17238
  exports.TOTPHelper = TOTPHelper;
16339
17239
  exports.TaskManager = TaskManager;
16340
17240
  exports.TelegramProvider = TelegramProvider;
@@ -16361,6 +17261,7 @@ exports.adminColumns = adminColumns;
16361
17261
  exports.adminInline = adminInline;
16362
17262
  exports.adminLens = adminLens;
16363
17263
  exports.adminThemeCss = adminThemeCss;
17264
+ exports.analyzeSql = analyzeSql;
16364
17265
  exports.attachWebSocketHub = attachWebSocketHub;
16365
17266
  exports.authResponseSchema = authResponseSchema;
16366
17267
  exports.authSettingsShape = authSettingsShape;
@@ -16377,6 +17278,7 @@ exports.buildPaginationLinkHeader = buildPaginationLinkHeader;
16377
17278
  exports.cached = cached3;
16378
17279
  exports.centsField = centsField;
16379
17280
  exports.cepField = cepField;
17281
+ exports.checkSqlPolicy = checkSqlPolicy;
16380
17282
  exports.citiesByUf = citiesByUf;
16381
17283
  exports.cnpjField = cnpjField;
16382
17284
  exports.coerceFlag = coerceFlag;
@@ -16405,6 +17307,7 @@ exports.envBoolean = looseBoolean;
16405
17307
  exports.envList = envList;
16406
17308
  exports.escapeHtml = escapeHtml;
16407
17309
  exports.filterForColumn = filterForColumn;
17310
+ exports.filterLogEntries = filterLogEntries;
16408
17311
  exports.foreignKeyFields = foreignKeyFields;
16409
17312
  exports.foreignKeyLabel = foreignKeyLabel;
16410
17313
  exports.foreignKeyTable = foreignKeyTable;
@@ -16444,6 +17347,7 @@ exports.keyByJwtSubject = keyByJwtSubject;
16444
17347
  exports.latitudeField = latitudeField;
16445
17348
  exports.listStates = listStates;
16446
17349
  exports.loadSettings = loadSettings;
17350
+ exports.loadSqlParser = loadSqlParser;
16447
17351
  exports.logEntrySchema = logEntrySchema;
16448
17352
  exports.logSettingsShape = logSettingsShape;
16449
17353
  exports.loginSchema = loginSchema;
@@ -16504,6 +17408,7 @@ exports.rabbitmqSettingsShape = rabbitmqSettingsShape;
16504
17408
  exports.rateLimitMiddleware = rateLimitMiddleware;
16505
17409
  exports.ratingField = ratingField;
16506
17410
  exports.ratioField = ratioField;
17411
+ exports.readLogEntries = readLogEntries;
16507
17412
  exports.redisSettingsShape = redisSettingsShape;
16508
17413
  exports.refreshSchema = refreshSchema;
16509
17414
  exports.registerExceptionHandlers = registerExceptionHandlers;
@@ -16514,9 +17419,15 @@ exports.renderFormPage = renderFormPage;
16514
17419
  exports.renderImportPage = renderImportPage;
16515
17420
  exports.renderLayout = renderLayout;
16516
17421
  exports.renderListPage = renderListPage;
17422
+ exports.renderLogEntriesJson = renderLogEntriesJson;
17423
+ exports.renderLogEntriesMarkdown = renderLogEntriesMarkdown;
16517
17424
  exports.renderLoginPage = renderLoginPage;
17425
+ exports.renderLogsPage = renderLogsPage;
16518
17426
  exports.renderMfaPage = renderMfaPage;
16519
17427
  exports.renderPasswordResetFormPage = renderPasswordResetFormPage;
17428
+ exports.renderSqlPage = renderSqlPage;
17429
+ exports.renderTaskDetailPage = renderTaskDetailPage;
17430
+ exports.renderTasksPage = renderTasksPage;
16520
17431
  exports.requestIdMiddleware = requestIdMiddleware;
16521
17432
  exports.requestTracingMiddleware = requestTracingMiddleware;
16522
17433
  exports.requireRoles = requireRoles;
@@ -16540,6 +17451,7 @@ exports.syncFilterSchema = syncFilterSchema;
16540
17451
  exports.syncPaginationSchema = syncPaginationSchema;
16541
17452
  exports.tableNameFor = tableNameFor;
16542
17453
  exports.toDict = toDict;
17454
+ exports.toLogEntry = toLogEntry;
16543
17455
  exports.toUtc = toUtc;
16544
17456
  exports.tokenFromUrl = tokenFromUrl;
16545
17457
  exports.tokenPairSchema = tokenPairSchema;