<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Harshwardhan S. Ranvir</title>
    <description>The latest articles on DEV Community by Harshwardhan S. Ranvir (@harshwardhan_ranvir).</description>
    <link>https://dev.to/harshwardhan_ranvir</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3980920%2F5fd53aa7-778f-46ca-8c91-bcefb6e5b245.jpg</url>
      <title>DEV Community: Harshwardhan S. Ranvir</title>
      <link>https://dev.to/harshwardhan_ranvir</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/harshwardhan_ranvir"/>
    <language>en</language>
    <item>
      <title>Voyage Canvas: I Built a Full-Stack AI Travel Planner for Two College Subjects at Once</title>
      <dc:creator>Harshwardhan S. Ranvir</dc:creator>
      <pubDate>Sat, 20 Jun 2026 14:40:36 +0000</pubDate>
      <link>https://dev.to/harshwardhan_ranvir/voyage-canvas-i-built-a-full-stack-ai-travel-planner-for-two-college-subjects-at-once-4b30</link>
      <guid>https://dev.to/harshwardhan_ranvir/voyage-canvas-i-built-a-full-stack-ai-travel-planner-for-two-college-subjects-at-once-4b30</guid>
      <description>&lt;p&gt;&lt;em&gt;Flask + Groq API + Supabase + OpenWeatherMap. Multi-page web app. Solo build. Here's how it works and what I learned.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Travel planning is fragmented. You open five tabs — flights on one, hotels on another, things-to-do on a third, budget calculator somewhere else. By the time you have a rough plan, you've spent two hours and you're not even sure the budget holds.&lt;/p&gt;

&lt;p&gt;I wanted one thing: enter a destination, set a budget, get a complete day-by-day plan. No switching tabs. No mental math.&lt;/p&gt;

&lt;p&gt;So I built Voyage Canvas.&lt;/p&gt;

