Troubleshooting Runbooks
Symptom-first playbooks for the tickets you'll actually get — "recharge not complete", "order stuck PENDING", "product not showing in the catalog", "can't log in", "can't pay in this currency". Each is a decision tree, an ordered file:line diagnostic checklist, the read-only SELECTs to run, and who can actually fix it.
This is the page you open when a ticket comes in. Each section starts from the words a client or ops person actually says, walks the real code path that produces that symptom, gives you the read-only SELECTs to pinpoint the cause, and says who can fix it (self-heals on a cron / needs an admin action / needs a config change / needs a deploy).
It is the deep companion to the Operations "Common incidents" table — that table is the 10-second version; this is the trace. For the inventory of what's structurally broken (not incident response), see Known Issues.
Read-only investigation only
Every query on this page is a SELECT. Never run INSERT/UPDATE/DELETE to "fix" a stuck order or a wallet — the recovery paths are crons and admin endpoints that keep the money ledger consistent. A hand-edit to orders.status or wallets.amount will desync the ledger and the vendor state. When a fix needs a write, it goes through an admin action or the app, done by a human operator.
How to triage anything here
Four moves, in order, before you touch any specific runbook:
- Name the surface. Voucher/gift-card? →
orders/order_items. Top-up/recharge? →recharges. eSIM? →esim_orders. Login? →client_users/clients. Money? →wallets/forex_values. The tables differ per surface and so do the crons. - Read the row, not the dashboard. Pull the single order/user/wallet row and read its
status+sub_status+retry_count+retry_after+failure_*. Almost every "stuck" answer is visible there. - Decide: mid-flight vs stuck. Most PENDING things are legitimately mid-flight and self-heal within a minute or two. A thing is only stuck when its retry counter is exhausted or a terminal item is trapped under a non-terminal order. Each runbook draws that line precisely.
- Pull the vendor evidence. The
*_responsestables (order_item_responses,recharge_responses,esim_order_responses) and the webhook logs hold the raw upstream request/response — that's the ground truth for "what did the supplier actually say."
Jobs are cron-driven, not queue-driven
A recurring root cause across every "stuck" symptom: the RabbitMQ consumers are not wired up — processorRegistry is initialised empty and never appended to (main.go:165), so no queue workers run. All order progression is 100% cron-driven. If the scheduler process is down, or the relevant feature flag is off, nothing advances. Confirm the crons are firing (job_executions table / admin → Job Executions) first. See Jobs & Observability.
Symptom index
| The ticket says… | Surface | Runbook |
|---|---|---|
| "The recharge / eSIM didn't complete" | recharges, esim_orders | Recharge is not complete |
| "The order is stuck on PENDING" | orders | Order is stuck in PENDING |
| "The product isn't showing in my catalog" | products, vendor_products | Product not visible in the client catalog |
| "I can't log into the portal" | client_users, clients | Cannot log in to the client panel |
| "It won't let me pay for this currency" | wallets, forex_values | Cannot pay for a product in this currency |
Recharge is not complete
A direct top-up (mobile/gaming recharge) or an eSIM order was placed but never reached its delivered state. These are two different tables with two different crons — identify which first.
- Top-up → table
recharges, status enumRechargeStatus(database/models/enum.go:319), recovered by thetopup-order-retrycron (every minute). - eSIM → table
esim_orders, status enumEsimOrderStatus+EsimOrderSubStatus(database/models/esim_order.go:14,:39), recovered by theesim-order-retrycron (every minute).
The happy terminal is DELIVERED. Everything else is either mid-flight (self-heals) or trapped (needs admin).
Diagnostic checklist
- Which table? Look up by
reference_number(the server-minted KSUID = the vendor merchant reference) orclient_reference(the client's idempotency key). If it's inrecharges, it's a top-up; ifesim_orders, it's an eSIM. - Read status + retry state. For top-up:
status,retry_count,retry_after,failure_code,failure_reason,failure_is_user_fixable,vendor_order_id(database/repo/recharge.go:456). For eSIM addsub_status,activation_code IS NOT NULL,iccid IS NOT NULL(database/migration/20260414000001_create_esim_tables.sql). - User-fixable failure? If
failure_is_user_fixable = true, the input (player id / mobile number) was wrong — the retry handler force-FAILs and refunds on the next tick (http/handler/topup.go:1589). It is not recoverable by retrying; the customer must re-order with correct details. - Recreate budget exhausted?
retry_count >= 5means the double-bill guard tripped (topupRecreateBudget/EsimRecreateBudget = 5,http/handler/topup.go:1550,services/esim_order.go:53). The order is parked at PENDING for an admin — the cron re-schedules a long backoff (eSIM:now + 6h) but never re-purchases. NeedsAdminResetRecharge/AdminRetryRecharge(http/handler/admin_recharge_actions.go). - eSIM airtight mid-state? An eSIM at
PENDING/VENDOR_ORDER_CREATEDwith a non-nullvendor_order_idbut nullactivation_codeis the normal state while the delivery webhook has landed but the code hasn't been polled yet. Theesim.deliveredwebhook is deliberately a nudge (mapped to PROCESSING, never terminal —services/external_vendors/esim/octopus/octopus_esim_vendor.go:474) because the webhook can't carry the activation code; the every-minuteesim-order-retrycron re-polls byvendor_order_idand the poll response carries the code, flipping it toDELIVERED/VENDOR_CODE_FETCHED(services/esim_order.go:805). Expect it to clear within ~1 min. If it doesn't, the cron isn't running or the poll keeps returning no code. - Otherwise mid-flight.
retry_count < 5andretry_afterin the near future = the cron will retry with exponential backoff. Confirmtopup-order-retry/esim-order-retryactually ran (job_executions). - Pull the vendor evidence.
recharge_responses(filterrecharge_id) oresim_order_responses(filteresim_order_id) hold the raw upstream request/response byrequest_type(CREATE_ORDER, GET_ORDER_STATUS, PURCHASE_ESIM…). Inbound top-up webhooks land indirect_topup_webhooks(status,raw_payload,error_message).
Read-only queries
-- Top-up: is it stuck, mid-flight, or parked-for-admin?
SELECT id, reference_number, client_reference, status, sub_status,
vendor_id, vendor_order_id, retry_count, retry_after,
failure_code, failure_reason, failure_is_user_fixable,
status_text, created_at, updated_at
FROM recharges
WHERE reference_number = :ref AND deleted_at IS NULL;
-- eSIM: has the airtight-rule poll fetched the code yet?
SELECT id, reference_number, status, sub_status, vendor_id, vendor_order_id,
(activation_code IS NOT NULL) AS has_code,
(iccid IS NOT NULL) AS has_iccid,
activation_status, retry_count, retry_after,
failure_code, failure_reason, created_at
FROM esim_orders
WHERE reference_number = :ref AND deleted_at IS NULL;
-- What did the supplier actually return? (top-up)
SELECT id, recharge_id, request_type, request, response, created_at
FROM recharge_responses
WHERE recharge_id = :recharge_id
ORDER BY created_at DESC;Trap: a dead supplier leaves recharges PENDING forever — never FAILED, never refunded
The transient-failure branch was deliberately changed to stop auto-FAILing on max retries (it conflicted with the recreate budget), so a genuinely down supplier leaves top-ups/eSIMs at PENDING on a backoff loop until the recreate budget (5) trips and parks them for admin — the money stays debited the whole time (services/esim_order.go:826, persistTransientFailure :981). Also note there is no circuit breaker on the top-up/eSIM path (only the voucher vendors have one), so a down supplier produces per-order timeouts, not a fast-fail. And the top-up cron selects retry_count <= 10 while the budget is 5 — rows between 6–10 are still selected but refuse to recreate; above 10 they silently drop out of the query entirely. These are logged in Known Issues.
Order is stuck in PENDING
A voucher / gift-card order (table orders) is not progressing. The order-level status is a roll-up of its items computed by getOrderDeliveryStatus (http/handler/create_voucher_order.go:272), and the recovery cron is pending-order-retry (every minute).
An all-FAILED voucher order still reads PENDING
The single most confusing fact here: when every item failed and none delivered, getOrderDeliveryStatus returns OrderPending, not OrderFailed (create_voucher_order.go:437). The order only ever reaches FAILED/CANCELLED via an admin action — the automated pipeline never sets those at the order level. So "stuck PENDING" frequently means "permanently failed and waiting for a human," not "still trying."
Diagnostic checklist
- Confirm the cron is alive.
pending-order-retryruns every minute only when the vouchers feature is enabled (main.go:181). No cron → nothing moves. Checkjob_executions. - Read the order's retry state.
ordershasretry_count,retry_after,delivered_quantity,refunded_quantity,refunded_amount— but no error column (database/models/order.go:20). The only free-text error lives on the items. - Inspect the items — this is where the truth is.
order_items.status/sub_status/failure_reason(the only error text in the whole flow,database/models/order_item.go:51) /vendor_order_id. An item atFAILED/VENDOR_ORDER_FAILEDunder a PENDING order is trapped: failed items are excluded from the next retry's pending-item filter (create_voucher_order.go:2251), so they're never retried and never refunded. Needs an admin refund/cancel. - Check the caps. The cron selects
status='PENDING' AND retry_after <= now() AND retry_count <= 10(database/repo/order.go:1541).retry_count > 10→ silently abandoned (parked at PENDING, no alert). For bulk orders, a separate recreate budget of 5 (http/handler/bulk_vendor_order_checkpoints.go:695) parks the order "for admin" when the supplier keeps losing the order. - Distinguish mid-flight from stuck. Mid-flight =
retry_count <= 10andretry_afternear future (backoff window: 1, 2, 4, 8, 16 min —ScheduleOrderRetry:3653). Large orders (quantity > 5) are async by design and sit atPENDING/Initialbriefly withretry_count=0(create_voucher_order.go:832). - Pull the vendor audit.
order_item_responses(byorder_item_id) records every AUTH/CREATE/GET_ORDER_STATUS/GET_VOUCHERS exchange, written outside the caller's transaction so it survives rollbacks (create_voucher_order.go:3490). - Rule out money problems. Insufficient balance is caught synchronously at create (400, no order row is ever written —
create_voucher_order.go:711), so it is not a stuck-PENDING cause. If an order exists, the wallet was already debited.
Read-only queries
-- The prime "stuck" set: PENDING orders the cron will no longer retry
SELECT id, client_id, reference_code, status, sub_status,
quantity, delivered_quantity, retry_count, retry_after,
remarks, created_at, updated_at
FROM orders
WHERE deleted_at IS NULL AND status = 'PENDING'
AND (retry_count > 10 OR retry_after < now() - interval '30 minutes')
ORDER BY created_at;
-- Item-level truth: look for FAILED / VENDOR_ORDER_FAILED, missing vendor_order_id, failure_reason
SELECT id, order_id, vendor_id, status, sub_status, failure_reason,
vendor_order_id, (code IS NOT NULL) AS has_code, created_at, updated_at
FROM order_items
WHERE order_id = :order_id AND deleted_at IS NULL
ORDER BY id;
-- Classify a single order: due now / backing off / abandoned
SELECT id, status, sub_status, retry_count, retry_after,
(retry_count <= 10 AND retry_after <= now()) AS due_now,
(retry_count <= 10 AND retry_after > now()) AS backing_off,
(retry_count > 10) AS abandoned
FROM orders WHERE id = :order_id;
-- What the supplier returned for the failing item
SELECT oir.request_type, oir.request, oir.response, oir.created_at
FROM order_item_responses oir
JOIN order_items oi ON oi.id = oir.order_item_id
WHERE oi.order_id = :order_id
ORDER BY oir.created_at DESC;The cron cannot auto-FAIL or auto-refund a voucher order — exhausted orders are silently parked and require an admin action (see Admin Order Actions).
Product not visible in the client catalog
A product that should be sellable doesn't appear in a client's catalog (/api/v1 list or the portal). Visibility is a gauntlet of gates in Repository.GetProducts → applyClientProductFilters (database/repo/product.go:22, :1470). The single most common cause is an inactive vendor.
Two different things are called 'catalog'
The vendor_catalog_products / vendor_catalog_snapshots tables are the admin-side raw supplier-sync + mapping tooling — not what a client sees. The client-facing catalog is products (master) + vendor_products (per-vendor sellable mappings). Ignore the vendor_catalog_* tables for this symptom.
For a product to be visible to a client, all of these must hold:
Diagnostic checklist
- Master product active & not deleted.
products.is_active = true AND deleted_at IS NULL. In the list path this is a hard filter (product.go:1533); in the detail-by-id pathis_activeisn't re-checked, so a direct fetch can still return an inactive product — trust the list filter. - Client is ACTIVE. If
clients.status != 'ACTIVE'the request never reaches the catalog query — it's rejected at auth (middleware/auth.go:93,Client.IsActive()database/models/client.go:48). That's a login/access problem, not a catalog one. - At least one usable vendor_product (the big one). The product is hidden if a
NOT EXISTSover itsvendor_productsfinds nothing withvp.is_active = true AND vp.deleted_at IS NULL AND vendor.is_active = true(GetProductsWithoutActiveVendorProducts,product.go:1633,:1688). A vendor turned inactive (vendors.is_active = false) hides every product for which it's the only mapping — this is the #1 real cause. Globally-inactive vendors come fromGetInactiveVendorIDs(database/repo/vendor.go:225). - Vendor not blacklisted for this client. An active row in
client_vendor_blacklistsfor the product's only vendor hides it (product.go:1584). - Product not blacklisted for this client. An active row in
client_product_blacklistsfor(client_id, product_id)hides it (Phase 2,product.go:1503). - Query-param filters.
country_id,currency_id,category, and search (name/categoryILIKE) only exclude when the portal actually sends them (product.go:112). - Suspect the cache last. Per-client results are cached 10–30 min, plus a
products_without_vendorscache (10 min,product.go:1641). A fix (re-activating a vendor) can look un-applied until the TTL expires orclearProductCachesfires.
What does NOT hide a product
- A missing
client_product_details(CPD) row does not hide anything — CPD is consumed only for the discount. No CPD row → the default discount applies and the product still shows (database/repo/vendor_product.go:153). This is a frequent false lead. - Out-of-stock does not hide a product. There is no inventory/FEFO gate on the catalog query — a zero-stock voucher product still lists and fails at order time instead. (There is in fact no FEFO logic anywhere in the Go codebase.)
- There is no voucher/topup/eSIM feature toggle on this catalog — top-up and eSIM are entirely separate product tables.
Read-only queries
-- One query that reproduces the whole hide decision for this client+product
SELECT p.id, p.name, p.is_active, p.deleted_at,
EXISTS (
SELECT 1 FROM vendor_products vp JOIN vendors v ON v.id = vp.vendor_id
WHERE vp.product_id = p.id AND vp.is_active AND vp.deleted_at IS NULL
AND v.is_active AND v.deleted_at IS NULL
AND vp.vendor_id NOT IN (
SELECT vendor_id FROM client_vendor_blacklists
WHERE client_id = :client_id AND is_active AND deleted_at IS NULL)
) AS has_sellable_vendor_product,
EXISTS (
SELECT 1 FROM client_product_blacklists cpb
WHERE cpb.client_id = :client_id AND cpb.product_id = p.id
AND cpb.is_active AND cpb.deleted_at IS NULL
) AS is_client_blacklisted
FROM products p WHERE p.id = :product_id;
-- Visible iff: is_active=true, deleted_at NULL, has_sellable_vendor_product=true, is_client_blacklisted=false
-- Why is there no sellable mapping? Inspect the product's vendors
SELECT vp.id, vp.vendor_id, v.name AS vendor_name,
vp.is_active AS vp_active, vp.deleted_at AS vp_deleted,
v.is_active AS vendor_active, v.deleted_at AS vendor_deleted
FROM vendor_products vp JOIN vendors v ON v.id = vp.vendor_id
WHERE vp.product_id = :product_id;Cannot log in to the client panel
A user can't get into the octopus-client Next.js portal. Split the failure into credentials/account rejected by the backend vs login succeeded but the cookie never stuck. The full auth flow is documented in Client Portal · Auth & 2FA; this section is the failure catalog.
Backend rejected the credentials
The login handler ClientLoginHandler (http/handler/client_auth.go:18) checks in this order, with distinct messages:
| Gate | file:line | Response | Meaning |
|---|---|---|---|
| User lookup by email | client_auth.go:32 | 401 Invalid credentials | unknown, or soft-deleted (deleted_at filter, database/repo/client_user.go:28) |
User status ≠ active | client_auth.go:39 | 401 Account not active | invited (never set a password) or suspended |
| bcrypt mismatch | client_auth.go:45 | 401 Invalid credentials | wrong password (hashing is bcrypt, utils/jwt.go:157) |
Client (tenant) status ≠ ACTIVE | client_auth.go:51 | 401 Organization not active | the whole org is INVITED/SUSPENDED/DELETED |
Note the case difference: client_users.status is lowercase (active), clients.status is UPPERCASE (ACTIVE). "Account not active" and "Organization not active" are the two top "correct password still won't log in" causes.
Rule these OUT for the portal
- No lockout / rate-limit on the client portal. Unlike the admin login (which has
failed_login_count/locked_until), there is no failed-attempt counter and nologin_attemptstable for client users. "Locked out after N tries" is impossible on this surface — don't chase it. - No email-verification gate. There is no
email_verifiedcheck in the client login path; a user is gated purely bystatus. - No "passkey-only" user. Every user keeps a password column; passkeys are additive and never block password login.
- IP whitelist is not on the portal.
IPWhitelistMiddlewareis attached only to/api/v1(http/routes/routes.go:77), never the/clientgroup. A misconfigured whitelist locks a client out of the external API, not the portal.
Login succeeded (200) but the user bounces back to /login
The backend logged Successful client portal login (client_auth.go:137) but the browser never stored or never sends the cookie, so the next GET /client/api/me is a 401 and the app bounces to login. Cookies are octopus_access (Path=/client) and octopus_refresh (Path=/client/auth), set by setAuthCookies with config from GetClientCookieConfig (config/cookie.go:30). Causes:
COOKIE_DOMAINmismatch — set to a domain the portal isn't served under (app.octopuscards.iovs a sandbox host, or.octopuscards.iowhile the portal runs on a different registrable domain). The browser silently drops theSet-Cookie. No backend error.Secure=trueover HTTP — production forcesSecure=true(config/cookie.go:36); reached over plain HTTP (proxy misconfig) the cookie is dropped.- Cross-domain
SameSite=Lax— if the API host and portal host are different registrable domains, thewithCredentialsXHR is cross-site and Lax cookies aren't sent at all. Works only when API and portal share a registrable domain (subdomains are fine). The code never setsSameSite=None. - CORS success ≠ cookie success — a cross-domain request can pass CORS (
web/domains.goallowsmypopupgiftcards.com,octopuscards.io,octopusrewards.com+ subdomains) yet still fail to persist the cookie because of #3.
2FA and passkey failure modes
- TOTP code rejected (
client_2fa.go:85, 401Invalid verification code): the top real cause is server clock skew > ±30s (pquernatotp.Validate, skew 1). Also an inconsistenttwo_factor_enabled=truebuttwo_factor_secret IS NULLstate makes every code fail with the same generic error — the user must fall back to a backup code. - Pending 2FA expired (
client_2fa.go:34, 4012FA session expired): theclient_2fa_pendingintermediate token is 5-minute TTL (utils/jwt.go:224). - Passkey fails (
services/webauthn.go:302): almost alwaysWEBAUTHN_RP_ID/WEBAUTHN_ORIGINSnot matching the current domain — the RP is baked in at credential creation, so credentials made under one domain cannot be used under another (after any domain/subdomain/scheme change).
All users logged out at once after a deploy = JWT_SECRET rotated
Every access/refresh cookie is HS256-signed with JWT_SECRET (middleware/client_cookie_auth.go:26). If it's rotated or lost, every existing cookie fails signature validation simultaneously → 401 → refresh also fails → forced logout for all users at once. Signature in the logs: a spike of Invalid access token / Invalid refresh token right after a deploy. New logins still work (signed with the new secret).
Read-only queries
-- The two "correct password but blocked" gates, in one shot
SELECT cu.id, cu.email, cu.status AS user_status,
(cu.password IS NOT NULL) AS has_password,
cu.two_factor_enabled, (cu.two_factor_secret IS NOT NULL) AS has_2fa_secret,
cu.deleted_at, c.status AS client_status
FROM client_users cu JOIN clients c ON c.id = cu.client_id
WHERE cu.email = :email;
-- Need: user_status='active' (lowercase), client_status='ACTIVE' (uppercase), deleted_at NULL
-- Session/token state (revoked or expired refresh = forced re-login)
SELECT id, user_id, token_type, is_revoked, expires_at, created_at
FROM auth_tokens WHERE user_id = :user_id
ORDER BY created_at DESC LIMIT 10;
-- 2FA recovery headroom
SELECT count(*) FROM client_backup_codes
WHERE client_user_id = :user_id AND used_at IS NULL;Cannot pay for a product in this currency
Placing an order fails because of the product's currency. Octopus is a prepaid wallet model: a client holds one wallet per currency (wallets_client_currency_unique (client_id, currency_id)), and an order in currency X paid from a wallet in currency Y needs an FX rate X → Y. The failure is almost always a missing FX rate.
Diagnostic checklist
- Find the product's currency.
products.currency_id→currencies.currency(ISO code). Thecurrenciestable hasid,currency,numeric_code,precision,is_active,deleted_at(database/models/currency.go:8). - Can a wallet be resolved?
ResolveWalletForOrderprefers a wallet in the product's currency, then falls back to the client's default-currency wallet (clients.currency) (services/wallet_resolver.go:90). If neither exists →ErrWalletNotFound→ the voucher handler returns 404Wallet not found(create_voucher_order.go:681). - Same currency? If the resolved wallet currency equals the product currency, no FX is needed — skip to the balance check.
- Cross-currency → FX rate required (the usual culprit).
computeChargeslooks upforex_valuesforproduct → walletviaGetForexRateForVendor→GetForexValue(database/repo/forex.go:56). A missing row returns(nil, nil)(silent), which becomesforex rate not available for X to Y conversionand surfaces as 400Failed to compute order charges(create_voucher_order.go:1273). For top-up/eSIM the equivalent isErrForexNotAvailable(services/order_charges.go:14). - Balance check. Converted
TotalPayablevswallets.amount: app pre-check returns 400Insufficient funds in your wallet(create_voucher_order.go:715); the stored proc also hard-guards withRAISE EXCEPTION 'Insufficient funds…'under aFOR UPDATElock. - Voucher vs top-up/eSIM cross-currency rules differ. Voucher auto-converts (
RequireCurrencyMatch=false). Top-up/eSIM setRequireCurrencyMatch=truefor an explicitly chosen wallet, so a mismatched explicit wallet is rejected withErrWalletCurrencyMismatch("wallet currency does not match variant currency",services/wallet_resolver.go:83).
Trap: deactivating a currency does NOT block orders in it
currencies.is_active exists but is enforced nowhere in the payment path — every currency lookup filters only deleted_at IS NULL, never is_active = true (GetCurrencyByCode database/repo/currency.go:270, VerifyDestinationCurrency forex.go:26). So setting is_active=false to "turn off" a currency has no effect. The only real ways to block a currency are to soft-delete the currencies row, remove the product, or remove the forex_values rate (which only blocks cross-currency, not same-currency). This is the most common "why can/can't I pay in X" surprise. Logged in Known Issues.
Read-only queries
-- Product currency + whether that currency row is present
SELECT p.id, p.name, p.currency_id, c.currency, c.is_active, c.deleted_at
FROM products p LEFT JOIN currencies c ON c.id = p.currency_id
WHERE p.id = :product_id;
-- Does the client hold a wallet in the product's currency? What's its balance?
SELECT w.id AS wallet_id, cur.currency, w.amount, w.type, w.deleted_at
FROM wallets w JOIN currencies cur ON cur.id = w.currency_id
WHERE w.client_id = :client_id AND w.deleted_at IS NULL;
-- The usual culprit: is there an active FX rate product_ccy -> wallet_ccy?
SELECT id, source_currency, destination_currency, value, created_at
FROM forex_values
WHERE source_currency = :product_ccy AND destination_currency = :wallet_ccy
AND deleted_at IS NULL
ORDER BY id DESC LIMIT 1; -- zero rows => "forex rate not available for X to Y conversion"
-- Confirm the debit actually recorded the currency + FX (post-mortem)
SELECT t.id, cur.currency, t.amount, t.source_currency, t.destination_currency,
t.forex_rate, t.conversion_charges, t.status, t.created_at
FROM transactions t JOIN currencies cur ON cur.id = t.currency_id
WHERE t.client_id = :client_id
ORDER BY t.id DESC LIMIT 20;forex_values rows are manual admin entries — there's no automatic rate feed and no staleness/TTL check (the newest non-deleted row wins regardless of age). A missing pair is fixed by an admin adding the rate, not by anything self-healing. See Wallet & Ledger.
When a runbook runs out
If none of the five fits, fall back to the Operations page for service health, logs, and the cron-run history, and to Metrics & Tracing to follow a single request's trace end-to-end (a claim on Grasshopper and its downstream Octopus calls share one W3C trace). Structural defects — the ones these runbooks keep pointing at — are consolidated in Known Issues.
Operations & Runbooks
Day-to-day operating procedures — deploying, rolling back, reading logs, checking health, and safe database investigation.
Test Infrastructure
How the codebase is tested — the test/ tree, repo integration tests with fault injection, the octopusfake in-process upstream, the mocky-balboa-driven vendor orchestration suites, concurrency/goleak, CI, and how to add a new vendor suite.