diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index a3c564a2..dab76b44 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -68,6 +68,8 @@ jobs: file: ./apps/${{ inputs.app }}/Dockerfile push: ${{ inputs.push }} tags: ghcr.io/pokt-network/${{ inputs.app }}:${{ inputs.tag }} + build-args: | + APP_VERSION=${{ inputs.tag }} no-cache: ${{ inputs.no-cache }} cache-from: ${{ inputs.no-cache && ' ' || 'type=gha' }} cache-to: type=gha,mode=max diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 518acf30..29e81133 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,6 +79,9 @@ jobs: - name: Test run: pnpm turbo test + - name: No-console guard + run: bash scripts/no-console-guard.sh + docker-build: needs: quality runs-on: ubuntu-latest diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml index 8eb44d4c..b84adda2 100644 --- a/.github/workflows/deploy-production.yml +++ b/.github/workflows/deploy-production.yml @@ -92,6 +92,8 @@ jobs: tags: | ghcr.io/pokt-network/${{ matrix.app }}:${{ env.NEW_VERSION }} ghcr.io/pokt-network/${{ matrix.app }}:latest + build-args: | + APP_VERSION=${{ env.NEW_VERSION }} cache-from: type=gha,scope=${{ matrix.app }} cache-to: type=gha,scope=${{ matrix.app }},mode=max,ignore-error=true diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml index 1656ccc3..bcd92110 100644 --- a/.github/workflows/deploy-staging.yml +++ b/.github/workflows/deploy-staging.yml @@ -1,7 +1,16 @@ name: Deploy Staging on: - pull_request: + # pull_request_target, NOT pull_request: a `pull_request` event raised by a PR + # from a FORK gets a read-only GITHUB_TOKEN and no secrets, regardless of the + # `permissions:` block below — so the GHCR push fails with "installation not + # allowed to Write organization package" and post-deploy never runs. That is + # exactly what happened when PR #326 (external contributor) was merged. + # pull_request_target runs in the base-repo context with a read/write token. + # The usual pull_request_target hazard (running untrusted PR-head code with a + # privileged token) does not apply here: every checkout below pins `ref: staging`, + # never the PR head, and the job only runs once the PR is already merged. + pull_request_target: types: [closed] branches: [staging] workflow_dispatch: @@ -65,6 +74,8 @@ jobs: file: ./apps/${{ matrix.app }}/Dockerfile push: true tags: ghcr.io/pokt-network/${{ matrix.app }}:${{ env.IMAGE_TAG }} + build-args: | + APP_VERSION=${{ env.IMAGE_TAG }} cache-from: type=gha,scope=${{ matrix.app }} cache-to: type=gha,scope=${{ matrix.app }},mode=max,ignore-error=true diff --git a/apps/middleman-workflows/Dockerfile b/apps/middleman-workflows/Dockerfile index ee1663d7..52ffdbc2 100644 --- a/apps/middleman-workflows/Dockerfile +++ b/apps/middleman-workflows/Dockerfile @@ -34,5 +34,11 @@ RUN pnpm turbo run build --filter=@igniter/middleman-workflows # Switch user USER app +ENV SERVICE_NAME=middleman-workflows +# APP_VERSION sits after the build RUN so a per-deploy version bump only +# invalidates this layer, not the install/build layers above. +ARG APP_VERSION=unknown +ENV APP_VERSION=${APP_VERSION} + # Start the server CMD ["node", "./apps/middleman-workflows/dist/src/worker.js"] diff --git a/apps/middleman-workflows/src/activities/index.ts b/apps/middleman-workflows/src/activities/index.ts index f16ef0bb..709e6685 100644 --- a/apps/middleman-workflows/src/activities/index.ts +++ b/apps/middleman-workflows/src/activities/index.ts @@ -162,6 +162,19 @@ const VERIFY_UNAVAILABLE_ALERT_THRESHOLD = Number(process.env.VERIFY_UNAVAILABLE /** Per-sweep hash-scan window, matching the on-chain mempool expiration window. */ // TX_EXPIRATION_BLOCKS imported from @igniter/tx-verify above +/** + * Warns once per worker process that `indexerApiUrl` is missing from app settings, + * instead of every sweep (this fired ~34x/2h in the debuggability audit — pure noise + * once the operator has seen it once). Module-level state is safe here: this file + * only runs activity/node-side, never inside the Temporal workflow sandbox. + */ +let indexerApiUrlWarned = false +function warnIndexerApiUrlMissingOnce(context: Record = {}) { + if (indexerApiUrlWarned) return + indexerApiUrlWarned = true + log.warn('indexerApiUrl not configured in app settings — skipping address group rewards fetch', context) +} + /** * Parses a transaction's unsigned payload into the expected on-chain supplier effect. * Returns null when the tx has no supplier-state path (send / OperationalFunds), so the @@ -199,7 +212,7 @@ export const delegatorActivities = (dal: DAL, pocketRpcClient: PocketBlockchain, async upsertSupplierStatus(params: UpsertSupplierStatusParams): Promise { try { - log.info('Querying supplier status', { params }) + log.debug('Querying supplier status', { params }) const [node, balance, supplier] = await Promise.all([ dal.node.loadNode(params.address), pocketRpcClient.getBalance(params.address), @@ -350,14 +363,14 @@ export const delegatorActivities = (dal: DAL, pocketRpcClient: PocketBlockchain, } } - log.info('Updating supplier', { params, update }) //NOTE: adding the update could result in an error due to BIGINT + log.debug('Updating supplier', { params, update }) //NOTE: adding the update could result in an error due to BIGINT try { await dal.node.updateNode(params.address, update, params.height) } catch (e) { log.error('Error updating node record', { error: e }) throw new ApplicationFailure('errored updating node record', 'update_error', false, null, e as Error) } - log.info('Upsert Supplier done!', { params }) + log.debug('Upsert Supplier done!', { params }) // Node update has persisted — now deliver the captured supplier-change // notifications (best-effort; dispatchUserNotification never throws). On a @@ -492,7 +505,20 @@ export const delegatorActivities = (dal: DAL, pocketRpcClient: PocketBlockchain, throw new Error('Transaction is not signed') } - return pocketRpcClient.sendTransaction(transaction.signedPayload) + const result = await pocketRpcClient.sendTransaction(transaction.signedPayload) + + if (result.transactionHash) { + log.info('transaction broadcast', { transactionId, hash: result.transactionHash, type: transaction.type }) + } else { + log.warn('transaction broadcast failed', { + transactionId, + type: transaction.type, + code: result.code, + message: result.message, + }) + } + + return result }, /** * Retrieves the current block height from the RPC client. @@ -584,8 +610,7 @@ export const delegatorActivities = (dal: DAL, pocketRpcClient: PocketBlockchain, return dal.node.insert(newNodes, transaction.id) } catch (error) { - console.log('Something went wrong while parsing the transaction to extract the staked nodes information.') - console.error(error) + log.error('Failed to parse transaction for staked nodes', { transactionId, error }) return [] } }, @@ -619,8 +644,7 @@ export const delegatorActivities = (dal: DAL, pocketRpcClient: PocketBlockchain, return addresses } catch (error) { - console.log('Something went wrong while parsing the transaction to extract the unstaking nodes information.') - console.error(error) + log.error('Failed to parse transaction for unstaking nodes', { transactionId, error }) return [] } }, @@ -815,7 +839,7 @@ export const delegatorActivities = (dal: DAL, pocketRpcClient: PocketBlockchain, const indexerApiUrl = appSettings?.indexerApiUrl if (!indexerApiUrl) { - log.warn('indexerApiUrl not configured in app settings — skipping address group rewards fetch') + warnIndexerApiUrlMissingOnce() return } @@ -1019,7 +1043,7 @@ export const delegatorActivities = (dal: DAL, pocketRpcClient: PocketBlockchain, const indexerApiUrl = appSettings?.indexerApiUrl if (!indexerApiUrl) { - log.warn('indexerApiUrl not configured in app settings — skipping address group rewards fetch', { providerIdentity }) + warnIndexerApiUrlMissingOnce({ providerIdentity }) return { statusResult } } @@ -1260,7 +1284,7 @@ export const delegatorActivities = (dal: DAL, pocketRpcClient: PocketBlockchain, * height (or its execution height on the first sweep). Maps the pocket tri-state * result down to the minimal shape the pure decision logic consumes. */ - async verifyTxHash(transactionId: number): Promise> { + async verifyTxHash(transactionId: number): Promise> { const txn = await dal.transaction.getTransaction(transactionId) if (!txn?.hash) { throw new Error('verifyTxHash: tx missing hash') @@ -1272,7 +1296,7 @@ export const delegatorActivities = (dal: DAL, pocketRpcClient: PocketBlockchain, // activity boundary (the default payload converter cannot encode BigInt). return { status: 'confirmed', - data: { success: out.data.success, code: out.data.code, gasUsed: out.data.gasUsed.toString() }, + data: { success: out.data.success, code: out.data.code, gasUsed: out.data.gasUsed.toString(), rawLog: out.data.rawLog }, } }, @@ -1375,14 +1399,34 @@ export const delegatorActivities = (dal: DAL, pocketRpcClient: PocketBlockchain, const status = decision.tx === 'success' ? TransactionStatus.Success : TransactionStatus.Failure const verificationHeight = await pocketRpcClient.getHeight().catch(() => undefined) + // Failure log: prefer the chain's own error text (rawLog, present when the tx + // was found on-chain and failed) so the UI can show the real reason; the + // hardcoded summaries remain for paths where no chain text exists (absent-tx + // failure) or as suffix context (sibling-met goal). const fields: { code?: number; consumedFee?: number; verificationHeight?: number; log?: string } = { verificationHeight, log: decision.tx === 'success' ? 'verified' - : decision.effects === 'apply-success' ? 'tx failed on-chain; goal met by sibling tx' - : 'verification negative (validity bound covered, no effect)', + : decision.effects === 'apply-success' + ? `tx failed on-chain; goal met by sibling tx${decision.rawLog ? ` (${decision.rawLog})` : ''}` + : decision.rawLog || 'verification negative (validity bound covered, no effect)', } if (decision.code !== undefined) fields.code = decision.code if (decision.gasUsed !== undefined) fields.consumedFee = Number(decision.gasUsed) + + const verifyLogFields = { + transactionId, + hash: txn.hash ?? undefined, + type: txn.type, + success: decision.tx === 'success', + ...(decision.code !== undefined ? { code: decision.code } : {}), + ...(decision.gasUsed !== undefined ? { gasUsed: decision.gasUsed } : {}), + } + if (decision.tx === 'success') { + log.info('transaction verified', verifyLogFields) + } else { + log.warn('transaction verification failed', { ...verifyLogFields, reason: fields.log }) + } + const claimed = await dal.transaction.claimTerminalTransition(transactionId, status, fields) // Only the CAS winner gets the row back, so the owner is notified exactly diff --git a/apps/middleman-workflows/src/bootstrap.ts b/apps/middleman-workflows/src/bootstrap.ts index ebb8f8f7..34c55d6e 100644 --- a/apps/middleman-workflows/src/bootstrap.ts +++ b/apps/middleman-workflows/src/bootstrap.ts @@ -61,11 +61,11 @@ async function bootstrapNamespace(client: Client, config: TemporalConfig, logger try { await workflowService.describeNamespace({ namespace }); - logger.info({ namespace }, 'Namespace already exists. Skipping registration...') + logger.info('Namespace already exists. Skipping registration...', { namespace }) } catch (error: any) { if (error.details.match(/not found/i)) { try { - logger.warn({ namespace }, 'Namespace does not exist. Registering...') + logger.warn('Namespace does not exist. Registering...', { namespace }) await workflowService.registerNamespace({ namespace, workflowExecutionRetentionPeriod: { @@ -74,14 +74,14 @@ async function bootstrapNamespace(client: Client, config: TemporalConfig, logger ), }, }); - logger.info({ namespace }, 'Namespace registered successfully, waiting 20s for it to be fully registered...') + logger.info('Namespace registered successfully, waiting 20s for it to be fully registered...', { namespace }) await new Promise((resolve) => setTimeout(resolve, 20000)); } catch (error) { - logger.error({ error, namespace }, 'Error registering namespace') + logger.error('Error registering namespace', { error, namespace }) throw error; } } else { - logger.error({ error, namespace }, 'Error describing namespace') + logger.error('Error describing namespace', { error, namespace }) throw error; } } @@ -95,8 +95,8 @@ export function buildWatchdogEntries(config: WatchdogConfig, logger?: Logger): W const parsed = parseDuration(rawOverride) if (parsed == null) { logger?.warn( - { scheduleId: `${wt}-scheduled`, envVar: wf.envVar, raw: rawOverride, fallback: wf.interval }, 'Invalid schedule interval override; falling back to default', + { scheduleId: `${wt}-scheduled`, envVar: wf.envVar, raw: rawOverride, fallback: wf.interval }, ) } // Never carry an invalid override forward: fall the STRING back to the default diff --git a/apps/middleman-workflows/src/lib/blockchain/index.ts b/apps/middleman-workflows/src/lib/blockchain/index.ts index 1f533a8a..53e20797 100644 --- a/apps/middleman-workflows/src/lib/blockchain/index.ts +++ b/apps/middleman-workflows/src/lib/blockchain/index.ts @@ -3,6 +3,7 @@ import { Buffer } from 'buffer' import { sha256 } from '@cosmjs/crypto' import { toHex } from '@cosmjs/encoding' import { connectComet } from '@cosmjs/tendermint-rpc' +import { getLogger, type Logger } from '@igniter/logger' export interface SendTransactionResult { transactionHash: string; @@ -32,16 +33,19 @@ export class Blockchain implements IBlockchain { private readonly rpcUrl: string; private readonly denom: string; private readonly apiUrl?: string; + private readonly logger: Logger; /** * @param rpcUrl bech32 Cosmos SDK RPC endpoint, e.g. https://rpc.cosmos.network * @param denom staking token denom, e.g. "uatom" or "upokt" * @param apiUrl optional REST API endpoint for Tier 2 tx lookup + * @param logger optional injected logger; defaults to the root app logger */ - constructor(rpcUrl: string, denom: string = 'upokt', apiUrl?: string) { + constructor(rpcUrl: string, denom: string = 'upokt', apiUrl?: string, logger: Logger = getLogger(['middleman-workflows', 'blockchain'])) { this.rpcUrl = rpcUrl; this.denom = denom; this.apiUrl = apiUrl; + this.logger = logger; } /** Returns the numeric token balance for `address` in the configured `denom`. */ @@ -59,7 +63,7 @@ export class Blockchain implements IBlockchain { try { return await client.getHeight(); } catch (err) { - console.error(err); + this.logger.error('Failed to fetch height from blockchain', { error: err }); throw new Error('Unable to fetch the height from the blockchain.'); } } @@ -113,12 +117,12 @@ export class Blockchain implements IBlockchain { }; } } catch (error) { - console.warn('Tier 1 (RPC getTx) failed:', error); + this.logger.warn('Tier 1 (RPC getTx) failed', { txHash, error }); } // Tier 2: REST API if (this.apiUrl) { - console.info(`Tier 1 returned null for ${txHash}, trying REST API fallback`); + this.logger.debug('Tier 1 returned null, trying REST API fallback', { txHash }); try { const url = `${this.apiUrl.replace(/\/$/, '')}/cosmos/tx/v1beta1/txs/${txHash}`; const response = await fetch(url); @@ -138,13 +142,13 @@ export class Blockchain implements IBlockchain { } } } catch (error) { - console.warn('Tier 2 (REST API) failed:', error); + this.logger.warn('Tier 2 (REST API) failed', { txHash, error }); } } // Tier 3: Block scan if (height) { - console.info(`Tier 2 returned null for ${txHash}, trying block scan at height ${height}`); + this.logger.debug('Tier 2 returned null, trying block scan', { txHash, height }); const maxBlocks = 30; try { const comet = await connectComet(this.rpcUrl); @@ -167,7 +171,7 @@ export class Blockchain implements IBlockchain { const results = await comet.blockResults(h); const txData = results.results[i]; if (!txData) { - console.warn(`Block results missing entry at index ${i} for height ${h}`); + this.logger.warn('Block results missing entry', { txHash, height: h, index: i }); return null; } return { @@ -182,16 +186,16 @@ export class Blockchain implements IBlockchain { } } } catch (blockError) { - console.warn(`Block scan error at height ${h}:`, blockError); + this.logger.warn('Block scan error', { txHash, height: h, error: blockError }); continue; } } } catch (error) { - console.warn('Tier 3 (block scan) failed to connect:', error); + this.logger.warn('Tier 3 (block scan) failed to connect', { txHash, error }); } } - console.warn(`All tiers failed to find transaction ${txHash}`); + this.logger.warn('All tiers failed to find transaction', { txHash }); return null; } } diff --git a/apps/middleman-workflows/src/lib/dal/DAL.ts b/apps/middleman-workflows/src/lib/dal/DAL.ts index 4acb124f..ff522c8a 100644 --- a/apps/middleman-workflows/src/lib/dal/DAL.ts +++ b/apps/middleman-workflows/src/lib/dal/DAL.ts @@ -35,7 +35,7 @@ export default class DAL { this.provider = new Provider(dbClient, logger) this.importSupplierAttempts = new ImportSupplierAttempts(dbClient, logger) this.supplierChanges = new SupplierChanges(dbClient, logger) - this.watchdog = new Watchdog(dbClient, logger.child({ context: 'Watchdog' })) + this.watchdog = new Watchdog(dbClient, logger.getChild('Watchdog')) this.notifications = new Notifications(dbClient, logger) } diff --git a/apps/middleman-workflows/src/lib/dal/applicationSettings.ts b/apps/middleman-workflows/src/lib/dal/applicationSettings.ts index 7abadf54..b4305820 100644 --- a/apps/middleman-workflows/src/lib/dal/applicationSettings.ts +++ b/apps/middleman-workflows/src/lib/dal/applicationSettings.ts @@ -27,14 +27,14 @@ export default class ApplicationSettings { async update(data: Partial<{ pocketRpcUrl: string; pocketApiUrl: string }>) { const first = await this.getFirst() if (!first) { - this.logger.warn({ data }, 'Cannot update settings — no settings row exists') + this.logger.warn('Cannot update settings — no settings row exists', { data }) return } await this.dbClient.db .update(schema.applicationSettingsTable) .set(data) .where(eq(schema.applicationSettingsTable.id, first.id)) - this.logger.info({ data }, 'Updated application settings') + this.logger.info('Updated application settings', { data }) } async isBootstrapped(): Promise { diff --git a/apps/middleman-workflows/src/lib/dal/watchdog.ts b/apps/middleman-workflows/src/lib/dal/watchdog.ts index acdc13f9..a04ac574 100644 --- a/apps/middleman-workflows/src/lib/dal/watchdog.ts +++ b/apps/middleman-workflows/src/lib/dal/watchdog.ts @@ -61,4 +61,8 @@ export default class Watchdog implements WatchdogStateStore { recordRecreate(scheduleId: string): Promise { return this.store.recordRecreate(scheduleId) } + + resetRecreations(scheduleId: string): Promise { + return this.store.resetRecreations(scheduleId) + } } diff --git a/apps/middleman-workflows/src/lib/provider/index.ts b/apps/middleman-workflows/src/lib/provider/index.ts index b32b872a..2211c567 100644 --- a/apps/middleman-workflows/src/lib/provider/index.ts +++ b/apps/middleman-workflows/src/lib/provider/index.ts @@ -55,7 +55,7 @@ export class ProviderService { } } } catch (error) { - this.logger.error(`Error fetching provider status for provider ${provider.name} (${provider.url}) with error: ${error}`, { provider, error }) + this.logger.error('Error fetching provider status for provider', { provider, error }) return { id: provider.id, status: ProviderStatus.Unreachable, diff --git a/apps/middleman-workflows/src/worker.ts b/apps/middleman-workflows/src/worker.ts index be6e34c7..29c3ad3d 100644 --- a/apps/middleman-workflows/src/worker.ts +++ b/apps/middleman-workflows/src/worker.ts @@ -2,6 +2,7 @@ import { delegatorActivities, governanceActivities } from './activities' import { importSupplierRecoveryActivities } from './activities/importSupplierRecovery' import bootstrap from './bootstrap' import { + configureLogging, getLogger, Logger, } from '@igniter/logger' @@ -35,7 +36,7 @@ async function waitForAppBootstrap(dal: DAL, logger: Logger) { } logger.warn('Application is not yet bootstrapped. Retrying...') } catch (error) { - logger.warn({ error }, 'Failed to check bootstrap status. Retrying...') + logger.warn('Failed to check bootstrap status. Retrying...', { error }) } await new Promise((resolve) => setTimeout(resolve, BOOTSTRAP_POLL_INTERVAL)) } @@ -57,10 +58,10 @@ export const registerGracefulShutdown = ( } shuttingDown = true - logger.info({ signal }, 'Received shutdown signal, attempting graceful shutdown...') + logger.info('Received shutdown signal, attempting graceful shutdown...', { signal }) const timeout = setTimeout(() => { - logger.error({ timeout: graceTimeoutMs }, 'Grace period exceeded. Forcing exit.') + logger.error('Grace period exceeded. Forcing exit.', { timeout: graceTimeoutMs }) process.exit(1) }, graceTimeoutMs) @@ -70,7 +71,7 @@ export const registerGracefulShutdown = ( logger.info('Graceful shutdown complete. Exiting.') process.exit(0) } catch (err) { - logger.error({ err }, 'Error during shutdown. Forcing exit.') + logger.error('Error during shutdown. Forcing exit.', { err }) process.exit(1) } } @@ -81,6 +82,8 @@ export const registerGracefulShutdown = ( } export async function setupTemporalWorker() { + await configureLogging({ serviceName: 'middleman-workflows' }) + const dbClient = getDb(logger) const dal = new DAL(dbClient, logger) @@ -136,7 +139,7 @@ export async function setupTemporalWorker() { entries: watchdogEntries, store: dal.watchdog, config: wdConfig, - logger: logger.child({ context: 'ScheduleWatchdog' }), + logger: logger.getChild('ScheduleWatchdog'), }) watchdog.start() } else { diff --git a/apps/middleman-workflows/src/workflows/ExecutePendingTransactions.ts b/apps/middleman-workflows/src/workflows/ExecutePendingTransactions.ts index 86309798..4f95212a 100644 --- a/apps/middleman-workflows/src/workflows/ExecutePendingTransactions.ts +++ b/apps/middleman-workflows/src/workflows/ExecutePendingTransactions.ts @@ -21,6 +21,11 @@ export async function ExecutePendingTransactions(args: ExecutePendingTransaction const txs = await listTransactions(); + if (txs.length === 0) { + log.debug('ExecutePendingTransactions: no pending transactions'); + return; + } + const limit = pLimit(MAX_CONCURRENT_TRANSACTIONS); const childPromises = txs.map(({ id, createdAt }) => diff --git a/apps/middleman-workflows/src/workflows/ExecuteTransaction.ts b/apps/middleman-workflows/src/workflows/ExecuteTransaction.ts index 7d6d8879..82702771 100644 --- a/apps/middleman-workflows/src/workflows/ExecuteTransaction.ts +++ b/apps/middleman-workflows/src/workflows/ExecuteTransaction.ts @@ -1,4 +1,5 @@ import { + log, proxyActivities, WorkflowError, } from '@temporalio/workflow' @@ -43,6 +44,7 @@ export async function ExecuteTransaction(args: TransactionArgs) { // Already broadcast (has a hash) → the verifier owns it; nothing to do here. if (transaction.hash) { + log.debug('ExecuteTransaction: already broadcast, handing off to verifier', { transactionId, hash: transaction.hash }); return { ...transaction }; } @@ -50,6 +52,7 @@ export async function ExecuteTransaction(args: TransactionArgs) { // No hash and the broadcast window already expired → mark failed immediately. if (transaction.executionHeight && txHeight - transaction.executionHeight > TX_EXPIRATION_BLOCKS) { + log.warn('ExecuteTransaction: expired before broadcast', { transactionId }); await updateTransaction(transactionId, { status: TransactionStatus.Failure, log: 'TX expired before broadcast', @@ -67,6 +70,7 @@ export async function ExecuteTransaction(args: TransactionArgs) { } if (!result.transactionHash) { + log.warn('ExecuteTransaction: broadcast returned no hash', { transactionId, code: result.code, message: result.message }); await updateTransaction(transactionId, { status: TransactionStatus.Failure, code: result.code, @@ -80,6 +84,8 @@ export async function ExecuteTransaction(args: TransactionArgs) { // by KeplrWalletConnection; null for external-wallet txs that omit it). const timeoutHeight = await getTxTimeoutHeight(transactionId); + log.info('ExecuteTransaction: broadcast succeeded', { transactionId, hash: result.transactionHash, executionHeight: txHeight }); + // Persist hash + height + timeoutHeight and hand off to the verifier. // executionHeight was sampled BEFORE broadcast (line ~47) — this must not move after // broadcast or the anchor would be ≥ the first possible inclusion height. diff --git a/apps/middleman-workflows/src/workflows/SupplierStatusRange.ts b/apps/middleman-workflows/src/workflows/SupplierStatusRange.ts index 9aa9fef6..17121530 100644 --- a/apps/middleman-workflows/src/workflows/SupplierStatusRange.ts +++ b/apps/middleman-workflows/src/workflows/SupplierStatusRange.ts @@ -72,4 +72,7 @@ export async function SupplierStatusByRange(input: SupplierStatusByRange): Promi if (allFailed) { throw new WorkflowError('All activities failed'); } + + const upserted = r.filter(r => r.status === 'fulfilled').length + log.info('supplier sync range done', { keysChecked: rows.length, upserted, minId: input.minId, maxId: input.maxId }) } diff --git a/apps/middleman-workflows/src/workflows/VerifyPendingTransactions.ts b/apps/middleman-workflows/src/workflows/VerifyPendingTransactions.ts index d7fb13bf..34c8365d 100644 --- a/apps/middleman-workflows/src/workflows/VerifyPendingTransactions.ts +++ b/apps/middleman-workflows/src/workflows/VerifyPendingTransactions.ts @@ -27,7 +27,10 @@ export async function VerifyPendingTransactions() { }) const txs = await listPendingWithHash() - if (txs.length === 0) return + if (txs.length === 0) { + log.debug('VerifyPendingTransactions: no pending transactions') + return + } const limit = pLimit(MAX_CONCURRENT) const results = await Promise.allSettled( diff --git a/apps/middleman/Dockerfile b/apps/middleman/Dockerfile index 5c7e81ae..23928cd8 100644 --- a/apps/middleman/Dockerfile +++ b/apps/middleman/Dockerfile @@ -63,5 +63,11 @@ COPY --from=base --chown=nextjs:nodejs /app/middleman.drizzle.config.ts ./ COPY --from=base --chown=nextjs:nodejs /app/apps/middleman/drizzle/ ./apps/middleman/drizzle/ COPY --from=base --chown=nextjs:nodejs /app/pnpm-workspace.yaml ./pnpm-workspace.yaml +ENV SERVICE_NAME=middleman +# APP_VERSION lives in the FINAL stage so a per-deploy version bump only +# invalidates this layer, not the install/build layers above. +ARG APP_VERSION=unknown +ENV APP_VERSION=${APP_VERSION} + # Start the development server CMD ["node", "apps/middleman/server.js"] diff --git a/apps/middleman/README.md b/apps/middleman/README.md index 839a8e62..35363bc5 100644 --- a/apps/middleman/README.md +++ b/apps/middleman/README.md @@ -49,7 +49,7 @@ All vars below are sourced from `docker-compose/apps/middleman/.env.sample` and | Variable | Required | Description | Example / Default | |----------|----------|-------------|-------------------| | `NODE_ENV` | Optional | Node runtime environment | `production` | -| `LOG_LEVEL` | Optional | Logging verbosity (`error`, `warn`, `info`, `debug`) | `info` | +| `LOG_LEVEL` | Optional | LogTape verbosity (`trace`, `debug`, `info`, `warning`, `error`, `fatal` — note `warning`, not `warn`) | `debug` | ### Temporal diff --git a/apps/middleman/src/actions/ImportSuppliers.ts b/apps/middleman/src/actions/ImportSuppliers.ts index d6fb2cd4..4ef0958d 100644 --- a/apps/middleman/src/actions/ImportSuppliers.ts +++ b/apps/middleman/src/actions/ImportSuppliers.ts @@ -11,6 +11,10 @@ import { getDb } from '@/db' import { ImportedSupplier } from '@/lib/services/importSuppliers' import { getExistingNodes, getNodeAddressesByOwnerAndProvider } from '@/lib/dal/nodes' import { getApplicationSettings } from '@/lib/dal/applicationSettings' +import { getLogger } from '@igniter/logger' +import { runWithRequestContext } from '@/lib/logging/withLogging' + +const log = getLogger(['middleman', 'import-suppliers']) async function getCurrentHeight(): Promise { try { @@ -44,19 +48,22 @@ export async function CreateImportAttempt( providerId: number, nonce: string, ): Promise { - const userIdentity = await requireAuth() - - const attempt: InsertImportSupplierAttempt = { - userIdentity, - ownerAddress, - providerIdentity, - providerId, - nonce, - status: ImportAttemptStatus.Initiated, - } + return runWithRequestContext(async () => { + const userIdentity = await requireAuth() + + const attempt: InsertImportSupplierAttempt = { + userIdentity, + ownerAddress, + providerIdentity, + providerId, + nonce, + status: ImportAttemptStatus.Initiated, + } - const created = await importAttemptsDal.create(attempt) - return created.id + const created = await importAttemptsDal.create(attempt) + log.info('import attempt created', { attemptId: created.id, ownerAddress, providerIdentity }) + return created.id + }) } /** @@ -72,16 +79,19 @@ export async function UpdateImportAttemptStatus( errorMessage?: string }, ): Promise { - const userIdentity = await requireAuth() - const attempt = await getOwnedImportAttempt(attemptId, userIdentity) + return runWithRequestContext(async () => { + const userIdentity = await requireAuth() + const attempt = await getOwnedImportAttempt(attemptId, userIdentity) - if (attempt.status === ImportAttemptStatus.Submitted) { - throw new Error(`Import attempt ${attemptId} already submitted. Cannot update status.`) - } + if (attempt.status === ImportAttemptStatus.Submitted) { + throw new Error(`Import attempt ${attemptId} already submitted. Cannot update status.`) + } - await importAttemptsDal.update(attemptId, { - status, - ...data, + await importAttemptsDal.update(attemptId, { + status, + ...data, + }) + log.info('import attempt status updated', { attemptId, status }) }) } @@ -93,50 +103,56 @@ export async function CompleteImportAttempt( suppliers: ImportedSupplier[], providerIdentity: string, ): Promise { - const userIdentity = await requireAuth() - const attempt = await getOwnedImportAttempt(attemptId, userIdentity) - - const db = getDb() - const supplierAddresses = suppliers.map((s) => s.address) - const existingSuppliers = await getExistingNodes(supplierAddresses, userIdentity) - const height = await getCurrentHeight() - - const nodesToInsert: Array = [] - - for (const supplier of suppliers) { - if (existingSuppliers.includes(supplier.address)) { - continue + return runWithRequestContext(async () => { + const userIdentity = await requireAuth() + const attempt = await getOwnedImportAttempt(attemptId, userIdentity) + + const db = getDb() + const supplierAddresses = suppliers.map((s) => s.address) + const existingSuppliers = await getExistingNodes(supplierAddresses, userIdentity) + const height = await getCurrentHeight() + + const nodesToInsert: Array = [] + + for (const supplier of suppliers) { + if (existingSuppliers.includes(supplier.address)) { + continue + } + + nodesToInsert.push({ + address: supplier.address, + ownerAddress: attempt.ownerAddress, + status: NodeStatus.Staked, + stakeAmount: supplier.stakeAmount, + providerId: providerIdentity, + createdBy: attempt.userIdentity, + balance: BigInt(0), + lastUpdatedHeight: height, + }) } - nodesToInsert.push({ - address: supplier.address, - ownerAddress: attempt.ownerAddress, - status: NodeStatus.Staked, - stakeAmount: supplier.stakeAmount, - providerId: providerIdentity, - createdBy: attempt.userIdentity, - balance: BigInt(0), - lastUpdatedHeight: height, - }) - } - - let importedAddresses: string[] = [] - if (nodesToInsert.length > 0) { - // The nodes.address unique constraint silently drops rows whose address already - // exists under ANOTHER user (getExistingNodes only sees this user's rows). - // Derive the completed set from what actually landed. - const inserted = await db - .insert(nodesTable) - .values(nodesToInsert) - .onConflictDoNothing() - .returning({ address: nodesTable.address }) - importedAddresses = inserted.map((r) => r.address) - const dropped = nodesToInsert.filter((n) => !importedAddresses.includes(n.address)) - if (dropped.length > 0) { - console.warn('CompleteImportAttempt: addresses skipped (already owned by another account)', dropped.map((n) => n.address)) + let importedAddresses: string[] = [] + if (nodesToInsert.length > 0) { + // The nodes.address unique constraint silently drops rows whose address already + // exists under ANOTHER user (getExistingNodes only sees this user's rows). + // Derive the completed set from what actually landed. + const inserted = await db + .insert(nodesTable) + .values(nodesToInsert) + .onConflictDoNothing() + .returning({ address: nodesTable.address }) + importedAddresses = inserted.map((r) => r.address) + const dropped = nodesToInsert.filter((n) => !importedAddresses.includes(n.address)) + if (dropped.length > 0) { + log.warn('import attempt addresses skipped (already owned by another account)', { + attemptId, + skippedCount: dropped.length, + }) + } } - } - await importAttemptsDal.markCompleted(attemptId, importedAddresses) + await importAttemptsDal.markCompleted(attemptId, importedAddresses) + log.info('import attempt completed', { attemptId, importedCount: importedAddresses.length }) + }) } /** diff --git a/apps/middleman/src/actions/NotificationChannels.ts b/apps/middleman/src/actions/NotificationChannels.ts index d1d48440..9164a624 100644 --- a/apps/middleman/src/actions/NotificationChannels.ts +++ b/apps/middleman/src/actions/NotificationChannels.ts @@ -176,8 +176,12 @@ export async function TestNotificationChannel(id: number) { }) } -export async function ListNotificationEvents(page = 0, pageSize = 25) { - return run(async () => dal.listNotificationEvents(await requireAuth(), page, pageSize)) +export async function ListNotificationEvents( + page = 0, + pageSize = 25, + filters?: dal.NotificationEventFilters, +) { + return run(async () => dal.listNotificationEvents(await requireAuth(), page, pageSize, filters)) } export async function GetNotificationEvent(uuid: string) { diff --git a/apps/middleman/src/actions/Providers.ts b/apps/middleman/src/actions/Providers.ts index 0e018253..417d3cdb 100644 --- a/apps/middleman/src/actions/Providers.ts +++ b/apps/middleman/src/actions/Providers.ts @@ -6,6 +6,10 @@ import { z } from "zod"; import { getCurrentUser, requireAuth } from '@/lib/utils/actions' import {ProviderStatus, UserRole} from "@igniter/db/middleman/enums"; import { getApplicationSettings } from '@/lib/dal/applicationSettings' +import { getLogger } from '@igniter/logger' +import { runWithRequestContext } from '@/lib/logging/withLogging' + +const log = getLogger(['middleman', 'providers']) export interface Provider { id: number; @@ -23,20 +27,23 @@ const updateProvidersSchema = z.object({ const GOVERNANCE_SYNC_SCHEDULE_ID = 'GovernanceSync-scheduled' export async function TriggerGovernanceSync(): Promise<{ success: boolean, error?: string }> { - try { - await requireAuth() - const { getTemporalClient } = await import('@/lib/temporal') - const client = getTemporalClient() - const handle = client.schedule.getHandle(GOVERNANCE_SYNC_SCHEDULE_ID) - await handle.trigger() - return { success: true } - } catch (error) { - console.error('Error triggering GovernanceSync:', error) - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error occurred', + return runWithRequestContext(async () => { + try { + await requireAuth() + const { getTemporalClient } = await import('@/lib/temporal') + const client = getTemporalClient() + const handle = client.schedule.getHandle(GOVERNANCE_SYNC_SCHEDULE_ID) + await handle.trigger() + log.info('governance sync triggered', { scheduleId: GOVERNANCE_SYNC_SCHEDULE_ID }) + return { success: true } + } catch (error) { + log.error('governance sync trigger failed', { error }) + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error occurred', + } } - } + }) } type CdnProvider = { name: string; identity: string; identityHistory: string[]; url: string } @@ -44,67 +51,72 @@ type CdnProvider = { name: string; identity: string; identityHistory: string[]; // This is here because when doing the setup, the middleman workflows can not start running because the app is not bootstrapped // so we cannot call TriggerGovernanceSync at setup time export async function SyncProvidersFromGovernance(): Promise<{ success: boolean; error?: string; data?: Provider[] }> { - try { - const user = await getCurrentUser() + return runWithRequestContext(async () => { + try { + const user = await getCurrentUser() - if (![UserRole.Owner].includes(user.role)) { - throw new Error('Forbidden') - } + if (![UserRole.Owner].includes(user.role)) { + throw new Error('Forbidden') + } - const cdnUrlTemplate = process.env.PROVIDERS_CDN_URL - if (!cdnUrlTemplate) { - return { success: false, error: 'PROVIDERS_CDN_URL environment variable is not defined' } - } + const cdnUrlTemplate = process.env.PROVIDERS_CDN_URL + if (!cdnUrlTemplate) { + return { success: false, error: 'PROVIDERS_CDN_URL environment variable is not defined' } + } - const settings = await getApplicationSettings() - const cdnUrl = cdnUrlTemplate.replace('{chainId}', settings.chainId.replace('lego-testnet', 'beta')) + const settings = await getApplicationSettings() + const cdnUrl = cdnUrlTemplate.replace('{chainId}', settings.chainId.replace('lego-testnet', 'beta')) - const response = await fetch(cdnUrl) - if (!response.ok) { - return { success: false, error: `Failed to fetch providers from CDN: ${response.statusText}` } - } + const response = await fetch(cdnUrl) + if (!response.ok) { + log.warn('governance sync CDN fetch failed', { status: response.status }) + return { success: false, error: `Failed to fetch providers from CDN: ${response.statusText}` } + } - const cdnProviders = (await response.json()) as CdnProvider[] - const current = await listAll() - const currentMap = new Map(current.map((p) => [p.identity, p])) + const cdnProviders = (await response.json()) as CdnProvider[] + const current = await listAll() + const currentMap = new Map(current.map((p) => [p.identity, p])) - const allCdnIdentities = new Set() - for (const p of cdnProviders) { - allCdnIdentities.add(p.identity) - p.identityHistory.forEach((h) => allCdnIdentities.add(h)) - } + const allCdnIdentities = new Set() + for (const p of cdnProviders) { + allCdnIdentities.add(p.identity) + p.identityHistory.forEach((h) => allCdnIdentities.add(h)) + } - const toInsert: { name: string; identity: string; url: string }[] = [] - const toUpdate: { id: number; name: string; identity: string; url: string }[] = [] + const toInsert: { name: string; identity: string; url: string }[] = [] + const toUpdate: { id: number; name: string; identity: string; url: string }[] = [] - for (const cdnProvider of cdnProviders) { - const possibleIds = [cdnProvider.identity, ...cdnProvider.identityHistory] - const matchingCurrent = possibleIds.map((id) => currentMap.get(id)).find(Boolean) ?? null + for (const cdnProvider of cdnProviders) { + const possibleIds = [cdnProvider.identity, ...cdnProvider.identityHistory] + const matchingCurrent = possibleIds.map((id) => currentMap.get(id)).find(Boolean) ?? null - if (matchingCurrent) { - if ( - matchingCurrent.identity !== cdnProvider.identity || - matchingCurrent.name !== cdnProvider.name || - matchingCurrent.url !== cdnProvider.url - ) { - toUpdate.push({ id: matchingCurrent.id, identity: cdnProvider.identity, name: cdnProvider.name, url: cdnProvider.url }) + if (matchingCurrent) { + if ( + matchingCurrent.identity !== cdnProvider.identity || + matchingCurrent.name !== cdnProvider.name || + matchingCurrent.url !== cdnProvider.url + ) { + toUpdate.push({ id: matchingCurrent.id, identity: cdnProvider.identity, name: cdnProvider.name, url: cdnProvider.url }) + } + } else { + toInsert.push({ identity: cdnProvider.identity, name: cdnProvider.name, url: cdnProvider.url }) } - } else { - toInsert.push({ identity: cdnProvider.identity, name: cdnProvider.name, url: cdnProvider.url }) } - } - await applyGovernanceSync(toInsert, toUpdate, user.identity) + await applyGovernanceSync(toInsert, toUpdate, user.identity) - const providers = await list(true) - return { success: true, data: providers } - } catch (error) { - console.error('Error syncing providers from governance:', error) - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error occurred', + log.info('governance sync applied', { inserted: toInsert.length, updated: toUpdate.length }) + + const providers = await list(true) + return { success: true, data: providers } + } catch (error) { + log.error('governance sync failed', { error }) + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error occurred', + } } - } + }) } /** @deprecated Use TriggerGovernanceSync instead */ @@ -165,8 +177,7 @@ export async function UpdateVisibility(identity: string, visible: boolean) { await update(identity, updates); } catch (error) { - console.log('UpdateVisibility: An error occurred while performing the update operation'); - console.error(error); + log.error('provider visibility update failed', { identity, visible, error }) } } @@ -174,8 +185,7 @@ export async function UpdateEnabled(identity: string, enabled: boolean) { try { await update(identity, { enabled }); } catch (error) { - console.log('UpdateEnabled: An error occurred while performing the update operation'); - console.error(error); + log.error('provider enabled update failed', { identity, enabled, error }) } } diff --git a/apps/middleman/src/actions/Stake.ts b/apps/middleman/src/actions/Stake.ts index 3c06ebc5..ae9f1579 100644 --- a/apps/middleman/src/actions/Stake.ts +++ b/apps/middleman/src/actions/Stake.ts @@ -26,6 +26,10 @@ import { getCompressedPublicKeyFromAppIdentity, signPayload, } from '@igniter/commons/crypto' +import { getLogger } from '@igniter/logger' +import { runWithRequestContext } from '@/lib/logging/withLogging' + +const log = getLogger(['middleman', 'stake']) export interface CreateStakeTransactionRequest { offer: StakeDistributionOffer; @@ -110,20 +114,36 @@ export async function CalculateStakeDistribution(stakeAmount: number, ownerAddre } export async function CreateStakeTransaction(request: CreateStakeTransactionRequest) { - const userIdentity = await requireAuth() - - return insert({ - type: TransactionType.Stake, - status: TransactionStatus.Pending, - signedPayload: request.transaction.signedPayload, - fromAddress: request.transaction.address, - unsignedPayload: request.transaction.unsignedPayload, - providerFee: Number(request.offer.fee), - providerId: request.offer.identity, - estimatedFee: request.transaction.estimatedFee, - consumedFee: 0, - typeProviderFee: request.offer.feeType, - createdBy: userIdentity, + return runWithRequestContext(async () => { + const userIdentity = await requireAuth() + + log.info('stake transaction requested', { + ownerAddress: request.transaction.address, + providerId: request.offer.identity, + estimatedFee: request.transaction.estimatedFee, + }) + + const created = await insert({ + type: TransactionType.Stake, + status: TransactionStatus.Pending, + signedPayload: request.transaction.signedPayload, + fromAddress: request.transaction.address, + unsignedPayload: request.transaction.unsignedPayload, + providerFee: Number(request.offer.fee), + providerId: request.offer.identity, + estimatedFee: request.transaction.estimatedFee, + consumedFee: 0, + typeProviderFee: request.offer.feeType, + createdBy: userIdentity, + }) + + log.info('stake transaction created', { + transactionId: created.id, + ownerAddress: request.transaction.address, + providerId: request.offer.identity, + }) + + return created }) } diff --git a/apps/middleman/src/actions/Unstake.ts b/apps/middleman/src/actions/Unstake.ts index 08f9c029..99a088f3 100644 --- a/apps/middleman/src/actions/Unstake.ts +++ b/apps/middleman/src/actions/Unstake.ts @@ -10,6 +10,10 @@ import { requireAuth } from '@/lib/utils/actions' import { InsertTransaction } from '@igniter/db/middleman/schema' import { TransactionStatus, TransactionType } from '@igniter/db/middleman/enums' import { insert } from '@/lib/dal/transaction' +import { getLogger } from '@igniter/logger' +import { runWithRequestContext } from '@/lib/logging/withLogging' + +const log = getLogger(['middleman', 'unstake']) export interface UnstakeDurationData { durationSeconds: number; @@ -80,16 +84,30 @@ export interface CreateUnstakeTransactionRequest { } export async function CreateUnstakeTransaction(request: CreateUnstakeTransactionRequest) { - const userIdentity = await requireAuth() - - return insert({ - type: TransactionType.Unstake, - status: TransactionStatus.Pending, - signedPayload: request.transaction.signedPayload, - fromAddress: request.transaction.address, - unsignedPayload: request.transaction.unsignedPayload, - estimatedFee: request.transaction.estimatedFee, - consumedFee: 0, - createdBy: userIdentity, + return runWithRequestContext(async () => { + const userIdentity = await requireAuth() + + log.info('unstake transaction requested', { + ownerAddress: request.transaction.address, + estimatedFee: request.transaction.estimatedFee, + }) + + const created = await insert({ + type: TransactionType.Unstake, + status: TransactionStatus.Pending, + signedPayload: request.transaction.signedPayload, + fromAddress: request.transaction.address, + unsignedPayload: request.transaction.unsignedPayload, + estimatedFee: request.transaction.estimatedFee, + consumedFee: 0, + createdBy: userIdentity, + }) + + log.info('unstake transaction created', { + transactionId: created.id, + ownerAddress: request.transaction.address, + }) + + return created }) } diff --git a/apps/middleman/src/actions/Workflows.test.ts b/apps/middleman/src/actions/Workflows.test.ts index 52b009c1..511d2aa6 100644 --- a/apps/middleman/src/actions/Workflows.test.ts +++ b/apps/middleman/src/actions/Workflows.test.ts @@ -40,6 +40,27 @@ function makeInfo(id: string) { } } +const schedulePause = jest.fn().mockResolvedValue(undefined) +const scheduleUnpause = jest.fn().mockResolvedValue(undefined) +const scheduleDelete = jest.fn().mockResolvedValue(undefined) +const scheduleGetHandle = jest.fn(() => ({ + describe: async () => ({ + scheduleId: 'GovernanceSync-scheduled', + state: { paused: true }, + spec: { intervals: [] }, + info: { + recentActions: [], + nextActionTimes: [], + runningActions: [], + numActionsTaken: 0, + createdAt: new Date('2026-07-01T00:00:00Z'), + }, + }), + pause: schedulePause, + unpause: scheduleUnpause, + delete: scheduleDelete, +})) + const fakeClient = { workflow: { list: () => ({ @@ -60,34 +81,37 @@ const fakeClient = { }, }), // GetScheduleHealth describe()s each schedule for running-actions-aware liveness. - getHandle: () => ({ - describe: async () => ({ - scheduleId: 'GovernanceSync-scheduled', - state: { paused: true }, - spec: { intervals: [] }, - info: { - recentActions: [], - nextActionTimes: [], - runningActions: [], - numActionsTaken: 0, - createdAt: new Date('2026-07-01T00:00:00Z'), - }, - }), - }), + getHandle: scheduleGetHandle, }, } jest.mock('@/lib/temporal', () => ({ getTemporalClient: () => fakeClient })) jest.mock('@/lib/dal/watchdogHealState', () => ({ listWatchdogHealState: jest.fn().mockResolvedValue([]), + resetWatchdogRecreations: jest.fn().mockResolvedValue(undefined), })) +import { resetWatchdogRecreations } from '@/lib/dal/watchdogHealState' +const resetRecreations = resetWatchdogRecreations as jest.Mock -import { ListWorkflows, GetScheduleHealth, TerminateWorkflow } from './Workflows' +import { + ListWorkflows, + GetScheduleHealth, + TerminateWorkflow, + PauseSchedule, + ResumeSchedule, + RecreateSchedule, +} from './Workflows' describe('middleman Workflows actions', () => { beforeEach(() => { requireAdmin.mockReset() terminate.mockClear() getHandle.mockClear() + schedulePause.mockClear() + scheduleUnpause.mockClear() + scheduleDelete.mockClear() + resetRecreations.mockClear() + scheduleDelete.mockResolvedValue(undefined) + schedulePause.mockResolvedValue(undefined) requireAdmin.mockResolvedValue(undefined) }) @@ -119,4 +143,62 @@ describe('middleman Workflows actions', () => { if (res.success) throw new Error('expected failure') expect(res.error.message).toBe('Unauthorized') }) + + it('PauseSchedule pauses with a default operator note', async () => { + const res = await PauseSchedule('GovernanceSync-scheduled') + expect(res.success).toBe(true) + expect(schedulePause).toHaveBeenCalledWith('Paused by operator from admin UI') + }) + + it('PauseSchedule forwards an explicit note', async () => { + const res = await PauseSchedule('GovernanceSync-scheduled', 'ops window') + expect(res.success).toBe(true) + expect(schedulePause).toHaveBeenCalledWith('ops window') + }) + + it('ResumeSchedule unpauses', async () => { + const res = await ResumeSchedule('GovernanceSync-scheduled') + expect(res.success).toBe(true) + expect(scheduleUnpause).toHaveBeenCalledWith('Resumed by operator from admin UI') + }) + + it('RecreateSchedule deletes the schedule', async () => { + const res = await RecreateSchedule('GovernanceSync-scheduled') + expect(res.success).toBe(true) + expect(scheduleDelete).toHaveBeenCalledTimes(1) + }) + + it('RecreateSchedule resets the recreate breaker BEFORE deleting (H1)', async () => { + const order: string[] = [] + resetRecreations.mockImplementationOnce(async () => { order.push('reset') }) + scheduleDelete.mockImplementationOnce(async () => { order.push('delete') }) + const res = await RecreateSchedule('GovernanceSync-scheduled') + expect(res.success).toBe(true) + expect(resetRecreations).toHaveBeenCalledWith('GovernanceSync-scheduled') + expect(order).toEqual(['reset', 'delete']) + }) + + it('RecreateSchedule treats NOT_FOUND as success (idempotent)', async () => { + scheduleDelete.mockRejectedValueOnce(Object.assign(new Error('schedule not found'), { code: 5 })) + const res = await RecreateSchedule('GovernanceSync-scheduled') + expect(res.success).toBe(true) + }) + + it('PauseSchedule on a corrupt scheduler workflow suggests Recreate', async () => { + schedulePause.mockRejectedValueOnce(Object.assign(new Error('Failed to pause schedule'), { + cause: Object.assign(new Error('9 FAILED_PRECONDITION: Unable to query workflow due to Workflow Task in failed state.'), { code: 9 }), + })) + const res = await PauseSchedule('GovernanceSync-scheduled') + expect(res.success).toBe(false) + if (res.success) throw new Error('expected failure') + expect(res.error.message).toMatch(/recreate/i) + }) + + it('schedule actions deny a non-owner', async () => { + requireAdmin.mockRejectedValue(new Error('Unauthorized')) + const res = await RecreateSchedule('GovernanceSync-scheduled') + expect(res.success).toBe(false) + if (res.success) throw new Error('expected failure') + expect(res.error.message).toBe('Unauthorized') + }) }) diff --git a/apps/middleman/src/actions/Workflows.ts b/apps/middleman/src/actions/Workflows.ts index 62971d2d..4b8ff262 100644 --- a/apps/middleman/src/actions/Workflows.ts +++ b/apps/middleman/src/actions/Workflows.ts @@ -3,11 +3,13 @@ import { type ActionResult } from '@igniter/ui/lib/actionResult' import { withRequireOwner } from '@/lib/utils/actions' import { getTemporalClient } from '@/lib/temporal' -import { listWatchdogHealState } from '@/lib/dal/watchdogHealState' +import { listWatchdogHealState, resetWatchdogRecreations } from '@/lib/dal/watchdogHealState' import { listWorkflowViews, mapScheduleToHealth, scheduleLiveness, + isCorruptSchedule, + isNotFound, type WorkflowListFilter, type WorkflowPageRequest, type WorkflowPageResult, @@ -84,3 +86,63 @@ export async function GetWorkflowHistoryJson( return getWorkflowHistoryJson(client, workflowId, runId) }) } + +/** Corrupt scheduler workflows reject pause/unpause; point the operator to the action that works. */ +function rethrowWithRecreateHint(e: unknown, verb: 'paused' | 'resumed'): never { + if (isCorruptSchedule(e)) { + throw new Error(`Schedule internals are corrupt and cannot be ${verb}; use Recreate instead`) + } + throw e +} + +export async function PauseSchedule( + scheduleId: string, + note?: string, +): Promise> { + return withRequireOwner(async () => { + const client = getTemporalClient() + try { + await client.schedule.getHandle(scheduleId).pause(note ?? 'Paused by operator from admin UI') + } catch (e) { + // pause() is a query/patch on the scheduler workflow — impossible when it + // is corrupt (WFT in failed state). Point the operator to the action that + // does work in that state. + rethrowWithRecreateHint(e, 'paused') + } + }) +} + +export async function ResumeSchedule(scheduleId: string): Promise> { + return withRequireOwner(async () => { + const client = getTemporalClient() + try { + await client.schedule.getHandle(scheduleId).unpause('Resumed by operator from admin UI') + } catch (e) { + rethrowWithRecreateHint(e, 'resumed') + } + }) +} + +/** + * "Recreate" is delete-only on purpose: canonical schedule config lives in the + * workflows worker (bootstrap + watchdog entries), which recreates a missing + * schedule with fresh heal counters within one watchdog tick (~30s). + * + * Reset the recreate breaker FIRST: manual Recreate is the documented operator + * reset for a tripped breaker. Without it, delete → the watchdog's next tick sees + * NOT_FOUND → the still-tripped breaker gates the recreate → the schedule stays + * deleted forever (H1). Reset-before-delete is the correct order since the reset + * only zeroes counters; the delete is what the watchdog reacts to. + */ +export async function RecreateSchedule(scheduleId: string): Promise> { + return withRequireOwner(async () => { + const client = getTemporalClient() + await resetWatchdogRecreations(scheduleId) + try { + await client.schedule.getHandle(scheduleId).delete() + } catch (e) { + if (isNotFound(e)) return // already gone — the watchdog is recreating it + throw e + } + }) +} diff --git a/apps/middleman/src/app/admin/(internal)/providers/Refresh.tsx b/apps/middleman/src/app/admin/(internal)/providers/Refresh.tsx index 7a919e26..94bec1ba 100644 --- a/apps/middleman/src/app/admin/(internal)/providers/Refresh.tsx +++ b/apps/middleman/src/app/admin/(internal)/providers/Refresh.tsx @@ -5,6 +5,9 @@ import React from 'react' import { TriggerGovernanceSync } from '@/actions/Providers' import { Button } from '@igniter/ui/components/button' import { LoaderIcon } from '@igniter/ui/assets' +import { getLogger } from '@igniter/logger' + +const log = getLogger(['middleman', 'refresh-providers']) export default function RefreshProviders() { const queryClient = useQueryClient(); @@ -19,7 +22,7 @@ export default function RefreshProviders() { await new Promise(resolve => setTimeout(resolve, 2000)); await queryClient.invalidateQueries({ queryKey: ['providers'] }); } catch (error) { - console.error("Failed to update providers from source:", error); + log.error("failed to update providers from source", { error }); } finally { setIsUpdatingProviders(false); } diff --git a/apps/middleman/src/app/admin/(internal)/settings/Form.tsx b/apps/middleman/src/app/admin/(internal)/settings/Form.tsx index df1afd5a..0d40c269 100644 --- a/apps/middleman/src/app/admin/(internal)/settings/Form.tsx +++ b/apps/middleman/src/app/admin/(internal)/settings/Form.tsx @@ -23,6 +23,9 @@ import { } from '@/actions/ApplicationSettings' import { LoaderIcon } from '@igniter/ui/assets' import { ChainId } from '@igniter/db/middleman/enums' +import { getLogger } from '@igniter/logger' + +const log = getLogger(['middleman', 'internal-settings-form']) const FormSchema = z.object({ name: z.string().min(1, 'Name is required'), @@ -235,7 +238,7 @@ export default function SettingsForm() { await refetchSettings() form.reset(values) } catch (error) { - console.error('Failed to update settings:', error) + log.error('failed to update settings', { error }) setSubmitError('Failed to save settings. Please try again.') } finally { setIsSubmitting(false) diff --git a/apps/middleman/src/app/admin/setup/blockchainFrom.tsx b/apps/middleman/src/app/admin/setup/blockchainFrom.tsx index 96c1050f..326d895e 100644 --- a/apps/middleman/src/app/admin/setup/blockchainFrom.tsx +++ b/apps/middleman/src/app/admin/setup/blockchainFrom.tsx @@ -22,6 +22,9 @@ import { } from '@/actions/ApplicationSettings' import { ApplicationSettings } from "@igniter/db/middleman/schema"; import { ChainId } from "@igniter/db/middleman/enums"; +import { getLogger } from "@igniter/logger"; + +const log = getLogger(['middleman', 'blockchain-form']) interface FormProps { defaultValues: Partial; @@ -197,7 +200,7 @@ const FormComponent: React.FC = ({ defaultValues, goNext }) => { await UpsertApplicationSettings(values, isUpdate); goNext(); } catch (error) { - console.error("Something failed while updating the application settings", error); + log.error("failed to update application settings", { error }); } finally { setIsLoading(false); } diff --git a/apps/middleman/src/app/admin/setup/providersForm.tsx b/apps/middleman/src/app/admin/setup/providersForm.tsx index 33df6a0d..63640066 100644 --- a/apps/middleman/src/app/admin/setup/providersForm.tsx +++ b/apps/middleman/src/app/admin/setup/providersForm.tsx @@ -14,6 +14,9 @@ import { import { Checkbox } from "@igniter/ui/components/checkbox"; import { SyncProvidersFromGovernance, Provider, submitProviders } from '@/actions/Providers' import { LoaderIcon } from '@igniter/ui/assets' +import { getLogger } from '@igniter/logger' + +const log = getLogger(['middleman', 'providers-form']) interface ProvidersFormProps { providers: Provider[]; @@ -52,7 +55,7 @@ const ProvidersForm: React.FC = ({ form.setValue('providers', (providersList || []).map((provider) => provider.identity)) } } catch (error) { - console.error("Failed to load providers list", error) + log.error("failed to load providers list", { error }) } finally { setIsLoading(false) } @@ -79,7 +82,7 @@ const ProvidersForm: React.FC = ({ await submitProviders(values, providers); goNext(); } catch (error) { - console.error(error); + log.error("failed to submit providers", { error }); } finally { setIsLoading(false); } @@ -91,10 +94,10 @@ const ProvidersForm: React.FC = ({ name="providers" render={() => ( -
+
- - + + diff --git a/apps/middleman/src/app/admin/setup/settingsForm.tsx b/apps/middleman/src/app/admin/setup/settingsForm.tsx index 0d290936..680ac03f 100644 --- a/apps/middleman/src/app/admin/setup/settingsForm.tsx +++ b/apps/middleman/src/app/admin/setup/settingsForm.tsx @@ -18,6 +18,9 @@ import React, {useMemo, useRef, useState} from "react"; import { UpsertApplicationSettings } from "@/actions/ApplicationSettings"; import {ApplicationSettings} from "@igniter/db/middleman/schema"; import {useNotifications} from "@igniter/ui/context/Notifications/index"; +import {getLogger} from "@igniter/logger"; + +const log = getLogger(['middleman', 'settings-form']) interface FormProps { defaultValues: Partial; @@ -69,11 +72,10 @@ const FormComponent: React.FC = ({ defaultValues, goNext, goBack }) = onSubmit={form.handleSubmit(async (values: any) => { setIsLoading(true); try { - console.log('values:', values, isUpdate); await UpsertApplicationSettings(values, isUpdate); goNext(); } catch (error) { - console.error(error); + log.error("failed to save settings", { isUpdate, error }); addNotification({ id: `settings-form-submit-error`, type: 'error', diff --git a/apps/middleman/src/app/admin/setup/stepper.tsx b/apps/middleman/src/app/admin/setup/stepper.tsx index f2479b45..d93e9580 100644 --- a/apps/middleman/src/app/admin/setup/stepper.tsx +++ b/apps/middleman/src/app/admin/setup/stepper.tsx @@ -10,6 +10,9 @@ import ApplicationSettingsForm from "./settingsForm"; import { Provider } from "@/actions/Providers"; import ProvidersForm from "./providersForm"; import BlockchainFormComponent from '@/app/admin/setup/blockchainFrom' +import { getLogger } from '@igniter/logger' + +const log = getLogger(['middleman', 'setup-stepper']) interface StepperProps { providers: Provider[]; @@ -99,7 +102,7 @@ export const Stepper: React.FC = ({ providers }) => { const dbSettings = await getApplicationSettings(); setSettings(dbSettings as ApplicationSettings); } catch (error) { - console.error("Something went wrong while retrieving the current applications settings", error); + log.error("failed to retrieve application settings", { error }); setHasError(true); } finally { setIsLoadingSettings(false); diff --git a/apps/middleman/src/app/api/health/route.ts b/apps/middleman/src/app/api/health/route.ts index 0488fb0b..aad68876 100644 --- a/apps/middleman/src/app/api/health/route.ts +++ b/apps/middleman/src/app/api/health/route.ts @@ -1,13 +1,17 @@ import { getDb } from '@/db' import {sql} from "drizzle-orm"; +import { getLogger } from '@igniter/logger' +import { withLogging } from '@/lib/logging/withLogging' + +const log = getLogger(['middleman', 'health']) // health check api -export async function GET(_: Request) { +export const GET = withLogging(async (_: Request) => { const db = getDb() try { await db.execute(sql`SELECT 1`); } catch (e) { - console.error(e) + log.error('health check database connection failed', { error: e }) return new Response(JSON.stringify({ error: 'Database connection failed' }), { status: 500, headers: { 'Content-Type': 'application/json' }, @@ -16,4 +20,4 @@ export async function GET(_: Request) { return new Response(JSON.stringify({}), { headers: { 'Content-Type': 'application/json' }, }) -} +}) diff --git a/apps/middleman/src/app/api/provider-rpc/route.ts b/apps/middleman/src/app/api/provider-rpc/route.ts index 66027007..f5a00b0d 100644 --- a/apps/middleman/src/app/api/provider-rpc/route.ts +++ b/apps/middleman/src/app/api/provider-rpc/route.ts @@ -5,9 +5,13 @@ import {Provider} from "@igniter/db/middleman/schema"; import {signPayload} from "@igniter/commons/crypto"; import {getApplicationSettings} from "@/lib/dal/applicationSettings"; import {REQUEST_IDENTITY_HEADER, REQUEST_SIGNATURE_HEADER} from "@igniter/commons/constants"; +import {getLogger, redactObject} from "@igniter/logger"; +import {withLogging} from "@/lib/logging/withLogging"; -export async function POST(request: Request) { - console.log('Preparing a request to a provider'); +const log = getLogger(['middleman', 'provider-rpc']) + +export const POST = withLogging(async (request: Request) => { + const startedAt = Date.now(); const schema = z.object({ provider: z.string(), @@ -23,22 +27,19 @@ export async function POST(request: Request) { try { const body = await request.json(); validatedData = schema.parse(body); - console.log('Request payload validated:', JSON.stringify(validatedData, null, 2)); } catch (error) { - console.error('Request payload validation failed:', error); + log.error('provider-rpc payload validation failed', { error }) return new Response("Invalid request payload", {status: 400}); } try { - console.log('Loading the provider'); provider = await GetProviderByIdentity(validatedData.provider); if (!provider) { - console.error('Provider not found'); + log.warn('provider-rpc provider not found', { provider: validatedData.provider }) return new Response("Provider not found", {status: 404}); } - console.log('Provider loaded:', JSON.stringify({ id: provider.id, name: provider.name, }, null, 2)) } catch (error) { - console.error(error); + log.error('provider-rpc provider lookup failed', { provider: validatedData.provider, error }) return new Response("Unable to load the provider", {status: 500}); } @@ -46,7 +47,7 @@ export async function POST(request: Request) { const applicationSettings = await getApplicationSettings(); identity = applicationSettings.appIdentity; } catch (error) { - console.error(error); + log.error('provider-rpc app identity lookup failed', { error }) return new Response("There has been an error while setting the identity of the app", {status: 500}); } @@ -54,12 +55,13 @@ export async function POST(request: Request) { const signatureBuffer = await signPayload(JSON.stringify(validatedData.data)); signature = signatureBuffer.toString('base64'); } catch (error) { - console.error(error); + log.error('provider-rpc payload signing failed', { provider: provider.identity, error }) return new Response("There has been an error while signing the payload.", {status: 500}); } try { - console.log('Executing the request'); + log.debug('provider-rpc request payload', redactObject({ provider: provider.identity, path: validatedData.path, data: validatedData.data })) + const response = await fetch(urlJoin(provider.url, validatedData.path), { method: 'POST', body: JSON.stringify(validatedData.data), @@ -72,9 +74,21 @@ export async function POST(request: Request) { const responseBody = await response.json(); + log.info('provider-rpc request completed', { + provider: provider.identity, + path: validatedData.path, + status: response.status, + durationMs: Date.now() - startedAt, + }) + return new Response(JSON.stringify(responseBody), {status: 200}); } catch (error) { - console.error(error); + log.error('provider-rpc request failed', { + provider: provider.identity, + path: validatedData.path, + durationMs: Date.now() - startedAt, + error, + }) return new Response("Unable to fetch the provider", {status: 500}); } -} +}) diff --git a/apps/middleman/src/app/app/(lists)/suppliers/ActivitiesSection.tsx b/apps/middleman/src/app/app/(lists)/suppliers/ActivitiesSection.tsx index b2697df7..02020d67 100644 --- a/apps/middleman/src/app/app/(lists)/suppliers/ActivitiesSection.tsx +++ b/apps/middleman/src/app/app/(lists)/suppliers/ActivitiesSection.tsx @@ -1,9 +1,10 @@ 'use client' -import React, { useMemo } from 'react' +import React, { useMemo, useState } from 'react' import { useQuery } from '@tanstack/react-query' import { clsx } from 'clsx' import { GetPendingState } from '@/actions/Pending' +import { Input } from '@igniter/ui/components/input' import Address from '@igniter/ui/components/Address' import TransactionHash from '@igniter/ui/components/TransactionHash' import Amount from '@igniter/ui/components/Amount' @@ -23,11 +24,6 @@ function hasPendingOrLinger(state: PendingStateSerialized | undefined): boolean return (state.pendingOperations?.length ?? 0) > 0 } -function pendingCount(state: PendingStateSerialized | undefined): number { - if (!state) return 0 - return Object.keys(state.byOperator).length -} - // Status-aware label and color per design spec: // pending+stake → "Staking…" yellow, pending+unstake → "Unstaking…" yellow // success+stake → "Staked" green, success+unstake → "Unstaked" green @@ -63,33 +59,42 @@ export default function ActivitiesSection() { refetchInterval: (q) => (hasPendingOrLinger(q.state.data) ? 7000 : false), }) + const [search, setSearch] = useState('') + const rows = useMemo(() => { return pendingState?.pendingOperations ?? [] }, [pendingState]) - const count = pendingCount(pendingState) - - // Render nothing when no pending and no recently-settled rows. - if (rows.length === 0) return null + // Client-side filter across supplier/owner addresses and provider name — + // mirrors the Suppliers tab search, scoped to the fields this strip shows. + const filteredRows = useMemo(() => { + const q = search.trim().toLowerCase() + if (!q) return rows + return rows.filter((row) => + [row.operatorAddress, row.ownerAddress, row.providerName] + .some((field) => field?.toLowerCase().includes(q)), + ) + }, [rows, search]) return (
- {/* Section heading — matches RecentChanges / Services Overview style. - Badge shows PENDING count only; settled-linger rows don't inflate it. */} -

- In progress - {count > 0 && ( - · {count} - )} -

- {/* Hand-built using the shared Table primitives (the same ones DataTable renders internally) + DataTable's exact header-cell classes. DataTable's own toolbar + pagination chrome would still render with no props, so we reuse the Table primitives directly for a compact, chrome-free strip that is visually identical to our tables. ~5 rows then internal scroll. */} -
Name Identity
- + {rows.length > 0 && ( + setSearch(e.target.value)} + className="max-w-xs" + /> + )} +
+ {/* Lock header to the top of the scroll box; opaque root bg so rows + don't bleed through, plus a bottom divider that stays put. */} + {/* Column order: Tx Hash · Submitted · Supplier · Owner · Provider · Amount · Op Funds · Status */} Tx Hash @@ -103,7 +108,17 @@ export default function ActivitiesSection() { - {rows.map((row) => { + {filteredRows.length === 0 && ( + + + {search.trim() ? 'No matches.' : 'No activity yet.'} + + + )} + {filteredRows.map((row) => { const statusLabel = getStatusLabel(row) const statusClass = getStatusClass(row) const submittedStr = row.createdAt diff --git a/apps/middleman/src/app/app/(lists)/suppliers/ChainOverview.tsx b/apps/middleman/src/app/app/(lists)/suppliers/ChainOverview.tsx index 6f31817d..082477ac 100644 --- a/apps/middleman/src/app/app/(lists)/suppliers/ChainOverview.tsx +++ b/apps/middleman/src/app/app/(lists)/suppliers/ChainOverview.tsx @@ -151,8 +151,6 @@ export default function ChainOverview() { const handleExport = useCallback(() => exportChainOverviewCsv(sortedRows), [sortedRows]) - if (!isLoading && !isError && !allRows.length) return null - const cardClasses = 'rounded-lg border border-[color:--divider] bg-[color:--main-background] base-shadow p-4' if (isLoading) { diff --git a/apps/middleman/src/app/app/(lists)/suppliers/SuppliersTabs.tsx b/apps/middleman/src/app/app/(lists)/suppliers/SuppliersTabs.tsx new file mode 100644 index 00000000..6fcfde9b --- /dev/null +++ b/apps/middleman/src/app/app/(lists)/suppliers/SuppliersTabs.tsx @@ -0,0 +1,68 @@ +'use client' + +import * as React from 'react' +import { usePathname, useRouter, useSearchParams } from 'next/navigation' +import { useQuery } from '@tanstack/react-query' + +import { Tabs, TabsContent, TabsList, TabsTrigger, TabsBadge } from '@igniter/ui/components/tabs' + +import { GetPendingState } from '@/actions/Pending' +import NodesTable from '@/app/app/(lists)/suppliers/table' +import ActivitiesSection from '@/app/app/(lists)/suppliers/ActivitiesSection' +import ChainOverview from '@/app/app/(lists)/suppliers/ChainOverview' + +const TABS = ['suppliers', 'activity', 'overview'] as const +type TabValue = (typeof TABS)[number] + +// The three data tables that used to stack on the Suppliers screen now live one +// per tab. Selection is persisted in the URL (?tab=) so refreshes / deep-links +// land on the same table — same pattern as WorkflowsTabs. +export default function SuppliersTabs() { + const router = useRouter() + const pathname = usePathname() + const searchParams = useSearchParams() + + const param = searchParams.get('tab') + const tab: TabValue = (TABS as readonly string[]).includes(param ?? '') + ? (param as TabValue) + : 'suppliers' + + // Lifted above the tab boundary so the pending count stays live regardless of + // the active tab — ActivitiesSection (its own consumer of this same queryKey) + // only mounts on the Activity tab, so the badge needs its own always-mounted + // read. Same queryKey → react-query dedups to a single poll + shared cache. + const { data: pendingState } = useQuery({ + queryKey: ['pendingState'], + queryFn: GetPendingState, + refetchInterval: (q) => ((q.state.data?.pendingOperations?.length ?? 0) > 0 ? 7000 : false), + }) + const pendingCount = Object.keys(pendingState?.byOperator ?? {}).length + + const onTabChange = (next: string) => { + const params = new URLSearchParams(searchParams.toString()) + params.set('tab', next) + router.replace(`${pathname}?${params.toString()}`, { scroll: false }) + } + + return ( + + + Suppliers + + Activity + + + Overview + + + + + + + + + + + + ) +} \ No newline at end of file diff --git a/apps/middleman/src/app/app/(lists)/suppliers/page.tsx b/apps/middleman/src/app/app/(lists)/suppliers/page.tsx index 59d4b66b..03ec142c 100644 --- a/apps/middleman/src/app/app/(lists)/suppliers/page.tsx +++ b/apps/middleman/src/app/app/(lists)/suppliers/page.tsx @@ -1,10 +1,8 @@ import type { Metadata } from 'next' import React, { Suspense } from 'react' -import NodesTable from '@/app/app/(lists)/suppliers/table' import ProviderStats from '@/app/app/(lists)/suppliers/ProviderStats' -import ChainOverview from '@/app/app/(lists)/suppliers/ChainOverview' import RecentChanges from '@/app/app/(lists)/suppliers/RecentChanges' -import ActivitiesSection from '@/app/app/(lists)/suppliers/ActivitiesSection' +import SuppliersTabs from '@/app/app/(lists)/suppliers/SuppliersTabs' import { GetAppName } from '@/actions/ApplicationSettings' import Link from 'next/link' import { Button } from '@igniter/ui/components/button' @@ -42,12 +40,12 @@ export default async function Page() { /> - - - + + + ); diff --git a/apps/middleman/src/app/app/(lists)/transactions/table/columns.tsx b/apps/middleman/src/app/app/(lists)/transactions/table/columns.tsx index c13a3a32..d9a15e0f 100644 --- a/apps/middleman/src/app/app/(lists)/transactions/table/columns.tsx +++ b/apps/middleman/src/app/app/(lists)/transactions/table/columns.tsx @@ -7,6 +7,8 @@ import {ActivitySuccessIcon, ActivityWarningIcon, RightArrowIcon} from '@igniter import { Button } from '@igniter/ui/components/button' import { FilterGroup, SortOption } from '@igniter/ui/components/DataTable/index' import { amountToPokt } from '@igniter/ui/lib/utils' +import { failureReasonDisplay } from '@igniter/commons/utils' +import { FailureReasonPopover } from '@igniter/ui/components/FailureReasonPopover' import { useAddItemToDetail } from '@igniter/ui/components/QuickDetails/Provider' import Amount from '@igniter/ui/components/Amount' import TransactionHash from '@igniter/ui/components/TransactionHash' @@ -27,6 +29,8 @@ export type Transaction = { provider: string, providerFee?: number | null, typeProviderFee?: ProviderFee | null, + log?: string | null, + code?: number | null, }; export const columns: (ColumnDef & CsvColumnDef)[] = [ @@ -67,6 +71,23 @@ export const columns: (ColumnDef & CsvColumnDef)[] = [ }, csvFormatterFn: ({status}) => status.charAt(0).toUpperCase() + status.slice(1), }, + { + id: "failureReason", + header: "Failure Reason", + cell: ({ row }) => { + const { status, log, code } = row.original; + const text = failureReasonDisplay(status === TransactionStatus.Failure, log, code); + if (text === null) { + return -; + } + // Friendly text in the cell; click to open the full raw log (copyable) inline. + return ; + }, + // CSV keeps the RAW chain log (more detail for debugging/support exports); + // the table cell shows the friendly mapped text. Intentionally no `code` arg. + csvFormatterFn: (item) => + failureReasonDisplay(item.status === TransactionStatus.Failure, item.log) ?? '', + }, { accessorKey: "hash", header: "Tx Hash", @@ -140,6 +161,8 @@ export const columns: (ColumnDef & CsvColumnDef)[] = [ provider: row.original.provider, providerFee: row.original.providerFee, typeProviderFee: row.original.typeProviderFee, + log: row.original.log, + code: row.original.code, } }) }} diff --git a/apps/middleman/src/app/app/(lists)/transactions/table/index.tsx b/apps/middleman/src/app/app/(lists)/transactions/table/index.tsx index 2d6f9645..5c1ebeea 100644 --- a/apps/middleman/src/app/app/(lists)/transactions/table/index.tsx +++ b/apps/middleman/src/app/app/(lists)/transactions/table/index.tsx @@ -64,7 +64,9 @@ export default function TransactionsTable() { consumedFee: newTx.consumedFee, provider: newTx.provider?.name || '', providerFee: newTx.providerFee, - typeProviderFee: newTx.typeProviderFee + typeProviderFee: newTx.typeProviderFee, + log: newTx.log, + code: newTx.code, } }, index) } @@ -115,6 +117,8 @@ export default function TransactionsTable() { provider: tx.provider?.name || 'Height Pending', providerFee: tx.providerFee, typeProviderFee: tx.typeProviderFee, + log: tx.log, + code: tx.code, } }) || [] } diff --git a/apps/middleman/src/app/app/import-suppliers/clientPage.tsx b/apps/middleman/src/app/app/import-suppliers/clientPage.tsx index 1ac92779..0c555b0c 100644 --- a/apps/middleman/src/app/app/import-suppliers/clientPage.tsx +++ b/apps/middleman/src/app/app/import-suppliers/clientPage.tsx @@ -18,6 +18,9 @@ import { } from '@/app/app/import-suppliers/types' import { ImportedSupplier } from '@/lib/services/importSuppliers' import { CancelPendingImportAttempts } from '@/actions/ImportSuppliers' +import { getLogger } from '@igniter/logger' + +const log = getLogger(['middleman', 'import-suppliers-page']) export default function ClientImportSuppliersPage() { const { connectedIdentity, connectedIdentities, isConnected } = @@ -123,7 +126,7 @@ export default function ClientImportSuppliersPage() { setIsAborting(false) await router.push('/app') } catch (error) { - console.error(error) + log.error('failed to abort import', { error }) addNotification({ id: `abort-import-error`, type: 'error', diff --git a/apps/middleman/src/app/app/import-suppliers/components/ImportProcess.tsx b/apps/middleman/src/app/app/import-suppliers/components/ImportProcess.tsx index 6d5d967d..3d955bdd 100644 --- a/apps/middleman/src/app/app/import-suppliers/components/ImportProcess.tsx +++ b/apps/middleman/src/app/app/import-suppliers/components/ImportProcess.tsx @@ -35,6 +35,9 @@ import { import AvatarByString from '@igniter/ui/components/AvatarByString' import { getShortAddress } from '@igniter/ui/lib/utils' import { fromBase64, toHex } from '@cosmjs/encoding' +import { getLogger } from '@igniter/logger' + +const log = getLogger(['middleman', 'import-process']) interface ImportProcessProps { provider: ProviderOption @@ -139,7 +142,7 @@ export function ImportProcess({ setCurrentStep(ImportProcessStep.SignNonce) } catch (err) { const { message } = err as Error - console.error('Failed to request import:', message) + log.error('failed to request import', { provider: provider.identity, ownerAddress, error: message }) // Set user-friendly error message let userFriendlyError: string @@ -197,7 +200,7 @@ export function ImportProcess({ setCurrentStep(ImportProcessStep.SubmitImport) } catch (err) { const { message } = err as Error - console.error('Failed to sign nonce:', message) + log.error('failed to sign nonce', { ownerAddress, error: message }) const userFriendlyError = message setProcessError(userFriendlyError) @@ -259,7 +262,7 @@ export function ImportProcess({ setCurrentStep(ImportProcessStep.Completed) } catch (err) { const { message } = err as Error - console.error('Failed to submit import:', message) + log.error('failed to submit import', { attemptId, provider: provider.identity, error: message }) const userFriendlyError = message setProcessError(userFriendlyError) diff --git a/apps/middleman/src/app/app/import-suppliers/components/SelectProviderStep.tsx b/apps/middleman/src/app/app/import-suppliers/components/SelectProviderStep.tsx index b05541ab..4acbc79f 100644 --- a/apps/middleman/src/app/app/import-suppliers/components/SelectProviderStep.tsx +++ b/apps/middleman/src/app/app/import-suppliers/components/SelectProviderStep.tsx @@ -7,6 +7,9 @@ import { Checkbox } from '@igniter/ui/components/checkbox' import { ListProviders } from '@/actions/Providers' import { ProviderOption } from '@/app/app/import-suppliers/types' import { ProviderStatus } from '@igniter/db/middleman/enums' +import { getLogger } from '@igniter/logger' + +const log = getLogger(['middleman', 'select-provider-step']) interface SelectProviderStepProps { ownerAddress: string @@ -52,7 +55,7 @@ export function SelectProviderStep({ setProviders(enabledProviders) setError(null) } catch (err) { - console.error('Failed to fetch providers:', err) + log.error('failed to fetch providers', { error: err }) setError('Failed to load providers. Please try again.') } finally { setLoading(false) diff --git a/apps/middleman/src/app/app/layout.tsx b/apps/middleman/src/app/app/layout.tsx index 6497bd29..532284ab 100644 --- a/apps/middleman/src/app/app/layout.tsx +++ b/apps/middleman/src/app/app/layout.tsx @@ -11,6 +11,9 @@ import { import { useNotifications } from '@igniter/ui/context/Notifications/index' import { Button } from '@igniter/ui/components/button' import { EVENT_LABELS, SUPPLIER_TYPES, describeEvent } from '@/lib/notificationEvents' +import { getLogger } from '@igniter/logger' + +const log = getLogger(['middleman', 'app-layout']) // Single source of truth for the in-app header feed: the per-wallet // notification_events store. Every event the user is subscribed to (supplier @@ -79,7 +82,7 @@ export default function AppLayout({ children }: { children: React.ReactNode }) { } catch (err) { // Marking viewed failed — drop from the seen-set so the next poll re-surfaces it. notifiedRef.current.delete(ev.id) - console.error('Failed to mark notification event viewed', err) + log.error('failed to mark notification event viewed', { eventId: ev.id, error: err }) } }, actions: isSupplier diff --git a/apps/middleman/src/app/app/notifications/NotificationHistorySection.tsx b/apps/middleman/src/app/app/notifications/NotificationHistorySection.tsx index d0149124..e7483b91 100644 --- a/apps/middleman/src/app/app/notifications/NotificationHistorySection.tsx +++ b/apps/middleman/src/app/app/notifications/NotificationHistorySection.tsx @@ -10,6 +10,17 @@ import { MarkAllNotificationEventsViewed, } from '@/actions/NotificationChannels' import { EVENT_LABELS, describeEvent } from '@/lib/notificationEvents' +import { NOTIFICATION_EVENT_TYPES, NotificationChannelType } from '@igniter/db/middleman/enums' + +// Event-type + channel dropdown options for the history filters. +const EVENT_TYPE_OPTIONS = NOTIFICATION_EVENT_TYPES.map((t) => ({ + value: t, + label: EVENT_LABELS[t] ?? t, +})) +const CHANNEL_OPTIONS = Object.values(NotificationChannelType).map((t) => ({ + value: t, + label: t.charAt(0).toUpperCase() + t.slice(1), +})) // Middleman wrapper around the shared history table: wallet-scoped actions + // middleman's event vocabulary. No detail drawer (provider-only), so a row click @@ -27,13 +38,15 @@ export function NotificationHistorySection() { return ( EVENT_LABELS[type] ?? 'Notification'} summaryFor={(type, metadata) => describeEvent(type, (metadata ?? {}) as Record) } - listEvents={async (page, pageSize) => { - const result = await ListNotificationEvents(page, pageSize) + listEvents={async (page, pageSize, filters) => { + const result = await ListNotificationEvents(page, pageSize, filters) if (!result.success) throw new Error(result.error.message) return result.data }} diff --git a/apps/middleman/src/app/app/overview/ProviderBreakdown.tsx b/apps/middleman/src/app/app/overview/ProviderBreakdown.tsx index ed0d1fca..1fe60b8a 100644 --- a/apps/middleman/src/app/app/overview/ProviderBreakdown.tsx +++ b/apps/middleman/src/app/app/overview/ProviderBreakdown.tsx @@ -157,9 +157,12 @@ export default function ProviderBreakdown({ providerCount }: { providerCount: nu
{/* Table card */}
+ {/* Bounded scroll box: invisible until the table outgrows it, then the + header below stays pinned. */} +
- - + + @@ -200,6 +203,7 @@ export default function ProviderBreakdown({ providerCount }: { providerCount: nu ))}
handleSort('name')}> Provider{sortIndicator('name')}
+
{/* Pie charts */} diff --git a/apps/middleman/src/app/app/stake/clientPage.tsx b/apps/middleman/src/app/app/stake/clientPage.tsx index 69731c55..d6487d1b 100644 --- a/apps/middleman/src/app/app/stake/clientPage.tsx +++ b/apps/middleman/src/app/app/stake/clientPage.tsx @@ -18,6 +18,9 @@ import {SupplierStake} from "@/lib/models/Transactions"; import {releaseSuppliers} from "@/lib/services/provider"; import {useNotifications} from "@igniter/ui/context/Notifications/index"; import OverrideSidebar from '@igniter/ui/components/OverrideSidebar' +import { getLogger } from '@igniter/logger' + +const log = getLogger(['middleman', 'stake-page']) enum StakeActivitySteps { @@ -113,7 +116,7 @@ export default function ClientStakePage() { setIsAborting(false); await router.push('/app'); } catch (error) { - console.error(error); + log.error('failed to abort staking process', { error }); addNotification({ id: `abort-stake-error`, type: 'error', diff --git a/apps/middleman/src/app/app/stake/components/PickOfferStep/index.tsx b/apps/middleman/src/app/app/stake/components/PickOfferStep/index.tsx index 934c1ca2..20550295 100644 --- a/apps/middleman/src/app/app/stake/components/PickOfferStep/index.tsx +++ b/apps/middleman/src/app/app/stake/components/PickOfferStep/index.tsx @@ -11,6 +11,9 @@ import {CalculateStakeDistribution} from "@/actions/Stake"; import {ActivityContentLoading} from "@/app/app/stake/components/ActivityContentLoading"; import {getApplicationSettings} from "@/actions/ApplicationSettings"; import {ProviderStatus} from "@igniter/db/middleman/enums"; +import {getLogger} from "@igniter/logger"; + +const log = getLogger(['middleman', 'pick-offer-step']) export interface PickOfferStepProps { amount: number; @@ -139,8 +142,7 @@ export function PickOfferStep({onOfferSelected, amount, ownerAddress, onBack, de } } } catch (error) { - console.warn('An error occurred while calculating the stake distribution!'); - console.error(error); + log.error('failed to calculate stake distribution', { error }); } finally { setIsLoadingOffers(false); } diff --git a/apps/middleman/src/app/app/stake/components/PickStakeAmountStep/index.tsx b/apps/middleman/src/app/app/stake/components/PickStakeAmountStep/index.tsx index 16d8ef04..395d7723 100644 --- a/apps/middleman/src/app/app/stake/components/PickStakeAmountStep/index.tsx +++ b/apps/middleman/src/app/app/stake/components/PickStakeAmountStep/index.tsx @@ -10,6 +10,9 @@ import {useApplicationSettings} from "@/app/context/ApplicationSettings"; import {ActivityContentLoading} from "@/app/app/stake/components/ActivityContentLoading"; import {QuickInfoPopOverIcon} from "@igniter/ui/components/QuickInfoPopOverIcon"; import { toCurrencyFormat } from '@igniter/ui/lib/utils' +import { getLogger } from '@igniter/logger' + +const log = getLogger(['middleman', 'pick-stake-amount-step']) export interface PickStakeAmountStepProps { defaultAmount: number; @@ -37,8 +40,8 @@ export function PickStakeAmountStep({onAmountSelected, defaultAmount, ownerAddre try { const balance = await getBalance(ownerAddress); setBalance(balance); - } catch { - console.log('An error occurred while getting the balance from your connected wallet.'); + } catch (error) { + log.warn('failed to get balance from connected wallet', { ownerAddress, error }); } })(); }, [ownerAddress]); diff --git a/apps/middleman/src/app/app/stake/components/ReviewStep/StakingProcess.tsx b/apps/middleman/src/app/app/stake/components/ReviewStep/StakingProcess.tsx index f4bcec62..c5f15b0c 100644 --- a/apps/middleman/src/app/app/stake/components/ReviewStep/StakingProcess.tsx +++ b/apps/middleman/src/app/app/stake/components/ReviewStep/StakingProcess.tsx @@ -22,6 +22,9 @@ import {StageStatus} from "@/app/app/stake/types"; import {stageFailed, stageSucceeded} from "@/app/app/stake/utils"; import {useNotifications} from "@igniter/ui/context/Notifications/index"; import {useQueryClient} from "@tanstack/react-query"; +import {getLogger} from "@igniter/logger"; + +const log = getLogger(['middleman', 'staking-process']) export interface StakingProcessStatus { requestSuppliersStatus: StageStatus; @@ -108,7 +111,7 @@ export function StakingProcess({offer, onStakeCompleted, ownerAddress, region, o setCurrentStep(StakingProcessStep.transactionSignature); } catch (err) { const { message } = err as Error; - console.log('An error occurred while retrieving the supplier stake info. Error:', message); + log.error('failed to retrieve supplier stake info', { ownerAddress, error: message }); handleFailedStage('requestSuppliersStatus', 'An error occurred while retrieving the supplier stake info. You have incurred no fees. You can try again. If the problem persists, please contact support.'); } })(); @@ -136,7 +139,7 @@ export function StakingProcess({offer, onStakeCompleted, ownerAddress, region, o setCurrentStep(StakingProcessStep.SchedulingTransaction); } catch (err) { const { message } = err as Error; - console.log('An error occurred while collecting the signature.Error:', message); + log.error('failed to collect signature', { ownerAddress, error: message }); handleFailedStage('transactionSignatureStatus', 'An unknown error occurred while collecting your signature for the transaction. If it was intentionally rejected, this is required in order to proceed. If not, please make sure you have a supported wallet extension enabled. You have incurred no fees. You can try again. If the problem persists, please contact support.'); } })(); @@ -167,7 +170,7 @@ export function StakingProcess({offer, onStakeCompleted, ownerAddress, region, o setCurrentStep(StakingProcessStep.Completed); } catch (err) { const { message } = err as Error; - console.log('An error occurred while scheduling the signed transactions. Error:', message); + log.error('failed to schedule signed transaction', { ownerAddress, error: message }); handleFailedStage('schedulingTransactionStatus', 'An unknown error occurred while scheduling the signed transactions.'); } })(); diff --git a/apps/middleman/src/app/app/unstake/components/ReviewStep/UnstakingProcess.tsx b/apps/middleman/src/app/app/unstake/components/ReviewStep/UnstakingProcess.tsx index 724f339d..97b96a08 100644 --- a/apps/middleman/src/app/app/unstake/components/ReviewStep/UnstakingProcess.tsx +++ b/apps/middleman/src/app/app/unstake/components/ReviewStep/UnstakingProcess.tsx @@ -19,6 +19,9 @@ import { stageFailed, stageSucceeded } from "@/app/app/unstake/utils"; import { useNotifications } from "@igniter/ui/context/Notifications/index"; import { useQueryClient } from "@tanstack/react-query"; import { CreateUnstakeTransaction } from '@/actions/Unstake' +import { getLogger } from '@igniter/logger' + +const log = getLogger(['middleman', 'unstaking-process']) export interface UnstakingProcessStatus { transactionSignatureStatus: StageStatus; @@ -87,7 +90,7 @@ export function UnstakingProcess({ setCurrentStep(UnstakingProcessStep.SchedulingTransaction); } catch (err) { const { message } = err as Error; - console.log('An error occurred while collecting the signature. Error:', message); + log.error('failed to collect signature', { ownerAddress, error: message }); handleFailedStage( 'transactionSignatureStatus', 'An unknown error occurred while collecting your signature for the transaction. If it was intentionally rejected, this is required in order to proceed. If not, please make sure you have a supported wallet extension enabled. You have incurred no fees. You can try again. If the problem persists, please contact support.' @@ -120,7 +123,7 @@ export function UnstakingProcess({ setCurrentStep(UnstakingProcessStep.Completed); } catch (err) { const { message } = err as Error; - console.log('An error occurred while scheduling the signed transactions. Error:', message); + log.error('failed to schedule signed transaction', { ownerAddress, selectedNodeAddresses, error: message }); handleFailedStage( 'schedulingTransactionStatus', 'An unknown error occurred while scheduling the signed transactions.' diff --git a/apps/middleman/src/app/components/Sidebar.tsx b/apps/middleman/src/app/components/Sidebar.tsx index 61a78cf3..e6ce2827 100644 --- a/apps/middleman/src/app/components/Sidebar.tsx +++ b/apps/middleman/src/app/components/Sidebar.tsx @@ -6,6 +6,7 @@ import { } from "@igniter/ui/components/sidebar"; import Link from "next/link"; import { usePathname } from "next/navigation"; +import { isInternalPath } from "@igniter/commons/utils"; import OverviewDark from "@/app/assets/icons/dark/overview.svg"; import ActivityDark from "@/app/assets/icons/dark/activity.svg"; import NodesDark from "@/app/assets/icons/dark/nodes.svg"; @@ -80,6 +81,12 @@ export const dynamic = "force-dynamic"; export default function Sidebar({}: Readonly) { const pathname = usePathname(); + // Sidebar chrome belongs only to the authenticated app/admin areas. On the + // portal (landing) and auth pages the whole rail is hidden — returning null + // drops both the fixed rail and its layout spacer so content is full width. + // Same gate as SidebarTriggerGate via the shared isInternalPath helper. + if (!isInternalPath(pathname)) return null; + const routes = pathname.startsWith("/admin") ? adminRoutes : mainRoutes; const MainRoutesMenu = routes.map((route) => ( @@ -91,7 +98,7 @@ export default function Sidebar({}: Readonly) { : "text-text-secondary" } > - + {route.title} diff --git a/apps/middleman/src/app/components/SidebarTriggerGate.tsx b/apps/middleman/src/app/components/SidebarTriggerGate.tsx new file mode 100644 index 00000000..0f29b293 --- /dev/null +++ b/apps/middleman/src/app/components/SidebarTriggerGate.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { usePathname } from "next/navigation"; +import { isInternalPath } from "@igniter/commons/utils"; +import { SidebarTrigger } from "@igniter/ui/components/sidebar"; + +// The sidebar toggle only makes sense where the sidebar exists: the +// authenticated app/admin areas. On the portal (landing) and auth pages there +// is no rail, so the trigger is hidden — same gate as Sidebar.tsx via the +// shared isInternalPath helper. +export default function SidebarTriggerGate() { + const pathname = usePathname(); + if (!isInternalPath(pathname)) return null; + + return ; +} \ No newline at end of file diff --git a/apps/middleman/src/app/context/ApplicationSettings/index.tsx b/apps/middleman/src/app/context/ApplicationSettings/index.tsx index e13614ae..88297d2f 100644 --- a/apps/middleman/src/app/context/ApplicationSettings/index.tsx +++ b/apps/middleman/src/app/context/ApplicationSettings/index.tsx @@ -9,6 +9,9 @@ import { } from "react"; import { getApplicationSettings } from "@/actions/ApplicationSettings"; import {ApplicationSettings} from "@igniter/db/middleman/schema"; +import {getLogger} from "@igniter/logger"; + +const log = getLogger(['middleman', 'application-settings-context']) const ApplicationSettingsContext = createContext(undefined); @@ -26,7 +29,7 @@ export const ApplicationSettingsProvider = ({ const settings = await getApplicationSettings(); setApplicationSettings(settings); } catch (error) { - console.error(error); + log.error("failed to load application settings", { error }); } })(); }, []); diff --git a/apps/middleman/src/app/detail/TransactionDetail.tsx b/apps/middleman/src/app/detail/TransactionDetail.tsx index 2359a1e9..9fd23ecc 100644 --- a/apps/middleman/src/app/detail/TransactionDetail.tsx +++ b/apps/middleman/src/app/detail/TransactionDetail.tsx @@ -15,6 +15,7 @@ import { BaseQuickInfoTooltip } from '@igniter/ui/components/BaseQuickInfoToolti import Address from '@igniter/ui/components/Address' import { useAddItemToDetail } from '@igniter/ui/components/QuickDetails/Provider' import { MessageType } from '@igniter/commons/constants' +import { failureReasonDisplay } from '@igniter/commons/utils' import { GetNode } from '@/actions/Nodes' import { TransactionsToNodesWithDetails, @@ -83,6 +84,8 @@ export interface TransactionDetailBody { provider: string providerFee?: number | null typeProviderFee?: ProviderFee | null + log?: string | null + code?: number | null } export interface TransactionDetail { @@ -301,10 +304,15 @@ export default function TransactionDetail({ provider, providerFee, typeProviderFee, + log, + code, }: TransactionDetailBody) { const addItemToDetail = useAddItemToDetail() const [isShowingTransactionDetails, setIsShowingTransactionDetails] = useState(false); + const isFailure = status === TransactionStatus.Failure + const failureReason = failureReasonDisplay(isFailure, log, code) + let onClickAddress: ((address: string) => void) | undefined = undefined if (status === TransactionStatus.Success) { @@ -333,6 +341,8 @@ export default function TransactionDetail({ providerFee: tx.providerFee, typeProviderFee: tx.typeProviderFee, operations: JSON.parse(tx.unsignedPayload).body.messages, + log: tx.log, + code: tx.code, } }), provider: node.provider || null, @@ -354,11 +364,10 @@ export default function TransactionDetail({ label: 'Status', value: (
- {status === TransactionStatus.Failure && ( + {isFailure && failureReason && ( + + + +
); } diff --git a/packages/ui/src/components/workflows/WorkflowDetailClient.tsx b/packages/ui/src/components/workflows/WorkflowDetailClient.tsx index 2af25836..e61df469 100644 --- a/packages/ui/src/components/workflows/WorkflowDetailClient.tsx +++ b/packages/ui/src/components/workflows/WorkflowDetailClient.tsx @@ -276,8 +276,8 @@ export function WorkflowDetailClient({

Child workflows ({detail.children.length})

- - +
+ Workflow ID Type @@ -456,8 +456,8 @@ function ActivitiesTable({ return

No activities recorded.

} return ( -
- +
+ # diff --git a/packages/ui/src/components/workflows/types.ts b/packages/ui/src/components/workflows/types.ts index b65200eb..b301c98a 100644 --- a/packages/ui/src/components/workflows/types.ts +++ b/packages/ui/src/components/workflows/types.ts @@ -22,6 +22,14 @@ export interface WorkflowsActions { TerminateWorkflow: (workflowId: string, runId?: string) => Promise> GetWorkflowDetail: (workflowId: string, runId?: string) => Promise> GetWorkflowHistoryJson: (workflowId: string, runId?: string) => Promise> + + /** + * Optional schedule controls. Buttons render only when the app injects them, + * so an app build that predates these actions keeps a read-only tab. + */ + PauseSchedule?: (scheduleId: string, note?: string) => Promise> + ResumeSchedule?: (scheduleId: string) => Promise> + RecreateSchedule?: (scheduleId: string) => Promise> } /** diff --git a/packages/ui/src/context/Height/InitializeContext.tsx b/packages/ui/src/context/Height/InitializeContext.tsx index a8984eee..1bcb2d17 100644 --- a/packages/ui/src/context/Height/InitializeContext.tsx +++ b/packages/ui/src/context/Height/InitializeContext.tsx @@ -1,6 +1,9 @@ import React from 'react' import HeightContextProvider from './height' import { getStatusQuery } from '../../api/blocks' +import { getLogger } from '@igniter/logger' + +const log = getLogger(['ui', 'height-context']) interface InitializeHeightContextProps { graphQlUrl: string @@ -16,7 +19,7 @@ export default async function InitializeHeightContext({ try { data = await getStatusQuery(graphQlUrl) } catch (e) { - console.error(e) + log.error('Failed to fetch height status', { error: e }) } return ( diff --git a/packages/ui/src/context/WalletConnection/PocketMorseWalletConnection.ts b/packages/ui/src/context/WalletConnection/PocketMorseWalletConnection.ts index 0102251c..1b59c806 100644 --- a/packages/ui/src/context/WalletConnection/PocketMorseWalletConnection.ts +++ b/packages/ui/src/context/WalletConnection/PocketMorseWalletConnection.ts @@ -2,6 +2,9 @@ import {ProviderInfo, TransactionMessage, WalletConnection} from "@igniter/ui/co import { Provider, SignedTransaction, } from "./"; +import { getLogger } from "@igniter/logger"; + +const log = getLogger(["ui", "wallet-connection", "pocket-morse"]); export enum PocketMethod { REQUEST_ACCOUNTS = "pokt_requestAccounts", @@ -39,7 +42,7 @@ export class PocketMorseWalletConnection implements WalletConnection { this.isConnected = true; this.connectedIdentity = connectedIdentity; } catch (err) { - console.error(err); + log.error("Failed to connect to Pocket Network wallet", { error: err }); throw err; } }; @@ -56,7 +59,7 @@ export class PocketMorseWalletConnection implements WalletConnection { return false; } catch (error) { - console.warn(`Something failed while interacting with the Pocket Network wallet provider. method: ${PocketMethod.ACCOUNTS}`); + log.warn("Failed to reconnect to Pocket Network wallet provider", { method: PocketMethod.ACCOUNTS, error }); return false; } } @@ -67,7 +70,7 @@ export class PocketMorseWalletConnection implements WalletConnection { PocketMethod.CHAIN, ); } catch (err) { - console.error(err); + log.error("Failed to get chain from Pocket Network wallet", { error: err }); throw err; } } @@ -80,7 +83,7 @@ export class PocketMorseWalletConnection implements WalletConnection { ); return publicKey; } catch (err) { - console.error(err); + log.error("Failed to get public key from Pocket Network wallet", { address, error: err }); throw err; } } @@ -93,7 +96,7 @@ export class PocketMorseWalletConnection implements WalletConnection { ); return balance; } catch (err) { - console.error(err); + log.error("Failed to get balance from Pocket Network wallet", { address, error: err }); throw err; } } @@ -105,7 +108,7 @@ export class PocketMorseWalletConnection implements WalletConnection { [{ chainId }], ); } catch (err) { - console.error(err); + log.error("Failed to switch chain in Pocket Network wallet", { chainId, error: err }); throw err; } } @@ -115,7 +118,7 @@ export class PocketMorseWalletConnection implements WalletConnection { const { signature } = await this.provider.send(PocketMethod.SIGN_MESSAGE, [{ message, address }]); return signature; } catch (err) { - console.error(err); + log.error("Failed to sign message with Pocket Network wallet", { address, error: err }); throw err; } } @@ -146,8 +149,7 @@ export class PocketMorseWalletConnection implements WalletConnection { }; signTransaction = async (transaction: TransactionMessage[]): Promise => { - console.warn( - 'Method not implemented: signTransaction. Something is wrong with the wallet connection provider.'); + log.warn("Method not implemented: signTransaction. Something is wrong with the wallet connection provider."); return { address: '', diff --git a/packages/ui/src/context/WalletConnection/PocketWalletConnection.ts b/packages/ui/src/context/WalletConnection/PocketWalletConnection.ts index 42f8ce3d..7fd518f5 100644 --- a/packages/ui/src/context/WalletConnection/PocketWalletConnection.ts +++ b/packages/ui/src/context/WalletConnection/PocketWalletConnection.ts @@ -1,6 +1,9 @@ import type {Provider, ProviderInfo} from "./"; import { WalletConnection, WalletSettings } from './WalletConnection' import {SignedMemo, SignedTransaction, TransactionMessage} from "../../lib/models"; +import { getLogger } from "@igniter/logger"; + +const log = getLogger(["ui", "wallet-connection", "pocket"]); export enum PocketMethod { REQUEST_ACCOUNTS = "pokt_requestAccounts", @@ -45,7 +48,7 @@ export class PocketWalletConnection extends WalletConnection { return connectedIdentities } catch (err) { - console.error(err); + log.error("Failed to connect to Pocket Network wallet", { error: err }); throw err; } }; @@ -63,7 +66,7 @@ export class PocketWalletConnection extends WalletConnection { return false; } catch (error) { - console.warn(`Something failed while interacting with the Pocket Network wallet provider. method: ${PocketMethod.ACCOUNTS}`); + log.warn("Failed to reconnect to Pocket Network wallet provider", { method: PocketMethod.ACCOUNTS, error }); return false; } } @@ -86,7 +89,7 @@ export class PocketWalletConnection extends WalletConnection { PocketMethod.CHAIN, ).then((res) => res.chain); } catch (err) { - console.error(err); + log.error("Failed to get chain from Pocket Network wallet", { error: err }); throw err; } } @@ -99,7 +102,7 @@ export class PocketWalletConnection extends WalletConnection { ); return publicKey; } catch (err) { - console.error(err); + log.error("Failed to get public key from Pocket Network wallet", { address, error: err }); throw err; } } @@ -112,7 +115,7 @@ export class PocketWalletConnection extends WalletConnection { const data = await response.json() return ((data.balance?.amount || 0) / 1e6) } catch (err) { - console.error(err); + log.error("Failed to get balance from Pocket Network wallet", { address, error: err }); throw err; } } @@ -124,7 +127,7 @@ export class PocketWalletConnection extends WalletConnection { [{ chainId }], ); } catch (err) { - console.error(err); + log.error("Failed to switch chain in Pocket Network wallet", { chainId, error: err }); throw err; } } @@ -134,7 +137,7 @@ export class PocketWalletConnection extends WalletConnection { const { signature } = await this.provider.send(PocketMethod.SIGN_MESSAGE, [{ message, address }]); return signature; } catch (err) { - console.error(err); + log.error("Failed to sign message with Pocket Network wallet", { address, error: err }); throw err; } } @@ -200,7 +203,7 @@ export class PocketWalletConnection extends WalletConnection { estimatedFee: fee, }; } catch (err) { - console.error(err); + log.error("Failed to sign transaction with Pocket Network wallet", { address: addr, error: err }); throw err; } } diff --git a/packages/ui/src/context/WalletConnection/index.tsx b/packages/ui/src/context/WalletConnection/index.tsx index c9eab32f..2af161eb 100644 --- a/packages/ui/src/context/WalletConnection/index.tsx +++ b/packages/ui/src/context/WalletConnection/index.tsx @@ -7,6 +7,9 @@ import { PROVIDER_COOKIE_KEY } from './constants'; import { KeplrWalletConnection } from './KeplrWalletConnection'; import {PocketWalletConnection} from "./PocketWalletConnection"; import { setCookie } from '../../lib/cookies' +import { getLogger } from '@igniter/logger' + +const log = getLogger(['ui', 'wallet-connection']) const WALLET_TIMEOUT_MS = 15_000 @@ -66,47 +69,47 @@ export const WalletConnectionContext = createContext({ isConnected: false, expectedChainId: '', connect: async () => { - console.warn('Method not implemented: connect. Something is wrong with the wallet connection provider.'); + log.warn('Method not implemented: connect. Something is wrong with the wallet connection provider.'); return []; }, getChain: async () => { - console.warn('Method not implemented: getChain. Something is wrong with the wallet connection provider.'); + log.warn('Method not implemented: getChain. Something is wrong with the wallet connection provider.'); return ''; }, connectIdentity: (address: string) => { - console.warn('Method not implemented: connectedIdentity. Something is wrong with the wallet connection provider.'); + log.warn('Method not implemented: connectedIdentity. Something is wrong with the wallet connection provider.'); }, clearConnectedIdentity: () => { - console.warn('Method not implemented: clearConnectedIdentity. Something is wrong with the wallet connection provider.'); + log.warn('Method not implemented: clearConnectedIdentity. Something is wrong with the wallet connection provider.'); }, getPublicKey: async (address: string) => { - console.warn('Method not implemented: getPublicKey. Something is wrong with the wallet connection provider.'); + log.warn('Method not implemented: getPublicKey. Something is wrong with the wallet connection provider.'); return ''; }, getBalance: async (address: string) => { - console.warn('Method not implemented: getBalance. Something is wrong with the wallet connection provider.'); + log.warn('Method not implemented: getBalance. Something is wrong with the wallet connection provider.'); return 0; }, switchChain: async () => { - console.warn('Method not implemented: switchChain. Something is wrong with the wallet connection provider.'); + log.warn('Method not implemented: switchChain. Something is wrong with the wallet connection provider.'); }, signMessage: async () => { - console.warn('Method not implemented: signMessage. Something is wrong with the wallet connection provider.'); + log.warn('Method not implemented: signMessage. Something is wrong with the wallet connection provider.'); return ''; }, getAvailableProviders: async (): Promise => { - console.warn('Method not implemented: getProvidersInfo. Something is wrong with the wallet connection provider.'); + log.warn('Method not implemented: getProvidersInfo. Something is wrong with the wallet connection provider.'); return []; }, reconnect: async ( address: string, provider: string )=> { - console.warn('Method not implemented: reconnect. Something is wrong with the wallet connection provider.'); + log.warn('Method not implemented: reconnect. Something is wrong with the wallet connection provider.'); return false; }, signTransaction: async (messages: TransactionMessage[]) : Promise => { - console.warn('Method not implemented: signTransaction. Something is wrong with the wallet connection provider.'); + log.warn('Method not implemented: signTransaction. Something is wrong with the wallet connection provider.'); return { address: '', signedPayload: '', @@ -199,7 +202,7 @@ export const WalletConnectionProvider = ({ return connectedIdentities } catch (error) { - console.error(error); + log.error('Failed to connect wallet', { error }); throw error; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49b0d23b..89a313c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -316,7 +316,7 @@ importers: version: 4.0.3 jest: specifier: ^30.1.3 - version: 30.1.3(@types/node@25.8.0)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@25.8.0)(typescript@5.7.3)) + version: 30.1.3(@types/node@22.14.1)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@22.14.1)(typescript@5.7.3)) nodemailer: specifier: ^6.9.16 version: 6.10.1 @@ -328,7 +328,7 @@ importers: version: 4.1.5 ts-jest: specifier: ^29.4.4 - version: 29.4.4(@babel/core@7.29.0)(@jest/transform@30.1.2)(@jest/types@30.0.5)(babel-jest@30.1.2(@babel/core@7.29.0))(esbuild@0.25.9)(jest-util@30.0.5)(jest@30.1.3(@types/node@25.8.0)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@25.8.0)(typescript@5.7.3)))(typescript@5.7.3) + version: 29.4.4(@babel/core@7.29.0)(@jest/transform@30.1.2)(@jest/types@30.0.5)(babel-jest@30.1.2(@babel/core@7.29.0))(esbuild@0.25.9)(jest-util@30.0.5)(jest@30.1.3(@types/node@22.14.1)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@22.14.1)(typescript@5.7.3)))(typescript@5.7.3) tsc-alias: specifier: ^1.8.15 version: 1.8.15 @@ -573,7 +573,7 @@ importers: version: 0.31.4 jest: specifier: ^30.1.3 - version: 30.1.3(@types/node@25.8.0)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@25.8.0)(typescript@5.7.3)) + version: 30.1.3(@types/node@22.14.1)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@22.14.1)(typescript@5.7.3)) nodemon: specifier: ^3.1.9 version: 3.1.10 @@ -582,7 +582,7 @@ importers: version: 4.1.5 ts-jest: specifier: ^29.4.4 - version: 29.4.4(@babel/core@7.29.0)(@jest/transform@30.1.2)(@jest/types@30.0.5)(babel-jest@30.1.2(@babel/core@7.29.0))(esbuild@0.25.9)(jest-util@30.0.5)(jest@30.1.3(@types/node@25.8.0)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@25.8.0)(typescript@5.7.3)))(typescript@5.7.3) + version: 29.4.4(@babel/core@7.29.0)(@jest/transform@30.1.2)(@jest/types@30.0.5)(babel-jest@30.1.2(@babel/core@7.29.0))(esbuild@0.25.9)(jest-util@30.0.5)(jest@30.1.3(@types/node@22.14.1)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@22.14.1)(typescript@5.7.3)))(typescript@5.7.3) tsc-alias: specifier: ^1.8.15 version: 1.8.15 @@ -690,10 +690,10 @@ importers: version: 30.0.0 jest: specifier: ^30.1.3 - version: 30.1.3(@types/node@25.8.0)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@25.8.0)(typescript@5.7.3)) + version: 30.1.3(@types/node@22.14.1)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@22.14.1)(typescript@5.7.3)) ts-jest: specifier: ^29.4.4 - version: 29.4.4(@babel/core@7.29.0)(@jest/transform@30.1.2)(@jest/types@30.0.5)(babel-jest@30.1.2(@babel/core@7.29.0))(esbuild@0.25.9)(jest-util@30.0.5)(jest@30.1.3(@types/node@25.8.0)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@25.8.0)(typescript@5.7.3)))(typescript@5.7.3) + version: 29.4.4(@babel/core@7.29.0)(@jest/transform@30.1.2)(@jest/types@30.0.5)(babel-jest@30.1.2(@babel/core@7.29.0))(esbuild@0.25.9)(jest-util@30.0.5)(jest@30.1.3(@types/node@22.14.1)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@22.14.1)(typescript@5.7.3)))(typescript@5.7.3) tsc-alias: specifier: ^1.8.15 version: 1.8.15 @@ -764,12 +764,15 @@ importers: packages/logger: dependencies: - pino: - specifier: 9.7.0 - version: 9.7.0 - pino-pretty: - specifier: 13.0.0 - version: 13.0.0 + '@logtape/logtape': + specifier: 2.2.1 + version: 2.2.1 + '@logtape/pretty': + specifier: 2.2.1 + version: 2.2.1(@logtape/logtape@2.2.1) + '@logtape/redaction': + specifier: 2.2.1 + version: 2.2.1(@logtape/logtape@2.2.1) devDependencies: '@igniter/eslint-config': specifier: workspace:* @@ -777,6 +780,24 @@ importers: '@igniter/typescript-config': specifier: workspace:* version: link:../typescript-config + '@temporalio/common': + specifier: 1.11.7 + version: 1.11.7 + '@temporalio/worker': + specifier: 1.11.7 + version: 1.11.7(@swc/helpers@0.5.15)(esbuild@0.25.9) + '@types/jest': + specifier: ^30.0.0 + version: 30.0.0 + jest: + specifier: ^30.1.3 + version: 30.1.3(@types/node@22.14.1)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@22.14.1)(typescript@5.8.2)) + ts-jest: + specifier: ^29.4.4 + version: 29.4.4(@babel/core@7.29.0)(@jest/transform@30.1.2)(@jest/types@30.0.5)(babel-jest@30.1.2(@babel/core@7.29.0))(esbuild@0.25.9)(jest-util@30.0.5)(jest@30.1.3(@types/node@22.14.1)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@22.14.1)(typescript@5.8.2)))(typescript@5.8.2) + typescript: + specifier: 5.8.2 + version: 5.8.2 packages/notifications: dependencies: @@ -953,6 +974,9 @@ importers: '@igniter/graphql': specifier: workspace:* version: link:../graphql + '@igniter/logger': + specifier: workspace:* + version: link:../logger '@igniter/temporal': specifier: workspace:* version: link:../temporal @@ -3618,9 +3642,6 @@ packages: '@jridgewell/source-map@0.3.11': resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} - '@jridgewell/source-map@0.3.6': - resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} - '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} @@ -3660,6 +3681,19 @@ packages: '@kurkle/color@0.3.4': resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + '@logtape/logtape@2.2.1': + resolution: {integrity: sha512-SkRptJUEAbGuf+/blXDxDa8sSq8no+lxJlfwPTMzrHIULOMsNZRaqT2qF3H9tmGOsaLYc+uF3orRCQfoH0qKzA==} + + '@logtape/pretty@2.2.1': + resolution: {integrity: sha512-JWUFYP79t9MFDrcSsG6yYZ8M92b7PRcvUDV0FTZnTrycpk98UrzgFLJbWAFewYx5wEcbNRVnISsza46GksDDUw==} + peerDependencies: + '@logtape/logtape': ^2.2.1 + + '@logtape/redaction@2.2.1': + resolution: {integrity: sha512-SggSeehG/AJTtJPcRRhmoFJpi/lz/ElMSCO4MQdocGVg08z2uLaWZqlqSJm+AkDmvRdNCSqT6jqeFnDZFEF28A==} + peerDependencies: + '@logtape/logtape': ^2.2.1 + '@mui/private-theming@5.17.1': resolution: {integrity: sha512-XMxU0NTYcKqdsG8LRmSoxERPXwMbp16sIXPcLVgLGII/bVNagX0xaheWAwFv8+zDK7tI3ajllkuD3GZZE++ICQ==} engines: {node: '>=12.0.0'} @@ -5631,10 +5665,6 @@ packages: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} - atomic-sleep@1.0.0: - resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} - engines: {node: '>=8.0.0'} - attr-accept@2.2.5: resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==} engines: {node: '>=4'} @@ -6327,9 +6357,6 @@ packages: date-fns@4.1.0: resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} - dateformat@4.6.3: - resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} - debounce@1.2.1: resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} @@ -6652,9 +6679,6 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} - end-of-stream@1.4.4: - resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} @@ -6948,9 +6972,6 @@ packages: resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} engines: {node: '>=4'} - fast-copy@3.0.2: - resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -6968,13 +6989,6 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fast-redact@3.5.0: - resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} - engines: {node: '>=6'} - - fast-safe-stringify@2.1.1: - resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - fast-uri@3.0.6: resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} @@ -7347,9 +7361,6 @@ packages: resolution: {integrity: sha512-trFMIq3PATiFRiQmNNeHtsrkwYRByIXUbYNbotiY9RLVfMkdwZdd2eQ38mGt7BRiCKBaj1DyBAIHmm7mmXPuuw==} engines: {node: '>=10.0.0'} - help-me@5.0.0: - resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} - hermes-estree@0.23.1: resolution: {integrity: sha512-eT5MU3f5aVhTqsfIReZ6n41X5sYn4IdQL0nvz6yO+MMlPxw49aSARHLg/MSehQftyjnrE8X6bYregzSumqc6cg==} @@ -7948,10 +7959,6 @@ packages: jose@6.0.11: resolution: {integrity: sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==} - joycon@3.1.1: - resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} - engines: {node: '>=10'} - js-sha256@0.11.0: resolution: {integrity: sha512-6xNlKayMZvds9h1Y1VWc0fQHQ82BxTXizWPEtEeGvmOUYpBRy4gbWroHLpzowe6xiQhHpelCQiE7HEdznyBL9Q==} @@ -8798,10 +8805,6 @@ packages: obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} - on-exit-leak-free@2.1.2: - resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} - engines: {node: '>=14.0.0'} - on-finished@2.3.0: resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} engines: {node: '>= 0.8'} @@ -9087,20 +9090,6 @@ packages: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} - pino-abstract-transport@2.0.0: - resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} - - pino-pretty@13.0.0: - resolution: {integrity: sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==} - hasBin: true - - pino-std-serializers@7.0.0: - resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} - - pino@9.7.0: - resolution: {integrity: sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg==} - hasBin: true - pirates@4.0.7: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} @@ -9215,9 +9204,6 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - process-warning@5.0.0: - resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} - progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} @@ -9260,9 +9246,6 @@ packages: public-encrypt@4.0.3: resolution: {integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==} - pump@3.0.2: - resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} - pump@3.0.4: resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} @@ -9294,9 +9277,6 @@ packages: queue@6.0.2: resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} - quick-format-unescaped@4.0.4: - resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} - randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -9421,10 +9401,6 @@ packages: readonly-date@1.0.0: resolution: {integrity: sha512-tMKIV7hlk0h4mO3JTmmVuIlJVXjKk3Sep9Bf5OH0O+758ruuVkUy2J9SttDLm91IEX/WHlXPSpxMGjPj4beMIQ==} - real-require@0.2.0: - resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} - engines: {node: '>= 12.13.0'} - recast@0.21.5: resolution: {integrity: sha512-hjMmLaUXAm1hIuTqOdeYObMslq/q+Xff6QE3Y2P+uoHAg2nmVlLBps2hzh1UJDdMtDTMXOFewK6ky51JQIeECg==} engines: {node: '>= 4'} @@ -9631,10 +9607,6 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} - safe-stable-stringify@2.5.0: - resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} - engines: {node: '>=10'} - safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -9659,9 +9631,6 @@ packages: scuid@1.1.0: resolution: {integrity: sha512-MuCAyrGZcTLfQoH2XoBlQ8C6bzwN88XT/0slOGz0pn8+gIP85BOAfYa44ZXQUTOwRwPU0QvgU+V+OSajl/59Xg==} - secure-json-parse@2.7.0: - resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} - selfsigned@2.4.1: resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} engines: {node: '>=10'} @@ -9842,9 +9811,6 @@ packages: resolution: {integrity: sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} - sonic-boom@4.2.0: - resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} - sonner@2.0.3: resolution: {integrity: sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==} peerDependencies: @@ -10152,11 +10118,6 @@ packages: uglify-js: optional: true - terser@5.39.0: - resolution: {integrity: sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==} - engines: {node: '>=10'} - hasBin: true - terser@5.47.1: resolution: {integrity: sha512-tPbLXTI6ohPASb/1YViL428oEHu6/qv1OxqYnfaonVCFHqx4+wCd95pHrQWsL5X4pl90CTyW9piSAsS2L0VoMw==} engines: {node: '>=10'} @@ -10179,9 +10140,6 @@ packages: peerDependencies: tslib: ^2 - thread-stream@3.1.0: - resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} - throat@5.0.0: resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==} @@ -10429,6 +10387,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@5.8.2: + resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} + engines: {node: '>=14.17'} + hasBin: true + ua-parser-js@1.0.40: resolution: {integrity: sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==} hasBin: true @@ -13155,7 +13118,7 @@ snapshots: '@cosmjs/socket@0.33.1(bufferutil@4.0.9)': dependencies: '@cosmjs/stream': 0.33.1 - isomorphic-ws: 4.0.1(ws@7.5.10(bufferutil@4.0.9)) + isomorphic-ws: 4.0.1(ws@7.5.10) ws: 7.5.10(bufferutil@4.0.9) xstream: 11.14.0 transitivePeerDependencies: @@ -14610,7 +14573,7 @@ snapshots: - supports-color - ts-node - '@jest/core@30.1.3(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@25.8.0)(typescript@5.7.3))': + '@jest/core@30.1.3(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@22.14.1)(typescript@5.8.2))': dependencies: '@jest/console': 30.1.2 '@jest/pattern': 30.0.1 @@ -14625,7 +14588,7 @@ snapshots: exit-x: 0.2.2 graceful-fs: 4.2.11 jest-changed-files: 30.0.5 - jest-config: 30.1.3(@types/node@22.14.1)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@25.8.0)(typescript@5.7.3)) + jest-config: 30.1.3(@types/node@22.14.1)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@22.14.1)(typescript@5.8.2)) jest-haste-map: 30.1.0 jest-message-util: 30.1.0 jest-regex-util: 30.0.1 @@ -14863,12 +14826,6 @@ snapshots: dependencies: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 - optional: true - - '@jridgewell/source-map@0.3.6': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/sourcemap-codec@1.5.0': {} @@ -14909,6 +14866,16 @@ snapshots: '@kurkle/color@0.3.4': {} + '@logtape/logtape@2.2.1': {} + + '@logtape/pretty@2.2.1(@logtape/logtape@2.2.1)': + dependencies: + '@logtape/logtape': 2.2.1 + + '@logtape/redaction@2.2.1(@logtape/logtape@2.2.1)': + dependencies: + '@logtape/logtape': 2.2.1 + '@mui/private-theming@5.17.1(@types/react@19.0.8)(react@19.0.1)': dependencies: '@babel/runtime': 7.26.7 @@ -16935,8 +16902,7 @@ snapshots: acorn@8.14.0: {} - acorn@8.16.0: - optional: true + acorn@8.16.0: {} aes-js@4.0.0-beta.5: {} @@ -17119,8 +17085,6 @@ snapshots: at-least-node@1.0.0: optional: true - atomic-sleep@1.0.0: {} - attr-accept@2.2.5: {} auto-bind@4.0.0: {} @@ -18077,8 +18041,6 @@ snapshots: date-fns@4.1.0: {} - dateformat@4.6.3: {} - debounce@1.2.1: {} debug@2.6.9: @@ -18309,10 +18271,6 @@ snapshots: encodeurl@2.0.0: optional: true - end-of-stream@1.4.4: - dependencies: - once: 1.4.0 - end-of-stream@1.4.5: dependencies: once: 1.4.0 @@ -18840,8 +18798,6 @@ snapshots: iconv-lite: 0.4.24 tmp: 0.0.33 - fast-copy@3.0.2: {} - fast-deep-equal@3.1.3: {} fast-glob@3.3.1: @@ -18864,10 +18820,6 @@ snapshots: fast-levenshtein@2.0.6: {} - fast-redact@3.5.0: {} - - fast-safe-stringify@2.1.1: {} - fast-uri@3.0.6: {} fastq@1.19.0: @@ -19308,8 +19260,6 @@ snapshots: heap-js@2.6.0: {} - help-me@5.0.0: {} - hermes-estree@0.23.1: optional: true @@ -19721,7 +19671,7 @@ snapshots: - expo - react-native - isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.0.9)): + isomorphic-ws@4.0.1(ws@7.5.10): dependencies: ws: 7.5.10(bufferutil@4.0.9) @@ -19856,15 +19806,15 @@ snapshots: - supports-color - ts-node - jest-cli@30.1.3(@types/node@25.8.0)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@25.8.0)(typescript@5.7.3)): + jest-cli@30.1.3(@types/node@22.14.1)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@22.14.1)(typescript@5.8.2)): dependencies: - '@jest/core': 30.1.3(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@25.8.0)(typescript@5.7.3)) + '@jest/core': 30.1.3(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@22.14.1)(typescript@5.8.2)) '@jest/test-result': 30.1.3 '@jest/types': 30.0.5 chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.1.3(@types/node@25.8.0)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@25.8.0)(typescript@5.7.3)) + jest-config: 30.1.3(@types/node@22.14.1)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@22.14.1)(typescript@5.8.2)) jest-util: 30.0.5 jest-validate: 30.1.0 yargs: 17.7.2 @@ -19977,7 +19927,7 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@30.1.3(@types/node@22.14.1)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@25.8.0)(typescript@5.7.3)): + jest-config@30.1.3(@types/node@22.14.1)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@22.14.1)(typescript@5.8.2)): dependencies: '@babel/core': 7.29.0 '@jest/get-type': 30.1.0 @@ -20006,41 +19956,7 @@ snapshots: optionalDependencies: '@types/node': 22.14.1 esbuild-register: 3.6.0(esbuild@0.25.9) - ts-node: 10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@25.8.0)(typescript@5.7.3) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-config@30.1.3(@types/node@25.8.0)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@25.8.0)(typescript@5.7.3)): - dependencies: - '@babel/core': 7.29.0 - '@jest/get-type': 30.1.0 - '@jest/pattern': 30.0.1 - '@jest/test-sequencer': 30.1.3 - '@jest/types': 30.0.5 - babel-jest: 30.1.2(@babel/core@7.29.0) - chalk: 4.1.2 - ci-info: 4.3.0 - deepmerge: 4.3.1 - glob: 10.5.0 - graceful-fs: 4.2.11 - jest-circus: 30.1.3(babel-plugin-macros@3.1.0) - jest-docblock: 30.0.1 - jest-environment-node: 30.1.2 - jest-regex-util: 30.0.1 - jest-resolve: 30.1.3 - jest-runner: 30.1.3 - jest-util: 30.0.5 - jest-validate: 30.1.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 30.0.5 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 25.8.0 - esbuild-register: 3.6.0(esbuild@0.25.9) - ts-node: 10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@25.8.0)(typescript@5.7.3) + ts-node: 10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@22.14.1)(typescript@5.8.2) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -20373,12 +20289,12 @@ snapshots: - supports-color - ts-node - jest@30.1.3(@types/node@25.8.0)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@25.8.0)(typescript@5.7.3)): + jest@30.1.3(@types/node@22.14.1)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@22.14.1)(typescript@5.8.2)): dependencies: - '@jest/core': 30.1.3(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@25.8.0)(typescript@5.7.3)) + '@jest/core': 30.1.3(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@22.14.1)(typescript@5.8.2)) '@jest/types': 30.0.5 import-local: 3.2.0 - jest-cli: 30.1.3(@types/node@25.8.0)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@25.8.0)(typescript@5.7.3)) + jest-cli: 30.1.3(@types/node@22.14.1)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@22.14.1)(typescript@5.8.2)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -20400,8 +20316,6 @@ snapshots: jose@6.0.11: {} - joycon@3.1.1: {} - js-sha256@0.11.0: {} js-tokens@4.0.0: {} @@ -21378,8 +21292,6 @@ snapshots: obuf@1.1.2: {} - on-exit-leak-free@2.1.2: {} - on-finished@2.3.0: dependencies: ee-first: 1.1.1 @@ -21713,42 +21625,6 @@ snapshots: pify@4.0.1: optional: true - pino-abstract-transport@2.0.0: - dependencies: - split2: 4.2.0 - - pino-pretty@13.0.0: - dependencies: - colorette: 2.0.20 - dateformat: 4.6.3 - fast-copy: 3.0.2 - fast-safe-stringify: 2.1.1 - help-me: 5.0.0 - joycon: 3.1.1 - minimist: 1.2.8 - on-exit-leak-free: 2.1.2 - pino-abstract-transport: 2.0.0 - pump: 3.0.2 - secure-json-parse: 2.7.0 - sonic-boom: 4.2.0 - strip-json-comments: 3.1.1 - - pino-std-serializers@7.0.0: {} - - pino@9.7.0: - dependencies: - atomic-sleep: 1.0.0 - fast-redact: 3.5.0 - on-exit-leak-free: 2.1.2 - pino-abstract-transport: 2.0.0 - pino-std-serializers: 7.0.0 - process-warning: 5.0.0 - quick-format-unescaped: 4.0.4 - real-require: 0.2.0 - safe-stable-stringify: 2.5.0 - sonic-boom: 4.2.0 - thread-stream: 3.1.0 - pirates@4.0.7: {} pkg-dir@3.0.0: @@ -21850,8 +21726,6 @@ snapshots: process-nextick-args@2.0.1: {} - process-warning@5.0.0: {} - progress@2.0.3: optional: true @@ -21937,11 +21811,6 @@ snapshots: randombytes: 2.1.0 safe-buffer: 5.2.1 - pump@3.0.2: - dependencies: - end-of-stream: 1.4.4 - once: 1.4.0 - pump@3.0.4: dependencies: end-of-stream: 1.4.5 @@ -21970,8 +21839,6 @@ snapshots: inherits: 2.0.4 optional: true - quick-format-unescaped@4.0.4: {} - randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -22146,8 +22013,6 @@ snapshots: readonly-date@1.0.0: {} - real-require@0.2.0: {} - recast@0.21.5: dependencies: ast-types: 0.15.2 @@ -22389,8 +22254,6 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 - safe-stable-stringify@2.5.0: {} - safer-buffer@2.1.2: {} sax@1.6.0: @@ -22413,8 +22276,6 @@ snapshots: scuid@1.1.0: {} - secure-json-parse@2.7.0: {} - selfsigned@2.4.1: dependencies: '@types/node-forge': 1.3.14 @@ -22661,10 +22522,6 @@ snapshots: ip-address: 9.0.5 smart-buffer: 4.2.0 - sonic-boom@4.2.0: - dependencies: - atomic-sleep: 1.0.0 - sonner@2.0.3(react-dom@19.0.1(react@19.0.1))(react@19.0.1): dependencies: react: 19.0.1 @@ -22984,30 +22841,22 @@ snapshots: terser-webpack-plugin@5.3.11(@swc/core@1.10.16(@swc/helpers@0.5.15))(esbuild@0.25.9)(webpack@5.98.0(@swc/core@1.10.16(@swc/helpers@0.5.15))(esbuild@0.25.9)): dependencies: - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.0 serialize-javascript: 6.0.2 - terser: 5.39.0 + terser: 5.47.1 webpack: 5.98.0(@swc/core@1.10.16(@swc/helpers@0.5.15))(esbuild@0.25.9) optionalDependencies: '@swc/core': 1.10.16(@swc/helpers@0.5.15) esbuild: 0.25.9 - terser@5.39.0: - dependencies: - '@jridgewell/source-map': 0.3.6 - acorn: 8.14.0 - commander: 2.20.3 - source-map-support: 0.5.21 - terser@5.47.1: dependencies: '@jridgewell/source-map': 0.3.11 acorn: 8.16.0 commander: 2.20.3 source-map-support: 0.5.21 - optional: true test-exclude@6.0.0: dependencies: @@ -23029,10 +22878,6 @@ snapshots: dependencies: tslib: 2.8.1 - thread-stream@3.1.0: - dependencies: - real-require: 0.2.0 - throat@5.0.0: optional: true @@ -23150,18 +22995,18 @@ snapshots: esbuild: 0.25.9 jest-util: 30.0.5 - ts-jest@29.4.4(@babel/core@7.29.0)(@jest/transform@30.1.2)(@jest/types@30.0.5)(babel-jest@30.1.2(@babel/core@7.29.0))(esbuild@0.25.9)(jest-util@30.0.5)(jest@30.1.3(@types/node@25.8.0)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@25.8.0)(typescript@5.7.3)))(typescript@5.7.3): + ts-jest@29.4.4(@babel/core@7.29.0)(@jest/transform@30.1.2)(@jest/types@30.0.5)(babel-jest@30.1.2(@babel/core@7.29.0))(esbuild@0.25.9)(jest-util@30.0.5)(jest@30.1.3(@types/node@22.14.1)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@22.14.1)(typescript@5.8.2)))(typescript@5.8.2): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.8 - jest: 30.1.3(@types/node@25.8.0)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@25.8.0)(typescript@5.7.3)) + jest: 30.1.3(@types/node@22.14.1)(babel-plugin-macros@3.1.0)(esbuild-register@3.6.0(esbuild@0.25.9))(ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@22.14.1)(typescript@5.8.2)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.8.0 type-fest: 4.41.0 - typescript: 5.7.3 + typescript: 5.8.2 yargs-parser: 21.1.1 optionalDependencies: '@babel/core': 7.29.0 @@ -23214,21 +23059,21 @@ snapshots: '@swc/core': 1.10.16(@swc/helpers@0.5.15) optional: true - ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@25.8.0)(typescript@5.7.3): + ts-node@10.9.2(@swc/core@1.10.16(@swc/helpers@0.5.15))(@types/node@22.14.1)(typescript@5.8.2): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 25.8.0 + '@types/node': 22.14.1 acorn: 8.14.0 acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 5.7.3 + typescript: 5.8.2 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 optionalDependencies: @@ -23364,6 +23209,8 @@ snapshots: typescript@5.7.3: {} + typescript@5.8.2: {} + ua-parser-js@1.0.40: {} uglify-js@3.19.3: @@ -23616,8 +23463,8 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.14.0 - browserslist: 4.24.4 + acorn: 8.16.0 + browserslist: 4.28.2 chrome-trace-event: 1.0.4 enhanced-resolve: 5.18.1 es-module-lexer: 1.6.0 diff --git a/scripts/no-console-exclusions.txt b/scripts/no-console-exclusions.txt new file mode 100644 index 00000000..bc5f1d35 --- /dev/null +++ b/scripts/no-console-exclusions.txt @@ -0,0 +1,17 @@ +# EXCLUSIONS for scripts/no-console-guard.sh (Otto#8/#9: default-safe). +# +# The guard scans EVERY apps/*/src and packages/*/src by default, so newly +# added source is covered automatically — you must opt a path OUT here, never +# opt one in. Each line is a path prefix (fixed-string) whose console.* calls +# are sanctioned. Blank lines and #-comments are ignored. +# +# The two entries below are standalone dev/CLI tooling that predate the logging +# initiative and live OUTSIDE any */src (so the default scan never reaches them +# anyway) — listed here to document the sanction and guard against a future +# widened scan: +# - packages/graphql/codegen.ts codegen runner; console is its UX. +# - packages/temporal/scripts/ manual live-validation reporters. +# packages/logger/src is intentionally NOT excluded: it contains zero console.* +# writers (verified), so the default scan keeps it honest. +packages/graphql/codegen.ts +packages/temporal/scripts/ diff --git a/scripts/no-console-guard.sh b/scripts/no-console-guard.sh new file mode 100755 index 00000000..8a4bcd01 --- /dev/null +++ b/scripts/no-console-guard.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# Hard gate: zero unsanctioned `console.*` calls in migrated paths (spec §11). +# ESLint cannot gate (only-warn downgrades everything), so we grep. +# +# DEFAULT-SAFE (Otto#9): scans EVERY apps/*/src and packages/*/src, minus an +# explicit EXCLUSIONS list. New source is covered automatically — a path must +# be opted OUT (in no-console-exclusions.txt), never opted in. The inverse of +# the old allowlist, which silently ignored any path nobody remembered to add. +# +# Uses POSIX grep (always present, no CI install step needed) instead of +# ripgrep. Exit codes are handled explicitly (NOT `set -e`) because grep's +# exit code is meaningful: 0 = matches found (guard must inspect/fail), 1 = no +# matches (clean), >=2 = grep itself errored (fail loudly, do not treat as +# clean). +set -uo pipefail + +# Run from repo root so the apps/*/src, packages/*/src globs resolve. +cd "$(dirname "$0")/.." || exit 2 + +EXCLUSIONS_FILE="scripts/no-console-exclusions.txt" +# Call-shape only: requires an opening paren, so it won't match prose +# mentions of "console." in comments/docs. Full console method alternation — +# not just the log-level six — so console.table()/dir()/assert()/group()/... +# can't drift in. KNOWN BLIND SPOTS (accepted): bracket access console['log'], +# aliasing (const c = console), a call split across lines, and .js/.mjs/.jsx +# sources (repo source is TS-only; revisit if that changes). +PATTERN='console\.(log|error|warn|info|debug|trace|table|dir|dirxml|assert|group|groupCollapsed|groupEnd|count|countReset|time|timeEnd|timeLog|profile|profileEnd)[[:space:]]*\(' + +# Scan roots: every app + package source dir. +roots=() +for d in apps/*/src packages/*/src; do + [ -d "$d" ] && roots+=("$d") +done +if [ "${#roots[@]}" -eq 0 ]; then + echo "::error::no-console guard: found no apps/*/src or packages/*/src roots to scan" + exit 2 +fi + +# Fixed-string path-prefix excludes (blank / #-comment lines skipped). +excludes=() +if [ -f "$EXCLUSIONS_FILE" ]; then + while IFS= read -r line; do + [ -z "$line" ] && continue + case "$line" in \#*) continue ;; esac + excludes+=("$line") + done < "$EXCLUSIONS_FILE" +fi + +hits=$(grep -rEn \ + --include='*.ts' --include='*.tsx' \ + --exclude='*.test.ts' --exclude='*.test.tsx' \ + "$PATTERN" "${roots[@]}" 2>&1) +rc=$? + +case "$rc" in + 0) + : # matches found — filter excluded paths below + ;; + 1) + echo "no-console guard: OK (no console.* in scanned source)" + exit 0 + ;; + *) + echo "::error::grep failed (exit $rc) — guard cannot verify, failing loudly" + echo "$hits" + exit 2 + ;; +esac + +# Drop lines under an excluded path prefix. Hits are `path:lineno:content` — +# anchor the match to the PATH FIELD ONLY (prefix match on column 1). A plain +# `grep -vFf` would match the exclusion substring anywhere in the line, +# including code content (e.g. a string literal mentioning an excluded path), +# silently dropping a real violation. +if [ "${#excludes[@]}" -gt 0 ]; then + hits=$(printf '%s\n' "$hits" | awk -F: ' + NR == FNR { if ($0 != "") ex[++n] = $0; next } + { + for (i = 1; i <= n; i++) if (index($1, ex[i]) == 1) next + print + } + ' <(printf '%s\n' "${excludes[@]}") -) +fi + +if [ -z "$hits" ]; then + echo "no-console guard: OK (all console.* are in excluded paths)" + exit 0 +fi + +echo "::error::Unsanctioned console.* found:" +echo "$hits" +exit 1