app
Mouser Electronics Search
Public Made by Adomby adom
Mouser component search in your workspace — stock, quantity price breaks, datasheets and lifecycle. CLI verbs, a Hydrogen app, and a shared backend service.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
//! Persistent quota tracker — counts searches + results returned per
//! calendar day (Central Time, where Mouser's daily quota resets). Fires
//! a one-shot Google Chat (Kel) webhook alert the moment we see the
//! first quota-exhausted response for the day.
//!
//! Stored at `/tmp/adom-mouser-quota.json` so it survives restarts but
//! not reboots (the counter resets at midnight CT anyway).
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
use std::sync::Mutex;
use std::time::Duration;
const STATE_PATH: &str = "/tmp/adom-mouser-quota.json";
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct QuotaState {
/// "YYYY-MM-DD" in America/Chicago (Central). Resets at midnight CT.
pub date_ct: String,
pub searches: u64,
pub results_returned: u64,
/// Unix epoch seconds (UTC) of first quota-exhausted response today.
pub first_quota_hit_ts: Option<i64>,
/// Whether the Kel alert webhook has been fired for today's quota hit.
pub alerted: bool,
}
static STATE: Mutex<Option<QuotaState>> = Mutex::new(None);
/// Current date in America/Chicago as "YYYY-MM-DD". Shells out to
/// `date` with `TZ=America/Chicago` so we inherit the system tzdb and
/// get DST handled for free. Falls back to UTC if `date` is unavailable.
fn today_ct() -> String {
let out = std::process::Command::new("date")
.env("TZ", "America/Chicago")
.arg("+%Y-%m-%d")
.output()
.ok()
.and_then(|o| if o.status.success() { String::from_utf8(o.stdout).ok() } else { None })
.map(|s| s.trim().to_string())
.filter(|s| s.len() == 10);
out.unwrap_or_else(|| {
// Fallback: UTC date (may be wrong by 1 day near midnight CT).
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0) as i64;
let days = secs / 86400;
let z = days + 719468;
let era = if z >= 0 { z } else { z - 146096 } / 146097;
let doe = (z - era * 146097) as u64;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
let m = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32;
let y = if m <= 2 { y + 1 } else { y };
format!("{:04}-{:02}-{:02}", y, m, d)
})
}
fn load() -> QuotaState {
let raw = fs::read_to_string(STATE_PATH).unwrap_or_default();
serde_json::from_str::<QuotaState>(&raw).unwrap_or_default()
}
fn save(s: &QuotaState) {
if let Ok(p) = serde_json::to_string(s) {
let _ = fs::write(Path::new(STATE_PATH), p);
}
}
/// Initialize (load-from-disk / reset-if-new-day) and return a clone.
pub fn get() -> QuotaState {
let mut guard = STATE.lock().unwrap();
if guard.is_none() {
*guard = Some(load());
}
let s = guard.as_mut().unwrap();
let today = today_ct();
if s.date_ct != today {
// New day — reset.
*s = QuotaState { date_ct: today, ..Default::default() };
save(s);
}
s.clone()
}
/// Increment on every successful search response.
pub fn record_success(results_returned: u64) {
let mut guard = STATE.lock().unwrap();
if guard.is_none() {
*guard = Some(load());
}
let s = guard.as_mut().unwrap();
let today = today_ct();
if s.date_ct != today {
*s = QuotaState { date_ct: today, ..Default::default() };
}
s.searches += 1;
s.results_returned += results_returned;
save(s);
}
/// Record a quota-exhausted event. Returns `true` exactly once per day
/// (first-hit-of-the-day), so callers can use the return value to
/// trigger a single Kel alert per day.
pub fn record_quota_hit() -> bool {
let mut guard = STATE.lock().unwrap();
if guard.is_none() {
*guard = Some(load());
}
let s = guard.as_mut().unwrap();
let today = today_ct();
if s.date_ct != today {
*s = QuotaState { date_ct: today, ..Default::default() };
}
s.searches += 1;
if s.first_quota_hit_ts.is_none() {
s.first_quota_hit_ts = Some(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0),
);
}
let should_alert = !s.alerted;
if should_alert {
s.alerted = true;
}
save(s);
should_alert
}
/// Post a one-line Google Chat message via an incoming webhook. No-op
/// if `KEL_ALERT_WEBHOOK` is unset. Uses a short timeout so we never
/// block the search-response path on a slow chat API.
pub fn post_kel_alert(text: &str) {
let url = match std::env::var("KEL_ALERT_WEBHOOK") {
Ok(u) if !u.is_empty() => u,
_ => return,
};
let body = serde_json::json!({ "text": text });
let agent = ureq::AgentBuilder::new()
.timeout(Duration::from_secs(3))
.build();
// Fire-and-forget: never panic on failure.
let _ = agent
.post(&url)
.set("Content-Type", "application/json")
.send_json(body);
}