mabdullahsibghatullah123 commited on
Commit
5ae7c8f
·
verified ·
1 Parent(s): 5c7108e

Upload 17 files

Browse files
Files changed (17) hide show
  1. Dockerfile +29 -0
  2. N8N_GUIDE.md +83 -0
  3. README.md +41 -11
  4. app.js +311 -0
  5. app.py +115 -0
  6. config.js +10 -0
  7. debug.html +88 -0
  8. expiry_model.pkl +3 -0
  9. firebase.json +25 -0
  10. food_waste.db +0 -0
  11. index.html +179 -0
  12. model.py +55 -0
  13. requirements.txt +5 -0
  14. style.css +351 -0
  15. workflow.json +259 -0
  16. workflow_real.json +298 -0
  17. 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
- title: Tayyab
3
- emoji: 💻
4
- colorFrom: indigo
5
- colorTo: green
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }