Upload 17 files
Browse files- Dockerfile +29 -0
- N8N_GUIDE.md +83 -0
- README.md +41 -11
- app.js +311 -0
- app.py +115 -0
- config.js +10 -0
- debug.html +88 -0
- expiry_model.pkl +3 -0
- firebase.json +25 -0
- food_waste.db +0 -0
- index.html +179 -0
- model.py +55 -0
- requirements.txt +5 -0
- style.css +351 -0
- workflow.json +259 -0
- workflow_real.json +298 -0
- workflow_real_safe.json +330 -0
Dockerfile
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use the official lightweight Python image.
|
| 2 |
+
# https://hub.docker.com/_/python
|
| 3 |
+
FROM python:3.9-slim
|
| 4 |
+
|
| 5 |
+
# Set the working directory to /app
|
| 6 |
+
WORKDIR /app
|
| 7 |
+
|
| 8 |
+
# Copy the requirements file into the container at /app
|
| 9 |
+
COPY requirements.txt .
|
| 10 |
+
|
| 11 |
+
# Install any needed packages specified in requirements.txt
|
| 12 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 13 |
+
|
| 14 |
+
# Copy the rest of the application code
|
| 15 |
+
COPY . .
|
| 16 |
+
|
| 17 |
+
# Create the SQLite database directory permissions if needed (HF spaces are writable in /app)
|
| 18 |
+
# But standard SQLite file in CWD is fine.
|
| 19 |
+
|
| 20 |
+
# Make sure the model is generated during build or on startup
|
| 21 |
+
# We run the training script during build to ensure model.pkl exists
|
| 22 |
+
RUN python -c "import model; model.train_model()"
|
| 23 |
+
|
| 24 |
+
# Expose port 7860 (Hugging Face Spaces default port)
|
| 25 |
+
EXPOSE 7860
|
| 26 |
+
|
| 27 |
+
# Run commands to start the server
|
| 28 |
+
# binding to 0.0.0.0 is crucial for Docker containers
|
| 29 |
+
CMD ["python", "app.py"]
|
N8N_GUIDE.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# N8N Setup Guide for Food Waste Assistant
|
| 2 |
+
|
| 3 |
+
This guide helps you set up the backend logic using n8n. You will need to create a new workflow (or multiple) to handle the 4 main API endpoints.
|
| 4 |
+
|
| 5 |
+
## Prerequisites
|
| 6 |
+
- An active [n8n](https://n8n.io/) instance (Cloud or Self-hosted).
|
| 7 |
+
- A database (Google Sheets, Airtable, or Postgres) to store inventory. This guide assumes **Google Sheets** for simplicity.
|
| 8 |
+
|
| 9 |
+
## Quick Start (Import Workflow)
|
| 10 |
+
I have updated `workflow.json` to be much simpler. It now uses a **Single Webhook URL** that handles everything (`add_item`, `get_items`, etc.).
|
| 11 |
+
|
| 12 |
+
1. Download the updated `workflow.json` from this project.
|
| 13 |
+
2. Go to your n8n dashboard.
|
| 14 |
+
3. Click **Workflows** > **Import from File**.
|
| 15 |
+
4. Select `workflow.json`.
|
| 16 |
+
5. **Activate** the workflow.
|
| 17 |
+
6. Open the "Webhook (Router)" node:
|
| 18 |
+
- Copy the **Production URL**.
|
| 19 |
+
- **CRITICAL**: Set **Respond** to **"Using 'Respond to Webhook' Node"**.
|
| 20 |
+
- **If you forget this, you will get "Unexpected End of JSON Input" error!**
|
| 21 |
+
7. Paste the URL into your app's Settings.
|
| 22 |
+
|
| 23 |
+
## 🚀 Making It Real (Google Sheets)
|
| 24 |
+
|
| 25 |
+
To save data permanently, use `workflow_real.json` instead.
|
| 26 |
+
|
| 27 |
+
### 1. Prepare Google Sheet
|
| 28 |
+
Create a new Sheet driven by these columns in Row 1:
|
| 29 |
+
`id` | `name` | `quantity` | `expiryDate` | `category`
|
| 30 |
+
|
| 31 |
+
### 2. Import Real Workflow
|
| 32 |
+
1. Import `workflow_real.json` into N8N.
|
| 33 |
+
2. Double-click **Google Sheets (Add)** and **Google Sheets (Read)** nodes.
|
| 34 |
+
3. Authenticate with your Google Account ("Sign in with Google").
|
| 35 |
+
4. Select your Spreadsheet and Sheet Name.
|
| 36 |
+
- For **Add Node**: Map the fields (`name` -> `{{$json.body.name}}`, etc).
|
| 37 |
+
5. Activate and copy the new Webhook URL to your app settings.
|
| 38 |
+
|
| 39 |
+
## Workflow Details
|
| 40 |
+
Below are the details if you want to build it manually or understand how it works.
|
| 41 |
+
|
| 42 |
+
Create a workflow with a **Webhook** node.
|
| 43 |
+
- **Method**: `POST` (and `GET` if you want a single entry point, but easier to use distinct hooks).
|
| 44 |
+
- **Authentication**: None (for this demo) or Header Auth.
|
| 45 |
+
|
| 46 |
+
### Endpoint 1: Add Item
|
| 47 |
+
1. **Webhook Node**: Listen for `POST` on `/add-item`.
|
| 48 |
+
2. **AI Node (Optional)**: Use an OpenAI node to predict expiry date based on the "Name" if "ExpiryDate" is empty.
|
| 49 |
+
3. **Google Sheets Node**: "Append" row to Sheet "Inventory".
|
| 50 |
+
- Map: `Name`, `Quantity`, `ExpiryDate`, `Category`.
|
| 51 |
+
4. **Respond to Webhook Node**: Return `{ "success": true }`.
|
| 52 |
+
|
| 53 |
+
### Endpoint 2: Get Inventory
|
| 54 |
+
1. **Webhook Node**: Listen for `GET` on `/get-items`.
|
| 55 |
+
2. **Google Sheets Node**: "Read" all rows from Sheet "Inventory".
|
| 56 |
+
3. **Code Node**: Calculate `daysRemaining` for each item.
|
| 57 |
+
4. **Respond to Webhook Node**: Return JSON `{ "items": [...] }`.
|
| 58 |
+
|
| 59 |
+
### Endpoint 3: Dashboard Stats
|
| 60 |
+
1. **Webhook Node**: Listen for `GET` on `/dashboard-stats`.
|
| 61 |
+
2. **Google Sheets Node**: Read all rows.
|
| 62 |
+
3. **Code Node**:
|
| 63 |
+
- Count total items.
|
| 64 |
+
- Filter items where `expiryDate` is within 3 days.
|
| 65 |
+
- Calculate potential waste savings.
|
| 66 |
+
4. **Respond to Webhook Node**: Return Stats JSON.
|
| 67 |
+
|
| 68 |
+
### Endpoint 4: Suggest Recipes
|
| 69 |
+
1. **Webhook Node**: Listen for `POST` on `/suggest-recipes`.
|
| 70 |
+
2. **Google Sheets Node**: Read "Inventory" (find items expiring soon).
|
| 71 |
+
3. **OpenAI Node** (or other LLM):
|
| 72 |
+
- **System Prompt**: "You are a chef. Suggest 3 recipes based on these ingredients: [List of ingredients]. Return valid JSON."
|
| 73 |
+
4. **Respond to Webhook Node**: Return the JSON list of recipes.
|
| 74 |
+
|
| 75 |
+
## Connecting to Frontend
|
| 76 |
+
1. After activating your workflow, copy the **Production URL** of your Webhook nodes.
|
| 77 |
+
2. If you used different URLs for each endpoint, you might need to adjust `app.js` or use a single Router workflow (recommended).
|
| 78 |
+
- **Router Approach**: Have one Webhook URL and route based on `body.action` or query param?
|
| 79 |
+
- **Current App Support**: The `app.js` assumes a base URL structure:
|
| 80 |
+
- `BASE_URL/add-item`
|
| 81 |
+
- `BASE_URL/get-items`
|
| 82 |
+
- etc.
|
| 83 |
+
- **Tip**: In n8n, you can set the path for the Webhook node. Ensure you set 4 separate Webhook nodes with these specific suffixes, or use a Reverse Proxy to route them.
|
README.md
CHANGED
|
@@ -1,11 +1,41 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Food Waste Reduction Assistant
|
| 2 |
+
|
| 3 |
+
A premium, sustainably-minded web application that helps users track their grocery inventory, predicts expiry dates, and suggests recipes to reduce food waste.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
- **Smart Inventory Tracking**: Log items and let AI (via n8n) predict expiry dates.
|
| 7 |
+
- **Recipe Suggestions**: Get recipe ideas based on what's expiring soon.
|
| 8 |
+
- **Waste Analytics**: Dashboard to track your impact (Waste Avoided, Money Saved).
|
| 9 |
+
- **Premium UI**: Glassmorphism design with a focus on aesthetics and usability.
|
| 10 |
+
|
| 11 |
+
## Tech Stack
|
| 12 |
+
- **Frontend**: HTML5, CSS3 (Custom Glassmorphism), JavaScript (Vanilla).
|
| 13 |
+
- **Backend**: n8n (Workflow Automation & AI Integration).
|
| 14 |
+
- **Deployment**: Compatible with Hugging Face Spaces (Static HTML).
|
| 15 |
+
|
| 16 |
+
## Deployment Guide (Hugging Face Spaces)
|
| 17 |
+
|
| 18 |
+
1. **Create a Space**:
|
| 19 |
+
- Go to [Hugging Face Spaces](https://huggingface.co/spaces).
|
| 20 |
+
- Create a new Space.
|
| 21 |
+
- Select **"Static HTML"** as the SDK.
|
| 22 |
+
|
| 23 |
+
2. **Upload Files**:
|
| 24 |
+
- Upload `index.html`, `style.css`, `app.js`, and `config.js` to the root of your Space.
|
| 25 |
+
|
| 26 |
+
3. **Live**:
|
| 27 |
+
- Your app will be live immediately!
|
| 28 |
+
|
| 29 |
+
## Setup with N8N
|
| 30 |
+
|
| 31 |
+
This application requires an n8n backend to handle data storage and AI predictions.
|
| 32 |
+
See [N8N_GUIDE.md](N8N_GUIDE.md) for detailed instructions on setting up the workflows.
|
| 33 |
+
|
| 34 |
+
### Configuration
|
| 35 |
+
1. Open your deployed app.
|
| 36 |
+
2. Go to the **Settings** tab.
|
| 37 |
+
3. Enter your n8n Webhook Base URL.
|
| 38 |
+
4. Click Save.
|
| 39 |
+
|
| 40 |
+
## Developer Note
|
| 41 |
+
- The app runs in **Mock Mode** by default if no n8n URL is provided. This allows you to explore the UI without a backend.
|
app.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// State Management
|
| 2 |
+
const state = {
|
| 3 |
+
inventory: [],
|
| 4 |
+
stats: {
|
| 5 |
+
totalItems: 0,
|
| 6 |
+
expiringSoon: 0,
|
| 7 |
+
wasteAvoided: 0,
|
| 8 |
+
moneySaved: 0
|
| 9 |
+
},
|
| 10 |
+
useMock: true
|
| 11 |
+
};
|
| 12 |
+
|
| 13 |
+
// Mock Data (Fallback)
|
| 14 |
+
const MOCK_INVENTORY = [
|
| 15 |
+
{ id: 1, name: 'Milk (Mock)', quantity: '1L', expiryDate: '2023-10-30', category: 'Dairy', price: 2.50 },
|
| 16 |
+
{ id: 2, name: 'Spinach (Mock)', quantity: '1 bag', expiryDate: '2023-10-26', category: 'Vegetables', price: 3.00 },
|
| 17 |
+
];
|
| 18 |
+
|
| 19 |
+
const MOCK_RECIPES = [
|
| 20 |
+
{ title: 'Mock Chicken Stir-fry', image: 'https://via.placeholder.com/300', ingredients: 'Chicken, Veggies' }
|
| 21 |
+
];
|
| 22 |
+
|
| 23 |
+
// Initialization
|
| 24 |
+
// Initialization
|
| 25 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 26 |
+
// Check if backend is reachable or just load UI
|
| 27 |
+
state.useMock = false; // We assume Python backend is primary now
|
| 28 |
+
fetchInventory();
|
| 29 |
+
setupFormListeners();
|
| 30 |
+
|
| 31 |
+
// Hide N8N settings if present (optional UX polish)
|
| 32 |
+
const settingsSection = document.getElementById('settings-section');
|
| 33 |
+
if (settingsSection) {
|
| 34 |
+
// We can inject a message or hide legacy N8N inputs here if we wanted
|
| 35 |
+
// For now, we just leave it as is
|
| 36 |
+
}
|
| 37 |
+
});
|
| 38 |
+
|
| 39 |
+
// Navigation
|
| 40 |
+
function showSection(sectionId) {
|
| 41 |
+
const sections = ['dashboard', 'inventory', 'recipes', 'settings'];
|
| 42 |
+
sections.forEach(id => document.getElementById(`${id}-section`).classList.add('hidden'));
|
| 43 |
+
document.getElementById(`${sectionId}-section`).classList.remove('hidden');
|
| 44 |
+
document.querySelectorAll('nav button').forEach(btn => btn.classList.remove('active'));
|
| 45 |
+
|
| 46 |
+
if (sectionId === 'dashboard' || sectionId === 'inventory') {
|
| 47 |
+
if (!state.useMock) fetchInventory();
|
| 48 |
+
else updateUI();
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// Data Handling - Python API
|
| 53 |
+
async function apiRequest(endpoint, method = 'GET', data = null) {
|
| 54 |
+
// If endpoint doesn't start with /, add it
|
| 55 |
+
const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
| 56 |
+
const url = `/api${path}`;
|
| 57 |
+
|
| 58 |
+
const options = {
|
| 59 |
+
method: method,
|
| 60 |
+
headers: { 'Content-Type': 'application/json' }
|
| 61 |
+
};
|
| 62 |
+
|
| 63 |
+
if (data) {
|
| 64 |
+
options.body = JSON.stringify(data);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
const response = await fetch(url, options);
|
| 68 |
+
if (!response.ok) {
|
| 69 |
+
throw new Error(`Server Error: ${response.status}`);
|
| 70 |
+
}
|
| 71 |
+
return await response.json();
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
async function fetchInventory() {
|
| 75 |
+
if (state.useMock) { updateUI(); return; } // Keep mock fallback for initial load if no URL was saved
|
| 76 |
+
|
| 77 |
+
try {
|
| 78 |
+
const data = await apiRequest('/get-items');
|
| 79 |
+
state.inventory = data.items || [];
|
| 80 |
+
updateUI();
|
| 81 |
+
} catch (error) {
|
| 82 |
+
console.error('API Error:', error);
|
| 83 |
+
showToast('Backend not running? ' + error.message, 'error');
|
| 84 |
+
// Fallback to mock data if API fails
|
| 85 |
+
if (state.inventory.length === 0) {
|
| 86 |
+
state.inventory = MOCK_INVENTORY;
|
| 87 |
+
state.useMock = true; // Switch to mock mode
|
| 88 |
+
updateUI();
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
async function addItem(itemData) {
|
| 94 |
+
if (state.useMock) {
|
| 95 |
+
state.inventory.push({ ...itemData, id: Date.now(), expiryDate: itemData.expiryDate || '2023-12-01' }); // Add ID for mock
|
| 96 |
+
updateUI();
|
| 97 |
+
showToast('Item added (Mock)!', 'success');
|
| 98 |
+
return;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
try {
|
| 102 |
+
const result = await apiRequest('/add-item', 'POST', itemData);
|
| 103 |
+
if (result.success) {
|
| 104 |
+
showToast(`Item added! Expiry: ${result.expiryDate}`, 'success');
|
| 105 |
+
fetchInventory();
|
| 106 |
+
} else {
|
| 107 |
+
showToast('Failed to add item: ' + (result.message || 'Unknown error'), 'error');
|
| 108 |
+
}
|
| 109 |
+
} catch (error) {
|
| 110 |
+
console.error('Add Item Error:', error);
|
| 111 |
+
showToast('Failed to add item.', 'error');
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
async function getRecipeSuggestions() {
|
| 116 |
+
const container = document.getElementById('recipes-container');
|
| 117 |
+
container.innerHTML = '<div class="loader"></div>';
|
| 118 |
+
|
| 119 |
+
if (state.useMock) {
|
| 120 |
+
await new Promise(r => setTimeout(r, 1000));
|
| 121 |
+
renderRecipes(MOCK_RECIPES);
|
| 122 |
+
return;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
try {
|
| 126 |
+
const recipes = await apiRequest('/suggest-recipes', 'POST', {});
|
| 127 |
+
renderRecipes(recipes);
|
| 128 |
+
} catch (e) {
|
| 129 |
+
console.error(e);
|
| 130 |
+
container.innerHTML = '<p class="text-muted">Error fetching recipes. Showing mock recipes.</p>';
|
| 131 |
+
setTimeout(() => renderRecipes(MOCK_RECIPES), 1000); // Fallback to mock recipes
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
// Settings & Testing
|
| 136 |
+
function saveSettings() {
|
| 137 |
+
const url = document.getElementById('config-webhook-url').value;
|
| 138 |
+
updateConfig(url);
|
| 139 |
+
showToast('Settings Saved! Reloading...', 'success');
|
| 140 |
+
setTimeout(() => window.location.reload(), 1000);
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
async function testConnection() {
|
| 144 |
+
const log = document.getElementById('connection-log');
|
| 145 |
+
log.style.display = 'block';
|
| 146 |
+
log.innerHTML = 'Testing connection...';
|
| 147 |
+
|
| 148 |
+
// Explicitly testing 'get_items' action which matches the Switch node logic
|
| 149 |
+
const payload = { action: 'get_items' };
|
| 150 |
+
|
| 151 |
+
const url = document.getElementById('config-webhook-url').value;
|
| 152 |
+
|
| 153 |
+
try {
|
| 154 |
+
const response = await fetch(url, {
|
| 155 |
+
method: 'POST',
|
| 156 |
+
headers: { 'Content-Type': 'application/json' },
|
| 157 |
+
body: JSON.stringify(payload)
|
| 158 |
+
});
|
| 159 |
+
|
| 160 |
+
if (response.ok) {
|
| 161 |
+
const data = await response.json();
|
| 162 |
+
log.innerHTML = `<span style="color: #10b981;">SUCCESS: Connected!</span><br>Response: ${JSON.stringify(data).slice(0, 50)}...`;
|
| 163 |
+
} else {
|
| 164 |
+
log.innerHTML = `<span style="color: #ef4444;">ERROR: Server returned ${response.status}</span>`;
|
| 165 |
+
}
|
| 166 |
+
} catch (e) {
|
| 167 |
+
log.innerHTML = `<span style="color: #ef4444;">FAIL: ${e.name} - ${e.message}</span><br>Possible causes: CORS, Network, or Wrong URL.`;
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
// UI Helpers
|
| 172 |
+
function showToast(msg, type = 'info') {
|
| 173 |
+
// Simple alert replacement
|
| 174 |
+
const div = document.createElement('div');
|
| 175 |
+
div.innerText = msg;
|
| 176 |
+
div.style.position = 'fixed';
|
| 177 |
+
div.style.bottom = '20px';
|
| 178 |
+
div.style.right = '20px';
|
| 179 |
+
div.style.background = type === 'error' ? '#ef4444' : '#10b981';
|
| 180 |
+
div.style.color = 'white';
|
| 181 |
+
div.style.padding = '12px 24px';
|
| 182 |
+
div.style.borderRadius = '8px';
|
| 183 |
+
div.style.zIndex = '1000';
|
| 184 |
+
div.style.animation = 'fadeInUp 0.3s ease';
|
| 185 |
+
document.body.appendChild(div);
|
| 186 |
+
setTimeout(() => div.remove(), 3000);
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
function renderRecipes(recipes) {
|
| 190 |
+
const container = document.getElementById('recipes-container');
|
| 191 |
+
container.innerHTML = '';
|
| 192 |
+
if (!recipes || recipes.length === 0) {
|
| 193 |
+
container.innerHTML = '<p class="text-muted">No recipes found.</p>';
|
| 194 |
+
return;
|
| 195 |
+
}
|
| 196 |
+
recipes.forEach(recipe => {
|
| 197 |
+
const div = document.createElement('div');
|
| 198 |
+
div.className = 'recipe-card';
|
| 199 |
+
div.innerHTML = `
|
| 200 |
+
<img src="${recipe.image || 'https://via.placeholder.com/300'}" alt="${recipe.title}">
|
| 201 |
+
<div class="recipe-content">
|
| 202 |
+
<div class="recipe-title">${recipe.title}</div>
|
| 203 |
+
<div class="recipe-ingredients">Ingredients: ${recipe.ingredients || 'Various'}</div>
|
| 204 |
+
</div>
|
| 205 |
+
`;
|
| 206 |
+
container.appendChild(div);
|
| 207 |
+
});
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
function calculateStats() {
|
| 211 |
+
const today = new Date();
|
| 212 |
+
const upcoming = new Date();
|
| 213 |
+
upcoming.setDate(today.getDate() + 3);
|
| 214 |
+
|
| 215 |
+
let expiringSoonCount = 0;
|
| 216 |
+
|
| 217 |
+
state.inventory.forEach(item => {
|
| 218 |
+
const expiry = new Date(item.expiryDate);
|
| 219 |
+
if (expiry <= upcoming && expiry >= today) {
|
| 220 |
+
expiringSoonCount++;
|
| 221 |
+
}
|
| 222 |
+
});
|
| 223 |
+
|
| 224 |
+
state.stats.totalItems = state.inventory.length;
|
| 225 |
+
state.stats.expiringSoon = expiringSoonCount;
|
| 226 |
+
state.stats.wasteAvoided = (state.inventory.length * 0.1).toFixed(1) + 'kg';
|
| 227 |
+
state.stats.moneySaved = '$' + (state.inventory.length * 2.5).toFixed(2);
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
function updateUI() {
|
| 231 |
+
calculateStats();
|
| 232 |
+
document.getElementById('total-items').innerText = state.stats.totalItems;
|
| 233 |
+
document.getElementById('expiring-soon').innerText = state.stats.expiringSoon;
|
| 234 |
+
document.getElementById('waste-avoided').innerText = state.stats.wasteAvoided;
|
| 235 |
+
document.getElementById('money-saved').innerText = state.stats.moneySaved;
|
| 236 |
+
|
| 237 |
+
const list = document.getElementById('inventory-list-ul');
|
| 238 |
+
list.innerHTML = '';
|
| 239 |
+
|
| 240 |
+
state.inventory.forEach(item => {
|
| 241 |
+
const li = document.createElement('li');
|
| 242 |
+
li.className = `inventory-item ${getExpiryClass(item.expiryDate)}`;
|
| 243 |
+
li.innerHTML = `
|
| 244 |
+
<div class="item-info">
|
| 245 |
+
<h4>${item.name}</h4>
|
| 246 |
+
<div class="item-meta"><span>${item.quantity || '-'}</span> • <span>${item.category || 'Other'}</span></div>
|
| 247 |
+
</div>
|
| 248 |
+
<div class="item-status">
|
| 249 |
+
<span class="expiry-badge ${getExpiryClass(item.expiryDate)}">${formatDate(item.expiryDate)}</span>
|
| 250 |
+
</div>
|
| 251 |
+
`;
|
| 252 |
+
list.appendChild(li);
|
| 253 |
+
});
|
| 254 |
+
|
| 255 |
+
generateDashboardAlerts();
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
function getExpiryClass(dateStr) {
|
| 259 |
+
if (!dateStr) return 'safe';
|
| 260 |
+
const today = new Date();
|
| 261 |
+
const expiry = new Date(dateStr);
|
| 262 |
+
const diffDays = Math.ceil((expiry - today) / (1000 * 60 * 60 * 24));
|
| 263 |
+
if (diffDays < 0) return 'expired';
|
| 264 |
+
if (diffDays <= 3) return 'soon';
|
| 265 |
+
return 'safe';
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
function formatDate(dateStr) {
|
| 269 |
+
if (!dateStr) return 'Unknown';
|
| 270 |
+
return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
function generateDashboardAlerts() {
|
| 274 |
+
const container = document.getElementById('alerts-container');
|
| 275 |
+
container.innerHTML = '';
|
| 276 |
+
const expiringItems = state.inventory.filter(i => getExpiryClass(i.expiryDate) === 'soon');
|
| 277 |
+
|
| 278 |
+
if (expiringItems.length === 0) {
|
| 279 |
+
container.innerHTML = '<p class="text-muted">No urgent alerts. Good job!</p>';
|
| 280 |
+
return;
|
| 281 |
+
}
|
| 282 |
+
expiringItems.forEach(item => {
|
| 283 |
+
const div = document.createElement('div');
|
| 284 |
+
div.style.marginBottom = '10px';
|
| 285 |
+
div.style.padding = '10px';
|
| 286 |
+
div.style.background = 'rgba(245, 158, 11, 0.1)';
|
| 287 |
+
div.style.borderRadius = '8px';
|
| 288 |
+
div.style.borderLeft = '3px solid var(--accent)';
|
| 289 |
+
div.innerHTML = `<strong>${item.name}</strong> is expiring soon!`;
|
| 290 |
+
container.appendChild(div);
|
| 291 |
+
});
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
// Event Listeners
|
| 295 |
+
function setupFormListeners() {
|
| 296 |
+
document.getElementById('quick-add-form').addEventListener('submit', (e) => {
|
| 297 |
+
e.preventDefault();
|
| 298 |
+
const name = document.getElementById('quick-name').value;
|
| 299 |
+
const date = document.getElementById('quick-date').value;
|
| 300 |
+
addItem({ name, quantity: '1 unit', expiryDate: date, category: 'Uncategorized' });
|
| 301 |
+
document.getElementById('quick-add-form').reset();
|
| 302 |
+
});
|
| 303 |
+
|
| 304 |
+
document.getElementById('add-item-form').addEventListener('submit', (e) => {
|
| 305 |
+
e.preventDefault();
|
| 306 |
+
const formData = new FormData(e.target);
|
| 307 |
+
addItem(Object.fromEntries(formData.entries()));
|
| 308 |
+
e.target.reset();
|
| 309 |
+
showSection('inventory');
|
| 310 |
+
});
|
| 311 |
+
}
|
app.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, request, jsonify, send_from_directory
|
| 2 |
+
from flask_cors import CORS
|
| 3 |
+
import sqlite3
|
| 4 |
+
import os
|
| 5 |
+
from datetime import datetime, timedelta
|
| 6 |
+
import model # Import our AI model
|
| 7 |
+
|
| 8 |
+
app = Flask(__name__, static_folder='.')
|
| 9 |
+
CORS(app) # Enable CORS for local development if needed
|
| 10 |
+
|
| 11 |
+
DB_FILE = 'food_waste.db'
|
| 12 |
+
|
| 13 |
+
# --- Database Setup ---
|
| 14 |
+
def init_db():
|
| 15 |
+
conn = sqlite3.connect(DB_FILE)
|
| 16 |
+
c = conn.cursor()
|
| 17 |
+
c.execute('''
|
| 18 |
+
CREATE TABLE IF NOT EXISTS inventory (
|
| 19 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 20 |
+
name TEXT NOT NULL,
|
| 21 |
+
category TEXT,
|
| 22 |
+
quantity TEXT,
|
| 23 |
+
expiry_date DATE,
|
| 24 |
+
added_date DATE DEFAULT CURRENT_DATE
|
| 25 |
+
)
|
| 26 |
+
''')
|
| 27 |
+
conn.commit()
|
| 28 |
+
conn.close()
|
| 29 |
+
|
| 30 |
+
# Initialize on start
|
| 31 |
+
if not os.path.exists(DB_FILE):
|
| 32 |
+
init_db()
|
| 33 |
+
|
| 34 |
+
# --- Serve Frontend ---
|
| 35 |
+
@app.route('/')
|
| 36 |
+
def serve_index():
|
| 37 |
+
return send_from_directory('.', 'index.html')
|
| 38 |
+
|
| 39 |
+
@app.route('/<path:path>')
|
| 40 |
+
def serve_static(path):
|
| 41 |
+
return send_from_directory('.', path)
|
| 42 |
+
|
| 43 |
+
# --- API Endpoints ---
|
| 44 |
+
|
| 45 |
+
@app.route('/api/add-item', methods=['POST'])
|
| 46 |
+
def add_item():
|
| 47 |
+
data = request.json
|
| 48 |
+
name = data.get('name')
|
| 49 |
+
quantity = data.get('quantity', '1 unit')
|
| 50 |
+
category = data.get('category', 'Uncategorized')
|
| 51 |
+
expiry_date = data.get('expiryDate')
|
| 52 |
+
|
| 53 |
+
# AI Prediction if no date provided
|
| 54 |
+
if not expiry_date:
|
| 55 |
+
days = model.predict_days(name)
|
| 56 |
+
expiry_date = (datetime.now() + timedelta(days=days)).strftime('%Y-%m-%d')
|
| 57 |
+
print(f"AI Predicted expiry for {name}: {expiry_date} ({days} days)")
|
| 58 |
+
|
| 59 |
+
conn = sqlite3.connect(DB_FILE)
|
| 60 |
+
c = conn.cursor()
|
| 61 |
+
c.execute('INSERT INTO inventory (name, category, quantity, expiry_date) VALUES (?, ?, ?, ?)',
|
| 62 |
+
(name, category, quantity, expiry_date))
|
| 63 |
+
conn.commit()
|
| 64 |
+
conn.close()
|
| 65 |
+
|
| 66 |
+
return jsonify({"success": True, "message": "Item added", "expiryDate": expiry_date})
|
| 67 |
+
|
| 68 |
+
@app.route('/api/get-items', methods=['GET'])
|
| 69 |
+
def get_items():
|
| 70 |
+
conn = sqlite3.connect(DB_FILE)
|
| 71 |
+
conn.row_factory = sqlite3.Row # Access by column name
|
| 72 |
+
c = conn.cursor()
|
| 73 |
+
c.execute('SELECT * FROM inventory ORDER BY expiry_date ASC')
|
| 74 |
+
rows = c.fetchall()
|
| 75 |
+
conn.close()
|
| 76 |
+
|
| 77 |
+
items = []
|
| 78 |
+
for row in rows:
|
| 79 |
+
items.append({
|
| 80 |
+
"id": row['id'],
|
| 81 |
+
"name": row['name'],
|
| 82 |
+
"quantity": row['quantity'],
|
| 83 |
+
"category": row['category'],
|
| 84 |
+
"expiryDate": row['expiry_date']
|
| 85 |
+
})
|
| 86 |
+
|
| 87 |
+
return jsonify({"items": items})
|
| 88 |
+
|
| 89 |
+
@app.route('/api/suggest-recipes', methods=['POST'])
|
| 90 |
+
def suggest_recipes():
|
| 91 |
+
# Simple Mock Logic for Recipes (Integration with Real Recipe API would go here)
|
| 92 |
+
# For now, we return static logic based on inventory
|
| 93 |
+
return jsonify([
|
| 94 |
+
{
|
| 95 |
+
"title": "Smart AI Salad",
|
| 96 |
+
"image": "https://images.unsplash.com/photo-1512621776951-a57141f2eefd?w=500",
|
| 97 |
+
"ingredients": "Based on your Veggies"
|
| 98 |
+
},
|
| 99 |
+
{
|
| 100 |
+
"title": "Leftover Stir Fry",
|
| 101 |
+
"image": "https://images.unsplash.com/photo-1603133872878-684f208fb74b?w=500",
|
| 102 |
+
"ingredients": "Mix of everything expiring soon"
|
| 103 |
+
}
|
| 104 |
+
])
|
| 105 |
+
|
| 106 |
+
@app.route('/api/dashboard-stats', methods=['GET'])
|
| 107 |
+
def get_stats():
|
| 108 |
+
# Helper to calculate stats backend-side if needed
|
| 109 |
+
pass
|
| 110 |
+
|
| 111 |
+
if __name__ == '__main__':
|
| 112 |
+
# Train model on start if needed
|
| 113 |
+
model.load_model()
|
| 114 |
+
print("Server running on http://0.0.0.0:7860")
|
| 115 |
+
app.run(host='0.0.0.0', port=7860)
|
config.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const CONFIG = {
|
| 2 |
+
// For Python Backend, we use relative paths since we serve frontend from the same server.
|
| 3 |
+
// If running separately, use 'http://localhost:5000'
|
| 4 |
+
N8N_BASE_URL: ''
|
| 5 |
+
};
|
| 6 |
+
|
| 7 |
+
function updateConfig(url) {
|
| 8 |
+
// Legacy function, no longer needed for Python version but kept for compatibility
|
| 9 |
+
console.log("Config update ignored in Python mode");
|
| 10 |
+
}
|
debug.html
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<title>N8N Connection Debugger</title>
|
| 7 |
+
<style>
|
| 8 |
+
body {
|
| 9 |
+
font-family: monospace;
|
| 10 |
+
background: #111;
|
| 11 |
+
color: #0f0;
|
| 12 |
+
padding: 20px;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
input {
|
| 16 |
+
width: 100%;
|
| 17 |
+
padding: 10px;
|
| 18 |
+
margin-bottom: 10px;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
button {
|
| 22 |
+
padding: 10px 20px;
|
| 23 |
+
cursor: pointer;
|
| 24 |
+
font-weight: bold;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
#log {
|
| 28 |
+
margin-top: 20px;
|
| 29 |
+
white-space: pre-wrap;
|
| 30 |
+
border: 1px solid #333;
|
| 31 |
+
padding: 10px;
|
| 32 |
+
}
|
| 33 |
+
</style>
|
| 34 |
+
</head>
|
| 35 |
+
|
| 36 |
+
<body>
|
| 37 |
+
<h1>N8N Connection Debugger</h1>
|
| 38 |
+
<p>Paste your Webhook URL exactly as it is in N8N:</p>
|
| 39 |
+
<input type="text" id="url" value="https://emotiondetectionsys.app.n8n.cloud/webhook/webhook">
|
| 40 |
+
|
| 41 |
+
<div style="margin-bottom: 20px;">
|
| 42 |
+
<button onclick="test('POST')">Test POST (Add Item)</button>
|
| 43 |
+
<button onclick="test('GET')">Test GET (If supported)</button>
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
<div id="log">Logs will appear here...</div>
|
| 47 |
+
|
| 48 |
+
<script>
|
| 49 |
+
async function test(method) {
|
| 50 |
+
const url = document.getElementById('url').value;
|
| 51 |
+
const log = document.getElementById('log');
|
| 52 |
+
log.innerHTML = `Testing ${method} to ${url}...\n`;
|
| 53 |
+
|
| 54 |
+
try {
|
| 55 |
+
const payload = method === 'POST' ? { action: 'get_items', test: true } : undefined;
|
| 56 |
+
|
| 57 |
+
const response = await fetch(url, {
|
| 58 |
+
method: method,
|
| 59 |
+
headers: { 'Content-Type': 'application/json' },
|
| 60 |
+
body: payload ? JSON.stringify(payload) : undefined
|
| 61 |
+
});
|
| 62 |
+
|
| 63 |
+
log.innerHTML += `Status: ${response.status} ${response.statusText}\n`;
|
| 64 |
+
|
| 65 |
+
const text = await response.text();
|
| 66 |
+
log.innerHTML += `Response Body: "${text}"\n`;
|
| 67 |
+
|
| 68 |
+
if (!text) {
|
| 69 |
+
log.innerHTML += `\n[ERROR] The response body is EMPTY. This causes the JSON error.\n`;
|
| 70 |
+
log.innerHTML += `Check N8N "Respond" setting again.`;
|
| 71 |
+
} else {
|
| 72 |
+
try {
|
| 73 |
+
const json = JSON.parse(text);
|
| 74 |
+
log.innerHTML += `\n[SUCCESS] Valid JSON received!\n`;
|
| 75 |
+
console.log(json);
|
| 76 |
+
} catch (e) {
|
| 77 |
+
log.innerHTML += `\n[ERROR] Response is not JSON.`;
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
} catch (error) {
|
| 82 |
+
log.innerHTML += `\n[FATAL ERROR] ${error.message}\n`;
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
</script>
|
| 86 |
+
</body>
|
| 87 |
+
|
| 88 |
+
</html>
|
expiry_model.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b7ed6b02e0e51f404bfbda0db25c93bc6b668051238fc18ff6367fe4897938fe
|
| 3 |
+
size 8868
|
firebase.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"hosting": {
|
| 3 |
+
"public": ".",
|
| 4 |
+
"ignore": [
|
| 5 |
+
"firebase.json",
|
| 6 |
+
"**/.*",
|
| 7 |
+
"**/node_modules/**"
|
| 8 |
+
],
|
| 9 |
+
"headers": [
|
| 10 |
+
{
|
| 11 |
+
"source": "**",
|
| 12 |
+
"headers": [
|
| 13 |
+
{
|
| 14 |
+
"key": "Access-Control-Allow-Origin",
|
| 15 |
+
"value": "*"
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"key": "Access-Control-Allow-Methods",
|
| 19 |
+
"value": "GET, POST, OPTIONS"
|
| 20 |
+
}
|
| 21 |
+
]
|
| 22 |
+
}
|
| 23 |
+
]
|
| 24 |
+
}
|
| 25 |
+
}
|
food_waste.db
ADDED
|
Binary file (12.3 kB). View file
|
|
|
index.html
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>SustainaBite | Food Waste Assistant</title>
|
| 8 |
+
<link rel="stylesheet" href="style.css">
|
| 9 |
+
<!-- Ionicons for Icons -->
|
| 10 |
+
<script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script>
|
| 11 |
+
<script nomodule src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script>
|
| 12 |
+
</head>
|
| 13 |
+
|
| 14 |
+
<body>
|
| 15 |
+
<div class="app-container">
|
| 16 |
+
<!-- Header -->
|
| 17 |
+
<header>
|
| 18 |
+
<div class="logo">
|
| 19 |
+
<ion-icon name="leaf"></ion-icon>
|
| 20 |
+
Sustaina<span>Bite</span>
|
| 21 |
+
</div>
|
| 22 |
+
<nav>
|
| 23 |
+
<ul>
|
| 24 |
+
<li><button class="active" onclick="showSection('dashboard')">Dashboard</button></li>
|
| 25 |
+
<li><button onclick="showSection('inventory')">Inventory</button></li>
|
| 26 |
+
<li><button onclick="showSection('recipes')">Recipes</button></li>
|
| 27 |
+
<li><button onclick="showSection('settings')"><ion-icon name="settings-outline"></ion-icon></button>
|
| 28 |
+
</li>
|
| 29 |
+
</ul>
|
| 30 |
+
</nav>
|
| 31 |
+
</header>
|
| 32 |
+
|
| 33 |
+
<!-- Main Content -->
|
| 34 |
+
<!-- DASHBOARD VIEW -->
|
| 35 |
+
<main id="dashboard-section">
|
| 36 |
+
<div class="stats-container">
|
| 37 |
+
<div class="card stat-card">
|
| 38 |
+
<span class="stat-label">Items Tracked</span>
|
| 39 |
+
<div class="stat-value" id="total-items">0</div>
|
| 40 |
+
</div>
|
| 41 |
+
<div class="card stat-card">
|
| 42 |
+
<span class="stat-label">Expiring Soon</span>
|
| 43 |
+
<div class="stat-value" id="expiring-soon">0</div>
|
| 44 |
+
</div>
|
| 45 |
+
<div class="card stat-card">
|
| 46 |
+
<span class="stat-label">Waste Avoided</span>
|
| 47 |
+
<div class="stat-value" id="waste-avoided">0kg</div>
|
| 48 |
+
</div>
|
| 49 |
+
<div class="card stat-card">
|
| 50 |
+
<span class="stat-label">Money Saved</span>
|
| 51 |
+
<div class="stat-value" id="money-saved">$0</div>
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
|
| 55 |
+
<div class="card action-section">
|
| 56 |
+
<h3 class="mb-4">Recent Alerts</h3>
|
| 57 |
+
<div id="alerts-container">
|
| 58 |
+
<!-- Dynamic Alerts -->
|
| 59 |
+
<p class="text-muted">No urgent alerts.</p>
|
| 60 |
+
</div>
|
| 61 |
+
</div>
|
| 62 |
+
|
| 63 |
+
<div class="card list-section">
|
| 64 |
+
<h3 class="mb-4">Quick Add Item</h3>
|
| 65 |
+
<form id="quick-add-form">
|
| 66 |
+
<div class="input-group">
|
| 67 |
+
<label>Item Name</label>
|
| 68 |
+
<input type="text" id="quick-name" placeholder="e.g. Milk" required>
|
| 69 |
+
</div>
|
| 70 |
+
<div class="input-group">
|
| 71 |
+
<label>Expiry Date</label>
|
| 72 |
+
<input type="date" id="quick-date" required>
|
| 73 |
+
</div>
|
| 74 |
+
<button type="submit" class="primary-btn">
|
| 75 |
+
<ion-icon name="add-circle-outline"></ion-icon> Add Item
|
| 76 |
+
</button>
|
| 77 |
+
<p id="quick-msg" style="margin-top:10px; font-size: 0.9rem;"></p>
|
| 78 |
+
</form>
|
| 79 |
+
</div>
|
| 80 |
+
</main>
|
| 81 |
+
|
| 82 |
+
<!-- INVENTORY VIEW -->
|
| 83 |
+
<main id="inventory-section" class="hidden">
|
| 84 |
+
<div class="card action-section">
|
| 85 |
+
<h3 class="mb-4">Log Grocery Item</h3>
|
| 86 |
+
<form id="add-item-form">
|
| 87 |
+
<div class="input-group">
|
| 88 |
+
<label>Item Name</label>
|
| 89 |
+
<input type="text" name="name" placeholder="Avocados" required>
|
| 90 |
+
</div>
|
| 91 |
+
<div class="input-group">
|
| 92 |
+
<label>Category</label>
|
| 93 |
+
<select name="category">
|
| 94 |
+
<option value="Vegetables">Vegetables</option>
|
| 95 |
+
<option value="Fruits">Fruits</option>
|
| 96 |
+
<option value="Dairy">Dairy</option>
|
| 97 |
+
<option value="Meat">Meat</option>
|
| 98 |
+
<option value="Grains">Grains</option>
|
| 99 |
+
<option value="Other">Other</option>
|
| 100 |
+
</select>
|
| 101 |
+
</div>
|
| 102 |
+
<div class="input-group">
|
| 103 |
+
<label>Quantity</label>
|
| 104 |
+
<input type="text" name="quantity" placeholder="e.g. 2 pcs or 500g">
|
| 105 |
+
</div>
|
| 106 |
+
<div class="input-group">
|
| 107 |
+
<label>Expiry Date (Optional - AI will predict)</label>
|
| 108 |
+
<input type="date" name="expiryDate">
|
| 109 |
+
</div>
|
| 110 |
+
<button type="submit" class="primary-btn">
|
| 111 |
+
<span>Add to Inventory</span>
|
| 112 |
+
</button>
|
| 113 |
+
</form>
|
| 114 |
+
</div>
|
| 115 |
+
|
| 116 |
+
<div class="card list-section">
|
| 117 |
+
<h3 class="mb-4">Current Inventory</h3>
|
| 118 |
+
<div class="tools mb-4" style="display: flex; gap: 10px;">
|
| 119 |
+
<input type="text" placeholder="Search items..." id="search-inventory">
|
| 120 |
+
<button class="primary-btn" style="width: auto;" onclick="refreshInventory()">
|
| 121 |
+
<ion-icon name="refresh"></ion-icon>
|
| 122 |
+
</button>
|
| 123 |
+
</div>
|
| 124 |
+
<ul class="inventory-list" id="inventory-list-ul">
|
| 125 |
+
<!-- Dynamic List Items -->
|
| 126 |
+
</ul>
|
| 127 |
+
</div>
|
| 128 |
+
</main>
|
| 129 |
+
|
| 130 |
+
<!-- RECIPES VIEW -->
|
| 131 |
+
<main id="recipes-section" class="hidden">
|
| 132 |
+
<div class="card action-section">
|
| 133 |
+
<h3 class="mb-4">Cook Assistant</h3>
|
| 134 |
+
<p class="mb-4 text-muted">Generate recipes based on ingredients that are expiring soon.</p>
|
| 135 |
+
<button class="primary-btn" onclick="getRecipeSuggestions()">
|
| 136 |
+
<ion-icon name="restaurant"></ion-icon> Suggest Recipes
|
| 137 |
+
</button>
|
| 138 |
+
</div>
|
| 139 |
+
|
| 140 |
+
<div class="card list-section">
|
| 141 |
+
<h3 class="mb-4">Suggested For You</h3>
|
| 142 |
+
<div id="recipes-container" class="recipes-grid">
|
| 143 |
+
<!-- Dynamic Recipes -->
|
| 144 |
+
<p class="text-muted">Click "Suggest Recipes" to see ideas.</p>
|
| 145 |
+
</div>
|
| 146 |
+
</div>
|
| 147 |
+
</main>
|
| 148 |
+
|
| 149 |
+
<!-- SETTINGS VIEW -->
|
| 150 |
+
<main id="settings-section" class="hidden">
|
| 151 |
+
<div class="card" style="grid-column: span 12; max-width: 600px; margin: 0 auto;">
|
| 152 |
+
<h3 class="mb-4">Settings</h3>
|
| 153 |
+
<p class="mb-4">Configure your N8N Webhook URLs below to connect the backend.</p>
|
| 154 |
+
|
| 155 |
+
<div class="input-group">
|
| 156 |
+
<label>Base N8N Webhook URL</label>
|
| 157 |
+
<input type="text" id="config-webhook-url" placeholder="https://your-n8n-instance.com/webhook/...">
|
| 158 |
+
</div>
|
| 159 |
+
|
| 160 |
+
<div style="display: flex; gap: 10px; margin-bottom: 20px;">
|
| 161 |
+
<button class="primary-btn" onclick="saveSettings()">Save Configuration</button>
|
| 162 |
+
<button class="primary-btn" style="background: var(--secondary);" onclick="testConnection()">Test
|
| 163 |
+
Connection</button>
|
| 164 |
+
</div>
|
| 165 |
+
|
| 166 |
+
<div id="connection-log"
|
| 167 |
+
style="font-family: monospace; font-size: 0.85rem; padding: 10px; background: rgba(0,0,0,0.3); border-radius: 8px; display: none;">
|
| 168 |
+
</div>
|
| 169 |
+
</div>
|
| 170 |
+
</main>
|
| 171 |
+
|
| 172 |
+
</div>
|
| 173 |
+
|
| 174 |
+
<!-- Scripts -->
|
| 175 |
+
<script src="config.js"></script>
|
| 176 |
+
<script src="app.js"></script>
|
| 177 |
+
</body>
|
| 178 |
+
|
| 179 |
+
</html>
|
model.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
from sklearn.feature_extraction.text import TfidfVectorizer
|
| 3 |
+
from sklearn.naive_bayes import MultinomialNB
|
| 4 |
+
from sklearn.pipeline import make_pipeline
|
| 5 |
+
import pickle
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
# 1. Dataset of common foods and their typical shelf life (in days)
|
| 9 |
+
# This acts as our "Knowledge Base"
|
| 10 |
+
data = [
|
| 11 |
+
# Dairy
|
| 12 |
+
("Milk", 7), ("Yogurt", 14), ("Cheese", 30), ("Butter", 60), ("Cream", 10),
|
| 13 |
+
# Vegetables
|
| 14 |
+
("Spinach", 5), ("Lettuce", 5), ("Tomato", 7), ("Carrot", 21), ("Potato", 30), ("Onion", 30),
|
| 15 |
+
# Fruits
|
| 16 |
+
("Apple", 21), ("Banana", 5), ("Orange", 14), ("Grapes", 7), ("Strawberry", 4),
|
| 17 |
+
# Meat
|
| 18 |
+
("Chicken", 2), ("Beef", 3), ("Pork", 3), ("Fish", 2), ("Ham", 5),
|
| 19 |
+
# Grains
|
| 20 |
+
("Bread", 5), ("Rice", 365), ("Pasta", 365), ("Cereal", 180),
|
| 21 |
+
# Misc
|
| 22 |
+
("Eggs", 21), ("Juice", 10), ("Sauce", 90), ("Canned Beans", 700)
|
| 23 |
+
]
|
| 24 |
+
|
| 25 |
+
MODEL_PATH = "expiry_model.pkl"
|
| 26 |
+
|
| 27 |
+
def train_model():
|
| 28 |
+
print("Training Expiry Prediction Model...")
|
| 29 |
+
df = pd.DataFrame(data, columns=["item", "days"])
|
| 30 |
+
|
| 31 |
+
# Simple NLP pipeline: Text -> Vector -> Classifier
|
| 32 |
+
model = make_pipeline(TfidfVectorizer(), MultinomialNB())
|
| 33 |
+
model.fit(df["item"], df["days"])
|
| 34 |
+
|
| 35 |
+
with open(MODEL_PATH, "wb") as f:
|
| 36 |
+
pickle.dump(model, f)
|
| 37 |
+
print("Model saved to", MODEL_PATH)
|
| 38 |
+
return model
|
| 39 |
+
|
| 40 |
+
def load_model():
|
| 41 |
+
if os.path.exists(MODEL_PATH):
|
| 42 |
+
with open(MODEL_PATH, "rb") as f:
|
| 43 |
+
return pickle.load(f)
|
| 44 |
+
else:
|
| 45 |
+
return train_model()
|
| 46 |
+
|
| 47 |
+
def predict_days(item_name):
|
| 48 |
+
model = load_model()
|
| 49 |
+
try:
|
| 50 |
+
# Predict
|
| 51 |
+
predicted_days = model.predict([item_name])[0]
|
| 52 |
+
return int(predicted_days)
|
| 53 |
+
except Exception as e:
|
| 54 |
+
print(f"Prediction Error: {e}")
|
| 55 |
+
return 7 # Default fallback
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask
|
| 2 |
+
flask-cors
|
| 3 |
+
pandas
|
| 4 |
+
scikit-learn
|
| 5 |
+
numpy
|
style.css
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Google Fonts */
|
| 2 |
+
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap');
|
| 3 |
+
|
| 4 |
+
:root {
|
| 5 |
+
/* Color Palette - Sustainable Tech */
|
| 6 |
+
--primary: #10b981; /* Emerald 500 */
|
| 7 |
+
--primary-hover: #059669;
|
| 8 |
+
--secondary: #3b82f6; /* Blue 500 */
|
| 9 |
+
--accent: #f59e0b; /* Amber 500 */
|
| 10 |
+
--danger: #ef4444; /* Red 500 */
|
| 11 |
+
|
| 12 |
+
--bg-dark: #0f172a; /* Slate 900 */
|
| 13 |
+
--bg-card: rgba(30, 41, 59, 0.7); /* Slate 800 with opacity */
|
| 14 |
+
--text-main: #f8fafc; /* Slate 50 */
|
| 15 |
+
--text-muted: #94a3b8; /* Slate 400 */
|
| 16 |
+
|
| 17 |
+
--glass-border: 1px solid rgba(255, 255, 255, 0.1);
|
| 18 |
+
--glass-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
| 19 |
+
--radius: 16px;
|
| 20 |
+
--transition: all 0.3s ease;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
* {
|
| 24 |
+
margin: 0;
|
| 25 |
+
padding: 0;
|
| 26 |
+
box-sizing: border-box;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
body {
|
| 30 |
+
font-family: 'Outfit', sans-serif;
|
| 31 |
+
background-color: var(--bg-dark);
|
| 32 |
+
color: var(--text-main);
|
| 33 |
+
background-image:
|
| 34 |
+
radial-gradient(at 0% 0%, hsla(253,16%,7%,1) 0, transparent 50%),
|
| 35 |
+
radial-gradient(at 50% 0%, hsla(225,39%,30%,1) 0, transparent 50%),
|
| 36 |
+
radial-gradient(at 100% 0%, hsla(339,49%,30%,1) 0, transparent 50%);
|
| 37 |
+
min-height: 100vh;
|
| 38 |
+
overflow-x: hidden;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
/* Layout */
|
| 42 |
+
.app-container {
|
| 43 |
+
max-width: 1200px;
|
| 44 |
+
margin: 0 auto;
|
| 45 |
+
padding: 20px;
|
| 46 |
+
display: grid;
|
| 47 |
+
grid-template-rows: auto 1fr;
|
| 48 |
+
gap: 30px;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/* Header */
|
| 52 |
+
header {
|
| 53 |
+
display: flex;
|
| 54 |
+
justify-content: space-between;
|
| 55 |
+
align-items: center;
|
| 56 |
+
padding: 20px 0;
|
| 57 |
+
animation: fadeInDown 0.8s ease-out;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.logo {
|
| 61 |
+
font-size: 1.5rem;
|
| 62 |
+
font-weight: 700;
|
| 63 |
+
color: var(--primary);
|
| 64 |
+
display: flex;
|
| 65 |
+
align-items: center;
|
| 66 |
+
gap: 10px;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.logo span {
|
| 70 |
+
color: var(--text-main);
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
nav ul {
|
| 74 |
+
display: flex;
|
| 75 |
+
gap: 20px;
|
| 76 |
+
list-style: none;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
nav button {
|
| 80 |
+
background: transparent;
|
| 81 |
+
border: none;
|
| 82 |
+
color: var(--text-muted);
|
| 83 |
+
font-size: 1rem;
|
| 84 |
+
font-weight: 500;
|
| 85 |
+
cursor: pointer;
|
| 86 |
+
transition: var(--transition);
|
| 87 |
+
padding: 8px 16px;
|
| 88 |
+
border-radius: 20px;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
nav button:hover, nav button.active {
|
| 92 |
+
color: var(--text-main);
|
| 93 |
+
background: rgba(255, 255, 255, 0.1);
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
/* Main Content Grid */
|
| 97 |
+
main {
|
| 98 |
+
display: grid;
|
| 99 |
+
grid-template-columns: repeat(12, 1fr);
|
| 100 |
+
gap: 24px;
|
| 101 |
+
animation: fadeInUp 0.8s ease-out;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
/* Glassmorphism Cards */
|
| 105 |
+
.card {
|
| 106 |
+
background: var(--bg-card);
|
| 107 |
+
backdrop-filter: blur(12px);
|
| 108 |
+
-webkit-backdrop-filter: blur(12px);
|
| 109 |
+
border: var(--glass-border);
|
| 110 |
+
border-radius: var(--radius);
|
| 111 |
+
padding: 24px;
|
| 112 |
+
box-shadow: var(--glass-shadow);
|
| 113 |
+
transition: var(--transition);
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
.card:hover {
|
| 117 |
+
transform: translateY(-5px);
|
| 118 |
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2);
|
| 119 |
+
border-color: rgba(255, 255, 255, 0.2);
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
/* Specific Sections */
|
| 123 |
+
.stats-container {
|
| 124 |
+
grid-column: span 12;
|
| 125 |
+
display: grid;
|
| 126 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 127 |
+
gap: 24px;
|
| 128 |
+
margin-bottom: 24px;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.stat-card {
|
| 132 |
+
display: flex;
|
| 133 |
+
flex-direction: column;
|
| 134 |
+
align-items: center;
|
| 135 |
+
text-align: center;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.stat-value {
|
| 139 |
+
font-size: 2.5rem;
|
| 140 |
+
font-weight: 700;
|
| 141 |
+
background: linear-gradient(to right, var(--primary), var(--secondary));
|
| 142 |
+
-webkit-background-clip: text;
|
| 143 |
+
-webkit-text-fill-color: transparent;
|
| 144 |
+
margin: 10px 0;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.stat-label {
|
| 148 |
+
color: var(--text-muted);
|
| 149 |
+
font-size: 0.9rem;
|
| 150 |
+
text-transform: uppercase;
|
| 151 |
+
letter-spacing: 1px;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.action-section {
|
| 155 |
+
grid-column: span 4;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.list-section {
|
| 159 |
+
grid-column: span 8;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
@media (max-width: 768px) {
|
| 163 |
+
.action-section, .list-section {
|
| 164 |
+
grid-column: span 12;
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
/* Forms */
|
| 169 |
+
.input-group {
|
| 170 |
+
margin-bottom: 16px;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
label {
|
| 174 |
+
display: block;
|
| 175 |
+
margin-bottom: 8px;
|
| 176 |
+
color: var(--text-muted);
|
| 177 |
+
font-size: 0.9rem;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
input, select {
|
| 181 |
+
width: 100%;
|
| 182 |
+
padding: 12px;
|
| 183 |
+
background: rgba(0, 0, 0, 0.2);
|
| 184 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 185 |
+
border-radius: 8px;
|
| 186 |
+
color: var(--text-main);
|
| 187 |
+
font-family: 'Outfit', sans-serif;
|
| 188 |
+
transition: var(--transition);
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
input:focus, select:focus {
|
| 192 |
+
outline: none;
|
| 193 |
+
border-color: var(--primary);
|
| 194 |
+
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
button.primary-btn {
|
| 198 |
+
width: 100%;
|
| 199 |
+
padding: 14px;
|
| 200 |
+
background: var(--primary);
|
| 201 |
+
color: white;
|
| 202 |
+
border: none;
|
| 203 |
+
border-radius: 8px;
|
| 204 |
+
font-size: 1rem;
|
| 205 |
+
font-weight: 600;
|
| 206 |
+
cursor: pointer;
|
| 207 |
+
transition: var(--transition);
|
| 208 |
+
display: flex;
|
| 209 |
+
justify-content: center;
|
| 210 |
+
align-items: center;
|
| 211 |
+
gap: 8px;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
button.primary-btn:hover {
|
| 215 |
+
background: var(--primary-hover);
|
| 216 |
+
transform: scale(1.02);
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
/* Inventory List */
|
| 220 |
+
.inventory-list {
|
| 221 |
+
list-style: none;
|
| 222 |
+
max-height: 400px;
|
| 223 |
+
overflow-y: auto;
|
| 224 |
+
padding-right: 10px;
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
.inventory-item {
|
| 228 |
+
display: flex;
|
| 229 |
+
justify-content: space-between;
|
| 230 |
+
align-items: center;
|
| 231 |
+
padding: 16px;
|
| 232 |
+
background: rgba(255, 255, 255, 0.03);
|
| 233 |
+
border-radius: 12px;
|
| 234 |
+
margin-bottom: 12px;
|
| 235 |
+
transition: var(--transition);
|
| 236 |
+
border-left: 3px solid var(--primary);
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.inventory-item.expiring {
|
| 240 |
+
border-left-color: var(--danger);
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.inventory-item.warning {
|
| 244 |
+
border-left-color: var(--accent);
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.inventory-item:hover {
|
| 248 |
+
background: rgba(255, 255, 255, 0.06);
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
.item-info h4 {
|
| 252 |
+
margin-bottom: 4px;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
.item-meta {
|
| 256 |
+
font-size: 0.85rem;
|
| 257 |
+
color: var(--text-muted);
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
.expiry-badge {
|
| 261 |
+
padding: 4px 10px;
|
| 262 |
+
border-radius: 12px;
|
| 263 |
+
font-size: 0.8rem;
|
| 264 |
+
font-weight: 600;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.expiry-badge.safe { background: rgba(16, 185, 129, 0.2); color: var(--primary); }
|
| 268 |
+
.expiry-badge.soon { background: rgba(245, 158, 11, 0.2); color: var(--accent); }
|
| 269 |
+
.expiry-badge.expired { background: rgba(239, 68, 68, 0.2); color: var(--danger); }
|
| 270 |
+
|
| 271 |
+
/* Recipes */
|
| 272 |
+
.recipes-grid {
|
| 273 |
+
display: grid;
|
| 274 |
+
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
| 275 |
+
gap: 20px;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
.recipe-card {
|
| 279 |
+
background: rgba(0, 0, 0, 0.2);
|
| 280 |
+
border-radius: 12px;
|
| 281 |
+
overflow: hidden;
|
| 282 |
+
transition: var(--transition);
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
.recipe-card img {
|
| 286 |
+
width: 100%;
|
| 287 |
+
height: 140px;
|
| 288 |
+
object-fit: cover;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
.recipe-content {
|
| 292 |
+
padding: 16px;
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
.recipe-title {
|
| 296 |
+
font-size: 1.1rem;
|
| 297 |
+
margin-bottom: 8px;
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
.recipe-ingredients {
|
| 301 |
+
font-size: 0.85rem;
|
| 302 |
+
color: var(--text-muted);
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
/* Utility */
|
| 306 |
+
.hidden { display: none; }
|
| 307 |
+
.text-center { text-align: center; }
|
| 308 |
+
.mb-4 { margin-bottom: 16px; }
|
| 309 |
+
|
| 310 |
+
/* Scrollbar */
|
| 311 |
+
::-webkit-scrollbar {
|
| 312 |
+
width: 8px;
|
| 313 |
+
}
|
| 314 |
+
::-webkit-scrollbar-track {
|
| 315 |
+
background: rgba(0, 0, 0, 0.1);
|
| 316 |
+
}
|
| 317 |
+
::-webkit-scrollbar-thumb {
|
| 318 |
+
background: rgba(255, 255, 255, 0.2);
|
| 319 |
+
border-radius: 4px;
|
| 320 |
+
}
|
| 321 |
+
::-webkit-scrollbar-thumb:hover {
|
| 322 |
+
background: rgba(255, 255, 255, 0.3);
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
/* Animations */
|
| 326 |
+
@keyframes fadeInDown {
|
| 327 |
+
from { opacity: 0; transform: translateY(-20px); }
|
| 328 |
+
to { opacity: 1; transform: translateY(0); }
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
@keyframes fadeInUp {
|
| 332 |
+
from { opacity: 0; transform: translateY(20px); }
|
| 333 |
+
to { opacity: 1; transform: translateY(0); }
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
/* Loading Spinner */
|
| 337 |
+
.loader {
|
| 338 |
+
width: 20px;
|
| 339 |
+
height: 20px;
|
| 340 |
+
border: 3px solid #FFF;
|
| 341 |
+
border-bottom-color: transparent;
|
| 342 |
+
border-radius: 50%;
|
| 343 |
+
display: inline-block;
|
| 344 |
+
box-sizing: border-box;
|
| 345 |
+
animation: rotation 1s linear infinite;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
@keyframes rotation {
|
| 349 |
+
0% { transform: rotate(0deg); }
|
| 350 |
+
100% { transform: rotate(360deg); }
|
| 351 |
+
}
|
workflow.json
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Food Waste Assistant Backend (CORS + POST)",
|
| 3 |
+
"nodes": [
|
| 4 |
+
{
|
| 5 |
+
"parameters": {
|
| 6 |
+
"content": "## Robust Backend\n\nThis workflow handles:\n1. **POST** requests (the main logic)\n2. **OPTIONS** requests (for CORS preflight checks)\n\n### Instructions\n1. Import & Activate.\n2. Both Webhook nodes allow CORS.\n3. Copy the URL from **Webhook (POST)** and put it in your app."
|
| 7 |
+
},
|
| 8 |
+
"id": "desc",
|
| 9 |
+
"name": "Note",
|
| 10 |
+
"type": "n8n-nodes-base.stickyNote",
|
| 11 |
+
"typeVersion": 1,
|
| 12 |
+
"position": [
|
| 13 |
+
0,
|
| 14 |
+
0
|
| 15 |
+
]
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
"parameters": {
|
| 19 |
+
"httpMethod": "POST",
|
| 20 |
+
"path": "webhook",
|
| 21 |
+
"options": {}
|
| 22 |
+
},
|
| 23 |
+
"id": "wh-post",
|
| 24 |
+
"name": "Webhook (POST)",
|
| 25 |
+
"type": "n8n-nodes-base.webhook",
|
| 26 |
+
"typeVersion": 1,
|
| 27 |
+
"position": [
|
| 28 |
+
300,
|
| 29 |
+
200
|
| 30 |
+
]
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"parameters": {
|
| 34 |
+
"httpMethod": "OPTIONS",
|
| 35 |
+
"path": "webhook",
|
| 36 |
+
"options": {}
|
| 37 |
+
},
|
| 38 |
+
"id": "wh-options",
|
| 39 |
+
"name": "Webhook (OPTIONS)",
|
| 40 |
+
"type": "n8n-nodes-base.webhook",
|
| 41 |
+
"typeVersion": 1,
|
| 42 |
+
"position": [
|
| 43 |
+
300,
|
| 44 |
+
500
|
| 45 |
+
]
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"parameters": {
|
| 49 |
+
"dataType": "string",
|
| 50 |
+
"value1": "={{ $json.body.action }}",
|
| 51 |
+
"rules": {
|
| 52 |
+
"rules": [
|
| 53 |
+
{
|
| 54 |
+
"value2": "add_item",
|
| 55 |
+
"output": 0
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
"value2": "get_items",
|
| 59 |
+
"output": 1
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
"value2": "suggest_recipes",
|
| 63 |
+
"output": 2
|
| 64 |
+
}
|
| 65 |
+
]
|
| 66 |
+
}
|
| 67 |
+
},
|
| 68 |
+
"id": "switch",
|
| 69 |
+
"name": "Switch Action",
|
| 70 |
+
"type": "n8n-nodes-base.switch",
|
| 71 |
+
"typeVersion": 2,
|
| 72 |
+
"position": [
|
| 73 |
+
500,
|
| 74 |
+
200
|
| 75 |
+
]
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
"parameters": {
|
| 79 |
+
"jsCode": "return [ { json: { success: true, message: \"Item added (Real N8N)\" } } ];"
|
| 80 |
+
},
|
| 81 |
+
"id": "mock-add",
|
| 82 |
+
"name": "Mock Add",
|
| 83 |
+
"type": "n8n-nodes-base.code",
|
| 84 |
+
"typeVersion": 2,
|
| 85 |
+
"position": [
|
| 86 |
+
750,
|
| 87 |
+
100
|
| 88 |
+
]
|
| 89 |
+
},
|
| 90 |
+
{
|
| 91 |
+
"parameters": {
|
| 92 |
+
"jsCode": "return [ { json: { items: [ { \"id\": 1, \"name\": \"N8N Connected Apple\", \"quantity\": \"5\", \"expiryDate\": \"2023-12-10\", \"category\": \"Fruits\" } ] } } ];"
|
| 93 |
+
},
|
| 94 |
+
"id": "mock-get",
|
| 95 |
+
"name": "Mock Get",
|
| 96 |
+
"type": "n8n-nodes-base.code",
|
| 97 |
+
"typeVersion": 2,
|
| 98 |
+
"position": [
|
| 99 |
+
750,
|
| 100 |
+
300
|
| 101 |
+
]
|
| 102 |
+
},
|
| 103 |
+
{
|
| 104 |
+
"parameters": {
|
| 105 |
+
"jsCode": "return [ { json: [ { \"title\": \"N8N Salad\", \"image\": \"https://via.placeholder.com/150\", \"ingredients\": \"Lettuce\" } ] } ];"
|
| 106 |
+
},
|
| 107 |
+
"id": "mock-recipes",
|
| 108 |
+
"name": "Mock Recipes",
|
| 109 |
+
"type": "n8n-nodes-base.code",
|
| 110 |
+
"typeVersion": 2,
|
| 111 |
+
"position": [
|
| 112 |
+
750,
|
| 113 |
+
500
|
| 114 |
+
]
|
| 115 |
+
},
|
| 116 |
+
{
|
| 117 |
+
"parameters": {
|
| 118 |
+
"options": {
|
| 119 |
+
"responseHeaders": {
|
| 120 |
+
"entries": [
|
| 121 |
+
{
|
| 122 |
+
"name": "Access-Control-Allow-Origin",
|
| 123 |
+
"value": "*"
|
| 124 |
+
},
|
| 125 |
+
{
|
| 126 |
+
"name": "Access-Control-Allow-Methods",
|
| 127 |
+
"value": "POST, OPTIONS"
|
| 128 |
+
},
|
| 129 |
+
{
|
| 130 |
+
"name": "Access-Control-Allow-Headers",
|
| 131 |
+
"value": "*"
|
| 132 |
+
}
|
| 133 |
+
]
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
},
|
| 137 |
+
"id": "response-cors",
|
| 138 |
+
"name": "Respond to Webhook (CORS)",
|
| 139 |
+
"type": "n8n-nodes-base.respondToWebhook",
|
| 140 |
+
"typeVersion": 1,
|
| 141 |
+
"position": [
|
| 142 |
+
1100,
|
| 143 |
+
300
|
| 144 |
+
]
|
| 145 |
+
},
|
| 146 |
+
{
|
| 147 |
+
"parameters": {
|
| 148 |
+
"options": {
|
| 149 |
+
"responseHeaders": {
|
| 150 |
+
"entries": [
|
| 151 |
+
{
|
| 152 |
+
"name": "Access-Control-Allow-Origin",
|
| 153 |
+
"value": "*"
|
| 154 |
+
},
|
| 155 |
+
{
|
| 156 |
+
"name": "Access-Control-Allow-Methods",
|
| 157 |
+
"value": "POST, OPTIONS"
|
| 158 |
+
},
|
| 159 |
+
{
|
| 160 |
+
"name": "Access-Control-Allow-Headers",
|
| 161 |
+
"value": "*"
|
| 162 |
+
}
|
| 163 |
+
]
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
},
|
| 167 |
+
"id": "response-options",
|
| 168 |
+
"name": "Respond OPTIONS",
|
| 169 |
+
"type": "n8n-nodes-base.respondToWebhook",
|
| 170 |
+
"typeVersion": 1,
|
| 171 |
+
"position": [
|
| 172 |
+
600,
|
| 173 |
+
500
|
| 174 |
+
]
|
| 175 |
+
}
|
| 176 |
+
],
|
| 177 |
+
"connections": {
|
| 178 |
+
"Webhook (POST)": {
|
| 179 |
+
"main": [
|
| 180 |
+
[
|
| 181 |
+
{
|
| 182 |
+
"node": "Switch Action",
|
| 183 |
+
"type": "main",
|
| 184 |
+
"index": 0
|
| 185 |
+
}
|
| 186 |
+
]
|
| 187 |
+
]
|
| 188 |
+
},
|
| 189 |
+
"Webhook (OPTIONS)": {
|
| 190 |
+
"main": [
|
| 191 |
+
[
|
| 192 |
+
{
|
| 193 |
+
"node": "Respond OPTIONS",
|
| 194 |
+
"type": "main",
|
| 195 |
+
"index": 0
|
| 196 |
+
}
|
| 197 |
+
]
|
| 198 |
+
]
|
| 199 |
+
},
|
| 200 |
+
"Switch Action": {
|
| 201 |
+
"main": [
|
| 202 |
+
[
|
| 203 |
+
{
|
| 204 |
+
"node": "Mock Add",
|
| 205 |
+
"type": "main",
|
| 206 |
+
"index": 0
|
| 207 |
+
}
|
| 208 |
+
],
|
| 209 |
+
[
|
| 210 |
+
{
|
| 211 |
+
"node": "Mock Get",
|
| 212 |
+
"type": "main",
|
| 213 |
+
"index": 0
|
| 214 |
+
}
|
| 215 |
+
],
|
| 216 |
+
[
|
| 217 |
+
{
|
| 218 |
+
"node": "Mock Recipes",
|
| 219 |
+
"type": "main",
|
| 220 |
+
"index": 0
|
| 221 |
+
}
|
| 222 |
+
]
|
| 223 |
+
]
|
| 224 |
+
},
|
| 225 |
+
"Mock Add": {
|
| 226 |
+
"main": [
|
| 227 |
+
[
|
| 228 |
+
{
|
| 229 |
+
"node": "Respond to Webhook (CORS)",
|
| 230 |
+
"type": "main",
|
| 231 |
+
"index": 0
|
| 232 |
+
}
|
| 233 |
+
]
|
| 234 |
+
]
|
| 235 |
+
},
|
| 236 |
+
"Mock Get": {
|
| 237 |
+
"main": [
|
| 238 |
+
[
|
| 239 |
+
{
|
| 240 |
+
"node": "Respond to Webhook (CORS)",
|
| 241 |
+
"type": "main",
|
| 242 |
+
"index": 0
|
| 243 |
+
}
|
| 244 |
+
]
|
| 245 |
+
]
|
| 246 |
+
},
|
| 247 |
+
"Mock Recipes": {
|
| 248 |
+
"main": [
|
| 249 |
+
[
|
| 250 |
+
{
|
| 251 |
+
"node": "Respond to Webhook (CORS)",
|
| 252 |
+
"type": "main",
|
| 253 |
+
"index": 0
|
| 254 |
+
}
|
| 255 |
+
]
|
| 256 |
+
]
|
| 257 |
+
}
|
| 258 |
+
}
|
| 259 |
+
}
|
workflow_real.json
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Food Waste Assistant (Real - Google Sheets)",
|
| 3 |
+
"nodes": [
|
| 4 |
+
{
|
| 5 |
+
"parameters": {
|
| 6 |
+
"content": "## Real Database Setup (Google Sheets)\n\nThis workflow reads/writes to a Google Sheet.\n\n### Setup Steps\n1. Create a Google Sheet.\n2. Create header row: `id`, `name`, `quantity`, `expiryDate`, `category`.\n3. Double-click **Google Sheets** nodes to authenticate.\n4. Select your Sheet in the node settings.",
|
| 7 |
+
"height": 240,
|
| 8 |
+
"width": 350
|
| 9 |
+
},
|
| 10 |
+
"id": "note-real",
|
| 11 |
+
"name": "Sticky Note",
|
| 12 |
+
"type": "n8n-nodes-base.stickyNote",
|
| 13 |
+
"typeVersion": 1,
|
| 14 |
+
"position": [
|
| 15 |
+
0,
|
| 16 |
+
0
|
| 17 |
+
]
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"parameters": {
|
| 21 |
+
"httpMethod": "POST",
|
| 22 |
+
"path": "webhook",
|
| 23 |
+
"options": {}
|
| 24 |
+
},
|
| 25 |
+
"id": "webhook-real",
|
| 26 |
+
"name": "Webhook (POST)",
|
| 27 |
+
"type": "n8n-nodes-base.webhook",
|
| 28 |
+
"typeVersion": 1,
|
| 29 |
+
"position": [
|
| 30 |
+
400,
|
| 31 |
+
200
|
| 32 |
+
]
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"parameters": {
|
| 36 |
+
"httpMethod": "OPTIONS",
|
| 37 |
+
"path": "webhook",
|
| 38 |
+
"options": {}
|
| 39 |
+
},
|
| 40 |
+
"id": "webhook-opt-real",
|
| 41 |
+
"name": "Webhook (OPTIONS)",
|
| 42 |
+
"type": "n8n-nodes-base.webhook",
|
| 43 |
+
"typeVersion": 1,
|
| 44 |
+
"position": [
|
| 45 |
+
400,
|
| 46 |
+
500
|
| 47 |
+
]
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"parameters": {
|
| 51 |
+
"dataType": "string",
|
| 52 |
+
"value1": "={{ $json.body.action }}",
|
| 53 |
+
"rules": {
|
| 54 |
+
"rules": [
|
| 55 |
+
{
|
| 56 |
+
"value2": "add_item",
|
| 57 |
+
"output": 0
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
"value2": "get_items",
|
| 61 |
+
"output": 1
|
| 62 |
+
}
|
| 63 |
+
]
|
| 64 |
+
}
|
| 65 |
+
},
|
| 66 |
+
"id": "switch-real",
|
| 67 |
+
"name": "Switch Action",
|
| 68 |
+
"type": "n8n-nodes-base.switch",
|
| 69 |
+
"typeVersion": 2,
|
| 70 |
+
"position": [
|
| 71 |
+
600,
|
| 72 |
+
200
|
| 73 |
+
]
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
"parameters": {
|
| 77 |
+
"operation": "append",
|
| 78 |
+
"sheetId": {
|
| 79 |
+
"__rl": true,
|
| 80 |
+
"mode": "fromStr"
|
| 81 |
+
},
|
| 82 |
+
"range": "A:E",
|
| 83 |
+
"options": {}
|
| 84 |
+
},
|
| 85 |
+
"id": "gs-add",
|
| 86 |
+
"name": "Google Sheets (Add)",
|
| 87 |
+
"type": "n8n-nodes-base.googleSheets",
|
| 88 |
+
"typeVersion": 4,
|
| 89 |
+
"position": [
|
| 90 |
+
850,
|
| 91 |
+
100
|
| 92 |
+
],
|
| 93 |
+
"credentials": {
|
| 94 |
+
"googleSheetsOAuth2Api": {
|
| 95 |
+
"id": "",
|
| 96 |
+
"name": "Google Sheets account"
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
},
|
| 100 |
+
{
|
| 101 |
+
"parameters": {
|
| 102 |
+
"operation": "read",
|
| 103 |
+
"sheetId": {
|
| 104 |
+
"__rl": true,
|
| 105 |
+
"mode": "fromStr"
|
| 106 |
+
},
|
| 107 |
+
"range": "A:E",
|
| 108 |
+
"options": {}
|
| 109 |
+
},
|
| 110 |
+
"id": "gs-read",
|
| 111 |
+
"name": "Google Sheets (Read)",
|
| 112 |
+
"type": "n8n-nodes-base.googleSheets",
|
| 113 |
+
"typeVersion": 4,
|
| 114 |
+
"position": [
|
| 115 |
+
850,
|
| 116 |
+
300
|
| 117 |
+
],
|
| 118 |
+
"credentials": {
|
| 119 |
+
"googleSheetsOAuth2Api": {
|
| 120 |
+
"id": "",
|
| 121 |
+
"name": "Google Sheets account"
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
},
|
| 125 |
+
{
|
| 126 |
+
"parameters": {
|
| 127 |
+
"jsCode": "const items = $input.all().map(i => i.json);\nreturn [{ json: { items: items } }];"
|
| 128 |
+
},
|
| 129 |
+
"id": "format-read",
|
| 130 |
+
"name": "Format Read",
|
| 131 |
+
"type": "n8n-nodes-base.code",
|
| 132 |
+
"typeVersion": 2,
|
| 133 |
+
"position": [
|
| 134 |
+
1100,
|
| 135 |
+
300
|
| 136 |
+
]
|
| 137 |
+
},
|
| 138 |
+
{
|
| 139 |
+
"parameters": {
|
| 140 |
+
"jsCode": "return [{ json: { success: true, message: \"Item saved to Sheets\" } }];"
|
| 141 |
+
},
|
| 142 |
+
"id": "format-add",
|
| 143 |
+
"name": "Format Add",
|
| 144 |
+
"type": "n8n-nodes-base.code",
|
| 145 |
+
"typeVersion": 2,
|
| 146 |
+
"position": [
|
| 147 |
+
1100,
|
| 148 |
+
100
|
| 149 |
+
]
|
| 150 |
+
},
|
| 151 |
+
{
|
| 152 |
+
"parameters": {
|
| 153 |
+
"options": {
|
| 154 |
+
"responseHeaders": {
|
| 155 |
+
"entries": [
|
| 156 |
+
{
|
| 157 |
+
"name": "Access-Control-Allow-Origin",
|
| 158 |
+
"value": "*"
|
| 159 |
+
},
|
| 160 |
+
{
|
| 161 |
+
"name": "Access-Control-Allow-Methods",
|
| 162 |
+
"value": "POST, OPTIONS"
|
| 163 |
+
},
|
| 164 |
+
{
|
| 165 |
+
"name": "Access-Control-Allow-Headers",
|
| 166 |
+
"value": "*"
|
| 167 |
+
}
|
| 168 |
+
]
|
| 169 |
+
}
|
| 170 |
+
}
|
| 171 |
+
},
|
| 172 |
+
"id": "resp-real",
|
| 173 |
+
"name": "Respond to Webhook",
|
| 174 |
+
"type": "n8n-nodes-base.respondToWebhook",
|
| 175 |
+
"typeVersion": 1,
|
| 176 |
+
"position": [
|
| 177 |
+
1350,
|
| 178 |
+
200
|
| 179 |
+
]
|
| 180 |
+
},
|
| 181 |
+
{
|
| 182 |
+
"parameters": {
|
| 183 |
+
"options": {
|
| 184 |
+
"responseHeaders": {
|
| 185 |
+
"entries": [
|
| 186 |
+
{
|
| 187 |
+
"name": "Access-Control-Allow-Origin",
|
| 188 |
+
"value": "*"
|
| 189 |
+
},
|
| 190 |
+
{
|
| 191 |
+
"name": "Access-Control-Allow-Methods",
|
| 192 |
+
"value": "POST, OPTIONS"
|
| 193 |
+
},
|
| 194 |
+
{
|
| 195 |
+
"name": "Access-Control-Allow-Headers",
|
| 196 |
+
"value": "*"
|
| 197 |
+
}
|
| 198 |
+
]
|
| 199 |
+
}
|
| 200 |
+
}
|
| 201 |
+
},
|
| 202 |
+
"id": "resp-opt-real",
|
| 203 |
+
"name": "Respond OPTIONS",
|
| 204 |
+
"type": "n8n-nodes-base.respondToWebhook",
|
| 205 |
+
"typeVersion": 1,
|
| 206 |
+
"position": [
|
| 207 |
+
600,
|
| 208 |
+
500
|
| 209 |
+
]
|
| 210 |
+
}
|
| 211 |
+
],
|
| 212 |
+
"connections": {
|
| 213 |
+
"Webhook (POST)": {
|
| 214 |
+
"main": [
|
| 215 |
+
[
|
| 216 |
+
{
|
| 217 |
+
"node": "Switch Action",
|
| 218 |
+
"type": "main",
|
| 219 |
+
"index": 0
|
| 220 |
+
}
|
| 221 |
+
]
|
| 222 |
+
]
|
| 223 |
+
},
|
| 224 |
+
"Webhook (OPTIONS)": {
|
| 225 |
+
"main": [
|
| 226 |
+
[
|
| 227 |
+
{
|
| 228 |
+
"node": "Respond OPTIONS",
|
| 229 |
+
"type": "main",
|
| 230 |
+
"index": 0
|
| 231 |
+
}
|
| 232 |
+
]
|
| 233 |
+
]
|
| 234 |
+
},
|
| 235 |
+
"Switch Action": {
|
| 236 |
+
"main": [
|
| 237 |
+
[
|
| 238 |
+
{
|
| 239 |
+
"node": "Google Sheets (Add)",
|
| 240 |
+
"type": "main",
|
| 241 |
+
"index": 0
|
| 242 |
+
}
|
| 243 |
+
],
|
| 244 |
+
[
|
| 245 |
+
{
|
| 246 |
+
"node": "Google Sheets (Read)",
|
| 247 |
+
"type": "main",
|
| 248 |
+
"index": 0
|
| 249 |
+
}
|
| 250 |
+
]
|
| 251 |
+
]
|
| 252 |
+
},
|
| 253 |
+
"Google Sheets (Add)": {
|
| 254 |
+
"main": [
|
| 255 |
+
[
|
| 256 |
+
{
|
| 257 |
+
"node": "Format Add",
|
| 258 |
+
"type": "main",
|
| 259 |
+
"index": 0
|
| 260 |
+
}
|
| 261 |
+
]
|
| 262 |
+
]
|
| 263 |
+
},
|
| 264 |
+
"Google Sheets (Read)": {
|
| 265 |
+
"main": [
|
| 266 |
+
[
|
| 267 |
+
{
|
| 268 |
+
"node": "Format Read",
|
| 269 |
+
"type": "main",
|
| 270 |
+
"index": 0
|
| 271 |
+
}
|
| 272 |
+
]
|
| 273 |
+
]
|
| 274 |
+
},
|
| 275 |
+
"Format Add": {
|
| 276 |
+
"main": [
|
| 277 |
+
[
|
| 278 |
+
{
|
| 279 |
+
"node": "Respond to Webhook",
|
| 280 |
+
"type": "main",
|
| 281 |
+
"index": 0
|
| 282 |
+
}
|
| 283 |
+
]
|
| 284 |
+
]
|
| 285 |
+
},
|
| 286 |
+
"Format Read": {
|
| 287 |
+
"main": [
|
| 288 |
+
[
|
| 289 |
+
{
|
| 290 |
+
"node": "Respond to Webhook",
|
| 291 |
+
"type": "main",
|
| 292 |
+
"index": 0
|
| 293 |
+
}
|
| 294 |
+
]
|
| 295 |
+
]
|
| 296 |
+
}
|
| 297 |
+
}
|
| 298 |
+
}
|
workflow_real_safe.json
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "Food Waste Assistant (Real - Safe Mode)",
|
| 3 |
+
"nodes": [
|
| 4 |
+
{
|
| 5 |
+
"parameters": {
|
| 6 |
+
"content": "## Safe Mode Workflow\n\nThis version has a Fallback route.\nIf the Switch node doesn't match 'get_items' or 'add_item', it goes to the **Fallback** node which tells you exactly what happened.\n\nDOUBLE CLICK THE GOOGLE SHEETS NODES TO CONNECT YOUR ACCOUNT.",
|
| 7 |
+
"height": 260,
|
| 8 |
+
"width": 350
|
| 9 |
+
},
|
| 10 |
+
"id": "note-safe",
|
| 11 |
+
"name": "Sticky Note",
|
| 12 |
+
"type": "n8n-nodes-base.stickyNote",
|
| 13 |
+
"typeVersion": 1,
|
| 14 |
+
"position": [
|
| 15 |
+
0,
|
| 16 |
+
0
|
| 17 |
+
]
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"parameters": {
|
| 21 |
+
"httpMethod": "POST",
|
| 22 |
+
"path": "webhook",
|
| 23 |
+
"options": {}
|
| 24 |
+
},
|
| 25 |
+
"id": "webhook-safe",
|
| 26 |
+
"name": "Webhook (POST)",
|
| 27 |
+
"type": "n8n-nodes-base.webhook",
|
| 28 |
+
"typeVersion": 1,
|
| 29 |
+
"position": [
|
| 30 |
+
400,
|
| 31 |
+
200
|
| 32 |
+
]
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"parameters": {
|
| 36 |
+
"httpMethod": "OPTIONS",
|
| 37 |
+
"path": "webhook",
|
| 38 |
+
"options": {}
|
| 39 |
+
},
|
| 40 |
+
"id": "webhook-opt-safe",
|
| 41 |
+
"name": "Webhook (OPTIONS)",
|
| 42 |
+
"type": "n8n-nodes-base.webhook",
|
| 43 |
+
"typeVersion": 1,
|
| 44 |
+
"position": [
|
| 45 |
+
400,
|
| 46 |
+
500
|
| 47 |
+
]
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"parameters": {
|
| 51 |
+
"dataType": "string",
|
| 52 |
+
"value1": "={{ $json.body.action }}",
|
| 53 |
+
"rules": {
|
| 54 |
+
"rules": [
|
| 55 |
+
{
|
| 56 |
+
"value2": "add_item",
|
| 57 |
+
"output": 0
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
"value2": "get_items",
|
| 61 |
+
"output": 1
|
| 62 |
+
}
|
| 63 |
+
]
|
| 64 |
+
},
|
| 65 |
+
"fallbackOutput": 2
|
| 66 |
+
},
|
| 67 |
+
"id": "switch-safe",
|
| 68 |
+
"name": "Switch Action",
|
| 69 |
+
"type": "n8n-nodes-base.switch",
|
| 70 |
+
"typeVersion": 2,
|
| 71 |
+
"position": [
|
| 72 |
+
600,
|
| 73 |
+
200
|
| 74 |
+
]
|
| 75 |
+
},
|
| 76 |
+
{
|
| 77 |
+
"parameters": {
|
| 78 |
+
"operation": "append",
|
| 79 |
+
"sheetId": {
|
| 80 |
+
"__rl": true,
|
| 81 |
+
"mode": "fromStr"
|
| 82 |
+
},
|
| 83 |
+
"range": "A:E",
|
| 84 |
+
"options": {}
|
| 85 |
+
},
|
| 86 |
+
"id": "gs-add-safe",
|
| 87 |
+
"name": "Google Sheets (Add)",
|
| 88 |
+
"type": "n8n-nodes-base.googleSheets",
|
| 89 |
+
"typeVersion": 4,
|
| 90 |
+
"position": [
|
| 91 |
+
850,
|
| 92 |
+
100
|
| 93 |
+
],
|
| 94 |
+
"credentials": {
|
| 95 |
+
"googleSheetsOAuth2Api": {
|
| 96 |
+
"id": "",
|
| 97 |
+
"name": "Google Sheets account"
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
},
|
| 101 |
+
{
|
| 102 |
+
"parameters": {
|
| 103 |
+
"operation": "read",
|
| 104 |
+
"sheetId": {
|
| 105 |
+
"__rl": true,
|
| 106 |
+
"mode": "fromStr"
|
| 107 |
+
},
|
| 108 |
+
"range": "A:E",
|
| 109 |
+
"options": {}
|
| 110 |
+
},
|
| 111 |
+
"id": "gs-read-safe",
|
| 112 |
+
"name": "Google Sheets (Read)",
|
| 113 |
+
"type": "n8n-nodes-base.googleSheets",
|
| 114 |
+
"typeVersion": 4,
|
| 115 |
+
"position": [
|
| 116 |
+
850,
|
| 117 |
+
300
|
| 118 |
+
],
|
| 119 |
+
"credentials": {
|
| 120 |
+
"googleSheetsOAuth2Api": {
|
| 121 |
+
"id": "",
|
| 122 |
+
"name": "Google Sheets account"
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
"parameters": {
|
| 128 |
+
"jsCode": "return [{ json: { success: false, message: \"Action not matched: \" + $json.body.action } }];"
|
| 129 |
+
},
|
| 130 |
+
"id": "fallback-safe",
|
| 131 |
+
"name": "Fallback Error",
|
| 132 |
+
"type": "n8n-nodes-base.code",
|
| 133 |
+
"typeVersion": 2,
|
| 134 |
+
"position": [
|
| 135 |
+
850,
|
| 136 |
+
500
|
| 137 |
+
]
|
| 138 |
+
},
|
| 139 |
+
{
|
| 140 |
+
"parameters": {
|
| 141 |
+
"jsCode": "const items = $input.all().map(i => i.json);\nreturn [{ json: { items: items } }];"
|
| 142 |
+
},
|
| 143 |
+
"id": "fmt-read-safe",
|
| 144 |
+
"name": "Format Read",
|
| 145 |
+
"type": "n8n-nodes-base.code",
|
| 146 |
+
"typeVersion": 2,
|
| 147 |
+
"position": [
|
| 148 |
+
1100,
|
| 149 |
+
300
|
| 150 |
+
]
|
| 151 |
+
},
|
| 152 |
+
{
|
| 153 |
+
"parameters": {
|
| 154 |
+
"jsCode": "return [{ json: { success: true, message: \"Item saved\" } }];"
|
| 155 |
+
},
|
| 156 |
+
"id": "fmt-add-safe",
|
| 157 |
+
"name": "Format Add",
|
| 158 |
+
"type": "n8n-nodes-base.code",
|
| 159 |
+
"typeVersion": 2,
|
| 160 |
+
"position": [
|
| 161 |
+
1100,
|
| 162 |
+
100
|
| 163 |
+
]
|
| 164 |
+
},
|
| 165 |
+
{
|
| 166 |
+
"parameters": {
|
| 167 |
+
"options": {
|
| 168 |
+
"responseHeaders": {
|
| 169 |
+
"entries": [
|
| 170 |
+
{
|
| 171 |
+
"name": "Access-Control-Allow-Origin",
|
| 172 |
+
"value": "*"
|
| 173 |
+
},
|
| 174 |
+
{
|
| 175 |
+
"name": "Access-Control-Allow-Methods",
|
| 176 |
+
"value": "POST, OPTIONS"
|
| 177 |
+
},
|
| 178 |
+
{
|
| 179 |
+
"name": "Access-Control-Allow-Headers",
|
| 180 |
+
"value": "*"
|
| 181 |
+
}
|
| 182 |
+
]
|
| 183 |
+
}
|
| 184 |
+
}
|
| 185 |
+
},
|
| 186 |
+
"id": "resp-safe",
|
| 187 |
+
"name": "Respond to Webhook",
|
| 188 |
+
"type": "n8n-nodes-base.respondToWebhook",
|
| 189 |
+
"typeVersion": 1,
|
| 190 |
+
"position": [
|
| 191 |
+
1350,
|
| 192 |
+
200
|
| 193 |
+
]
|
| 194 |
+
},
|
| 195 |
+
{
|
| 196 |
+
"parameters": {
|
| 197 |
+
"options": {
|
| 198 |
+
"responseHeaders": {
|
| 199 |
+
"entries": [
|
| 200 |
+
{
|
| 201 |
+
"name": "Access-Control-Allow-Origin",
|
| 202 |
+
"value": "*"
|
| 203 |
+
},
|
| 204 |
+
{
|
| 205 |
+
"name": "Access-Control-Allow-Methods",
|
| 206 |
+
"value": "POST, OPTIONS"
|
| 207 |
+
},
|
| 208 |
+
{
|
| 209 |
+
"name": "Access-Control-Allow-Headers",
|
| 210 |
+
"value": "*"
|
| 211 |
+
}
|
| 212 |
+
]
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
},
|
| 216 |
+
"id": "resp-opt-safe",
|
| 217 |
+
"name": "Respond OPTIONS",
|
| 218 |
+
"type": "n8n-nodes-base.respondToWebhook",
|
| 219 |
+
"typeVersion": 1,
|
| 220 |
+
"position": [
|
| 221 |
+
600,
|
| 222 |
+
600
|
| 223 |
+
]
|
| 224 |
+
}
|
| 225 |
+
],
|
| 226 |
+
"connections": {
|
| 227 |
+
"Webhook (POST)": {
|
| 228 |
+
"main": [
|
| 229 |
+
[
|
| 230 |
+
{
|
| 231 |
+
"node": "Switch Action",
|
| 232 |
+
"type": "main",
|
| 233 |
+
"index": 0
|
| 234 |
+
}
|
| 235 |
+
]
|
| 236 |
+
]
|
| 237 |
+
},
|
| 238 |
+
"Webhook (OPTIONS)": {
|
| 239 |
+
"main": [
|
| 240 |
+
[
|
| 241 |
+
{
|
| 242 |
+
"node": "Respond OPTIONS",
|
| 243 |
+
"type": "main",
|
| 244 |
+
"index": 0
|
| 245 |
+
}
|
| 246 |
+
]
|
| 247 |
+
]
|
| 248 |
+
},
|
| 249 |
+
"Switch Action": {
|
| 250 |
+
"main": [
|
| 251 |
+
[
|
| 252 |
+
{
|
| 253 |
+
"node": "Google Sheets (Add)",
|
| 254 |
+
"type": "main",
|
| 255 |
+
"index": 0
|
| 256 |
+
}
|
| 257 |
+
],
|
| 258 |
+
[
|
| 259 |
+
{
|
| 260 |
+
"node": "Google Sheets (Read)",
|
| 261 |
+
"type": "main",
|
| 262 |
+
"index": 0
|
| 263 |
+
}
|
| 264 |
+
],
|
| 265 |
+
[
|
| 266 |
+
{
|
| 267 |
+
"node": "Fallback Error",
|
| 268 |
+
"type": "main",
|
| 269 |
+
"index": 0
|
| 270 |
+
}
|
| 271 |
+
]
|
| 272 |
+
]
|
| 273 |
+
},
|
| 274 |
+
"Google Sheets (Add)": {
|
| 275 |
+
"main": [
|
| 276 |
+
[
|
| 277 |
+
{
|
| 278 |
+
"node": "Format Add",
|
| 279 |
+
"type": "main",
|
| 280 |
+
"index": 0
|
| 281 |
+
}
|
| 282 |
+
]
|
| 283 |
+
]
|
| 284 |
+
},
|
| 285 |
+
"Google Sheets (Read)": {
|
| 286 |
+
"main": [
|
| 287 |
+
[
|
| 288 |
+
{
|
| 289 |
+
"node": "Format Read",
|
| 290 |
+
"type": "main",
|
| 291 |
+
"index": 0
|
| 292 |
+
}
|
| 293 |
+
]
|
| 294 |
+
]
|
| 295 |
+
},
|
| 296 |
+
"Fallback Error": {
|
| 297 |
+
"main": [
|
| 298 |
+
[
|
| 299 |
+
{
|
| 300 |
+
"node": "Respond to Webhook",
|
| 301 |
+
"type": "main",
|
| 302 |
+
"index": 0
|
| 303 |
+
}
|
| 304 |
+
]
|
| 305 |
+
]
|
| 306 |
+
},
|
| 307 |
+
"Format Add": {
|
| 308 |
+
"main": [
|
| 309 |
+
[
|
| 310 |
+
{
|
| 311 |
+
"node": "Respond to Webhook",
|
| 312 |
+
"type": "main",
|
| 313 |
+
"index": 0
|
| 314 |
+
}
|
| 315 |
+
]
|
| 316 |
+
]
|
| 317 |
+
},
|
| 318 |
+
"Format Read": {
|
| 319 |
+
"main": [
|
| 320 |
+
[
|
| 321 |
+
{
|
| 322 |
+
"node": "Respond to Webhook",
|
| 323 |
+
"type": "main",
|
| 324 |
+
"index": 0
|
| 325 |
+
}
|
| 326 |
+
]
|
| 327 |
+
]
|
| 328 |
+
}
|
| 329 |
+
}
|
| 330 |
+
}
|