Files
Sage/SAGE_PROJECT.md
Dani 7be7b270e6 Initial commit: Sage Kitchen Management App v1.0.0
 Features implemented:
- Smart inventory tracking with Hive database
- Barcode scanning with auto-populated product info
- Multiple API fallbacks (Open Food Facts, UPCItemDB)
- Smart expiration date predictions by category
- Discord webhook notifications (persisted)
- Custom sage leaf vector icon
- Material Design 3 UI with sage green theme
- Privacy Policy & Terms of Service
- Local-first, privacy-focused architecture

🎨 UI/UX:
- Home dashboard with inventory stats
- Add Item screen with barcode integration
- Inventory list with expiration indicators
- Settings with persistent preferences
- About section with legal docs

🔧 Technical:
- Flutter 3.35.5 with Riverpod state management
- Hive 2.2.3 for local database
- Mobile scanner for barcode detection
- Feature-first architecture

🤖 Generated with Claude Code (https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 13:54:21 -04:00

42 KiB

SAGE - COMPLETE PROJECT DOCUMENTATION

Smart Kitchen Management System 🌿

Version: 1.0.0
Created: October 3, 2025
Platform: Flutter (Android & iOS)
Status: Active Development 🔥


📖 TABLE OF CONTENTS

  1. Project Overview
  2. Core Features
  3. Technical Architecture
  4. Data Models
  5. API Integrations
  6. User Flows
  7. Notification System
  8. Multi-User & Sync
  9. Security & Privacy
  10. Testing Strategy
  11. Deployment

📱 PROJECT OVERVIEW

The Problem

  • People forget what's in their fridge/pantry
  • Food expires and gets wasted → money wasted
  • Constant grocery store trips forgetting items
  • End up ordering takeout when food at home expires
  • No organization = kitchen chaos

The Solution: SAGE

A comprehensive kitchen management app that:

  1. Tracks all your food inventory with expiration dates
  2. Alerts you before food expires (with Discord integration!)
  3. Manages your recipes and connects them to inventory
  4. Generates smart shopping lists
  5. Shares with household members
  6. Works offline-first with optional cloud sync

Why "Sage"?

  • 🌿 The herb (kitchen theme)
  • 🧠 Wisdom and smart decisions
  • 💚 Simple, clean, memorable

Target Users

  • Busy individuals who want to save money
  • Families managing shared kitchens
  • Anyone tired of wasting food
  • People who hate grocery shopping inefficiency
  • Budget-conscious cooks

🎯 CORE FEATURES

1. INVENTORY TRACKER 📦

The Foundation: Track everything in your kitchen

Key Capabilities:

  • Add items via barcode scan OR manual entry
  • Auto-populate from Open Food Facts (2M+ products!)
  • Track quantity, expiration date, location
  • Organize by location (fridge, freezer, pantry, etc.)
  • Categorize automatically or manually
  • Attach photos (cached locally)
  • Search and filter inventory
  • Quick edit/delete items

Barcode Scanning:

User scans barcode
    ↓
Query Open Food Facts API
    ↓
Product found?
    ├─ YES → Auto-fill: name, photo, category, typical shelf life
    │         User adds: quantity, actual expiration, location
    │         → Save to local database
    │
    └─ NO → Manual entry form
             User fills everything
             → Save to local database

Data Tracked Per Item:

  • Name
  • Barcode (if scanned)
  • Quantity & unit
  • Purchase date
  • Expiration date
  • Location (fridge/freezer/pantry/other)
  • Category (auto or manual)
  • Photo URL (cached)
  • Notes (optional)
  • Last modified timestamp
  • Sync status

2. SMART ALERTS 🔔

The Brain: Never let food go to waste

Alert Timeline:

Time Before Expiration Alert Type Message
1 month FYI notification "Ranch dressing expires in 30 days"
2 weeks Heads up "Sour cream expires in 14 days"
1 week Use soon! "Milk expires in 7 days - Tap for recipes"
Before next shopping day Shopping alert "3 items expire before next week!"

Shopping Day Intelligence:

User sets shopping frequency:
  - Weekly (every Tuesday)
  - Bi-weekly (every other Monday)
  - Monthly (first Saturday)
  - Pay cycle (15th and 30th)
  - Custom days

System calculates next shopping day
    ↓
For each item expiring before next shopping day:
    → Send notification
    → Add to "Add to Shopping List" quick action
    → Send to Discord channel (if enabled)

Notification Types:

  1. Local Push Notifications

    • Scheduled in advance
    • Tappable (opens relevant screen)
    • Customizable (can disable certain types)
  2. Discord Webhooks

    • Critical alerts only (shopping day, urgent expirations)
    • Formatted with emojis and priority
    • @everyone tag for household

Example Discord Message:

🌿 **Sage Alert** 🌿
@everyone Shopping day is tomorrow!

**Expiring before next week:**
🔴 Ranch dressing (expires Oct 10) - 3 days
🔴 Croutons (expires Oct 12) - 5 days
🟡 Milk (expires Oct 15) - 8 days

**Suggested Actions:**
• Add to shopping list
• Check "Use It Up" recipes
• Update quantities if already replaced

Don't forget to restock! 🛒

3. RECIPE BOOK 📖

The Inspiration: Never wonder "what's for dinner?" again

Core Functions:

  • Add recipes manually or copy/paste from websites
  • Store ingredients with quantities
  • Write/paste instructions
  • Add optional photos
  • Tag recipes (Quick, Vegetarian, Fancy, etc.)
  • Share recipes with community (optional)
  • Mark favorites

Smart Features:

  • "What Can I Make?" - Shows recipes you can make with current inventory

    • Displays % of ingredients you have
    • Sorts by completeness
    • Shows what's missing
  • "Use It Up!" - Suggests recipes using expiring items

    • Prioritizes items expiring soonest
    • Helps prevent waste

Recipe Data Structure:

Recipe {
  name: "Caesar Salad"
  ingredients: [
    {name: "Romaine lettuce", quantity: 1, unit: "head"},
    {name: "Caesar dressing", quantity: 0.5, unit: "cup"},
    {name: "Croutons", quantity: 1, unit: "cup"},
    {name: "Parmesan", quantity: 0.25, unit: "cup", optional: true}
  ]
  instructions: "Chop lettuce. Toss with dressing. Top with croutons and parmesan."
  prepTime: 10 minutes
  servings: 2
  tags: ["Quick", "Salad", "Vegetarian"]
  photo: "url_or_local_path"
  isPublic: true  // Share with community
  createdBy: user_id
}

Copy/Paste from Websites: User pastes URL or text block → Parser extracts:

  • Recipe title
  • Ingredient list (with quantities)
  • Instructions
  • Optional: photo, prep time, servings

Common formats supported:

  • Recipe schema (schema.org/Recipe)
  • Plain text with smart parsing
  • Popular recipe sites (AllRecipes, Food Network, etc.)

4. SHOPPING LISTS 🛒

The Planner: Efficient grocery trips, every time

Key Features:

  • Multiple Lists - Separate lists for different stores

    • "Costco"
    • "Trader Joe's"
    • "Farmers Market"
    • Custom names
  • Smart List Building

    • Add items manually
    • One-click add from recipes
    • Suggested items based on expiring inventory
  • While Shopping

    • Check off items as you shop
    • Uncheck if needed
    • Sort by store section (optional)
    • Quick quantity adjust
  • After Shopping

    • "Add All to Inventory" quick action
    • Batch-add checked items
    • Auto-populate likely expiration dates
    • Clear completed items

Sharing:

  • Share lists with household members
  • Real-time updates (if cloud sync enabled)
  • See who added what
  • Collaborative shopping

List Organization:

Shopping List: "Costco"
  Items:
    ☐ Milk (2 gallons) - Added from: Ranch expires alert
    ☑ Chicken breast (2 lbs) - Added from: Recipe "Chicken Tacos"
    ☐ Romaine lettuce (2 heads) - Added manually
    ☐ Croutons (1 bag) - Added from: Recipe "Caesar Salad"
    
  Quick Actions:
    - Add recipe ingredients
    - Add expiring items
    - Share list
    - Sort by aisle

5. MULTI-USER & SYNC ☁️

The Connector: Share with your household

Three Sync Modes (User's Choice):

Mode 1: Cloud Sync (Supabase) ☁️

Best for: Most users, easy setup, reliable

Features:

  • Real-time sync across devices
  • User authentication
  • Household groups (shared inventory/lists)
  • Recipe community sharing
  • Automatic backups

Technical:

  • PostgreSQL database (Supabase)
  • Row-level security
  • Real-time subscriptions
  • Free tier: 500MB DB, 2GB bandwidth/month

User Flow:

Sign up with email
    ↓
Create or join household
    ↓
All devices sync automatically
    ↓
Changes propagate in real-time

Mode 2: Completely Free (Self-Hosted) 🏠

Best for: Privacy-conscious users, no cloud dependency

Options:

A. Export/Import

  • Export inventory/recipes to JSON
  • Share file via any method (email, cloud storage, etc.)
  • Import on other device
  • Manual sync process

B. Local WiFi Sync

  • Devices on same network
  • Peer-to-peer sync (no internet needed!)
  • Automatic discovery
  • One device = "host"

C. Self-Host Backend

  • Docker Compose setup (we provide!)
  • Run on Raspberry Pi or home server
  • Full control of data
  • Zero recurring costs

Self-Hosted Setup:

# We provide this in docs!
git clone https://github.com/sage-app/backend
cd backend
docker-compose up -d

# Configure app to point to your server
# http://192.168.1.100:3000

Mode 3: Local Only 📱

Best for: Solo users, maximum privacy, no sharing needed

Features:

  • Everything on device
  • Zero external dependencies
  • Export for backup
  • Fast and simple

Offline-First Philosophy (ALL MODES):

Every action saves locally FIRST
    ↓
If online & sync enabled:
    Queue for sync
    ↓
    Sync when possible
    ↓
    Update local cache

App works PERFECTLY offline
Never wait for network
Graceful sync when available

Conflict Resolution:

  • Last-write-wins (timestamp-based)
  • Show notification if conflict detected
  • User can view both versions and choose

🏗️ TECHNICAL ARCHITECTURE

Tech Stack Overview

┌─────────────────────────────────────┐
│         FLUTTER APP (Dart)          │
│  ┌───────────────────────────────┐  │
│  │    UI Layer (Widgets)         │  │
│  └──────────┬────────────────────┘  │
│             │                        │
│  ┌──────────▼────────────────────┐  │
│  │  State Management (Riverpod)  │  │
│  └──────────┬────────────────────┘  │
│             │                        │
│  ┌──────────▼────────────────────┐  │
│  │    Business Logic Layer       │  │
│  │  - Controllers                │  │
│  │  - Services                   │  │
│  │  - Repositories               │  │
│  └──────────┬────────────────────┘  │
│             │                        │
│  ┌──────────▼────────────────────┐  │
│  │      Data Layer               │  │
│  │  ┌──────────┐  ┌────────────┐ │  │
│  │  │   Isar   │  │ Supabase   │ │  │
│  │  │  (Local) │  │  (Cloud)   │ │  │
│  │  └──────────┘  └────────────┘ │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘
            │
            ▼
   ┌────────────────────┐
   │  External APIs     │
   │  - Open Food Facts │
   │  - Discord Webhook │
   └────────────────────┘

Key Technologies

Component Technology Purpose
Framework Flutter 3.x Cross-platform UI
Language Dart 3.x Type-safe, fast, AOT compiled
State Management Riverpod 2.x Reactive state, dependency injection
Local Database Isar 3.x Fast NoSQL, offline-first
Cloud Database Supabase PostgreSQL, real-time, auth
Barcode Scanner mobile_scanner Camera-based scanning
Notifications flutter_local_notifications Scheduled alerts
Image Caching cached_network_image Performance optimization
HTTP Client http API requests

Project Structure

sage/
├── lib/
│   ├── main.dart                    # App entry point
│   ├── app.dart                     # App widget, theme, routing
│   │
│   ├── core/                        # Core utilities
│   │   ├── constants/
│   │   │   ├── app_constants.dart
│   │   │   ├── api_constants.dart
│   │   │   └── colors.dart
│   │   ├── utils/
│   │   │   ├── date_utils.dart
│   │   │   ├── validators.dart
│   │   │   └── formatters.dart
│   │   └── extensions/
│   │       ├── date_extensions.dart
│   │       └── string_extensions.dart
│   │
│   ├── features/                    # Feature modules
│   │   ├── inventory/
│   │   │   ├── models/
│   │   │   │   └── food_item.dart
│   │   │   ├── repositories/
│   │   │   │   └── inventory_repository.dart
│   │   │   ├── controllers/
│   │   │   │   └── inventory_controller.dart
│   │   │   ├── screens/
│   │   │   │   ├── inventory_screen.dart
│   │   │   │   ├── add_item_screen.dart
│   │   │   │   └── item_detail_screen.dart
│   │   │   └── widgets/
│   │   │       ├── inventory_list.dart
│   │   │       ├── expiration_badge.dart
│   │   │       └── barcode_scanner.dart
│   │   │
│   │   ├── recipes/
│   │   │   ├── models/
│   │   │   │   └── recipe.dart
│   │   │   ├── repositories/
│   │   │   │   └── recipe_repository.dart
│   │   │   ├── controllers/
│   │   │   │   └── recipe_controller.dart
│   │   │   ├── screens/
│   │   │   │   ├── recipes_screen.dart
│   │   │   │   ├── add_recipe_screen.dart
│   │   │   │   ├── recipe_detail_screen.dart
│   │   │   │   └── what_can_i_make_screen.dart
│   │   │   └── widgets/
│   │   │       ├── recipe_card.dart
│   │   │       └── ingredient_list.dart
│   │   │
│   │   ├── shopping/
│   │   │   ├── models/
│   │   │   │   └── shopping_list.dart
│   │   │   ├── repositories/
│   │   │   │   └── shopping_repository.dart
│   │   │   ├── controllers/
│   │   │   │   └── shopping_controller.dart
│   │   │   ├── screens/
│   │   │   │   ├── shopping_lists_screen.dart
│   │   │   │   └── list_detail_screen.dart
│   │   │   └── widgets/
│   │   │       └── shopping_item.dart
│   │   │
│   │   ├── home/
│   │   │   ├── screens/
│   │   │   │   └── home_screen.dart
│   │   │   └── widgets/
│   │   │       ├── expiring_soon_carousel.dart
│   │   │       └── quick_stats.dart
│   │   │
│   │   ├── settings/
│   │   │   ├── models/
│   │   │   │   └── user_settings.dart
│   │   │   ├── repositories/
│   │   │   │   └── settings_repository.dart
│   │   │   ├── controllers/
│   │   │   │   └── settings_controller.dart
│   │   │   └── screens/
│   │   │       ├── settings_screen.dart
│   │   │       ├── notifications_settings.dart
│   │   │       ├── sync_settings.dart
│   │   │       └── discord_settings.dart
│   │   │
│   │   └── auth/
│   │       ├── controllers/
│   │       │   └── auth_controller.dart
│   │       └── screens/
│   │           ├── login_screen.dart
│   │           └── signup_screen.dart
│   │
│   ├── services/                    # Business logic services
│   │   ├── notification_service.dart
│   │   ├── sync_service.dart
│   │   ├── barcode_service.dart
│   │   ├── discord_service.dart
│   │   ├── openfoodfacts_service.dart
│   │   └── expiration_tracker_service.dart
│   │
│   ├── data/                        # Data layer
│   │   ├── local/
│   │   │   └── isar_database.dart
│   │   └── remote/
│   │       └── supabase_client.dart
│   │
│   └── shared/                      # Shared widgets & components
│       ├── widgets/
│       │   ├── custom_button.dart
│       │   ├── custom_text_field.dart
│       │   └── loading_indicator.dart
│       └── navigation/
│           └── app_router.dart
│
├── assets/                          # Static assets
│   ├── images/
│   ├── icons/
│   └── fonts/
│
├── test/                            # Tests
│   ├── unit/
│   ├── widget/
│   └── integration/
│
├── android/                         # Android-specific
├── ios/                            # iOS-specific
├── pubspec.yaml                    # Dependencies
└── README.md

💾 DATA MODELS (DETAILED)

FoodItem Model

import 'package:isar/isar.dart';

part 'food_item.g.dart';

@collection
class FoodItem {
  Id id = Isar.autoIncrement;
  
  // Basic Info
  late String name;
  String? barcode;
  late int quantity;
  String? unit;  // "bottles", "lbs", "oz", "items"
  
  // Dates
  late DateTime purchaseDate;
  late DateTime expirationDate;
  
  // Organization
  @enumerated
  late Location location;
  String? category;  // Auto from barcode or manual
  
  // Media & Notes
  String? photoUrl;  // Cached from API or user uploaded
  String? notes;
  
  // Multi-user support
  String? userId;
  String? householdId;
  
  // Sync tracking
  DateTime? lastModified;
  bool syncedToCloud = false;
  
  // Computed properties (not stored)
  @ignore
  int get daysUntilExpiration {
    return expirationDate.difference(DateTime.now()).inDays;
  }
  
  @ignore
  ExpirationStatus get expirationStatus {
    final days = daysUntilExpiration;
    if (days < 0) return ExpirationStatus.expired;
    if (days <= 3) return ExpirationStatus.critical;
    if (days <= 7) return ExpirationStatus.warning;
    if (days <= 14) return ExpirationStatus.caution;
    return ExpirationStatus.fresh;
  }
}

enum Location {
  fridge,
  freezer,
  pantry,
  spiceRack,
  countertop,
  other
}

enum ExpirationStatus {
  fresh,      // > 14 days
  caution,    // 8-14 days
  warning,    // 4-7 days
  critical,   // 1-3 days
  expired     // 0 or negative days
}

Recipe Model

import 'package:isar/isar.dart';

part 'recipe.g.dart';

@collection
class Recipe {
  Id id = Isar.autoIncrement;
  
  // Basic Info
  late String name;
  late String instructions;
  
  // Ingredients (embedded objects)
  late List<Ingredient> ingredients;
  
  // Media
  String? photoUrl;
  
  // Metadata
  List<String> tags = [];
  int? prepTimeMinutes;
  int? cookTimeMinutes;
  int? servings;
  
  @enumerated
  DifficultyLevel? difficulty;
  
  // Sharing
  String? createdBy;  // user_id
  bool isPublic = false;
  bool isFavorite = false;
  
  // Timestamps
  DateTime? created;
  DateTime? lastModified;
  
  // Sync
  bool syncedToCloud = false;
  
  // Computed
  @ignore
  int get totalTimeMinutes {
    return (prepTimeMinutes ?? 0) + (cookTimeMinutes ?? 0);
  }
}

@embedded
class Ingredient {
  late String name;
  double? quantity;
  String? unit;
  bool optional = false;
  
  // For matching against inventory
  String? matchedItemId;  // Link to FoodItem
}

enum DifficultyLevel {
  easy,
  medium,
  hard
}

ShoppingList Model

import 'package:isar/isar.dart';

part 'shopping_list.g.dart';

@collection
class ShoppingList {
  Id id = Isar.autoIncrement;
  
  late String name;  // "Costco", "Trader Joe's"
  
  List<ShoppingItem> items = [];
  
  // Sharing
  String? householdId;
  List<String> sharedWith = [];
  
  // Metadata
  DateTime? created;
  DateTime? lastModified;
  
  // Sync
  bool syncedToCloud = false;
  
  // Computed
  @ignore
  int get totalItems => items.length;
  
  @ignore
  int get checkedItems => items.where((i) => i.checked).length;
  
  @ignore
  double get completionPercentage {
    if (totalItems == 0) return 0;
    return (checkedItems / totalItems) * 100;
  }
}

@embedded
class ShoppingItem {
  late String name;
  double? quantity;
  String? unit;
  bool checked = false;
  
  // Metadata
  @enumerated
  Priority priority = Priority.normal;
  
  String? addedFrom;  // "recipe:123", "inventory:456", "manual"
  String? addedBy;    // user_id
  DateTime? addedAt;
  
  String? notes;
}

enum Priority {
  low,
  normal,
  high
}

UserSettings Model

import 'package:isar/isar.dart';

part 'user_settings.g.dart';

@collection
class UserSettings {
  Id id = Isar.autoIncrement;
  
  // Shopping Schedule
  @enumerated
  ShoppingFrequency frequency = ShoppingFrequency.weekly;
  
  // For weekly/biweekly
  int? dayOfWeek;  // 1-7 (Monday-Sunday)
  
  // For monthly
  int? dayOfMonth;  // 1-31
  
  // For pay cycle
  List<int>? payCycleDays;  // [15, 30]
  
  // For custom
  List<int>? customDays;  // [1, 4, 6] = Mon, Thu, Sat
  
  // Calculated
  DateTime? nextShoppingDay;
  
  // Notifications
  bool enableNotifications = true;
  bool notify1Month = true;
  bool notify2Weeks = true;
  bool notify1Week = true;
  bool notifyShoppingDay = true;
  
  // Discord
  bool enableDiscord = false;
  String? discordWebhookUrl;
  bool discordCriticalOnly = true;
  
  // Sync
  @enumerated
  SyncMode syncMode = SyncMode.localOnly;
  
  String? supabaseUserId;
  String? householdId;
  
  // Display
  bool showPhotos = true;
  bool showExpirationBadges = true;
  
  @enumerated
  ThemeMode themeMode = ThemeMode.system;
  
  @enumerated
  SortOrder inventorySortOrder = SortOrder.expirationDate;
}

enum ShoppingFrequency {
  weekly,      // Every X day of week
  biweekly,    // Every 2 weeks on X day
  monthly,     // Every month on X date
  payCycle,    // Specific dates (15th, 30th)
  custom       // User-selected days
}

enum SyncMode {
  localOnly,
  cloudSync,
  wifiSync,
  selfHosted
}

enum ThemeMode {
  light,
  dark,
  system
}

enum SortOrder {
  name,
  expirationDate,
  purchaseDate,
  location,
  quantity
}

🔌 API INTEGRATIONS

1. Open Food Facts API

Purpose: Auto-populate product info from barcodes

Base URL: https://world.openfoodfacts.org/api/v2

Endpoint: GET /product/{barcode}

Example Request:

final response = await http.get(
  Uri.parse('https://world.openfoodfacts.org/api/v2/product/0041220576500')
);

// Returns product data including:
// - product_name
// - brands
// - categories
// - image_url
// - ingredients
// - nutriscore

Example Response:

{
  "code": "0041220576500",
  "product": {
    "product_name": "Hidden Valley Ranch Dressing",
    "brands": "Hidden Valley",
    "categories": "Dressings, Ranch dressing",
    "image_url": "https://images.openfoodfacts.org/...",
    "quantity": "16 fl oz (473 mL)"
  },
  "status": 1,
  "status_verbose": "product found"
}

Service Implementation:

class OpenFoodFactsService {
  static const baseUrl = 'https://world.openfoodfacts.org/api/v2';
  
  Future<ProductInfo?> getProductByBarcode(String barcode) async {
    try {
      final response = await http.get(
        Uri.parse('$baseUrl/product/$barcode')
      );
      
      if (response.statusCode == 200) {
        final data = json.decode(response.body);
        if (data['status'] == 1) {
          return ProductInfo.fromJson(data['product']);
        }
      }
      return null;  // Product not found
    } catch (e) {
      print('Error fetching product: $e');
      return null;
    }
  }
}

Fallback Strategy:

  1. Try Open Food Facts
  2. If not found, try UPC Database API (backup)
  3. If still not found, manual entry

2. Discord Webhooks

Purpose: Send expiration alerts to Discord channel

Setup:

  1. User creates webhook in Discord server settings
  2. Copies webhook URL
  3. Pastes in Sage settings
  4. App sends JSON POST requests to webhook

Example Webhook URL:

https://discord.com/api/webhooks/123456789/abcdef...

Sending a Message:

class DiscordService {
  Future<void> sendAlert(String webhookUrl, List<FoodItem> expiringItems) async {
    final message = _buildAlertMessage(expiringItems);
    
    final response = await http.post(
      Uri.parse(webhookUrl),
      headers: {'Content-Type': 'application/json'},
      body: json.encode({
        'username': 'Sage Kitchen Alert',
        'avatar_url': 'https://sage-app.com/icon.png',
        'embeds': [
          {
            'title': '🌿 Sage Alert 🌿',
            'description': message,
            'color': 0x4CAF50,  // Green
            'footer': {
              'text': 'Sage Kitchen Management'
            },
            'timestamp': DateTime.now().toIso8601String()
          }
        ]
      })
    );
    
    if (response.statusCode != 204) {
      throw Exception('Failed to send Discord alert');
    }
  }
  
  String _buildAlertMessage(List<FoodItem> items) {
    final buffer = StringBuffer();
    buffer.writeln('@everyone Shopping day is tomorrow!\n');
    buffer.writeln('**Expiring before next week:**');
    
    for (final item in items) {
      final emoji = _getEmoji(item.daysUntilExpiration);
      buffer.writeln('$emoji ${item.name} (expires in ${item.daysUntilExpiration} days)');
    }
    
    buffer.writeln('\nDon\'t forget to restock! 🛒');
    return buffer.toString();
  }
  
  String _getEmoji(int days) {
    if (days <= 3) return '🔴';
    if (days <= 7) return '🟡';
    return '🟢';
  }
}

Rate Limiting:

  • Discord allows ~5 requests per second per webhook
  • Batch alerts together
  • Use exponential backoff if rate limited

📱 USER FLOWS

Flow 1: First Time Setup

User downloads Sage
    ↓
Welcome screen
    ↓
Choose sync mode:
  - Local only (skip to next)
  - Cloud sync (sign up/login)
  - Self-hosted (enter URL)
    ↓
Set shopping frequency:
  "How often do you shop?"
  - Weekly → Pick day
  - Bi-weekly → Pick day
  - Monthly → Pick date
  - Pay cycle → Enter dates
  - Custom → Select days
    ↓
Enable notifications:
  "Get alerts for expiring items?"
  - Yes (request permission)
  - No (can enable later)
    ↓
Optional: Discord setup
  "Send alerts to Discord?"
  - Yes → Paste webhook URL → Test
  - Skip
    ↓
Tutorial:
  "Let's add your first item!"
  - Scan barcode demo
  - OR manual entry
    ↓
Ready to use! 🎉

Flow 2: Adding Item via Barcode

User taps "+ Add Item"
    ↓
Choose method:
  [📷 Scan Barcode] or [✏️ Manual Entry]
    ↓
User taps "Scan Barcode"
    ↓
Camera opens with barcode overlay
    ↓
User scans barcode
    ↓
Query Open Food Facts API
    ↓
┌─────────────────────┐
│  Product Found?     │
└────┬────────────┬───┘
     │            │
    YES          NO
     │            │
     ▼            ▼
Auto-fill:    Manual Entry:
- Name        - Type name
- Photo       - Optional photo
- Category    - Select category
     │            │
     └────┬───────┘
          ▼
User adds:
  - Quantity (1, 2, 3...)
  - Unit (bottles, lbs, etc.)
  - Expiration date (calendar picker)
  - Location (dropdown: Fridge/Freezer/Pantry)
  - Optional notes
    ↓
[Save] button
    ↓
Save to local database
    ↓
If cloud sync enabled:
  Queue for sync
    ↓
Show success message
Navigate to inventory screen

Flow 3: Meal Planning with Recipes

User opens Recipes tab
    ↓
Browses recipes OR taps "What Can I Make?"
    ↓
IF "What Can I Make?":
  System matches recipes to current inventory
  Shows % of ingredients available
  "Caesar Salad: 75% (missing croutons)"
    ↓
User selects recipe
    ↓
Recipe details screen shows:
  - Photo
  - Ingredients with checkmarks:
    ✅ Romaine lettuce (in fridge)
    ✅ Ranch dressing (in pantry)
    ❌ Croutons (not in inventory)
  - Instructions
  - Prep time
    ↓
User taps "Add Missing Items to Shopping List"
    ↓
Dialog: "Add to which list?"
  - Select existing list
  - OR create new list
    ↓
Croutons added to shopping list
    ↓
User cooks meal!
    ↓
Optional: Mark ingredients as used
  (Reduces quantity in inventory)

Flow 4: Shopping Trip

User opens Shopping tab
    ↓
Selects "Costco" shopping list
    ↓
List shows:
  ☐ Milk (2 gallons)
  ☐ Chicken (2 lbs)
  ☐ Croutons (1 bag)
  ☑ Eggs (already checked off yesterday)
    ↓
At store, user checks off items:
  ☑ Milk
  ☑ Chicken
  ☑ Croutons
    ↓
All items checked!
    ↓
Banner appears:
  "Shopping complete! Add items to inventory?"
  [Yes] [Not yet]
    ↓
User taps [Yes]
    ↓
Bulk add screen:
  Pre-filled with checked items
  User adds:
    - Expiration dates (smart defaults shown)
    - Locations (remembered from last time)
  Quick swipe interface
    ↓
[Save All] button
    ↓
All items added to inventory
Shopping list cleared (or moved to archive)
    ↓
Success! 🎉

Flow 5: Responding to Expiration Alert

Morning notification:
  "🌿 Sage: Use soon!
   Ranch dressing expires in 3 days"
    ↓
User taps notification
    ↓
App opens to "Use It Up" screen
    ↓
Shows recipes featuring ranch:
  - Buffalo Chicken Wrap
  - Veggie Dip Platter
  - Ranch Chicken Tacos
    ↓
User selects "Buffalo Chicken Wrap"
    ↓
Checks ingredients:
  ✅ Ranch dressing (expiring!)
  ✅ Tortillas
  ✅ Lettuce
  ❌ Chicken (needs to buy)
    ↓
Adds chicken to shopping list
    ↓
Makes dinner, ranch dressing used!
    ↓
User marks ranch as "used up" in app
  OR reduces quantity to 0
    ↓
Item removed from inventory
Food waste prevented! 💪

🔔 NOTIFICATION SYSTEM (DETAILED)

Background Job Architecture

Strategy: Scheduled daily check + immediate calculations

class ExpirationTrackerService {
  final InventoryRepository _inventory;
  final SettingsRepository _settings;
  final NotificationService _notifications;
  
  Future<void> runDailyCheck() async {
    // Run every morning at 9 AM
    final items = await _inventory.getAllItems();
    final settings = await _settings.getSettings();
    final nextShoppingDay = settings.nextShoppingDay;
    
    for (final item in items) {
      await _checkItemAndNotify(item, nextShoppingDay);
    }
  }
  
  Future<void> _checkItemAndNotify(FoodItem item, DateTime? nextShopping) async {
    final daysUntil = item.daysUntilExpiration;
    
    // 1 month notification
    if (daysUntil == 30) {
      await _notifications.schedule(
        title: '🌿 Sage: FYI',
        body: '${item.name} expires in 30 days',
        payload: 'item:${item.id}',
      );
    }
    
    // 2 weeks notification
    if (daysUntil == 14) {
      await _notifications.schedule(
        title: '🌿 Sage: Heads up!',
        body: '${item.name} expires in 2 weeks',
        payload: 'item:${item.id}',
      );
    }
    
    // 1 week notification
    if (daysUntil == 7) {
      await _notifications.schedule(
        title: '🌿 Sage: Use soon!',
        body: '${item.name} expires in 1 week - Tap for recipes',
        payload: 'useItUp:${item.id}',
      );
    }
    
    // Shopping day check
    if (nextShopping != null) {
      if (item.expirationDate.isBefore(nextShopping) && daysUntil > 0) {
        await _notifications.schedule(
          title: '🌿 Sage: Add to shopping list!',
          body: '${item.name} expires before your next shopping trip',
          payload: 'addToList:${item.id}',
        );
      }
    }
  }
}

Shopping Day Calculation

class ShoppingDayCalculator {
  DateTime? calculateNextShoppingDay(UserSettings settings) {
    final now = DateTime.now();
    
    switch (settings.frequency) {
      case ShoppingFrequency.weekly:
        return _nextWeekday(now, settings.dayOfWeek!);
        
      case ShoppingFrequency.biweekly:
        return _nextBiweekly(now, settings.dayOfWeek!);
        
      case ShoppingFrequency.monthly:
        return _nextMonthlyDate(now, settings.dayOfMonth!);
        
      case ShoppingFrequency.payCycle:
        return _nextPayCycleDate(now, settings.payCycleDays!);
        
      case ShoppingFrequency.custom:
        return _nextCustomDay(now, settings.customDays!);
    }
  }
  
  DateTime _nextWeekday(DateTime from, int targetDay) {
    // targetDay: 1 = Monday, 7 = Sunday
    final currentDay = from.weekday;
    int daysToAdd = (targetDay - currentDay) % 7;
    if (daysToAdd == 0) daysToAdd = 7;  // Next week
    return from.add(Duration(days: daysToAdd));
  }
  
  DateTime _nextBiweekly(DateTime from, int targetDay) {
    final nextWeek = _nextWeekday(from, targetDay);
    // Check if last shopping was less than a week ago
    // If yes, return 2 weeks from last shopping
    // If no, return next week
    // (Would need to store last shopping date)
    return nextWeek.add(Duration(days: 7));  // Simplified
  }
  
  DateTime _nextMonthlyDate(DateTime from, int targetDate) {
    var next = DateTime(from.year, from.month, targetDate);
    if (next.isBefore(from) || next.isAtSameMomentAs(from)) {
      // Next month
      next = DateTime(from.year, from.month + 1, targetDate);
    }
    return next;
  }
  
  DateTime _nextPayCycleDate(DateTime from, List<int> dates) {
    // dates like [15, 30]
    final upcoming = dates
        .map((d) => DateTime(from.year, from.month, d))
        .where((date) => date.isAfter(from))
        .toList()
      ..sort();
    
    if (upcoming.isEmpty) {
      // Next month
      final firstDate = dates.first;
      return DateTime(from.year, from.month + 1, firstDate);
    }
    
    return upcoming.first;
  }
  
  DateTime _nextCustomDay(DateTime from, List<int> days) {
    // days like [1, 4, 6] = Monday, Thursday, Saturday
    final upcoming = days
        .map((d) => _nextWeekday(from, d))
        .toList()
      ..sort();
    
    return upcoming.first;
  }
}

🔐 SECURITY & PRIVACY

Data Privacy Principles

  1. Local-First: All data stored locally first, cloud is optional
  2. User Control: Users choose sync mode and what to share
  3. No Tracking: Zero analytics, no user tracking
  4. Open Source: Code is public, verifiable
  5. Self-Hostable: Users can run own backend

Cloud Security (Supabase)

Row-Level Security (RLS) Policies:

-- Users can only see their own items
CREATE POLICY "Users see own items"
  ON food_items
  FOR SELECT
  USING (auth.uid() = user_id);

-- Household members see shared items
CREATE POLICY "Household access"
  ON food_items
  FOR SELECT
  USING (
    household_id IN (
      SELECT household_id 
      FROM household_members 
      WHERE user_id = auth.uid()
    )
  );

-- Users can only modify their own items
CREATE POLICY "Users modify own items"
  ON food_items
  FOR UPDATE
  USING (auth.uid() = user_id);

-- Similar policies for recipes, shopping lists, etc.

Authentication:

  • Email/password (Supabase Auth)
  • Optional: OAuth (Google, Apple) in future
  • JWT tokens for API requests
  • Refresh tokens for sessions

Local Security

Database Encryption:

// Isar supports encryption
final isar = await Isar.open(
  [FoodItemSchema, RecipeSchema, ...],
  directory: dir.path,
  inspector: false,
  encryptionKey: _getOrCreateEncryptionKey(),
);

Secure Storage:

  • User settings encrypted
  • Discord webhook URL secured
  • Auth tokens in secure storage (flutter_secure_storage)

🧪 TESTING STRATEGY

Unit Tests

Test individual functions and business logic

// Example: Test expiration calculation
test('daysUntilExpiration calculates correctly', () {
  final item = FoodItem()
    ..name = 'Milk'
    ..expirationDate = DateTime.now().add(Duration(days: 5));
  
  expect(item.daysUntilExpiration, equals(5));
});

test('expirationStatus returns correct status', () {
  final item = FoodItem()
    ..name = 'Milk'
    ..expirationDate = DateTime.now().add(Duration(days: 2));
  
  expect(item.expirationStatus, equals(ExpirationStatus.critical));
});

Widget Tests

Test UI components

testWidgets('ExpirationBadge shows correct color', (tester) async {
  await tester.pumpWidget(
    MaterialApp(
      home: ExpirationBadge(daysUntil: 2),
    ),
  );
  
  expect(find.text('2 days'), findsOneWidget);
  
  final badge = tester.widget<Container>(find.byType(Container));
  expect(badge.color, equals(Colors.red));  // Critical = red
});

Integration Tests

Test complete user flows

testWidgets('Add item flow works end-to-end', (tester) async {
  await tester.pumpWidget(MyApp());
  
  // Tap add button
  await tester.tap(find.byIcon(Icons.add));
  await tester.pumpAndSettle();
  
  // Choose manual entry
  await tester.tap(find.text('Manual Entry'));
  await tester.pumpAndSettle();
  
  // Fill form
  await tester.enterText(find.byKey(Key('name_field')), 'Milk');
  await tester.enterText(find.byKey(Key('quantity_field')), '1');
  
  // Select expiration date
  await tester.tap(find.byKey(Key('expiration_picker')));
  // ... date picker interaction
  
  // Save
  await tester.tap(find.text('Save'));
  await tester.pumpAndSettle();
  
  // Verify item appears in inventory
  expect(find.text('Milk'), findsOneWidget);
});

🚀 DEPLOYMENT

Android Release Process

  1. Build Release APK:
flutter build apk --release
  1. Build App Bundle (for Play Store):
flutter build appbundle --release
  1. Sign the Build:

    • Configure signing in android/app/build.gradle
    • Store keystore securely
    • Add to .gitignore
  2. Upload to Play Store:

    • Internal testing → Alpha → Beta → Production
    • Gradual rollout recommended

iOS Release Process

  1. Prerequisites:

    • Apple Developer account ($99/year)
    • Xcode installed
    • Provisioning profiles configured
  2. Build:

flutter build ios --release
  1. Archive in Xcode:

    • Open ios/Runner.xcworkspace
    • Product → Archive
    • Upload to App Store Connect
  2. TestFlight:

    • Internal testing
    • External beta testing
    • Submit for review

Self-Hosted Backend Deployment

Docker Compose Setup:

version: '3.8'

services:
  postgres:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: sage
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  api:
    image: sage-backend:latest
    depends_on:
      - postgres
    environment:
      DATABASE_URL: postgresql://postgres:${DB_PASSWORD}@postgres:5432/sage
      JWT_SECRET: ${JWT_SECRET}
    ports:
      - "3000:3000"

volumes:
  postgres_data:

Deploy on Raspberry Pi:

# On Raspberry Pi
git clone https://github.com/sage-app/backend
cd backend
cp .env.example .env
# Edit .env with your settings
docker-compose up -d

📊 METRICS & ANALYTICS

Privacy-Respecting Metrics

What we DON'T track:

  • Personal data
  • Specific food items
  • User behavior
  • Location data

What we CAN track (locally, opt-in):

  • Items saved (count only, for user's stats)
  • Food waste prevented (estimated)
  • Money saved (estimated from item costs)
  • App usage (locally, for user dashboard)

User Dashboard:

Your Stats (This Month):
- 🛒 24 items tracked
- 💰 Estimated savings: $47
- 🌱 Food waste prevented: 3 lbs
- 📖 Recipes tried: 5
- ⭐ Most used recipe: Caesar Salad

🎨 DESIGN SYSTEM

Color Palette

class AppColors {
  // Primary - Sage Green theme
  static const primary = Color(0xFF4CAF50);
  static const primaryDark = Color(0xFF388E3C);
  static const primaryLight = Color(0xFF81C784);
  
  // Expiration Status Colors
  static const fresh = Color(0xFF4CAF50);      // Green
  static const caution = Color(0xFFFFEB3B);    // Yellow
  static const warning = Color(0xFFFF9800);    // Orange
  static const critical = Color(0xFFF44336);   // Red
  static const expired = Color(0xFF9E9E9E);    // Gray
  
  // UI
  static const background = Color(0xFFFAFAFA);
  static const surface = Color(0xFFFFFFFF);
  static const text = Color(0xFF212121);
  static const textSecondary = Color(0xFF757575);
}

Typography

class AppTextStyles {
  static const headline1 = TextStyle(
    fontSize: 24,
    fontWeight: FontWeight.bold,
    color: AppColors.text,
  );
  
  static const headline2 = TextStyle(
    fontSize: 20,
    fontWeight: FontWeight.bold,
    color: AppColors.text,
  );
  
  static const body = TextStyle(
    fontSize: 16,
    color: AppColors.text,
  );
  
  static const caption = TextStyle(
    fontSize: 12,
    color: AppColors.textSecondary,
  );
}

🔮 FUTURE FEATURES

Phase 8+ (Post-MVP)

  1. Meal Planning Calendar

    • Plan meals for the week
    • Auto-generate shopping list
    • Track what you ate
  2. Nutrition Tracking

    • From Open Food Facts nutrition data
    • Track calories, macros (optional)
    • Health insights
  3. Price Tracking

    • Log item costs
    • Track spending over time
    • Price comparison between stores
  4. Voice Input

    • "Hey Sage, add milk to my shopping list"
    • Voice-activated barcode scanning
  5. AI Recipe Suggestions

    • "What can I make with chicken and rice?"
    • Generate recipes from ingredients
    • Dietary restrictions
  6. Household Gamification

    • Compete to waste less food
    • Achievements & badges
    • Family leaderboard
  7. Integration with Smart Appliances

    • Samsung Family Hub fridge
    • Smart scales for quantity tracking

Built with 💚 by enthusiastic developers who hate wasting food!

Let's make kitchens smarter, one scan at a time! 🌿