[{"data":1,"prerenderedAt":1219},["ShallowReactive",2],{"nav-stories":3,"projects-list":61},[4,16,25,34,43,52],{"id":5,"color":6,"extension":7,"image":8,"label":9,"link":10,"meta":11,"order":12,"stem":13,"text":14,"__hash__":15},"stories\u002Fstories\u002F01-data-center.yml",null,"yml","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1558494949-ef010cbdcc31?w=1080","DATA_CENTER","https:\u002F\u002Fx.com\u002Fabbeytetteh_",{},1,"stories\u002F01-data-center","Racking new servers. 40gbit backbone online.","0QUZQbaANhdO8WemZxkDdO7vbVopfnynHtH9FxBZb_w",{"id":17,"color":6,"extension":7,"image":18,"label":19,"link":6,"meta":20,"order":21,"stem":22,"text":23,"__hash__":24},"stories\u002Fstories\u002F02-thoughts.yml","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1498050108023-c5249f4df085?w=1080","THOUGHTS",{},2,"stories\u002F02-thoughts","Late night bug hunting. Found the memory leak.","Gd1am954aasY6HRHD7hCtOuessXb6zYZ8iizS501ICg",{"id":26,"color":27,"extension":7,"image":6,"label":28,"link":6,"meta":29,"order":30,"stem":31,"text":32,"__hash__":33},"stories\u002Fstories\u002F03-coding.yml","#3b82f6","CODING",{},3,"stories\u002F03-coding","Just thinking about how much easier life is with Swarm. https:\u002F\u002Fgoogle.com","-WTk-47jnLM-TZRWBg0VbJyZJfIM7FpQ5HGbc8LEdhQ",{"id":35,"color":6,"extension":7,"image":36,"label":37,"link":6,"meta":38,"order":39,"stem":40,"text":41,"__hash__":42},"stories\u002Fstories\u002F04-update.yml","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1591799264318-7e6ef8ddb7ea?w=1080","UPDATE",{},4,"stories\u002F04-update","New cluster nodes arrived. Prepping for installation.","kyT60N5C6Re_jMonZbgNy0PbQhzXmUWxDbD0D_v43ts",{"id":44,"color":45,"extension":7,"image":6,"label":46,"link":6,"meta":47,"order":48,"stem":49,"text":50,"__hash__":51},"stories\u002Fstories\u002F05-setup.yml","#86868b","SETUP",{},5,"stories\u002F05-setup","Optimizing the telemetry pipeline for 1M req\u002Fs.","cPOBkzoyXsCmPgRO2d80Hj3vm4MP-6nAejtlQ5iuSzw",{"id":53,"color":6,"extension":7,"image":54,"label":55,"link":6,"meta":56,"order":57,"stem":58,"text":59,"__hash__":60},"stories\u002Fstories\u002F06-travel.yml","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1560969184-10fe8719e047?w=1080","TRAVEL",{},6,"stories\u002F06-travel","Travel log — system architecture workshop in Berlin.","jnOxerdF6usAIHdR35Z-opx0LJAy9kZluXnZhtz62Z0",[62,445,522],{"id":63,"title":64,"body":65,"description":433,"extension":434,"liveUrl":6,"meta":435,"navigation":436,"order":21,"path":437,"seo":438,"stack":439,"stem":443,"thumbnail":6,"__hash__":444},"projects\u002Fprojects\u002Flleven-v1.md","Lleven V1: The Genesis",{"type":66,"value":67,"toc":418},"minimark",[68,72,77,85,90,118,122,130,134,137,159,163,178,223,227,251,255,314,322,325,385,389,415],[69,70,71],"p",{},"The first version of Lleven was born out of a simple need: to make sense of Mobile Money (MoMo) statements without the manual hassle. It was a lean, focused tool designed to transform a raw PDF into a \"Wrapped\" experience—much like Spotify, but for your spending.",[73,74,76],"h2",{"id":75},"architecture-overview","Architecture Overview",[69,78,79,80,84],{},"The V1 architecture was a classic ",[81,82,83],"strong",{},"Synchronous Processing"," model. It prioritized simplicity and immediate feedback for small-to-medium statements.",[86,87,89],"h3",{"id":88},"tech-stack","Tech Stack",[91,92,93,100,106,112],"ul",{},[94,95,96,99],"li",{},[81,97,98],{},"Framework:"," FastAPI",[94,101,102,105],{},[81,103,104],{},"Processing:"," Pandas & PDFPlumber",[94,107,108,111],{},[81,109,110],{},"Caching:"," Redis",[94,113,114,117],{},[81,115,116],{},"Security:"," Fernet (AES-128) Encryption for cached data",[73,119,121],{"id":120},"the-processing-engine","The Processing Engine",[69,123,124,125,129],{},"Lleven V1 used a specialized parsing engine built on top of ",[126,127,128],"code",{},"pdfplumber",".",[86,131,133],{"id":132},"_1-validation-logic","1. Validation Logic",[69,135,136],{},"Before processing, the system checked for specific \"magic strings\" in the first page of the PDF to ensure it was a valid MTN MoMo statement. These included:",[91,138,139,144,149,154],{},[94,140,141],{},[126,142,143],{},"MSISDN:",[94,145,146],{},[126,147,148],{},"Time Run:",[94,150,151],{},[126,152,153],{},"TRANSACTION DATE",[94,155,156],{},[126,157,158],{},"ACCOUNT HOLDER NAME:",[86,160,162],{"id":161},"_2-data-extraction","2. Data Extraction",[69,164,165,166,169,170,173,174,177],{},"The engine targeted tables with a ",[126,167,168],{},"vertical_strategy"," and ",[126,171,172],{},"horizontal_strategy"," set to ",[126,175,176],{},"\"lines\"",". It mapped raw PDF columns to a structured internal format:",[91,179,180,213],{},[94,181,182,185,186,188,189,188,192,188,195,188,198,188,201,188,204,188,207,188,210,129],{},[81,183,184],{},"Raw Mapping:"," ",[126,187,153],{},", ",[126,190,191],{},"FROM ACCT",[126,193,194],{},"FROM NO.",[126,196,197],{},"TRANS. TYPE",[126,199,200],{},"AMOUNT",[126,202,203],{},"TO NO.",[126,205,206],{},"TO NAME",[126,208,209],{},"REF",[126,211,212],{},"OVA",[94,214,215,218,219,222],{},[81,216,217],{},"Cleaning:"," It used regex to identify the ",[126,220,221],{},"ACCOUNT_HOLDER_NO"," from the header text to distinguish between incoming and outgoing funds.",[86,224,226],{"id":225},"_3-data-cleaning-pipeline","3. Data Cleaning Pipeline",[91,228,229,239,245],{},[94,230,231,234,235,238],{},[81,232,233],{},"Date Normalization:"," Converted string dates (e.g., ",[126,236,237],{},"21-May-2023 10:30:00 AM",") into proper Python datetime objects.",[94,240,241,244],{},[81,242,243],{},"Type Casting:"," Converted currency strings into floats for arithmetic operations.",[94,246,247,250],{},[81,248,249],{},"Normalization:"," Removed newline characters and extra whitespace from names and references using custom regex cleaning.",[73,252,254],{"id":253},"the-upload-workflow","The Upload Workflow",[256,257,258,267,277,283,305],"ol",{},[94,259,260,263,264,129],{},[81,261,262],{},"Request:"," User uploads a PDF to ",[126,265,266],{},"\u002Fprocess-file",[94,268,269,272,273,276],{},[81,270,271],{},"Deduplication:"," A SHA-256 hash of the file is generated. If the hash exists in Redis, the system returns the existing ",[126,274,275],{},"file_hash"," immediately.",[94,278,279,282],{},[81,280,281],{},"Parsing:"," If new, the system extracts the table, cleans the data, and caps it to the requested year (e.g., 2023).",[94,284,285,288],{},[81,286,287],{},"Encrypted Caching:",[91,289,290,296,302],{},[94,291,292,293,129],{},"The resulting DataFrame is serialized using ",[126,294,295],{},"pickle",[94,297,298,299,129],{},"It is then encrypted using ",[81,300,301],{},"Fernet (symmetric encryption)",[94,303,304],{},"The encrypted blob is stored in Redis with a 1-hour TTL.",[94,306,307,310,311,313],{},[81,308,309],{},"Response:"," Returns the ",[126,312,275],{}," and an expiry timestamp.",[73,315,317,318,321],{"id":316},"the-retrieval-workflow-get-wrapped","The Retrieval Workflow (",[126,319,320],{},"\u002Fget-wrapped",")",[69,323,324],{},"When the user requests their \"Wrapped\" results:",[256,326,327,330,333],{},[94,328,329],{},"The system pulls the encrypted blob from Redis.",[94,331,332],{},"It decrypts and deserializes the DataFrame.",[94,334,335,338,339],{},[81,336,337],{},"On-the-Fly Analytics:"," It runs a series of summary algorithms:\n",[91,340,341,360,366,372],{},[94,342,343,346,347,188,350,188,353,356,357,129],{},[81,344,345],{},"Spending Summary:"," Aggregates totals for ",[126,348,349],{},"PAYMENT",[126,351,352],{},"CASH_OUT",[126,354,355],{},"TRANSFER",", and ",[126,358,359],{},"DEBIT",[94,361,362,365],{},[81,363,364],{},"Frequency Analysis:"," Calculates the top 5 recipients by amount and frequency.",[94,367,368,371],{},[81,369,370],{},"Monthly Trends:"," Groups transactions by month to visualize spending patterns.",[94,373,374,377,378,380,381,384],{},[81,375,376],{},"Credit Summary:"," Identifies salary or incoming transfers by filtering for the user's ",[126,379,221],{}," in the ",[126,382,383],{},"TO_NO"," column.",[73,386,388],{"id":387},"limitations-of-v1","Limitations of V1",[91,390,391,397,403,409],{},[94,392,393,396],{},[81,394,395],{},"The \"Timeout\" Wall:"," Large PDFs (50+ pages) often caused HTTP timeouts because the API waited for the entire extraction to finish before responding.",[94,398,399,402],{},[81,400,401],{},"Memory Pressure:"," Since processing happened on the API workers, high concurrent uploads could lead to OOM (Out of Memory) errors.",[94,404,405,408],{},[81,406,407],{},"Stateless Persistence:"," Data only lived in Redis. If the cache expired, the user had to re-upload the file.",[94,410,411,414],{},[81,412,413],{},"Lack of Identity:"," No user accounts meant users couldn't see a history of their past uploads without keeping the file hashes themselves.",[69,416,417],{},"Lleven V1 proved the concept, but the stage was set for a more robust, scalable, and secure V2.",{"title":419,"searchDepth":21,"depth":21,"links":420},"",[421,424,429,430,432],{"id":75,"depth":21,"text":76,"children":422},[423],{"id":88,"depth":30,"text":89},{"id":120,"depth":21,"text":121,"children":425},[426,427,428],{"id":132,"depth":30,"text":133},{"id":161,"depth":30,"text":162},{"id":225,"depth":30,"text":226},{"id":253,"depth":21,"text":254},{"id":316,"depth":21,"text":431},"The Retrieval Workflow (\u002Fget-wrapped)",{"id":387,"depth":21,"text":388},"The first version of Lleven, a synchronous processing engine designed to transform raw Mobile Money PDFs into structured spending analytics.","md",{},true,"\u002Fprojects\u002Flleven-v1",{"title":64,"description":433},[440,441,442],"FastAPI","Pandas","Redis","projects\u002Flleven-v1","HUCFXtkqCH7TKZwiJKSCxzJevh-30bUSAoJtS1E_9Mc",{"id":446,"title":447,"body":448,"description":511,"extension":434,"liveUrl":512,"meta":513,"navigation":436,"order":12,"path":514,"seo":515,"stack":516,"stem":520,"thumbnail":6,"__hash__":521},"projects\u002Fprojects\u002Flleven-v2.md","Lleven V2: Scale & Security",{"type":66,"value":449,"toc":506},[450,453,456,460,463,483,486,490,493,496,500,503],[69,451,452],{},"Following the success and the hard lessons learned from the initial prototype, Lleven V2 was completely re-architected. Where V1 hit memory limits and HTTP timeouts with large PDFs, V2 adopts a robust, event-driven architecture designed for scale and security.",[454,455],"reference",{"path":437},[73,457,459],{"id":458},"the-asynchronous-pipeline","The Asynchronous Pipeline",[69,461,462],{},"To solve the \"Timeout Wall\" of V1, the entire parsing engine was decoupled from the HTTP request cycle.",[256,464,465,471,477],{},[94,466,467,470],{},[81,468,469],{},"Upload:"," Files are securely uploaded and placed into an encrypted object store.",[94,472,473,476],{},[81,474,475],{},"Queueing:"," An event is fired to Redis Streams, queuing the document for processing.",[94,478,479,482],{},[81,480,481],{},"Background Workers:"," Dedicated consumer services written in Go pick up the job, run the intensive parsing, and update the database asynchronously.",[69,484,485],{},"This means a user uploading a 200-page statement gets an immediate response, while the system processes the file safely in the background.",[73,487,489],{"id":488},"identity-and-persistence","Identity and Persistence",[69,491,492],{},"A core limitation of V1 was the lack of user identity. In V2, we introduced a robust identity management system. Users can securely authenticate, manage their accounts, and review their processing history.",[69,494,495],{},"Data is no longer strictly ephemeral in a volatile cache; insights are persisted securely, allowing users to track their financial trends over time without re-uploading the same documents.",[73,497,499],{"id":498},"real-time-telemetry","Real-time Telemetry",[69,501,502],{},"With the new architecture, we implemented Server-Sent Events (SSE) to push real-time status updates back to the UI. The frontend (rebuilt in Nuxt and Vue) provides a live, reactive experience as the statement goes through validation, extraction, and analytics stages.",[69,504,505],{},"V2 sets a new standard for performance, reliability, and user experience.",{"title":419,"searchDepth":21,"depth":21,"links":507},[508,509,510],{"id":458,"depth":21,"text":459},{"id":488,"depth":21,"text":489},{"id":498,"depth":21,"text":499},"The next generation of the Lleven engine, featuring asynchronous processing, resilient data pipelines, and a secure user identity layer.","https:\u002F\u002Flleven.app",{},"\u002Fprojects\u002Flleven-v2",{"title":447,"description":511},[517,518,519],"Vue","Nuxt","Go","projects\u002Flleven-v2","OqhBSYnbfS0zohtEx0Nj449VSmaWGjiut8rKc8oODdA",{"id":523,"title":524,"body":525,"description":1210,"extension":434,"liveUrl":6,"meta":1211,"navigation":436,"order":30,"path":1212,"seo":1213,"stack":1214,"stem":1217,"thumbnail":6,"__hash__":1218},"projects\u002Fprojects\u002Fproject-overview.md","Iris — Giving LGTM a brain",{"type":66,"value":526,"toc":1187},[527,531,534,537,540,544,547,553,559,565,567,571,575,604,608,625,629,650,654,671,675,692,696,725,729,743,747,764,768,791,795,812,816,830,834,848,852,863,865,869,872,980,983,1003,1005,1009,1020,1025,1073,1083,1086,1094,1097,1099,1103,1129,1131,1135],[73,528,530],{"id":529},"overview","Overview",[69,532,533],{},"Iris is a production-grade infrastructure monitoring platform built for KNUST (Kwame Nkrumah University of Science and Technology). It automates the onboarding of servers into a Prometheus-based observability stack, manages the full lifecycle of infrastructure incidents, and gives the monitoring environment an intelligent layer through AI-powered analysis, automated digests, and operational runbooks.",[69,535,536],{},"The system is named after the Greek goddess of the rainbow and messenger of the gods — fitting for a platform whose job is to relay the state of infrastructure to the people responsible for it.",[538,539],"hr",{},[73,541,543],{"id":542},"what-it-does","What It Does",[69,545,546],{},"At its core, Iris solves a real operational problem: getting dozens (or hundreds) of servers properly instrumented, monitored, and connected to the right people when something goes wrong — without doing it manually each time.",[69,548,549,552],{},[81,550,551],{},"Host Enrollment"," is the entry point. An administrator submits a host — Linux or Windows — and Iris SSH or WinRM's into it, installs the Grafana Alloy metrics agent, deploys a configuration tailored to the services running on that host, registers it in Prometheus service discovery, and sends a confirmation notification. What would take 20 minutes manually takes under 2 minutes, consistently, for every host.",[69,554,555,558],{},[81,556,557],{},"Incident Management"," is where Iris does its most important work. AlertManager fires a webhook when Prometheus rules trigger. Iris receives that webhook, enriches the alert with context from a vector knowledge base (relevant runbooks, similar past incidents, infrastructure documentation), generates an AI-authored notification with recommended remediation steps, routes it to the right team via Microsoft Teams and email, and stores the incident for future learning. Every alert becomes more useful than it would be alone.",[69,560,561,564],{},[81,562,563],{},"Operational Intelligence"," sits on top of all of this. Iris collects metrics snapshots, tracks maintenance windows with full lifecycle management, notifies teams when services are deployed, delivers daily, weekly, and monthly digest reports scoped to each maintainer's specific responsibilities, and continuously builds a richer knowledge base that makes future incidents faster to resolve.",[538,566],{},[73,568,570],{"id":569},"key-features","Key Features",[86,572,574],{"id":573},"automated-host-enrollment","Automated Host Enrollment",[91,576,577,580,583,586,589,592,595,598,601],{},[94,578,579],{},"SSH-based enrollment for Linux hosts (Debian, RHEL, SUSE, and derivatives)",[94,581,582],{},"WinRM-based enrollment for Windows hosts",[94,584,585],{},"Automatic detection of host OS, architecture, and installed services",[94,587,588],{},"Grafana Alloy agent installation with version management",[94,590,591],{},"Jinja2-templated Alloy configurations backed by a database template store",[94,593,594],{},"Support for service-specific configurations: node metrics, Nginx, Apache, MySQL, PostgreSQL, MongoDB, Windows Performance Counters",[94,596,597],{},"Prometheus target file registration with full label management (hostname, job, environment, service type, host ID)",[94,599,600],{},"Validation of prerequisites: disk space, connectivity, systemd availability",[94,602,603],{},"Firewall rule management for metrics ports",[86,605,607],{"id":606},"batch-enrollment","Batch Enrollment",[91,609,610,613,616,619,622],{},[94,611,612],{},"Bulk enrollment via JSON, CSV, or YAML file upload",[94,614,615],{},"Sequential or concurrent execution strategies",[94,617,618],{},"Per-host progress tracking and result storage",[94,620,621],{},"Retry logic for failed hosts",[94,623,624],{},"Downloadable templates for batch file formats",[86,626,628],{"id":627},"prometheus-grafana-alloy-integration","Prometheus & Grafana Alloy Integration",[91,630,631,634,637,640,643],{},[94,632,633],{},"File-based service discovery for Prometheus",[94,635,636],{},"Support for multiple federated Prometheus instances",[94,638,639],{},"Atomic target file updates with Prometheus reload",[94,641,642],{},"Prometheus API querying for target verification and metrics collection",[94,644,645,646,649],{},"Alloy configuration validation using ",[126,647,648],{},"alloy fmt"," on the remote host",[86,651,653],{"id":652},"alertmanager-webhook-processing","AlertManager Webhook Processing",[91,655,656,659,662,665,668],{},[94,657,658],{},"Receives AlertManager v4 webhook payloads",[94,660,661],{},"Asynchronous processing via Celery task queue",[94,663,664],{},"Alert enrichment with runbooks, similar incidents, and infrastructure context",[94,666,667],{},"LLM-generated incident notifications with remediation recommendations",[94,669,670],{},"Team-aware routing based on service and host ownership",[86,672,674],{"id":673},"ai-powered-incident-intelligence-rag","AI-Powered Incident Intelligence (RAG)",[91,676,677,680,683,686,689],{},[94,678,679],{},"Weaviate vector database for semantic storage and retrieval",[94,681,682],{},"Ollama-backed embeddings (nomic-embed-text) and language model (Llama 3)",[94,684,685],{},"Three knowledge collections: runbooks, past incidents, infrastructure documentation",[94,687,688],{},"Semantic search across all collections at incident time",[94,690,691],{},"Continuously growing knowledge base as incidents are processed and resolved",[86,693,695],{"id":694},"host-tagging-system","Host Tagging System",[91,697,698,716,719,722],{},[94,699,700,701,188,704,188,707,188,710,188,713],{},"Operational tags: ",[126,702,703],{},"ignore_alerts",[126,705,706],{},"known_issue",[126,708,709],{},"under_maintenance",[126,711,712],{},"flaky",[126,714,715],{},"custom",[94,717,718],{},"Optional expiry timestamps for temporary tags",[94,720,721],{},"Metadata key-value pairs for custom annotation",[94,723,724],{},"Notification suppression for tagged hosts",[86,726,728],{"id":727},"incident-lifecycle-management","Incident Lifecycle Management",[91,730,731,734,737,740],{},[94,732,733],{},"Full incident storage with status tracking (firing → resolved)",[94,735,736],{},"Resolution notes and root cause documentation",[94,738,739],{},"Filter and query by alert name, instance, severity, service type, status",[94,741,742],{},"Resolution time tracking and SLA flagging in digest reports",[86,744,746],{"id":745},"automated-digests","Automated Digests",[91,748,749,752,755,758,761],{},[94,750,751],{},"Daily, weekly, and monthly digest reports via Celery Beat",[94,753,754],{},"Personalized: each maintainer receives only their hosts and services",[94,756,757],{},"Severity distribution, SLA flags, and resolution status",[94,759,760],{},"Delivered via Microsoft Teams adaptive cards and HTML email",[94,762,763],{},"Stakeholder segmentation: scoped maintainers, service stakeholders, infrastructure stakeholders, general recipients",[86,765,767],{"id":766},"maintenance-window-management","Maintenance Window Management",[91,769,770,773,776,779,782,785,788],{},[94,771,772],{},"Full lifecycle: plan → start → extend → end \u002F cancel",[94,774,775],{},"Types: scheduled and emergency",[94,777,778],{},"Categories: infrastructure, application, network, database, security",[94,780,781],{},"Automated notifications at window start, during, and on completion",[94,783,784],{},"AI-generated summaries of maintenance activities",[94,786,787],{},"Configurable reminder scheduling before planned windows",[94,789,790],{},"Bulk cancel operations",[86,792,794],{"id":793},"deployment-notifications","Deployment Notifications",[91,796,797,800,803,806,809],{},[94,798,799],{},"Deployment event ingestion with version, environment, and service information",[94,801,802],{},"Semantic version comparison (upgrade vs rollback detection)",[94,804,805],{},"AI-generated change summaries from commit messages",[94,807,808],{},"Notifications routed to service maintainers",[94,810,811],{},"Deployment record storage for audit trail",[86,813,815],{"id":814},"metrics-collection-snapshots","Metrics Collection & Snapshots",[91,817,818,821,824,827],{},[94,819,820],{},"Prometheus metrics scraped and stored in PostgreSQL every 15 minutes",[94,822,823],{},"Configurable retention (default 90 days) with automated pruning",[94,825,826],{},"CPU, memory, and disk thresholds with warning and critical levels",[94,828,829],{},"Historical trend data for digest and reporting card generation",[86,831,833],{"id":832},"service-maintainer-registry","Service & Maintainer Registry",[91,835,836,839,842,845],{},[94,837,838],{},"Service registry with maintainer and escalation manager assignments",[94,840,841],{},"Host-level maintainer overrides independent of service assignments",[94,843,844],{},"Notification preference flags per host and service",[94,846,847],{},"Used across enrollment, incident routing, digests, and deployment notifications",[86,849,851],{"id":850},"observability-of-iris-itself","Observability of Iris Itself",[91,853,854,857,860],{},[94,855,856],{},"OpenTelemetry integration for distributed tracing, metrics, and structured logging",[94,858,859],{},"Configurable OTLP export to a collector endpoint",[94,861,862],{},"Per-service tracing across FastAPI routes and Celery tasks",[538,864],{},[73,866,868],{"id":867},"architecture","Architecture",[69,870,871],{},"Iris is built on a modern async Python stack:",[873,874,875,888],"table",{},[876,877,878],"thead",{},[879,880,881,885],"tr",{},[882,883,884],"th",{},"Layer",[882,886,887],{},"Technology",[889,890,891,900,908,916,924,932,940,948,956,964,972],"tbody",{},[879,892,893,897],{},[894,895,896],"td",{},"API Framework",[894,898,899],{},"FastAPI (async)",[879,901,902,905],{},[894,903,904],{},"Task Queue",[894,906,907],{},"Celery with Redis broker",[879,909,910,913],{},[894,911,912],{},"Scheduler",[894,914,915],{},"Celery Beat",[879,917,918,921],{},[894,919,920],{},"Primary Database",[894,922,923],{},"PostgreSQL 16 (via asyncpg \u002F SQLModel)",[879,925,926,929],{},[894,927,928],{},"Vector Database",[894,930,931],{},"Weaviate 1.33",[879,933,934,937],{},[894,935,936],{},"AI \u002F Embeddings",[894,938,939],{},"Ollama (Llama 3, nomic-embed-text)",[879,941,942,945],{},[894,943,944],{},"Remote Execution",[894,946,947],{},"Paramiko (SSH), PyWinRM (WinRM)",[879,949,950,953],{},[894,951,952],{},"Notifications",[894,954,955],{},"Microsoft Teams (webhooks + Graph API), KNUST Email Gateway",[879,957,958,961],{},[894,959,960],{},"Monitoring Agent",[894,962,963],{},"Grafana Alloy",[879,965,966,969],{},[894,967,968],{},"Metrics Source",[894,970,971],{},"Prometheus",[879,973,974,977],{},[894,975,976],{},"Observability",[894,978,979],{},"OpenTelemetry",[69,981,982],{},"The application is split into three cooperating processes:",[91,984,985,991,997],{},[94,986,987,990],{},[81,988,989],{},"iris"," — the FastAPI API server, handling synchronous request\u002Fresponse and dispatching async work",[94,992,993,996],{},[81,994,995],{},"iris-worker"," — one or more Celery worker processes executing enrollment, incident processing, digest generation, maintenance, and deployment tasks",[94,998,999,1002],{},[81,1000,1001],{},"iris-beat"," — the Celery Beat scheduler driving periodic tasks (digests, metrics collection, metric pruning)",[538,1004],{},[73,1006,1008],{"id":1007},"deployment","Deployment",[69,1010,1011,1012,1015,1016,1019],{},"Iris runs on Docker Swarm in production, deployed to KNUST's internal Docker registry (",[126,1013,1014],{},"dreg.knust.edu.gh","). The stack is pinned to a specific monitoring node (",[126,1017,1018],{},"knust-monitoring",") and shares a Docker overlay network with the rest of the monitoring stack (Prometheus, Grafana, AlertManager, Weaviate, Ollama).",[69,1021,1022],{},[81,1023,1024],{},"Production resource allocation:",[873,1026,1027,1040],{},[876,1028,1029],{},[879,1030,1031,1034,1037],{},[882,1032,1033],{},"Container",[882,1035,1036],{},"Memory",[882,1038,1039],{},"CPU",[889,1041,1042,1053,1063],{},[879,1043,1044,1047,1050],{},[894,1045,1046],{},"iris (API)",[894,1048,1049],{},"2 GB limit \u002F 512 MB reserved",[894,1051,1052],{},"1.0 \u002F 0.25",[879,1054,1055,1057,1060],{},[894,1056,995],{},[894,1058,1059],{},"4 GB limit \u002F 1 GB reserved",[894,1061,1062],{},"2.0 \u002F 0.5",[879,1064,1065,1067,1070],{},[894,1066,1001],{},[894,1068,1069],{},"Lightweight",[894,1071,1072],{},"Minimal",[69,1074,1075,1076,1079,1080,129],{},"Prometheus target files are mounted directly from the host at ",[126,1077,1078],{},"\u002Fopt\u002Fmonitoring-stack\u002Fshared\u002Fprometheus\u002Ftargets",", allowing Iris to write target files that Prometheus reads without any additional network hop. Iris configuration files (templates, credentials) are mounted from ",[126,1081,1082],{},"\u002Fopt\u002Fmonitoring-stack\u002Fshared\u002Firis\u002F",[69,1084,1085],{},"The application is built from a two-stage Dockerfile:",[256,1087,1088,1091],{},[94,1089,1090],{},"A builder stage installs Python dependencies from KNUST's private PyPI registry",[94,1092,1093],{},"A slim runtime stage runs the application as a non-root user",[69,1095,1096],{},"Database migrations are managed with Alembic, and schema evolution is handled as part of the deployment pipeline.",[538,1098],{},[73,1100,1102],{"id":1101},"security","Security",[91,1104,1105,1108,1111,1114,1117,1120,1123,1126],{},[94,1106,1107],{},"API key authentication middleware (configurable, disabled in dev)",[94,1109,1110],{},"All secrets externalized to environment variables",[94,1112,1113],{},"SSH private key support for host enrollment (no password storage required)",[94,1115,1116],{},"Azure AD app registration for Microsoft Teams Graph API access",[94,1118,1119],{},"KNUST email gateway uses API key authentication over HTTPS",[94,1121,1122],{},"CORS configured per environment (open in dev, restricted in production)",[94,1124,1125],{},"Non-root container user in production images",[94,1127,1128],{},"Internal Docker network isolation between services",[538,1130],{},[73,1132,1134],{"id":1133},"technology-summary","Technology Summary",[69,1136,1137,1140,1141,1140,1143,1140,1146,1140,1149,1140,1152,1140,1155,1140,1157,1140,1159,1140,1162,1140,1165,1140,1167,1140,1170,1140,1172,1140,1175,1140,1178,1140,1181,1140,1184],{},[81,1138,1139],{},"Python 3.12"," · ",[81,1142,440],{},[81,1144,1145],{},"Celery",[81,1147,1148],{},"PostgreSQL",[81,1150,1151],{},"Weaviate",[81,1153,1154],{},"Ollama",[81,1156,963],{},[81,1158,971],{},[81,1160,1161],{},"AlertManager",[81,1163,1164],{},"Microsoft Teams",[81,1166,979],{},[81,1168,1169],{},"Docker Swarm",[81,1171,442],{},[81,1173,1174],{},"Paramiko",[81,1176,1177],{},"SQLModel",[81,1179,1180],{},"Alembic",[81,1182,1183],{},"Jinja2",[81,1185,1186],{},"LangChain",{"title":419,"searchDepth":21,"depth":21,"links":1188},[1189,1190,1191,1206,1207,1208,1209],{"id":529,"depth":21,"text":530},{"id":542,"depth":21,"text":543},{"id":569,"depth":21,"text":570,"children":1192},[1193,1194,1195,1196,1197,1198,1199,1200,1201,1202,1203,1204,1205],{"id":573,"depth":30,"text":574},{"id":606,"depth":30,"text":607},{"id":627,"depth":30,"text":628},{"id":652,"depth":30,"text":653},{"id":673,"depth":30,"text":674},{"id":694,"depth":30,"text":695},{"id":727,"depth":30,"text":728},{"id":745,"depth":30,"text":746},{"id":766,"depth":30,"text":767},{"id":793,"depth":30,"text":794},{"id":814,"depth":30,"text":815},{"id":832,"depth":30,"text":833},{"id":850,"depth":30,"text":851},{"id":867,"depth":21,"text":868},{"id":1007,"depth":21,"text":1008},{"id":1101,"depth":21,"text":1102},{"id":1133,"depth":21,"text":1134},"A production-grade infrastructure monitoring platform built for KNUST that automates server onboarding and incident management.",{},"\u002Fprojects\u002Fproject-overview",{"title":524,"description":1210},[971,1215,1216],"Grafana","Alloy","projects\u002Fproject-overview","Sn0pP1YAnd1n2VzaB74IfOLY6HyS1GRFqlRJWYlqNO0",1780657374434]