Skill: wysh3/mrc-monitor (version 1.0.0)

--- name: mrc-monitor description: Real-time token monitoring for MRC canteen order system. Monitors Firebase Firestore for token status and notifies when orders are ready. Use when user sends commands like "mrc 73", "token 97", or "monitor 42" to monitor one or multiple canteen tokens. Handles multiple tokens simultaneously, sends independent notifications per token, and auto-exits when all tokens are ready. --- # MRC Canteen Monitor Monitor MRC canteen order tokens and notify when they're ready for pickup. ## Quick Start When user sends any command containing canteen tokens: 1. Extract all token numbers from the message 2. Start the background monitor script 3. Respond immediately with confirmation ## Command Recognition Users may send tokens with various prefixes: - "mrc 73" or "mrc 73 97 42" - "token 73" or "token 73 97" - "monitor 73" - "check 73" (one-time check only) ## Starting the Monitor Extract all numbers from the user message and start the background monitor: ```bash python3 skills/mrc-monitor/scripts/monitor.py <platform> <channel_id> <token1> <token2> ... ``` Where: - `platform`: "telegram" or "discord" - `channel_id`: Current channel identifier (platform prefix is optional, e.g., `telegram_123` or `123` both work) - `token1`, `token2`, ...: Token numbers to monitor Example: ```bash python3 skills/mrc-monitor/scripts/monitor.py telegram telegram_6046286675 73 97 42 # or python3 skills/mrc-monitor/scripts/monitor.py telegram 6046286675 73 97 42 ``` ## Background Execution Start the monitor as a background process so the agent responds immediately: ```python import subprocess # channel_id can be with or without platform prefix (both work) cmd = ['python3', 'skills/mrc-monitor/scripts/monitor.py', platform, channel_id] + [str(t) for t in tokens] subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) ``` ## Agent Response After starting the monitor, respond immediately with: ``` ✅ Monitoring tokens: 73, 97, 42 Checking every 15 seconds. I'll notify you here when they're ready! 🍕 ``` ## One-Time Check For "check 73" commands, perform a single Firebase query and respond with status without starting a background monitor. ## Monitor Behavior The monitor script: - Polls Firebase Firestore every 15 seconds - Checks all monitored tokens in each poll - Sends "🍕 Order X is ready!" notification when a token's status is "Ready" - Removes notified tokens from the watch list - Exits automatically when all tokens are notified - Handles errors gracefully with retries - Logs all activity to `skills/mrc-monitor/logs/monitor_YYYYMMDD_HHMMSS.log` ## Error Handling The script automatically handles: - Network timeouts (retries up to 5 times) - HTTP errors (including rate limits) - Unexpected errors (stops after 5 consecutive failures) - Signal termination (SIGTERM, SIGINT) On fatal errors, the script sends a notification before exiting. ## Firebase Details - **Project**: kanteen-mrc-blr-24cfa - **Collection**: orders - **Document fields**: - `studentId` (string): "student-{token_number}" - `status` (string): "Preparing", "Ready", "Completed"

path: scripts/monitor.py
content: #!/usr/bin/env python3
"""
MRC Token Monitor
Starts on demand, monitors given tokens, exits when done.
"""

import sys
import time
import signal
import logging
import subprocess
from datetime import datetime
from pathlib import Path

# ───────────────────── CONFIG ─────────────────────
FIREBASE_API_KEY = [REDACTED_SECRET]
FIREBASE_PROJECT = "kanteen-mrc-blr-24cfa"
FIREBASE_URL = f"https://firestore.googleapis.com/v1/projects/{FIREBASE_PROJECT}/databases/(default)/documents/orders"

POLL_INTERVAL = 15  # seconds
API_TIMEOUT = 8  # seconds
MAX_ERRORS = 5  # consecutive before quit
MAX_CHECKS = 720  # ~3 hours

