mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-14 06:16:20 -06:00
Item 2: when the heartbeat checker marks a device offline it now also disconnects any socket it still holds for it, so DB-offline can't diverge from socket-state into a silent half-open (defensive — the live-socket guard already defers genuinely-live sockets). Item 3: tighten half-open detection WITHOUT reintroducing the TV-WebKit decode-load risk the 30s pong-timeout was chosen for — lower only pingInterval 30s->15s (probe more often), KEEP pingTimeout at 30s. Detection = interval+timeout = 45s (was 60s), and the client inherits these via the handshake so BOTH ends detect a dead peer ~25% sooner. (Deliberately did NOT drop pingTimeout to ~20s: MAXHUB is a video-playing TV-class device and the code comment warns tighter timeouts cause spurious drops under decode load.) Item 4: SO_KEEPALIVE on every accepted connection (lib/tcp-keepalive.js) so a half-open TCP can't persist indefinitely at the OS layer, independent of the app ping. Tests: server closes a non-ponging peer within ~pingInterval+pingTimeout while a ponging peer survives; a device whose transport dies ends offline with its connection torn down; keepalive applied to each accepted connection (and never breaks setup on error). Suite 328/328.
284 lines
19 KiB
JavaScript
284 lines
19 KiB
JavaScript
const path = require('path');
|
||
|
||
// Data locations. Everything defaults to the in-repo layout, so existing installs
|
||
// (including production) are byte-for-byte unchanged when these are unset. Set
|
||
// DATA_DIR - or the individual *_PATH / *_DIR vars - to relocate state onto a
|
||
// mounted volume (used by the Docker image). UNSET resolves to exactly the legacy
|
||
// paths: server/db/remote_display.db, server/uploads/, server/certs/.
|
||
const DATA_DIR = process.env.DATA_DIR || __dirname;
|
||
const uploadsDir = process.env.UPLOADS_DIR || path.join(DATA_DIR, 'uploads');
|
||
const certsDir = process.env.CERTS_DIR || path.join(DATA_DIR, 'certs');
|
||
|
||
// #146 billing: optional JSON override for the rate card (else the agreement defaults).
|
||
// Must be a non-empty array of {minScreens, rate}; anything malformed falls back to null.
|
||
function parseBillingRateTable(raw) {
|
||
if (!raw) return null;
|
||
try {
|
||
const t = JSON.parse(raw);
|
||
if (Array.isArray(t) && t.length && t.every(r => typeof r.minScreens === 'number' && typeof r.rate === 'number')) return t;
|
||
} catch (_) { /* fall through to defaults */ }
|
||
return null;
|
||
}
|
||
|
||
module.exports = {
|
||
port: process.env.PORT || 3001,
|
||
httpsPort: process.env.HTTPS_PORT || 3443,
|
||
dataDir: DATA_DIR,
|
||
dbPath: process.env.DB_PATH || path.join(DATA_DIR, 'db', 'remote_display.db'),
|
||
uploadsDir,
|
||
contentDir: path.join(uploadsDir, 'content'),
|
||
screenshotsDir: path.join(uploadsDir, 'screenshots'),
|
||
certsDir,
|
||
frontendDir: path.join(__dirname, '..', 'frontend'),
|
||
// App-level heartbeat. Checker runs every heartbeatInterval and marks
|
||
// devices offline if last_heartbeat is older than heartbeatTimeout.
|
||
// Env override for self-hosters on slow/jittery networks (issue #3:
|
||
// reporter found raising HEARTBEAT_TIMEOUT to 60s reduced false offlines).
|
||
heartbeatInterval: parseInt(process.env.HEARTBEAT_INTERVAL) || 10000,
|
||
heartbeatTimeout: parseInt(process.env.HEARTBEAT_TIMEOUT) || 45000,
|
||
// How long the server holds commands/playlist-updates for a device that's
|
||
// offline at emit time (ms). On reconnect within this window, queued events
|
||
// are flushed in order. Past TTL they're dropped. See lib/command-queue.js.
|
||
commandQueueTtlMs: parseInt(process.env.COMMAND_QUEUE_TTL_MS) || 30000,
|
||
// Engine.IO transport-level ping/pong. Raised from Socket.IO defaults
|
||
// (25000/20000) because TV WebKits (LG webOS, older Tizen) miss pongs
|
||
// under decode load - tighter values cause spurious transport drops.
|
||
// #148: faster half-open detection WITHOUT reintroducing that risk — we lower only the
|
||
// PING INTERVAL (probe more often), keeping the deliberately-generous 30s pong TIMEOUT so a
|
||
// decode-loaded TV WebKit still has the full window to answer. Detection = interval +
|
||
// timeout = 45s (was 60s); the client inherits these via the handshake so BOTH ends detect
|
||
// a dead peer ~25% sooner. Do NOT drop pingTimeout below ~30s (see the decode-load note).
|
||
pingInterval: parseInt(process.env.PING_INTERVAL) || 15000,
|
||
pingTimeout: parseInt(process.env.PING_TIMEOUT) || 30000,
|
||
// #148 Item 4: TCP SO_KEEPALIVE idle delay — OS-level dead-peer probing independent of the
|
||
// app ping, so a half-open TCP can't persist indefinitely.
|
||
tcpKeepAliveMs: parseInt(process.env.TCP_KEEPALIVE_MS) || 20000,
|
||
maxFileSize: 500 * 1024 * 1024, // 500MB
|
||
thumbnailWidth: 320,
|
||
screenshotQuality: 70,
|
||
// SSL: drop your Cloudflare Origin cert + key in certs/ folder
|
||
// or set env vars SSL_CERT and SSL_KEY to custom paths
|
||
sslCert: process.env.SSL_CERT || path.join(certsDir, 'cert.pem'),
|
||
sslKey: process.env.SSL_KEY || path.join(certsDir, 'key.pem'),
|
||
// Auth
|
||
jwtSecret: process.env.JWT_SECRET || (() => {
|
||
const secretFile = path.join(certsDir, '.jwt_secret');
|
||
const fs = require('fs');
|
||
if (fs.existsSync(secretFile)) return fs.readFileSync(secretFile, 'utf8').trim();
|
||
const secret = require('crypto').randomBytes(64).toString('hex');
|
||
try { fs.mkdirSync(path.dirname(secretFile), { recursive: true }); fs.writeFileSync(secretFile, secret); } catch {}
|
||
return secret;
|
||
})(),
|
||
jwtExpiry: '7d',
|
||
// Google OAuth - set these in env or here
|
||
googleClientId: process.env.GOOGLE_CLIENT_ID || '',
|
||
// Microsoft OAuth - set these in env or here
|
||
microsoftClientId: process.env.MICROSOFT_CLIENT_ID || '',
|
||
microsoftTenantId: process.env.MICROSOFT_TENANT_ID || 'common',
|
||
// Stripe (optional - for paid subscriptions)
|
||
stripeSecretKey: process.env.STRIPE_SECRET_KEY || '',
|
||
stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET || '',
|
||
// Microsoft Graph email sender (services/email.js). Required for actual
|
||
// delivery; absent values short-circuit to a stdout fallback for local dev.
|
||
graphTenantId: process.env.GRAPH_TENANT_ID || '',
|
||
graphClientId: process.env.GRAPH_CLIENT_ID || '',
|
||
graphClientSecret: process.env.GRAPH_CLIENT_SECRET || '',
|
||
graphSenderEmail: process.env.GRAPH_SENDER_EMAIL || '',
|
||
graphSenderName: process.env.GRAPH_SENDER_NAME || 'ScreenTinker',
|
||
// Dev safety net: comma-separated allow-list of recipient emails. When set,
|
||
// sends to any address NOT in the list are suppressed (logged but not posted
|
||
// to Graph). Intended for local dev that pulls fresh prod DB copies - keeps
|
||
// us from accidentally emailing real prod users. UNSET on prod systemd unit.
|
||
graphDevRestrictTo: process.env.GRAPH_DEV_RESTRICT_TO || '',
|
||
// Self-hosted mode: if true, first user gets enterprise plan and no billing
|
||
selfHosted: process.env.SELF_HOSTED === 'true',
|
||
// #116: opt-in UI gate. When true, hides the Subscription nav item + billing view
|
||
// and bounces #/billing to the dashboard. Default off, so existing deployments are
|
||
// unchanged. UI-only — /api/subscription/* stays in place (internal usage reads).
|
||
hideBilling: process.env.HIDE_BILLING === 'true',
|
||
// Disable public registration (OAuth auto-signup is also blocked when set).
|
||
// First-user setup is still allowed so a fresh install can be initialized.
|
||
disableRegistration: ['true', '1'].includes(String(process.env.DISABLE_REGISTRATION || '').toLowerCase()),
|
||
// Redirect / -> /app instead of serving the marketing landing page.
|
||
// For self-hosted internal deployments that don't want the public homepage.
|
||
disableHomepage: ['true', '1'].includes(String(process.env.DISABLE_HOMEPAGE || '').toLowerCase()),
|
||
// Issue #12: auto-create a personal org + Default workspace for self-service
|
||
// signups (public register + OAuth). Defaults TRUE so single-tenant and the
|
||
// hosted self-service flow are unaffected; set AUTO_CREATE_ORG_ON_SIGNUP=false
|
||
// on MSP-style deployments where an admin/operator assigns users to existing
|
||
// orgs after signup instead.
|
||
autoCreateOrgOnSignup: !['false', '0'].includes(String(process.env.AUTO_CREATE_ORG_ON_SIGNUP || '').toLowerCase()),
|
||
|
||
// #142 event-loop lag telemetry (services/loop-lag.js). perf_hooks
|
||
// monitorEventLoopDelay is C++-backed, so continuous sampling is cheap. Each
|
||
// window's p99 is persisted to event_loop_lag (bounded: indexed + pruned from
|
||
// day one) and drives the banded load level the reconnect throttle reads.
|
||
lagSampleIntervalMs: parseInt(process.env.LAG_SAMPLE_INTERVAL_MS) || 1000,
|
||
lagResolutionMs: parseInt(process.env.LAG_RESOLUTION_MS) || 20,
|
||
lagTelemetryRetentionDays: parseFloat(process.env.LAG_TELEMETRY_RETENTION_DAYS) || 3,
|
||
lagPruneIntervalMs: parseInt(process.env.LAG_PRUNE_INTERVAL_MS) || 3600000,
|
||
// Banded load levels from the window p99 (ms). Asymmetric by design: a band is
|
||
// entered immediately when its up-threshold is crossed (tighten fast), but
|
||
// released only one step at a time after lagReleaseSamples consecutive samples
|
||
// fall below a deadband (release slow), so small fluctuations don't flap it.
|
||
// Bands ONLY scale how hard an already-flagged device is throttled; a healthy
|
||
// device is never gated by global lag.
|
||
lagElevatedMs: parseInt(process.env.LAG_ELEVATED_MS) || 100,
|
||
lagCriticalMs: parseInt(process.env.LAG_CRITICAL_MS) || 250,
|
||
lagReleaseSamples: parseInt(process.env.LAG_RELEASE_SAMPLES) || 5,
|
||
|
||
// #142 load-aware per-device reconnect throttle (lib/reconnect-throttle.js).
|
||
// The verdict of WHO is misbehaving is ALWAYS per-device (keyed on device_id):
|
||
// a device is flagged only when it exceeds reconnectBaseMax genuine reconnects
|
||
// per reconnectWindowMs. Global lag never flags a healthy device — the lag band
|
||
// only MULTIPLIES how hard an already-flagged device is backed off.
|
||
reconnectWindowMs: parseInt(process.env.RECONNECT_WINDOW_MS) || 10000,
|
||
reconnectBaseMax: parseInt(process.env.RECONNECT_BASE_MAX) || 5,
|
||
// Absolute per-device ceiling, independent of band AND of warm-up: no device may
|
||
// exceed this many reconnects/window no matter what the adaptive logic computes,
|
||
// so a slow-ramp attacker can't train its way through.
|
||
reconnectHardCeiling: parseInt(process.env.RECONNECT_HARD_CEILING) || 20,
|
||
// Server-enforced backoff for a flagged device: baseBackoff * 2^(level-1) * band
|
||
// multiplier, capped at maxBackoff. Level escalates while it keeps storming
|
||
// (tighten fast) and decays one step per reconnectReleaseMs of calm (release slow).
|
||
reconnectBaseBackoffMs: parseInt(process.env.RECONNECT_BASE_BACKOFF_MS) || 1000,
|
||
reconnectMaxBackoffMs: parseInt(process.env.RECONNECT_MAX_BACKOFF_MS) || 60000,
|
||
reconnectMaxLevel: parseInt(process.env.RECONNECT_MAX_LEVEL) || 10,
|
||
reconnectReleaseMs: parseInt(process.env.RECONNECT_RELEASE_MS) || 30000,
|
||
// #146 evict idle reconnect-throttle buckets so per-device state can't grow
|
||
// unbounded over churned device_ids (the sweep ota-breaker has but the #142
|
||
// throttle lacked). A device quiet this long has its bucket dropped.
|
||
reconnectIdleResetMs: parseInt(process.env.RECONNECT_IDLE_RESET_MS) || 60 * 60 * 1000,
|
||
// #146 hardening — SUSTAINED flap limiter (lib/flap-limiter.js). The #142 burst
|
||
// throttle trips at reconnectBaseMax/reconnectWindowMs (5/10s) — a device flapping
|
||
// every 3-5s does ~2-3/10s and passes clean, yet each cycle is an expensive
|
||
// register+playlist build+acks and one status_log row. This SEPARATE limiter catches
|
||
// sustained flapping over a long window. Keyed via the identity fallback chain
|
||
// (device_id -> fingerprint -> device_token -> ONE global anon bucket), NEVER IP
|
||
// (SNAT collapses the fleet into one key). In-memory (persists now that Item A ends
|
||
// the restart loop); bounded by an idle sweep + the single anon bucket.
|
||
flapLimiterEnabled: process.env.FLAP_LIMITER_ENABLED !== 'false', // #146 P1.3 kill switch
|
||
connectRateWindowMs: parseInt(process.env.CONNECT_RATE_WINDOW_MS) || 300000, // 5 min
|
||
connectRateMax: parseInt(process.env.CONNECT_RATE_MAX) || 20, // per identity per window
|
||
connectRateAnonMax: parseInt(process.env.CONNECT_RATE_ANON_MAX) || 60, // the shared global anon bucket, higher (collective)
|
||
connectRateCooldownMs: parseInt(process.env.CONNECT_RATE_COOLDOWN_MS) || 60000, // refuse window after a trip
|
||
connectRateIdleMs: parseInt(process.env.CONNECT_RATE_IDLE_MS) || 2 * 300000, // sweep buckets idle this long
|
||
// after this many trips within a window the identity is auto-quarantined — an
|
||
// IN-MEMORY, TIME-LIMITED refusal (NOT a DB block; the devices.blocked column is only
|
||
// ever written by an operator). Self-heals after connectRateQuarantineMs. 0 = off.
|
||
connectRateQuarantineTrips: parseInt(process.env.CONNECT_RATE_QUARANTINE_TRIPS) || 5,
|
||
connectRateQuarantineMs: parseInt(process.env.CONNECT_RATE_QUARANTINE_MS) || 30 * 60 * 1000,
|
||
// Cold start: for this long after process start, lag is high while the whole
|
||
// fleet reconnects at once. Treat leniently — force the 'normal' band and apply
|
||
// only the hard ceiling (no rate-band throttle) so a deploy can't throttle
|
||
// healthy screens. Throttle state is in-memory and resets on restart.
|
||
reconnectWarmupMs: parseInt(process.env.RECONNECT_WARMUP_MS) || 30000,
|
||
reconnectBandElevatedMult: parseFloat(process.env.RECONNECT_BAND_ELEVATED_MULT) || 2,
|
||
reconnectBandCriticalMult: parseFloat(process.env.RECONNECT_BAND_CRITICAL_MULT) || 4,
|
||
|
||
// #142 device_status_log retention. A GLOBAL scheduled sweep (pruneStatusLog in
|
||
// db/database.js, run on startup + the heartbeat interval) deletes rows older
|
||
// than this across ALL devices — covering what the per-device insert-time prune
|
||
// in deviceSocket.js misses: removed/idle devices that never insert again, and
|
||
// the heartbeat.js offline_timeout insert that bypasses logDeviceStatus. Default
|
||
// is LOWER than the old hardcoded 7 days (the reporter's bloat happened under 7d);
|
||
// 2-3 days is plenty for the dashboard's 24h uptime view + diagnostics.
|
||
statusLogRetentionDays: parseFloat(process.env.STATUS_LOG_RETENTION_DAYS) || 3,
|
||
// #146 HARD per-device row-count ceiling on device_status_log, enforced by the
|
||
// global sweep alongside the age delete above. Age-based retention can't bound a
|
||
// write storm (rows are all younger than the window), so a reconnect storm grew
|
||
// the table to 1.1M. This cap keeps only the newest N transitions per device, so
|
||
// the table is bounded by (devices * N) REGARDLESS of churn — and the very first
|
||
// sweep trims the existing backlog (table healthy now, not in retentionDays).
|
||
statusLogMaxRowsPerDevice: parseInt(process.env.STATUS_LOG_MAX_ROWS_PER_DEVICE) || 500,
|
||
// #146 hardening: max rows any single synchronous maintenance DELETE may touch. All
|
||
// table-growth sweeps (status_log, play_logs, provisioning cascade, event_loop_lag,
|
||
// telemetry) delete in batches of this size, yielding to the event loop between
|
||
// batches, so no sweep can block the loop regardless of table size. Keep well under
|
||
// the ~50ms invariant per batch.
|
||
statusLogPruneBatch: parseInt(process.env.STATUS_LOG_PRUNE_BATCH) || 2000,
|
||
// #146 P1.3 kill switch: when false, interval maintenance runs regardless of loop-lag
|
||
// band (disables the band-gate that skips maintenance while loaded). Startup prune is
|
||
// never band-gated regardless.
|
||
maintenanceBandGateEnabled: process.env.MAINTENANCE_BAND_GATE_ENABLED !== 'false',
|
||
// #146 hardening (Item C) — /download/apk GLOBAL guards (NOT per-IP; SNAT collapses
|
||
// the fleet to one IP). Concurrency + rate caps + critical-band shed protect the loop
|
||
// and IO from a download flood; the aggregate counter makes a flood VISIBLE (the old
|
||
// per-IP-per-10min log throttle hid it under SNAT).
|
||
otaDownloadGuardEnabled: process.env.OTA_DOWNLOAD_GUARD_ENABLED !== 'false', // #146 P1.3 kill switch
|
||
otaDownloadMaxConcurrent: parseInt(process.env.OTA_DOWNLOAD_MAX_CONCURRENT) || 10,
|
||
otaDownloadMaxPerWindow: parseInt(process.env.OTA_DOWNLOAD_MAX_PER_WINDOW) || 120,
|
||
otaDownloadWindowMs: parseInt(process.env.OTA_DOWNLOAD_WINDOW_MS) || 60000,
|
||
otaApkRefreshMs: parseInt(process.env.OTA_APK_REFRESH_MS) || 60000,
|
||
// #146 observability: rolling window for the /api/status.debug throughput counters, so
|
||
// "lastWindow" is comparable across subsystems.
|
||
debugStatsWindowMs: parseInt(process.env.DEBUG_STATS_WINDOW_MS) || 60000,
|
||
// #146: env DEFAULT for the /api/status debug block; a persisted app_settings value
|
||
// (admin toggle) overrides this once set. Default on (matches prior behavior).
|
||
statusDebugEnabled: process.env.STATUS_DEBUG_ENABLED !== 'false',
|
||
|
||
// #146 BILLING — usage metering per the ByteTinker–Bold Media distribution agreement.
|
||
// This is the contractual system-of-record; the DEFAULTS BELOW ARE THE AGREEMENT. Change
|
||
// them only if the contract changes. Single GLOBAL rate card for now — per-tenant rate
|
||
// cards are a future concern (would key the table by workspace/org).
|
||
billing: {
|
||
// ASD (Active Screen-Day) denominator = a "standard 8-hour day" → 8*3600 = 28800s.
|
||
hoursPerDay: parseInt(process.env.BILLING_HOURS_PER_DAY) || 8,
|
||
// FLAT (not marginal) tier: the single rate whose minScreens is the greatest ≤ the
|
||
// month's total Billable Screens applies to ALL of them. Ascending by minScreens.
|
||
rateTable: parseBillingRateTable(process.env.BILLING_RATE_TABLE) || [
|
||
{ minScreens: 1, rate: 1.50 },
|
||
{ minScreens: 500, rate: 1.25 },
|
||
{ minScreens: 1000, rate: 1.00 },
|
||
],
|
||
// device_usage_daily is tiny (1 row/device/day) and retained long; pruned (chunked)
|
||
// beyond this so it can never bloat-then-freeze.
|
||
usageRetentionDays: parseInt(process.env.BILLING_USAGE_RETENTION_DAYS) || 400,
|
||
// Accumulator: UPSERT chunk size per transaction (chunked so a huge fleet can't block
|
||
// the loop), and the max seconds credited per accrual tick — a stall/restart guard so a
|
||
// long gap between ticks can't inject a bogus large credit (default 30s = 3× the 10s tick).
|
||
accrualBatch: parseInt(process.env.BILLING_ACCRUAL_BATCH) || 2000,
|
||
accrualCapSeconds: parseInt(process.env.BILLING_ACCRUAL_CAP_SECONDS) || 30,
|
||
},
|
||
// #146 Item E — coalescing log flush + batched event_loop_lag telemetry.
|
||
logCoalesceFlushMs: parseInt(process.env.LOG_COALESCE_FLUSH_MS) || 30000,
|
||
lagFlushMs: parseInt(process.env.LAG_FLUSH_MS) || 10000,
|
||
lagBufferMax: parseInt(process.env.LAG_BUFFER_MAX) || 2000,
|
||
// #146 device_status_log write batching (lib/status-log-writer.js). Status
|
||
// transitions are buffered and coalesced to the NET state per device per flush,
|
||
// so a flapping device writes ~1 row/flush instead of a row per transition —
|
||
// breaking the storm -> table-growth -> slow-writes -> more-lag feedback loop.
|
||
statusLogFlushMs: parseInt(process.env.STATUS_LOG_FLUSH_MS) || 1000,
|
||
|
||
// #142 content-ack dedup window (deviceSocket.js). A device (esp. older apps)
|
||
// can spam "content <id>: ready" for the same item; suppress identical
|
||
// (device_id, content_id, status) reports within this window. A status CHANGE
|
||
// has a different key and passes immediately. In-memory; resets on restart.
|
||
contentAckDedupMs: parseInt(process.env.CONTENT_ACK_DEDUP_MS) || 10000,
|
||
|
||
// #143 content-ack RATE budget (lib/content-ack-limiter.js), layered on top of the
|
||
// dedup above. Caps TOTAL acks per device per window REGARDLESS of differing
|
||
// content_id — the flood the dedup misses (a device cycling 2-4 ids makes every
|
||
// ack look unique, so dedup never fires, yet aggregate volume blocks the loop).
|
||
// TUNING GUESSES — validate against Bold's real fleet. Legit playlist cadence is
|
||
// roughly <=1 ack/s/device; the flood is many/s. 20 per 10s (=2/s) sits above
|
||
// legit and below the flood. Easy to retune via env.
|
||
contentAckMaxPerWindow: parseInt(process.env.CONTENT_ACK_MAX_PER_WINDOW) || 20,
|
||
contentAckRateWindowMs: parseInt(process.env.CONTENT_ACK_RATE_WINDOW_MS) || 10000,
|
||
|
||
// #143 fingerprint-reclaim liveness. A reinstalled app (same fingerprint, no
|
||
// device_id, has pairing_code) may reclaim its old device's identity once that
|
||
// device is gone by RUNTIME signals: no live socket AND last heartbeat older than
|
||
// this settle window. Previously an effective 24h calendar grace treated a device
|
||
// merely offline <24h as "active", so a legitimately-gone device (liveConn=false,
|
||
// status=offline, stale heartbeat) could never reclaim and retried every ~2s,
|
||
// flooding logs (Bold beta1). Settle = the max reclaim wait; keep it comfortably
|
||
// above heartbeatTimeout (45s) so a brief blip isn't mistaken for "gone".
|
||
// SECURITY TRADEOFF: this also shortens the anti-(fingerprint-theft) window from
|
||
// 24h — raise it to re-tighten, at the cost of reinstall latency. Tuning guess.
|
||
reclaimSettleSeconds: parseInt(process.env.RECLAIM_SETTLE_SECONDS) || 300,
|
||
// #143 throttle the reclaim-deferred log to once per device per window, so a
|
||
// retrying/stuck device can't flood stdout (same discipline as the content-ack shed log).
|
||
reclaimRejectLogWindowMs: parseInt(process.env.RECLAIM_REJECT_LOG_WINDOW_MS) || 60000,
|
||
};
|