Time unification

This commit is contained in:
2026-06-17 15:06:38 -04:00
parent 069a44bd54
commit 54a2460eac
67 changed files with 1442 additions and 1329 deletions
@@ -140,6 +140,12 @@ console.log('DB Connection Info:', {
password: (dbConfig.password || dbConfig.connectionString) ? '****' : 'MISSING' // Only show if credentials exist
});
// Pin the session timezone to business time (see docs/TIME.md) — window
// anchors (CURRENT_DATE etc.) in the metrics SQL must mean Chicago dates.
if (!dbConfig.options) {
dbConfig.options = '-c TimeZone=America/Chicago';
}
const pool = new Pool(dbConfig);
const getConnection = () => {
@@ -18,6 +18,18 @@ import json
import time
import logging
from datetime import datetime, date, timedelta
from zoneinfo import ZoneInfo
BUSINESS_TZ = ZoneInfo('America/Chicago')
def business_today() -> date:
"""The business date (Chicago calendar date — see docs/TIME.md).
Never business_today(): the server runs on Europe/Berlin, which flips to the
next day at 5/6pm Central and would shift forecast_date anchors.
"""
return datetime.now(BUSINESS_TZ).date()
import numpy as np
import pandas as pd
@@ -740,7 +752,7 @@ def batch_load_product_data(conn, products):
GROUP BY pid
"""
adf = execute_query(conn, arrival_sql, [preorder_pids])
today = date.today()
today = business_today()
for _, row in adf.iterrows():
pid = int(row['pid'])
fa = row['future_arrival']
@@ -948,7 +960,7 @@ def forecast_mature(product, history_df):
hist['snapshot_date'] = pd.to_datetime(hist['snapshot_date'])
hist = hist.set_index('snapshot_date')['units_sold']
full_index = pd.date_range(
end=pd.Timestamp(date.today() - timedelta(days=1)),
end=pd.Timestamp(business_today() - timedelta(days=1)),
periods=EXP_SMOOTHING_WINDOW, freq='D')
series = hist.reindex(full_index, fill_value=0.0).values.astype(float)
@@ -1070,7 +1082,7 @@ def generate_all_forecasts(conn, curves_df, dow_indices, monthly_indices=None,
log.info("Batch loading product data...")
batch_data = batch_load_product_data(conn, products)
today = date.today()
today = business_today()
forecast_dates = [today + timedelta(days=i) for i in range(FORECAST_HORIZON_DAYS)]
# Pre-compute DOW and seasonal multipliers for each forecast date.
@@ -1676,7 +1688,7 @@ def backfill_accuracy_data(conn, backfill_days=30):
# Batch load product data for per-product scaling
batch_data = batch_load_product_data(conn, active)
today = date.today()
today = business_today()
backfill_start = today - timedelta(days=backfill_days)
# Create a synthetic run entry
@@ -14,6 +14,10 @@
* /var/www/inventory/.env (or current process env).
*/
// Run in business time (see docs/TIME.md); the spawned Python engine and any
// incidental Date math inherit it.
process.env.TZ = process.env.TZ || 'America/Chicago';
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
+4
View File
@@ -1,3 +1,7 @@
// Run the whole update cycle in business time (see docs/TIME.md). The cron
// that invokes this script does not set TZ; child scripts inherit it.
process.env.TZ = process.env.TZ || 'America/Chicago';
const path = require('path');
const fs = require('fs');
const { spawn } = require('child_process');
+17 -1
View File
@@ -23,6 +23,17 @@ const IMPORT_STOCK_SNAPSHOTS = true;
const INCREMENTAL_UPDATE = process.env.INCREMENTAL_UPDATE !== 'false'; // Default to true unless explicitly set to false
// SSH configuration
// Current UTC offset of America/Chicago ('-05:00' or '-06:00'), DST-aware.
function currentChicagoOffset() {
const tzName = new Intl.DateTimeFormat('en-US', {
timeZone: 'America/Chicago',
timeZoneName: 'longOffset',
})
.formatToParts(new Date())
.find((p) => p.type === 'timeZoneName').value; // e.g. 'GMT-05:00'
return tzName.replace('GMT', '') || '+00:00';
}
const sshConfig = {
ssh: {
host: process.env.PROD_SSH_HOST,
@@ -40,7 +51,12 @@ const sshConfig = {
password: process.env.PROD_DB_PASSWORD,
database: process.env.PROD_DB_NAME,
port: process.env.PROD_DB_PORT || 3306,
timezone: '-05:00', // mysql2 driver timezone — corrected at runtime via adjustDateForMySQL() in utils.js
// DATETIMEs on this server store Central wall-clock. Use the CURRENT
// Chicago offset (DST-aware) so both inbound parsing and outbound
// serialization are correct in winter (CST=-06:00) and summer
// (CDT=-05:00). adjustDateForMySQL() in utils.js remains as a runtime
// safety net (its correction is 0 when this offset matches the server).
timezone: currentChicagoOffset(),
},
localDbConfig: {
// PostgreSQL config for local
+13 -3
View File
@@ -58,7 +58,12 @@ async function setupConnections(sshConfig) {
"SELECT TIMESTAMPDIFF(SECOND, NOW(), UTC_TIMESTAMP()) as utcDiffSec"
);
const mysqlOffsetMs = -utcDiffSec * 1000; // MySQL UTC offset in ms (e.g., -21600000 for CST)
const driverOffsetMs = -5 * 3600 * 1000; // Driver's -05:00 in ms (-18000000)
// Driver offset from the actual config (e.g. '-06:00' in winter, '-05:00'
// in summer — set dynamically in import-from-prod.js).
const tzMatch = /^([+-])(\d{2}):(\d{2})$/.exec(sshConfig.prodDbConfig.timezone || '');
const driverOffsetMs = tzMatch
? (tzMatch[1] === '-' ? -1 : 1) * (Number(tzMatch[2]) * 3600 + Number(tzMatch[3]) * 60) * 1000
: -5 * 3600 * 1000;
const tzCorrectionMs = driverOffsetMs - mysqlOffsetMs;
// CST (winter): -18000000 - (-21600000) = +3600000 (1 hour correction needed)
// CDT (summer): -18000000 - (-18000000) = 0 (no correction needed)
@@ -79,8 +84,13 @@ async function setupConnections(sshConfig) {
}
prodConnection.adjustDateForMySQL = adjustDateForMySQL;
// Setup PostgreSQL connection pool for local
const localPool = new Pool(sshConfig.localDbConfig);
// Setup PostgreSQL connection pool for local.
// Pin the session timezone to business time (see docs/TIME.md) so import
// writes and any CURRENT_DATE/NOW()::date logic mean Chicago dates.
const localPool = new Pool({
options: '-c TimeZone=America/Chicago',
...sshConfig.localDbConfig,
});
// Test the PostgreSQL connection
try {
@@ -1,3 +1,7 @@
-- SESSION-TZ ASSUMPTION: this script runs with TimeZone=America/Chicago
-- (pinned in the pool config and as the inventory_db database default).
-- CURRENT_DATE / NOW()::date / ts::date here mean the BUSINESS date
-- (Chicago calendar day = 1am-Eastern day rule). See docs/TIME.md.
-- Description: Calculates and updates aggregated metrics per brand.
-- Dependencies: product_metrics, products, calculate_status table.
-- Frequency: Daily (after product_metrics update).
@@ -1,3 +1,7 @@
-- SESSION-TZ ASSUMPTION: this script runs with TimeZone=America/Chicago
-- (pinned in the pool config and as the inventory_db database default).
-- CURRENT_DATE / NOW()::date / ts::date here mean the BUSINESS date
-- (Chicago calendar day = 1am-Eastern day rule). See docs/TIME.md.
-- Description: Calculates and updates aggregated metrics per category with hierarchy rollups.
-- Dependencies: product_metrics, products, categories, product_categories, category_hierarchy, calculate_status table.
-- Frequency: Daily (after product_metrics update).
@@ -1,3 +1,7 @@
-- SESSION-TZ ASSUMPTION: this script runs with TimeZone=America/Chicago
-- (pinned in the pool config and as the inventory_db database default).
-- CURRENT_DATE / NOW()::date / ts::date here mean the BUSINESS date
-- (Chicago calendar day = 1am-Eastern day rule). See docs/TIME.md.
-- Description: Calculates and updates aggregated metrics per vendor.
-- Dependencies: product_metrics, products, purchase_orders, calculate_status table.
-- Frequency: Daily (after product_metrics update).
@@ -1,3 +1,7 @@
-- SESSION-TZ ASSUMPTION: this script runs with TimeZone=America/Chicago
-- (pinned in the pool config and as the inventory_db database default).
-- CURRENT_DATE / NOW()::date / ts::date here mean the BUSINESS date
-- (Chicago calendar day = 1am-Eastern day rule). See docs/TIME.md.
-- Description: Calculates and updates daily aggregated product data.
-- Self-healing: detects gaps (missing snapshots), stale data (snapshot
-- aggregates that don't match source tables after backfills), and always
@@ -1,3 +1,7 @@
-- SESSION-TZ ASSUMPTION: this script runs with TimeZone=America/Chicago
-- (pinned in the pool config and as the inventory_db database default).
-- CURRENT_DATE / NOW()::date / ts::date here mean the BUSINESS date
-- (Chicago calendar day = 1am-Eastern day rule). See docs/TIME.md.
-- Description: Populates lifecycle forecast columns on product_metrics from product_forecasts.
-- Runs AFTER update_product_metrics.sql so that lead time / days of stock settings are available.
-- Dependencies: product_metrics (fully populated), product_forecasts, settings tables.
@@ -1,3 +1,7 @@
-- SESSION-TZ ASSUMPTION: this script runs with TimeZone=America/Chicago
-- (pinned in the pool config and as the inventory_db database default).
-- CURRENT_DATE / NOW()::date / ts::date here mean the BUSINESS date
-- (Chicago calendar day = 1am-Eastern day rule). See docs/TIME.md.
-- Description: Calculates metrics that don't need hourly updates, like ABC class
-- and average lead time.
-- Dependencies: product_metrics, purchase_orders, settings_global, calculate_status.
@@ -1,3 +1,7 @@
-- SESSION-TZ ASSUMPTION: this script runs with TimeZone=America/Chicago
-- (pinned in the pool config and as the inventory_db database default).
-- CURRENT_DATE / NOW()::date / ts::date here mean the BUSINESS date
-- (Chicago calendar day = 1am-Eastern day rule). See docs/TIME.md.
-- Description: Calculates and updates the main product_metrics table based on current data
-- and aggregated daily snapshots. Uses UPSERT for idempotency.
-- Dependencies: Core import tables, daily_product_snapshots, configuration tables, calculate_status.
@@ -10,6 +10,9 @@ const dbConfig = {
database: process.env.DB_NAME,
port: process.env.DB_PORT || 5432,
ssl: process.env.DB_SSL === 'true',
// Pin the session timezone to business time (see docs/TIME.md) — the
// metrics SQL window anchors (CURRENT_DATE etc.) must mean Chicago dates.
options: '-c TimeZone=America/Chicago',
// Add performance optimizations
max: 10, // connection pool max size
idleTimeoutMillis: 30000,