# ───────────────────── MAIN ─────────────────────
def main():
    if len(sys.argv) < 4:
        print("Usage: monitor.py <platform> <channel_id> <token1> [token2 ...]", file=sys.stderr)
        sys.exit(1)

    platform = sys.argv[1]
    channel_id = sys.argv[2]
    # Strip platform prefix if present (e.g., telegram_12345 -> 12345)
    if channel_id.startswith(platform + '_'):
        channel_id = channel_id[len(platform) + 1:]
    tokens = [int(t) for t in sys.argv[3:]]

    # Setup logging
    logs_dir = Path("skills/mrc-monitor/logs")
    logs_dir.mkdir(parents=True, exist_ok=True)

    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    log_file = logs_dir / f"monitor_{timestamp}.log"

    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s | %(levelname)s | %(message)s',
        handlers=[
            logging.FileHandler(log_file),
            logging.StreamHandler(sys.stdout)
        ]
    )

    logger = logging.getLogger("mrc_monitor")

    # Signal handlers
    def handle_signal(signum, frame):
        logger.info(f"🛑 Signal {signum}, shutting down...")
        sys.exit(0)

    signal.signal(signal.SIGTERM, handle_signal)
    signal.signal(signal.SIGINT, handle_signal)

    logger.info(f"🚀 Monitoring tokens: {tokens}")
    logger.info(f"📱 {platform}:{channel_id}")

    waiting = set(tokens)
    notified = set()
    status_map = {token: None for token in tokens}
    consecutive_errors = 0
    checks_count = 0

    def send_notification(text):
        """Send notification via openclaw message tool."""
        try:
            # Map platform names for openclaw CLI
            platform_map = {
                'telegram': 'telegram',
                'discord': 'discord',
                'whatsapp': 'whatsapp',
            }
            channel_platform = platform_map.get(platform, platform)

            cmd = ['openclaw', 'message', 'send',
                   '--channel', channel_platform,
                   '--target', channel_id,
                   '--message', text]

            result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)

            if result.returncode != 0:
                logger.error(f"❌ CLI error: {result.stderr}")
                return False
            return True
        except Exception as e:
            logger.error(f"❌ Notification failed: {e}")
            return False

    def fetch_orders():
        """Fetch all orders from Firebase."""
        import requests
        params = {'key': FIREBASE_API_KEY}
        response = requests.get(FIREBASE_URL, params=params, timeout=API_TIMEOUT)
        response.raise_for_status()
        return response.json()

    def check_tokens(orders, waiting_tokens):
        """Check all tokens against orders."""
        if 'documents' not in orders:
            return {t: {'found': False, 'ready': False, 'status': None} for t in waiting_tokens}

        lookup = {}
        for doc in orders['documents']:
            fields = doc.get('fields', {})
            student_id = fields.get('studentId', {}).get('stringValue', '')
            status = fields.get('status', {}).get('stringValue', '')

            if student_id.startswith('student-'):
                token_num = int(student_id.split('-')[1])
                lookup[token_num] = {'found': True, 'status': status, 'ready': status == 'Ready'}

        return {t: lookup.get(t, {'found': False, 'ready': False, 'status': None}) for t in waiting_tokens}

    # Main loop
    while waiting and consecutive_errors < MAX_ERRORS and checks_count < MAX_CHECKS:
        try:
            import requests
            orders = fetch_orders()
            results = check_tokens(orders, waiting)
            checks_count += 1

            newly_ready = []
            for token in waiting:
                r = results[token]

                if r['status'] and r['status'] != status_map[token]:
                    logger.info(f"📝 Token {token}: {status_map[token]} → {r['status']}")
                    status_map[token] = r['status']

                if r['found'] and not r['ready']:
                    logger.info(f"🔍 Token {token}: Found, status={r['status']}")

                if r['ready'] and token not in notified:
                    newly_ready.append(token)

            for token in newly_ready:
                msg = f"🍕 Order {token} is ready!\nGo pick up your food! 🏃"
                if send_notification(msg):
                    notified.add(token)
                    waiting.remove(token)
                    logger.info(f"✅ Token {token} notified")

            if waiting:
                logger.info(f"✅ Poll {checks_count}: Waiting for {len(waiting)} tokens: {sorted(waiting)}")
            else:
                logger.info(f"✅ All done! {len(notified)} tokens notified")
                break

            consecutive_errors = 0
            time.sleep(POLL_INTERVAL)

        except requests.Timeout:
            consecutive_errors += 1
            logger.warning(f"⚠️ Timeout {consecutive_errors}/{MAX_ERRORS}")
            if consecutive_errors >= MAX_ERRORS:
                send_notification(f"⚠️ Stopped (timeouts). Waiting: {sorted(waiting)}")
                return 1
            time.sleep(POLL_INTERVAL)

        except requests.HTTPError as e:
            consecutive_errors += 1
            logger.error(f"❌ HTTP: {e}")
            if e.response.status_code == 429:
                time.sleep(60)
            elif consecutive_errors >= MAX_ERRORS:
                send_notification(f"⚠️ Stopped (errors). Waiting: {sorted(waiting)}")
                return 1
            else:
                time.sleep(POLL_INTERVAL)

        except Exception as e:
            consecutive_errors += 1
            logger.error(f"❌ Error: {e}")
            if consecutive_errors >= MAX_ERRORS:
                send_notification(f"⚠️ Stopped (errors). Waiting: {sorted(waiting)}")
                return 1
            time.sleep(POLL_INTERVAL)

    if checks_count >= MAX_CHECKS and waiting:
        logger.warning("⏰ Timeout reached")
        send_notification(f"⏰ Timeout. Never appeared: {sorted(waiting)}")

    logger.info("✅ Shutdown complete")
    return 0

if __name__ == "__main__":
    sys.exit(main())


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)