&lt;p&gt;This was a college project — two subjects, one deadline. I architected it to satisfy both: a full-stack web application for my main project and a cloud computing project requiring SaaS, DBaaS, and PaaS components.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Ranvir2028/Voyage-Canvas" rel="noopener noreferrer"&gt;github.com/Ranvir2028/Voyage-Canvas&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;Voyage Canvas is an AI-powered travel planner. You give it a destination, travel dates, budget, interests, group type, and accommodation preference. It returns a complete, day-by-day itinerary with morning/afternoon/evening activities, real cost estimates, local tips, weather, and a budget breakdown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Core features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI-generated itineraries (Groq API — Llama 3.3 70B with fallback chain)&lt;/li&gt;
&lt;li&gt;Smart budget allocation across accommodation, food, transport, activities&lt;/li&gt;
&lt;li&gt;Live weather + 5-day forecast via OpenWeatherMap&lt;/li&gt;
&lt;li&gt;Auto local currency detection for 80+ destinations&lt;/li&gt;
&lt;li&gt;User accounts with saved itinerary history&lt;/li&gt;
&lt;li&gt;Destination explorer with AI-powered search&lt;/li&gt;
&lt;li&gt;PDF export and print&lt;/li&gt;
&lt;li&gt;Deployed on Render with Supabase PostgreSQL as the database&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What it does NOT do:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No real flight/hotel booking (it's a planner, not a booking engine)&lt;/li&gt;
&lt;li&gt;No live pricing — cost estimates are AI-generated approximations&lt;/li&gt;
&lt;li&gt;Currency rates are hardcoded (not live exchange rates)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;voyage-canvas/
├── backend/
│   ├── app.py              ← Flask server + all API routes
│   ├── ai_service.py       ← Groq API integration + fallback chain
│   ├── weather_service.py  ← OpenWeatherMap integration
│   ├── budget_service.py   ← Budget allocation logic
│   ├── currency_service.py ← Local currency detection
│   ├── database.py         ← PostgreSQL via Supabase
│   └── settings.py         ← Config + env vars
│
└── frontend/
    ├── pages/              ← 6 HTML pages
    ├── css/                ← 5 stylesheets
    └── js/                 ← 6 JS modules
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Python 3.12, Flask, Flask-CORS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI&lt;/td&gt;
&lt;td&gt;Groq API (Llama 3.3 70B)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;PostgreSQL via Supabase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weather&lt;/td&gt;
&lt;td&gt;OpenWeatherMap API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;Token-based (SHA-256 + secrets)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Render (web service) + Supabase (DBaaS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;Vanilla HTML/CSS/JS — no framework&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Flask serves both the API and the frontend pages from a single server. No React, no Vue — just clean vanilla JS with a modular file structure.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deep Dive: How Each Component Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. AI Itinerary Generation — &lt;code&gt;ai_service.py&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is the core of the project. The &lt;code&gt;generate_itinerary()&lt;/code&gt; function takes user inputs and builds a structured prompt that tells the model exactly what JSON to return.&lt;/p&gt;

&lt;p&gt;The prompt enforces strict rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;You are a professional travel planner. Generate a complete &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-day itinerary for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.

CRITICAL RULES:
1. You MUST generate EXACTLY &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; day objects in the &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; array
2. Return ONLY valid JSON — no markdown, no explanation, no extra text
3. Every field must have a real value — no nulls, no empty strings
4. Budget numbers must be realistic for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;currency_sym&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; total over &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; days
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response schema is detailed — each day object has morning, afternoon, and evening slots, each with time, title, description, location, cost, and an insider tip. Plus top-level fields for summary, best time to visit, local cuisine, packing list, weather, and budget breakdown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fallback chain:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;LLMs hit rate limits. Groq's free tier is especially aggressive. So &lt;code&gt;ai_service.py&lt;/code&gt; doesn't just call one model — it tries a chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;FALLBACK_MODELS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llama-3.3-70b-versatile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# Primary
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llama-3.1-8b-instant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# Fallback 1
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mixtral-8x7b-32768&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;# Fallback 2
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemma2-9b-it&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;              &lt;span class="c1"&gt;# Fallback 3
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the primary model hits a 429, it waits (up to 20 seconds, per the &lt;code&gt;Retry-After&lt;/code&gt; header) and tries the next model. This makes the app resilient without requiring a paid API plan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The day count problem:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI models don't always return the exact number of days you ask for. A 7-day request might come back with 5 days. The function detects this and fills the gap programmatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;existing_days&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing_days&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing_days&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;existing_days&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;day&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Day &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;theme&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Day &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; — Exploration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="bp"&gt;...&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="n"&gt;existing_days&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not elegant, but it guarantees the frontend never breaks because an itinerary is missing days.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Database — &lt;code&gt;database.py&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Supabase provides the managed PostgreSQL instance — the schema itself is still mine to define. &lt;code&gt;init_db()&lt;/code&gt; runs this on startup:&lt;/p&gt;

&lt;p&gt;Three tables: &lt;code&gt;users&lt;/code&gt;, &lt;code&gt;sessions&lt;/code&gt;, &lt;code&gt;itineraries&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;           &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;username&lt;/span&gt;     &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt;        &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;password&lt;/span&gt;     &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;full_name&lt;/span&gt;    &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt;   &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;sessions&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt;      &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;    &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;itineraries&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;     &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;destination&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;duration&lt;/span&gt;    &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;budget&lt;/span&gt;      &lt;span class="nb"&gt;REAL&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;data&lt;/span&gt;        &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;-- Full JSON blob&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt;  &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Auth is token-based. On login, a 64-character hex token is generated with &lt;code&gt;secrets.token_hex(32)&lt;/code&gt; and stored in the sessions table. Every protected API request checks this token via the &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;Passwords are SHA-256 hashed with a salt before storage. Not bcrypt — a tradeoff made for a deadline-bound college project, not something I'd ship to production as-is.&lt;/p&gt;

&lt;p&gt;Generated itineraries are stored as JSON blobs in the &lt;code&gt;data&lt;/code&gt; column. The full AI response goes in as a string, retrieved and parsed on demand. Simple, and it works.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Budget Service — &lt;code&gt;budget_service.py&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The budget allocator splits the total budget across six categories using percentage weights:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accommodation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;food&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transport&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;activities&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shopping&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;misc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="mf"&gt;0.05&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The weights adjust based on selected interests. Shopping-heavy trips shift 5% from misc to shopping. Adventure/outdoor trips shift 5% from food to activities. The allocation is also broken down per day, so users see both the total and the daily allowance for each category.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. Currency Service — &lt;code&gt;currency_service.py&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;80+ destinations mapped to their local currencies with hardcoded exchange rates. The function does keyword matching — if "tokyo" or "japan" appears in the destination string, it returns JPY with rate 149.5.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_currency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;dest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;currency&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;CURRENCY_MAP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;dest&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;dest&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;USD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symbol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;US Dollar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The limitation is obvious: exchange rates change and these don't. A live rates API would fix this. I didn't have time to wire one in before the deadline.&lt;/p&gt;




&lt;h3&gt;
  
  
  5. Flask Backend — &lt;code&gt;app.py&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Flask serves both the frontend pages and the REST API from a single process. Page routes serve HTML files directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/planner&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;planner&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;send_from_directory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PAGES_DIR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;planner.html&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;API routes handle data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/generate-itinerary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_itinerary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Auto-save if user is logged in
&lt;/span&gt;    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_current_user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;save_itinerary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;itinerary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generated itineraries auto-save to the database if the user is logged in. No extra step required.&lt;/p&gt;

&lt;p&gt;For deployment, the host is set to &lt;code&gt;0.0.0.0&lt;/code&gt; — required for Render, which injects the &lt;code&gt;$PORT&lt;/code&gt; environment variable dynamically. &lt;code&gt;gunicorn&lt;/code&gt; handles production serving.&lt;/p&gt;




&lt;h3&gt;
  
  
  6. Frontend Architecture
&lt;/h3&gt;

&lt;p&gt;Six pages, no framework. Each page has its own JS module:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;main.js&lt;/code&gt; — global utilities, API helper, toast notifications, date formatting&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;auth.js&lt;/code&gt; — login, register, logout, auth guard&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;planner.js&lt;/code&gt; — 4-step form with progress indicator&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;itinerary.js&lt;/code&gt; — render the full AI response, budget pie chart, maps iframe&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;explore.js&lt;/code&gt; — destination cards, modal, AI-powered search&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dashboard.js&lt;/code&gt; — user stats, saved itineraries, profile&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The API helper in &lt;code&gt;main.js&lt;/code&gt; automatically attaches the auth token to every request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;apiCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vc_token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Authorization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The design is a dark luxury travel magazine aesthetic — deep navy, gold accents, Cormorant Garamond for display text, Bebas Neue for stamps. CSS variables handle the full design system.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Planner Flow
&lt;/h2&gt;

&lt;p&gt;Four steps, one page, no reloads:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1 — Destination &amp;amp; Dates:&lt;/strong&gt; City name, start date, number of days&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2 — Budget:&lt;/strong&gt; Slider with USD/INR toggle. Budget updates in real time. Currency auto-detects based on destination.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3 — Preferences:&lt;/strong&gt; Interest tags (Culture, Food, Adventure, Nightlife, etc.), group type (Solo/Couple/Family/Friends), accommodation tier (1-star to 5-star, Airbnb)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4 — Summary &amp;amp; Generate:&lt;/strong&gt; Review all inputs, then hit Generate. The AI call takes 10-20 seconds. A loading overlay with animated text keeps the user informed.&lt;/p&gt;

&lt;p&gt;On success, the full itinerary JSON is saved to &lt;code&gt;localStorage&lt;/code&gt; and the user is redirected to &lt;code&gt;/itinerary&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cloud Architecture (How It Satisfied Two Subjects)
&lt;/h2&gt;

&lt;p&gt;This is the part I planned deliberately.&lt;/p&gt;

&lt;p&gt;The project had to satisfy a cloud computing subject's requirements for SaaS, DBaaS, and PaaS components alongside the main web project. Instead of building two separate projects, I designed Voyage Canvas to cover both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SaaS&lt;/strong&gt; — Voyage Canvas itself. A web application delivered as a service, accessible from any browser&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DBaaS&lt;/strong&gt; — Supabase. Managed PostgreSQL. No server to configure, no backups to manage manually&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PaaS&lt;/strong&gt; — Render. The Flask application deploys from a &lt;code&gt;render.yaml&lt;/code&gt; config. Push to GitHub, it deploys&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI as a Service&lt;/strong&gt; — Groq API. The AI capability is consumed as an external API, not self-hosted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One codebase. Two subjects. One deadline. That's the decision I'm most satisfied with in this project.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sample Output
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Itinerary generation request:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"destination"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tokyo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"days"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"budget"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"interests"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Culture"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Food"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Shopping"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"groupType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Solo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"accommodation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3-star"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What comes back (abbreviated):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"destination"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tokyo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"totalDays"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A 5-day solo journey through Tokyo..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"days"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"day"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"theme"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Arrival &amp;amp; First Impressions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"morning"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Senso-ji Temple, Asakusa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"cost"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"tips"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Arrive before 8am to avoid crowds"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"budgetBreakdown"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"accommodation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"food"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"transport"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"activities"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"shopping"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"misc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Voyage Canvas's Home Page Screenshot:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Figi8f3lply7gipnz8l9p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Figi8f3lply7gipnz8l9p.png" alt="Voyage Canvas's Home Page" width="800" height="425"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Engineering Decisions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why Flask over Django or FastAPI?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Scope and speed. Flask gave me exactly what I needed — routing, JSON responses, static file serving — without the overhead of learning Django's ORM or FastAPI's async patterns under a deadline. For a project this size, Flask is the right call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why no frontend framework?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;React or Vue would have added a build step, npm dependency management, and component architecture overhead. The app has six pages and predictable state. Vanilla JS with modular files handled it cleanly. The choice kept the project deployable without a separate frontend build process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Supabase over SQLite?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The cloud computing requirement pushed me toward a managed database. But even without that constraint, SQLite breaks the moment you deploy to a stateless server — Render's filesystem resets on every deploy. Supabase gave me a persistent PostgreSQL instance with a free tier that covers the project's needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why store itineraries as JSON blobs?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The AI response structure is nested and variable — different trip lengths produce different shapes of data. Normalizing it into relational tables would have required 5+ tables and complex joins. Storing the full JSON blob and parsing it on retrieval is simpler, faster to build, and easier to change. The tradeoff is you can't query inside the itinerary data — acceptable for this use case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why a fallback model chain?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Groq's free tier has aggressive rate limits. A single model would fail under any real load. The fallback chain — Llama 3.3 70B → Llama 3.1 8B → Mixtral → Gemma — means the app degrades gracefully instead of returning a 429 error to the user. The user might get a slightly less capable model, but they get a response.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Prompt engineering is real engineering&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Getting the AI to return valid, consistently structured JSON every time took more iteration than any other part of the project. Vague prompts produce vague output. The final prompt is explicit to the point of being verbose — it restates the day count requirement three times, includes an example JSON structure, and lists critical rules as numbered items.&lt;/p&gt;

&lt;p&gt;The model still sometimes returns fewer days than requested. Hence the programmatic fallback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Stateless servers break assumptions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First deployment on Render: the app worked locally, failed in production. The issue was &lt;code&gt;localhost&lt;/code&gt; vs &lt;code&gt;0.0.0.0&lt;/code&gt; for the Flask host and SQLite file paths that assumed persistent storage. Switching to &lt;code&gt;0.0.0.0&lt;/code&gt; and Supabase fixed both. Local development assumptions don't always hold on cloud platforms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Rate limits require architecture decisions, not just error handling&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A try-catch around an API call handles the error. A fallback model chain handles the user experience. The difference between the two is the difference between an app that crashes gracefully and one that keeps working.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Currency and cost data ages badly&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The currency rates in &lt;code&gt;currency_service.py&lt;/code&gt; are hardcoded to values from when I built this. They're already stale. For anything meant to run long-term, external rate APIs are not optional — they're infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Designing for two constraints produces better architecture&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Having to satisfy both a web project rubric and a cloud computing rubric forced decisions I wouldn't have made otherwise — Supabase instead of SQLite, Render instead of running it locally, separating services cleanly so each had a clear cloud equivalent. Constraints pushed the architecture toward something more production-realistic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Currency rates are hardcoded — they drift from real rates over time&lt;/li&gt;
&lt;li&gt;Password hashing uses SHA-256, not bcrypt — fine for a college project, not for production&lt;/li&gt;
&lt;li&gt;No real-time pricing — cost estimates are AI approximations, not scraped data&lt;/li&gt;
&lt;li&gt;The Supabase free tier pauses after inactivity — cold start delay on first request&lt;/li&gt;
&lt;li&gt;No mobile-responsive CSS — the frontend is desktop-only&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where It Stands
&lt;/h2&gt;

&lt;p&gt;Voyage Canvas is done. It works, it's deployed, and it satisfied both subjects it was built for. I'm not actively extending it — there are other projects ahead of it in the queue.&lt;br&gt;
The codebase is on GitHub. It's a working full-stack app — clone it, set up your own API keys and database, and it runs locally.&lt;/p&gt;


&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Ranvir2028/Voyage-Canvas.git
&lt;span class="nb"&gt;cd &lt;/span&gt;Voyage-Canvas/backend
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Create &lt;code&gt;backend/.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VISION_KEY=your_groq_api_key
OPENWEATHER_API_KEY=your_openweather_key
DATABASE_URL=your_supabase_connection_string
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://localhost:5000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You'll need a Groq API key (free at console.groq.com) and an OpenWeatherMap key (free tier works). Supabase is optional if you modify &lt;code&gt;database.py&lt;/code&gt; to use SQLite for local testing.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Ranvir2028/Voyage-Canvas" rel="noopener noreferrer"&gt;github.com/Ranvir2028/Voyage-Canvas&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Portfolio:&lt;/strong&gt; &lt;a href="https://harshwardhan-ranvir.vercel.app/" rel="noopener noreferrer"&gt;harshwardhan-ranvir.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LinkedIn:&lt;/strong&gt; &lt;a href="https://www.linkedin.com/in/harshwardhan-s-ranvir-20a328378/" rel="noopener noreferrer"&gt;linkedin.com/in/harshwardhan-s-ranvir-20a328378&lt;/a&gt;&lt;/p&gt;




</description>
      <category>python</category>
      <category>flask</category>
      <category>webdev</category>
      <category>llm</category>
    </item>
    <item>
      <title>CERBERUS: I Built a Network Sentinel Because My Router Wasn't Enough</title>
      <dc:creator>Harshwardhan S. Ranvir</dc:creator>
      <pubDate>Fri, 12 Jun 2026 10:31:32 +0000</pubDate>
      <link>https://dev.to/harshwardhan_ranvir/cerberus-i-built-a-network-sentinel-because-my-router-wasnt-enough-5bmg</link>
      <guid>https://dev.to/harshwardhan_ranvir/cerberus-i-built-a-network-sentinel-because-my-router-wasnt-enough-5bmg</guid>
      <description>&lt;p&gt;&lt;em&gt;A deep dive into building a Python-based network surveillance tool with Scapy, automatic router detection, and a two-phase watch system.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Most people never think about what's actually on their home network. The router has a device list somewhere behind three login screens and a UI that looks like it was designed in 2009. You check it once, forget about it, and move on.&lt;/p&gt;

&lt;p&gt;That's not good enough.&lt;/p&gt;

&lt;p&gt;I wanted to know my network — not by logging into the router every time, but through automation. A tool that runs, learns what's supposed to be there, and then tells me when something isn't. So I built CERBERUS.&lt;/p&gt;

&lt;p&gt;This post covers what it does, how every component works, the engineering decisions behind it, and what I actually learned building it — including the parts that were genuinely difficult.&lt;/p&gt;




&lt;h2&gt;
  
  
  What CERBERUS Does
&lt;/h2&gt;

&lt;p&gt;CERBERUS is a Python-based network sentinel. It scans your local network using ARP, learns which devices are trusted, and runs a continuous surveillance loop that alerts you when something unknown shows up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feature list:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automatic router and subnet detection (no manual config)&lt;/li&gt;
&lt;li&gt;ARP-based device discovery with wake-up broadcast&lt;/li&gt;
&lt;li&gt;Two-phase operation: Learning Mode → Surveillance Mode&lt;/li&gt;
&lt;li&gt;Unknown device alerts with CRITICAL log entries&lt;/li&gt;
&lt;li&gt;Structured file + console logging (reusable module)&lt;/li&gt;
&lt;li&gt;Auto-generated &lt;code&gt;known_devices.json&lt;/code&gt; and &lt;code&gt;cerberus.log&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Windows Npcap auto-installer&lt;/li&gt;
&lt;li&gt;Cross-platform: Linux, macOS, Windows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What it does NOT do (...yet):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No port scanning (that's CERBERUS v2)&lt;/li&gt;
&lt;li&gt;Won't detect devices that block ARP responses entirely&lt;/li&gt;
&lt;li&gt;No persistent background daemon — runs in terminal&lt;/li&gt;
&lt;li&gt;No push/email alerts, just logged output&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Architecture: Four Modules, One Purpose
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cerberus/
├── cerberus_scan.py        # Main orchestrator
├── cerberus_logger.py      # Reusable logging setup
├── router_detector.py      # Network auto-configuration
├── npcap_installer.py      # Windows Npcap helper
├── known_devices.json      # Auto-generated trusted list
└── cerberus.log            # Auto-generated activity log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The design is intentionally modular. Each file has one job. &lt;code&gt;cerberus_scan.py&lt;/code&gt; orchestrates everything but doesn't do the heavy lifting itself — it delegates to the other modules and handles the main execution flow.&lt;/p&gt;

&lt;p&gt;The two auto-generated files (&lt;code&gt;known_devices.json&lt;/code&gt; and &lt;code&gt;cerberus.log&lt;/code&gt;) are created at runtime. You don't set them up. CERBERUS creates them when it first runs.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Security note:&lt;/strong&gt; &lt;code&gt;known_devices.json&lt;/code&gt; stores MAC addresses of your trusted devices.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Deep Dive: How Each Component Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Router Detection — &lt;code&gt;router_detector.py&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Before scanning anything, CERBERUS needs to know what network to scan. This is where most tools either require manual configuration or make a dumb assumption. CERBERUS tries to be smarter.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;RouterDetector&lt;/code&gt; class uses Python's &lt;code&gt;netifaces&lt;/code&gt; library to query the system's default gateway:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;gateways&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;netifaces&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gateways&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;gateway_ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;interface&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gateways&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;netifaces&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AF_INET&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once it has the interface, it fetches the actual subnet mask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;addrs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;netifaces&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ifaddresses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;netmask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;addrs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;netifaces&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AF_INET&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;netmask&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;cidr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;netmask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;network_address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;network_addr&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cidr&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you the correct CIDR notation — say, &lt;code&gt;192.168.1.0/24&lt;/code&gt; or &lt;code&gt;10.0.0.0/22&lt;/code&gt; — based on your actual network, not a guess.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fallback:&lt;/strong&gt; If subnet mask detection fails (some systems don't return this reliably), CERBERUS falls back to a &lt;code&gt;/24&lt;/code&gt; assumption:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;network_base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;router_ip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.0/24&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="n"&gt;TARGET_NETWORK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;network_base&lt;/span&gt;
&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Using default /24 network assumption: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TARGET_NETWORK&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This covers the vast majority of home networks. If your network is different, you'll see the warning and can hardcode &lt;code&gt;TARGET_NETWORK&lt;/code&gt; in the config section of &lt;code&gt;cerberus_scan.py&lt;/code&gt;. It's a practical trade-off — automatic detection works 95% of the time, and the fallback handles the rest without crashing.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. ARP Scanning — The Core of CERBERUS
&lt;/h3&gt;

&lt;p&gt;ARP (Address Resolution Protocol) is how devices on a network map IP addresses to MAC addresses. It's stateless, has no authentication, and every device on your network responds to it. That makes it ideal for discovery.&lt;/p&gt;

&lt;p&gt;CERBERUS builds a broadcast ARP request using Scapy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;arp_request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ARP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdst&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TARGET_NETWORK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ether_frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Ether&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ff:ff:ff:ff:ff:ff&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;packet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ether_frame&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;arp_request&lt;/span&gt;

&lt;span class="n"&gt;answered_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;srp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;packet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;clients&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;received&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;answered_list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;psrc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mac&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hwsrc&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ARP(pdst=TARGET_NETWORK)&lt;/code&gt; creates an ARP request asking "who has every IP in this range?"&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Ether(dst="ff:ff:ff:ff:ff:ff")&lt;/code&gt; wraps it in an Ethernet broadcast frame — every device on the network receives this.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;srp()&lt;/code&gt; (send/receive at Layer 2) sends the packet and listens for responses. Whatever responds gets added to the clients list with their IP and MAC.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The wake-up call:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before the ARP scan, CERBERUS sends a broadcast ICMP ping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;broadcast_ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TARGET_NETWORK&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.255&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;IP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;broadcast_ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nc"&gt;ICMP&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some devices go into a low-power state and take a moment to respond to ARP. The broadcast ping nudges them awake before the scan. It doesn't work on every device — anything that ignores pings or blocks ICMP stays invisible — but it improves discovery rates on typical home networks.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Learning Mode vs Surveillance Mode
&lt;/h3&gt;

&lt;p&gt;This two-phase design is the core logic of CERBERUS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1 — Learning Mode&lt;/strong&gt; (first run only):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;learn_network_mode&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;current_devices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;scan_network&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;known_macs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mac&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;current_devices&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;save_known_devices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;known_macs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;known_macs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On first run, there's no &lt;code&gt;known_devices.json&lt;/code&gt;. CERBERUS detects this, scans the network, saves every discovered MAC as trusted, and creates the file. No alerts. Everything found in learning mode is considered legitimate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2 — Surveillance Mode&lt;/strong&gt; (every run after):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;surveillance_mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;known_macs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;current_devices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;scan_network&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;current_devices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mac&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;known_macs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unknown: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ip&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; - &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mac&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;unknown_devices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;critical&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ALERT: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unknown_devices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; intruder(s) detected!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SCAN_INTERVAL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every 60 seconds (configurable), CERBERUS scans, loads the trusted MAC list, and compares. Any MAC not in the trusted list triggers a &lt;code&gt;WARNING&lt;/code&gt;. If unknown devices are found, &lt;code&gt;CRITICAL&lt;/code&gt; level alerts fire for each one.&lt;/p&gt;

&lt;p&gt;The loop runs until &lt;code&gt;Ctrl+C&lt;/code&gt;. No scan limit. CERBERUS watches until you tell it to stop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why MAC addresses and not IPs?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;IP addresses change. Devices get new IPs via DHCP on every reconnect. MAC addresses are hardware identifiers — they're stable. Tracking by MAC means a device can reconnect on a different IP and CERBERUS still recognizes it.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. The Logging Module — &lt;code&gt;cerberus_logger.py&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setup_logging&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log_file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cerberus.log&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;silent_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;formatter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Formatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%(asctime)s - %(name)s - %(levelname)s - %(message)s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;datefmt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d %H-%M-%S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Always writes to file
&lt;/span&gt;    &lt;span class="n"&gt;file_handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FileHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Console output optional
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;silent_mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;console_handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StreamHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I built this as a separate module for a reason: I'll use it in future projects. Same interface, drop it in, configure the log file name and level. No rewriting.&lt;/p&gt;

&lt;p&gt;Key behaviors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Always appends to file — you don't lose previous scan history&lt;/li&gt;
&lt;li&gt;Console output is optional via &lt;code&gt;silent_mode=True&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Five log levels available: DEBUG, INFO, WARNING, ERROR, CRITICAL&lt;/li&gt;
&lt;li&gt;Millisecond-precision timestamps on every entry&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CERBERUS uses WARNING for unknown devices and CRITICAL for confirmed intruder alerts. The distinction matters when parsing logs programmatically.&lt;/p&gt;




&lt;h3&gt;
  
  
  5. Windows Npcap Auto-Installer — &lt;code&gt;npcap_installer.py&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Windows doesn't give raw socket access out of the box. Scapy needs Npcap (a packet capture driver) to function on Windows. Without it, nothing works.&lt;/p&gt;

&lt;p&gt;Most tools say "install Npcap manually" and leave you to figure it out. I didn't want that. The auto-installer handles it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check if already installed&lt;/strong&gt; — registry lookup + file system check + live Scapy test&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If not found&lt;/strong&gt; — offer options: auto-install, manual install, or skip&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-install path&lt;/strong&gt; — downloads installer, runs silent install with these flags:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;/&lt;span class="n"&gt;S&lt;/span&gt;                    &lt;span class="c"&gt;# Silent mode, no GUI
&lt;/span&gt;&lt;span class="n"&gt;winpcap_mode&lt;/span&gt;=&lt;span class="n"&gt;yes&lt;/span&gt;      &lt;span class="c"&gt;# WinPcap API compatibility
&lt;/span&gt;&lt;span class="n"&gt;loopback_support&lt;/span&gt;=&lt;span class="n"&gt;yes&lt;/span&gt;  &lt;span class="c"&gt;# Capture localhost traffic
&lt;/span&gt;&lt;span class="n"&gt;admin_only&lt;/span&gt;=&lt;span class="n"&gt;no&lt;/span&gt;         &lt;span class="c"&gt;# Allow non-admin capture
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The version is currently hardcoded to Npcap 1.79. It'll work fine for the foreseeable future — Npcap doesn't have breaking releases frequently — but it's something to revisit in v2.&lt;/p&gt;

&lt;p&gt;This module ended up being more code than the scanner itself. Building it taught me more about Windows internals than I expected: registry access with &lt;code&gt;winreg&lt;/code&gt;, process management with &lt;code&gt;subprocess&lt;/code&gt;, installer flags, and the gap between "it works on my machine" and "it works on someone else's machine."&lt;/p&gt;




&lt;h2&gt;
  
  
  Sample Output
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Learning Mode (first run):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;==================================================
PROJECT CERBERUS: The Network Sentinel
==================================================
✔️ Network detected: 192.168.1.0/24
Router: 192.168.1.1
Your IP: 192.168.1.x
Interface: wlan0
--------------------------------------------------
No known devices file found. Starting learning mode...
Scanning 192.168.1.0/24...
Found 5 devices.
Learned 5 devices. They are now trusted.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Surveillance Mode — clean scan:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;------------------------- Scan 1 -------------------------
Scanning 192.168.1.0/24...
Found 5 devices.
All devices are trusted.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Surveillance Mode — intruder detected:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;------------------------- Scan 4 -------------------------
Scanning 192.168.1.0/24...
Found 6 devices.
WARNING  - Unknown: 192.168.1.108 - xx:xx:xx:xx:xx:xx
CRITICAL - ALERT: 1 intruder(s) device detected!
CRITICAL - INTRUDER: 192.168.1.108 - xx:xx:xx:xx:xx:xx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;For security reasons i'm not putting the original terminal outputs here.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Engineering Decisions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why Scapy over &lt;code&gt;subprocess&lt;/code&gt; + system &lt;code&gt;arp&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The alternative was calling &lt;code&gt;arp -a&lt;/code&gt; via subprocess and parsing text output. That breaks when output format changes across OS versions (and it does). Scapy gives you structured data directly, cross-platform control, and fine-grained timeout/retry configuration. The learning curve is steeper, but you're not writing a brittle text parser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why JSON for device storage?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No database overhead for a list of MAC addresses that'll never exceed 50 entries on a home network. JSON is human-readable, directly editable, and parseable without any dependencies. If you want to add a device manually or remove one, open the file and edit it. Simple.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why 60-second scan intervals?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;30 seconds felt aggressive for passive home network monitoring — unnecessary traffic and CPU overhead. 120 seconds means an intruder goes unnoticed for two minutes. 60 is the balance. It's also directly configurable: change &lt;code&gt;SCAN_INTERVAL&lt;/code&gt; in &lt;code&gt;cerberus_scan.py&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why separate the logging module?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Reusability. &lt;code&gt;cerberus_logger.py&lt;/code&gt; has no CERBERUS-specific logic. It's a clean logging setup function. I'll drop it into the next project without touching it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Actually Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Scapy's documentation is scattered&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most tutorials cover sending a single ping. Understanding &lt;code&gt;srp()&lt;/code&gt; (Layer 2 send/receive) versus &lt;code&gt;sr()&lt;/code&gt; (Layer 3) took actual time — reading source code, not just docs. There's no single good resource that covers it end-to-end. You have to piece it together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. ARP is stateless. That's the attack surface.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Building this made the theory real. ARP has no authentication. Any device can respond to any ARP request, including one it didn't ask for — that's how ARP spoofing works. I understood this conceptually before. After building CERBERUS, I understood it practically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Device discovery is harder than it looks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not all devices respond to ARP every time. Sleeping devices, aggressively firewalled devices, some IoT hardware — they don't always reply. The wake-up broadcast ping helps, but it's not a complete solution. This is the core limitation that's driving CERBERUS v2: multiple discovery methods, not just ARP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Windows is a different world for networking&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Linux gives you raw socket access with &lt;code&gt;sudo&lt;/code&gt;. Windows gates it behind a kernel driver (Npcap). Building the auto-installer taught me about Windows registry interaction, silent installer flags, and the difference between making a tool that works on your development machine versus one that someone else can actually run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. AI helped with syntax. Debugging was still mine.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I used AI assistance for Scapy syntax and parts of the Npcap implementation. But when something failed — wrong subnet detection, missed devices, logging output inconsistencies — figuring out why required reading logs, testing across configurations, and sometimes reading library source code. AI accelerates. It doesn't replace the diagnostic work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Limitations and CERBERUS v2
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What v1 doesn't solve:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Devices that don't respond to ARP stay invisible&lt;/li&gt;
&lt;li&gt;No port/service fingerprinting — you see a device, not what it's running&lt;/li&gt;
&lt;li&gt;No persistent daemon — closes when terminal closes&lt;/li&gt;
&lt;li&gt;No alerting beyond log output (no email, no webhook, no SMS)&lt;/li&gt;
&lt;li&gt;Npcap version hardcoded on Windows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What CERBERUS v2 is adding:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker containerization — runs anywhere, including Raspberry Pi&lt;/li&gt;
&lt;li&gt;Nmap integration for port scanning and service detection&lt;/li&gt;
&lt;li&gt;Multiple discovery methods (not just ARP)&lt;/li&gt;
&lt;li&gt;SQLite for persistent device history&lt;/li&gt;
&lt;li&gt;REST API for status queries&lt;/li&gt;
&lt;li&gt;Webhook/email notifications&lt;/li&gt;
&lt;li&gt;Smarter intruder classification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;v2 is in active development.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.7+&lt;/li&gt;
&lt;li&gt;Linux/macOS: run with &lt;code&gt;sudo&lt;/code&gt; (raw packet access requires root)&lt;/li&gt;
&lt;li&gt;Windows: run as Administrator (auto-installer handles Npcap)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Ranvir2028/Cerberus-The-Network-Sentinel.git
&lt;span class="nb"&gt;cd &lt;/span&gt;Cerberus-The-Network-Sentinel
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Run:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Linux/macOS&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;python3 cerberus_scan.py

&lt;span class="c"&gt;# Windows (Administrator terminal)&lt;/span&gt;
python cerberus_scan.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First run enters learning mode automatically. Every run after that is surveillance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dependencies&lt;/strong&gt; (from &lt;code&gt;requirements.txt&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;scapy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;=2.6.1&lt;/span&gt;
&lt;span class="py"&gt;netifaces-plus&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;=0.12.5&lt;/span&gt;
&lt;span class="py"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;=2.32.5&lt;/span&gt;
&lt;span class="py"&gt;colorama&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;=0.4.6&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Security Note
&lt;/h2&gt;

&lt;p&gt;Only use CERBERUS on networks you own or have explicit permission to monitor. ARP scanning sends packets to every IP in your network range. On a network you don't own, that's unauthorized network activity.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;CERBERUS v2 is being built now. The goal is a containerized network sentinel that runs continuously on a home server or Raspberry Pi, scans with multiple discovery methods, and notifies you without you having to watch a terminal.&lt;/p&gt;

&lt;p&gt;The v1 code is on GitHub named &lt;em&gt;Cerberus: The Network Sentinel&lt;/em&gt;. Read it, break it, use it to understand ARP scanning.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Ranvir2028/Cerberus-The-Network-Sentinel" rel="noopener noreferrer"&gt;github.com/Ranvir2028/Cerberus-The-Network-Sentinel&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Portfolio:&lt;/strong&gt; &lt;a href="https://harshwardhan-ranvir.vercel.app/" rel="noopener noreferrer"&gt;harshwardhan-ranvir.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LinkedIn:&lt;/strong&gt; &lt;a href="https://www.linkedin.com/in/harshwardhan-s-ranvir-20a328378/" rel="noopener noreferrer"&gt;linkedin.com/in/harshwardhan-s-ranvir-20a328378&lt;/a&gt;&lt;/p&gt;




</description>
      <category>python</category>
      <category>security</category>
      <category>networking</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
