Build a Verifiable Traceability App

Build a React and Django app that verifies tamper-evident herb records.

Introduction

30 Second Summary

A herbal product can pass through several hands before it reaches a customer. Without a trustworthy history, its label asks the buyer to accept claims they cannot inspect.

In this project, you will build a farm-to-shelf traceability application using React, Django, and SQLite. You will record each supply-chain event in a local SHA-256 hash chain that reveals altered history.

What You'll Build

You will open a customer's QR-backed trace link to inspect a complete herb journey with its live integrity result.

By the end of this project, you'll have:

  • A responsive farmer workspace where you register a herb batch that remains available after a browser refresh.
  • A verifiable supply-chain timeline for appending processing or custody events with linked hashes visible for every record.
  • A customer trace experience where a QR-backed link opens the matching batch timeline with a live integrity result.
  • Secret Mission: Modify a stored ledger event outside the application flow to prove that the verifier reports an integrity failure.

Are there any prerequisites?

You do not need any prior traceability or ledger experience. The project guides you through the required Windows setup before you build the application.

Before We Start

Before any hands-on work, commit to the honest scope of the AyurDAO traceability flow. This project combines React, Django, SQLite, and a local SHA-256 hash chain to simulate a ledger. A production decentralized blockchain remains outside this project's scope.

Set Up the Windows Workspace

The supplied AyurDAO mockup depends on browser scripts plus hardcoded data. A repeatable Windows workspace gives the project stable versions.

You will use Visual Studio Code for the project files. Node.js runs Vite.

Python runs Django against SQLite. The browser renders the initial React shell.

In this step, get ready to:
  • Confirm the Windows development tools at their required versions.
  • Create the ayurdao-ledger workspace with its frontend and backend files.
  • Install both dependency sets before running the initial application shell.
Confirm the Windows toolchain

Each version check answers a different setup question. Resolve a missing tool before moving to the workspace.

  • Press the Windows key to open Windows search.
  • Type PowerShell before pressing Enter.
  • Check whether the Visual Studio Code command is available by running:
code --version

What does this check show?

The command prints the installed Visual Studio Code version. It also confirms that the editor command is available from PowerShell.

✔️ I see a version number

Visual Studio Code is ready. Continue to the Node.js check below.

ⓧ Command not found

  • Use the official Windows setup guide to install the Visual Studio Code User setup.
  • Close PowerShell after the installation finishes.
  • Reopen PowerShell through Windows search.
  • Return to the main flow for the final combined version check.
  • Check the installed Node.js version by running:
node --version

Why check Node.js now?

Vite requires a compatible Node.js runtime. This project pins Node.js to v24.20.0 LTS for a repeatable setup.

✔️ I see v24.20.0

The required Node.js release is installed. Continue to the Python check.

ⓧ I see another version

  • Download Node.js v24.20.0 LTS from the official Node.js download page.
  • Run the Windows installer.
  • Close PowerShell after the installation finishes.
  • Reopen PowerShell through Windows search.

ⓧ Command not found

  • Download Node.js v24.20.0 LTS from the official Node.js download page.
  • Run the Windows installer to add Node.js plus npm.
  • Close PowerShell after the installation finishes.
  • Reopen PowerShell through Windows search.
  • Check the installed Python version by running:
python --version

Why pin Python?

Django 6.1.1 supports Python 3.12 or later. This project uses Python 3.14.7 throughout the backend.

✔️ I see Python 3.14.7

The required Python release is ready. Continue to the combined check below.

ⓧ I see an older version

  • Download Python 3.14.7 from the official Python Windows releases page.
  • Run the Windows installer.
  • Close PowerShell after the installation finishes.
  • Reopen PowerShell through Windows search.

ⓧ Command not found

  • Download Python 3.14.7 from the official Python Windows releases page.
  • Run the Windows installer.
  • Close PowerShell after the installation finishes.
  • Reopen PowerShell through Windows search.
  • Close the current PowerShell window.
  • Reopen PowerShell through Windows search.
  • Confirm all four commands from the restarted session by running:
code --version
node --version
npm --version
python --version

What does the combined check prove?

  • The first result confirms that PowerShell can launch Visual Studio Code.
  • The second result should begin with v24.20.0.
  • The third result confirms that npm arrived with Node.js.
  • The fourth result should report Python 3.14.7.

You should see a result from every command. Node.js should report v24.20.0. Python should report 3.14.7.

Is one command still missing?

Close every PowerShell window after installing a tool. A fresh session loads the updated command path.

If a version still differs, rerun the matching installer from the outcome tab above.

Help me diagnose the missing Windows development command.

Create the workspace and backend environment

The workspace separates browser code from server code. The backend also gets a virtual environment so its Django installation stays inside this project.

  • Create the project directories plus empty files on your Desktop by running:
Set-Location -Path "$HOME\Desktop"
New-Item -Path ".\ayurdao-ledger" -ItemType Directory
New-Item -Path ".\ayurdao-ledger\frontend\src" -ItemType Directory
New-Item -Path ".\ayurdao-ledger\backend\config" -ItemType Directory
New-Item -Path ".\ayurdao-ledger\backend\traceability\migrations" -ItemType Directory
New-Item -Path ".\ayurdao-ledger\frontend\package.json" -ItemType File
New-Item -Path ".\ayurdao-ledger\frontend\vite.config.js" -ItemType File
New-Item -Path ".\ayurdao-ledger\frontend\index.html" -ItemType File
New-Item -Path ".\ayurdao-ledger\frontend\src\main.jsx" -ItemType File
New-Item -Path ".\ayurdao-ledger\frontend\src\App.jsx" -ItemType File
New-Item -Path ".\ayurdao-ledger\frontend\src\styles.css" -ItemType File
New-Item -Path ".\ayurdao-ledger\backend\requirements.txt" -ItemType File
New-Item -Path ".\ayurdao-ledger\backend\manage.py" -ItemType File
New-Item -Path ".\ayurdao-ledger\backend\config\__init__.py" -ItemType File
New-Item -Path ".\ayurdao-ledger\backend\config\settings.py" -ItemType File
New-Item -Path ".\ayurdao-ledger\backend\config\urls.py" -ItemType File
New-Item -Path ".\ayurdao-ledger\backend\config\wsgi.py" -ItemType File
New-Item -Path ".\ayurdao-ledger\backend\traceability\__init__.py" -ItemType File
New-Item -Path ".\ayurdao-ledger\backend\traceability\migrations\__init__.py" -ItemType File

What does this workspace command do?

PowerShell creates ayurdao-ledger on your Desktop. Its nested paths establish the complete frontend and backend file tree.

  • Confirm the project tree exists by running:
Get-ChildItem -Path ".\ayurdao-ledger" -Recurse

What should the listing contain?

The recursive listing shows both top-level folders. It also shows the source, configuration, traceability, and migrations paths inside them.

You should see frontend plus backend in the output. Their nested project files should appear underneath.

  • Move into the new workspace before opening it in Visual Studio Code by running:
Set-Location -Path ".\ayurdao-ledger"
code .

What does code dot do?

The first line makes ayurdao-ledger the current location. The second line opens that folder as the Visual Studio Code workspace.

You should see frontend and backend in the left file sidebar. The workspace is ready for its pinned configuration.

  • Select backend/requirements.txt in the left file sidebar.
  • Set the backend dependency by replacing the file with:
Django==6.1.1

What does this requirement control?

The exact pin keeps every backend installation on Django 6.1.1. This prevents dependency drift between runs.

  • Save backend/requirements.txt.
  • Create the backend virtual environment before installing its dependency by running:
Set-Location -Path ".\backend"
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install -r requirements.txt

What does the environment setup do?

  • The first line moves PowerShell into backend.
  • The second line creates the isolated .venv environment.
  • The third line activates that environment for the current terminal.
  • The final line installs the exact requirement from requirements.txt.

The command prompt should now identify the active virtual environment. The installation should finish with Django available inside it.

Did activation or installation fail?

Confirm that PowerShell is inside the backend folder. The activation path starts from that location.

If PowerShell blocks the activation script, review the execution policy configured on your Windows account before retrying.

Help me fix the AyurDAO backend environment setup.

  • Confirm the installed Django version by running:
python -m django --version

What does this Django check prove?

The command asks the active Python environment for its Django version. A result of 6.1.1 confirms the pinned backend dependency is available.

You should see 6.1.1 in the backend terminal. Keep this terminal open so .venv remains active.

  • Select backend/manage.py in the left file sidebar.
  • Add the Django management entry point by pasting:
import os
import sys


def main():
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
    from django.core.management import execute_from_command_line
    execute_from_command_line(sys.argv)


if __name__ == "__main__":
    main()

What does the management entry point do?

The script points Django commands at config.settings. It then passes terminal arguments into Django's command runner.

  • Save backend/manage.py.
  • Select backend/config/settings.py in the left file sidebar.
  • Add the first settings section by pasting:
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = "prototype-only-change-before-deployment"
DEBUG = True
ALLOWED_HOSTS = ["127.0.0.1", "localhost"]

INSTALLED_APPS = [
    "django.contrib.contenttypes",
    "django.contrib.staticfiles",
    "traceability",
]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
]

What does this settings section configure?

This section defines the backend directory plus local development hosts. It also registers the traceability application and Django middleware.

  • Append the remaining settings below the middleware list by pasting:
ROOT_URLCONF = "config.urls"
WSGI_APPLICATION = "config.wsgi.application"
TEMPLATES = []

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}

LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
STATIC_URL = "static/"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
CSRF_TRUSTED_ORIGINS = ["http://localhost:5173", "http://127.0.0.1:5173"]

What do the remaining settings control?

These values connect Django to its URL configuration and WSGI application. The database setting places db.sqlite3 inside backend.

The trusted origins allow local requests from the fixed Vite development address. Later steps keep Django's CSRF protection active.

  • Save backend/config/settings.py.
  • Select backend/config/urls.py in the left file sidebar.
  • Add the root URL configuration by pasting:
from django.urls import include, path

urlpatterns = [
    path("api/", include("traceability.urls")),
]

What does the root URL configuration do?

The URL list reserves /api/ for the traceability application. The application routes arrive in the persistence step.

  • Save backend/config/urls.py.
  • Select backend/config/wsgi.py in the left file sidebar.
  • Add the WSGI application configuration by pasting:
import os
from django.core.wsgi import get_wsgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
application = get_wsgi_application()

What does the WSGI configuration do?

This module loads the same Django settings module as manage.py. It then exposes Django's WSGI application object.

  • Save backend/config/wsgi.py.
  • Select backend/config/__init__.py in the left file sidebar.
  • Add its package marker by pasting:
# Package marker.

Why include a package marker?

The marker gives the configuration package a clear purpose in the workspace. It contains no runtime logic.

  • Save backend/config/__init__.py.
  • Select backend/traceability/__init__.py in the left file sidebar.
  • Add its package marker by pasting:
# Package marker.

What does this marker identify?

This marker identifies the directory that holds the traceability application. Later steps add its models, views, URLs, and ledger logic.

  • Save backend/traceability/__init__.py.
  • Select backend/traceability/migrations/__init__.py in the left file sidebar.
  • Add its migration marker by pasting:
# Migration package marker.

What does the migration marker identify?

The marker reserves this package for Django database migrations. The initial schema arrives when you add persistence.

  • Save backend/traceability/migrations/__init__.py.
Install the frontend and run the shell

The frontend package manifest locks the browser dependencies before Vite starts. A small shell confirms that module loading, styling, and the fixed development port work together.

  • Select frontend/package.json in the left file sidebar.
  • Add the pinned frontend manifest by pasting:
{
  "name": "ayurdao-frontend",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "qrcode.react": "4.2.0",
    "react": "19.2.8",
    "react-dom": "19.2.8"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "6.1.1",
    "vite": "8.2.2"
  }
}

What does the package manifest control?

The manifest pins React, React DOM, and the QR component. It also pins Vite plus its React plugin.

The dev script starts the local server. The other scripts prepare or preview a production build.

  • Save frontend/package.json.
  • Press the Windows key to open Windows search.
  • Type PowerShell before pressing Enter.
  • Install the frontend dependencies from frontend by running:
Set-Location -Path "$HOME\Desktop\ayurdao-ledger\frontend"
npm install

What does npm install do?

The first line moves the new PowerShell session into frontend. The second line installs every package pinned in package.json.

The installation should finish with a new node_modules folder. Keep this frontend terminal open for Vite.

Did npm installation fail?

Confirm that Node.js reports v24.20.0 in this new PowerShell session. Also confirm that package.json is saved inside frontend.

Help me diagnose the AyurDAO npm installation.

  • Select frontend/vite.config.js in the left file sidebar.
  • Configure the React plugin plus local development server by pasting:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  server: {
    port: 5173,
    strictPort: true,
    proxy: {
      "/api": {
        target: "http://127.0.0.1:8000",
        changeOrigin: true
      }
    }
  }
});

What does the Vite configuration do?

The React plugin lets Vite process JSX. The server uses fixed port 5173.

The proxy reserves relative /api requests for the Django server at http://127.0.0.1:8000.

  • Save frontend/vite.config.js.
  • Select frontend/index.html in the left file sidebar.
  • Add the browser entry document by pasting:
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="AyurDAO local herb traceability prototype" />
    <title>AyurDAO Traceability</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

What does the HTML entry document do?

The root element gives React a browser container. The module script loads /src/main.jsx as the application entry point.

  • Save frontend/index.html.
  • Select frontend/src/App.jsx in the left file sidebar.
  • Add the initial application shell by pasting:
function App() {
  return (
    <div className="app-shell">
      <main>
        <section className="content-section">
          <p className="eyebrow">Prototype flow</p>
          <h2>Farmer input becomes customer evidence</h2>
        </section>
      </main>
      <footer>
        <strong>AyurDAO prototype</strong>
        <span>Local SHA-256 ledger simulation. Not a decentralized blockchain.</span>
      </footer>
    </div>
  );
}

export default App;

What does the application shell render?

The component returns a visible heading inside the main application container. Its footer labels the local SHA-256 ledger simulation honestly.

  • Save frontend/src/App.jsx.
  • Select frontend/src/styles.css in the left file sidebar.
  • Add the initial global styles by pasting:
:root {
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  color: #183126;
  background: #f3f6ef;
  font-synthesis: none;
  --green: #1f6b45;
  --dark: #123526;
  --mint: #dcebdc;
  --cream: #fbfaf4;
  --line: #d9e2d7;
  --amber: #b46a1d;
  --danger: #a23636;
}

* { box-sizing: border-box; }
body { margin: 0; min-width: 320px; min-height: 100vh; }
.app-shell { min-height: 100vh; display: flex; flex-direction: column; }
main { flex: 1; }
.eyebrow { margin: 0 0 8px; color: var(--green); font-size: 0.76rem; font-weight: 800; letter-spacing: 0.16em; text-transform: uppercase; }
h2 { margin-bottom: 14px; font-family: Georgia, serif; font-size: clamp(1.6rem, 3vw, 2.3rem); color: var(--dark); }
.content-section { width: min(1180px, calc(100% - 40px)); margin: 0 auto; padding: 58px 0 72px; }
footer { display: flex; justify-content: space-between; gap: 20px; padding: 22px 5vw; background: var(--dark); color: #dce9e0; }
footer span { color: #9db4a7; }

What do the initial styles control?

The global values establish the project colours plus typography. The remaining selectors give the shell a full-height layout and a dark prototype footer.

  • Save frontend/src/styles.css.
  • Confirm the frontend files now contain saved data by running:
Get-ChildItem -Path "$HOME\Desktop\ayurdao-ledger\frontend" -Recurse

What should the frontend listing prove?

The listing should show nonzero lengths for the files you have filled. It should also show the installed node_modules directory.

  • Select frontend/src/main.jsx in the left file sidebar.
  • Connect React to the browser root by pasting:
import { createRoot } from "react-dom/client";
import App from "./App.jsx";
import "./styles.css";

createRoot(document.getElementById("root")).render(<App />);

What does the React entry point do?

The imports load React's browser renderer plus the application component and stylesheet. createRoot() mounts the shell into the HTML element named root.

  • Save frontend/src/main.jsx.

Before you start Vite, what do you expect the browser to show at http://localhost:5173?

  • Start the fixed-port Vite server from the frontend terminal by running:
npm run dev

What does the development command do?

The npm script launches Vite using the configuration you saved. The process keeps this terminal occupied while it serves the React application.

  • Open http://localhost:5173 in your browser.

You should see the AyurDAO shell with the heading Farmer input becomes customer evidence. A dark footer should label the local ledger simulation.

That is the first local win. React now renders through Vite from your pinned Windows workspace.

Is the AyurDAO shell missing?

Confirm that the frontend terminal is still running. Vite must keep control of that terminal while the page is open.

Check that main.jsx imports both App.jsx and styles.css with matching letter case.

Help me debug the initial AyurDAO Vite page.

✔️ Awesome, I've got everything!

Your backend environment is activated. Your frontend server is rendering the initial shell on port 5173.

ⓧ I'd like to double check the full code

Compare every saved file with these complete workspace references.

# Package marker.

This file marks the Django configuration package.

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = "prototype-only-change-before-deployment"
DEBUG = True
ALLOWED_HOSTS = ["127.0.0.1", "localhost"]

INSTALLED_APPS = [
    "django.contrib.contenttypes",
    "django.contrib.staticfiles",
    "traceability",
]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
]

ROOT_URLCONF = "config.urls"
WSGI_APPLICATION = "config.wsgi.application"
TEMPLATES = []

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}

LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
STATIC_URL = "static/"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
CSRF_TRUSTED_ORIGINS = ["http://localhost:5173", "http://127.0.0.1:5173"]

This file contains the complete initial Django settings for the workspace.

from django.urls import include, path

urlpatterns = [
    path("api/", include("traceability.urls")),
]

This file reserves the backend API route for the traceability application.

import os
from django.core.wsgi import get_wsgi_application

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
application = get_wsgi_application()

This file exposes the Django WSGI application.

import os
import sys


def main():
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
    from django.core.management import execute_from_command_line
    execute_from_command_line(sys.argv)


if __name__ == "__main__":
    main()

This file provides the backend management entry point.

Django==6.1.1

This file pins the backend framework version.

# Package marker.

This file marks the traceability application package.

# Migration package marker.

This file marks the future migration package.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="AyurDAO local herb traceability prototype" />
    <title>AyurDAO Traceability</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

This file provides the browser document plus React root.

{
  "name": "ayurdao-frontend",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "qrcode.react": "4.2.0",
    "react": "19.2.8",
    "react-dom": "19.2.8"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "6.1.1",
    "vite": "8.2.2"
  }
}

This file contains every pinned frontend dependency plus npm script.

function App() {
  return (
    <div className="app-shell">
      <main>
        <section className="content-section">
          <p className="eyebrow">Prototype flow</p>
          <h2>Farmer input becomes customer evidence</h2>
        </section>
      </main>
      <footer>
        <strong>AyurDAO prototype</strong>
        <span>Local SHA-256 ledger simulation. Not a decentralized blockchain.</span>
      </footer>
    </div>
  );
}

export default App;

This file contains the runnable application shell for this step.

import { createRoot } from "react-dom/client";
import App from "./App.jsx";
import "./styles.css";

createRoot(document.getElementById("root")).render(<App />);

This file mounts the application component into the browser root.

:root {
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  color: #183126;
  background: #f3f6ef;
  font-synthesis: none;
  --green: #1f6b45;
  --dark: #123526;
  --mint: #dcebdc;
  --cream: #fbfaf4;
  --line: #d9e2d7;
  --amber: #b46a1d;
  --danger: #a23636;
}

* { box-sizing: border-box; }
body { margin: 0; min-width: 320px; min-height: 100vh; }
.app-shell { min-height: 100vh; display: flex; flex-direction: column; }
main { flex: 1; }
.eyebrow { margin: 0 0 8px; color: var(--green); font-size: 0.76rem; font-weight: 800; letter-spacing: 0.16em; text-transform: uppercase; }
h2 { margin-bottom: 14px; font-family: Georgia, serif; font-size: clamp(1.6rem, 3vw, 2.3rem); color: var(--dark); }
.content-section { width: min(1180px, calc(100% - 40px)); margin: 0 auto; padding: 58px 0 72px; }
footer { display: flex; justify-content: space-between; gap: 20px; padding: 22px 5vw; background: var(--dark); color: #dce9e0; }
footer span { color: #9db4a7; }

This file provides the initial shell styling used in this step.

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  server: {
    port: 5173,
    strictPort: true,
    proxy: {
      "/api": {
        target: "http://127.0.0.1:8000",
        changeOrigin: true
      }
    }
  }
});

This file fixes the frontend port plus future API proxy.

  • Leave the Vite terminal running for the next step.
  • Keep the activated backend terminal open for later Django commands.

Your pinned workspace is ready. Next, you will turn this small shell into the complete AyurDAO interface and observe its deliberate backend-unavailable state.

Build the AyurDAO Frontend

Your local Vite workspace already renders a basic React root. The supplied mockup still relies on hardcoded browser data, so it cannot prove that a farmer's records reach a persistent traceability system.

This step turns that shell into the complete AyurDAO interface. Every workflow uses an API client instead of sample transaction arrays, which gives you an honest view of the application's current state.

In this step, get ready to:
  • Create a shared API client for batch registration, event submission, trace retrieval, and CSRF protection.
  • Build the Home, Explore, Farmer, and Verify workflows with React state and effects.
  • Apply the responsive AyurDAO design and observe the deliberate backend failure.
Create the API client

A shared client keeps request handling consistent across every view. It also checks each response before the interface treats returned data as valid.

  • Return to Visual Studio Code with the ayurdao-ledger workspace from the previous step.
  • Select the frontend/src folder in the Explorer sidebar.
  • Click the New File control in the Explorer toolbar.
  • Name the file api.js.
  • Select the double-check tab below.
  • Copy the complete API client into frontend/src/api.js.

✔️ Awesome, I've got everything!

Your api.js file should define the shared request helpers and four AyurDAO API operations.

ⓧ I'd like to double check the full code

function getCookie(name) {
  const cookies = document.cookie ? document.cookie.split(";") : [];
  for (const item of cookies) {
    const cookie = item.trim();
    if (cookie.startsWith(`${name}=`)) {
      return decodeURIComponent(cookie.slice(name.length + 1));
    }
  }
  return null;
}

async function readJson(response) {
  try {
    return await response.json();
  } catch {
    return {};
  }
}

async function request(path, options = {}) {
  const response = await fetch(path, {
    credentials: "same-origin",
    ...options
  });
  const data = await readJson(response);
  if (!response.ok) {
    throw new Error(data.error || `Request failed with status ${response.status}`);
  }
  return data;
}

async function post(path, body) {
  await request("/api/csrf/");
  return request(path, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-CSRFToken": getCookie("csrftoken")
    },
    body: JSON.stringify(body)
  });
}

export const api = {
  listBatches: () => request("/api/batches/"),
  createBatch: (batch) => post("/api/batches/", batch),
  addEvent: (batchId, event) => post(`/api/batches/${encodeURIComponent(batchId)}/events/`, event),
  trace: (batchId) => request(`/api/trace/${encodeURIComponent(batchId)}/`)
};

What Does This Client Do?

  • The getCookie() helper reads the browser's CSRF cookie before a protected request.
  • The request() helper sends same-origin credentials and checks response.ok before returning data.
  • The post() helper requests a CSRF cookie before sending JSON with the X-CSRFToken header.
  • The exported api object gives the interface one place to list batches, create batches, append events, and retrieve traces.
  • Save frontend/src/api.js.
  • Confirm that api.js now appears beneath frontend/src in the Explorer sidebar.

Can't Find api.js?

Check that the file sits beside App.jsx inside frontend/src. A file created directly inside frontend cannot be imported with the path used by the application.

Ask for help with the file location or JavaScript syntax: help me check my AyurDAO api.js file

Build the React workflows

The interface uses useState() to hold the active view, forms, batches, trace results, and status messages. The useEffect() hook performs the first batch request when the application loads.

  • Select frontend/src/App.jsx in the Explorer sidebar.
  • Select the double-check tab below.
  • Replace the initial shell with the complete AyurDAO interface.

✔️ Awesome, I've got everything!

Your App.jsx file should contain the navigation, four page workflows, trace panel, forms, and backend status handling.

ⓧ I'd like to double check the full code

import { useEffect, useState } from "react";
import { QRCodeSVG } from "qrcode.react";
import { api } from "./api.js";

const HERBS = ["Ashwagandha", "Tulsi", "Turmeric", "Amla"];

const EMPTY_BATCH = {
  batch_id: "",
  herb_name: "",
  botanical_name: "",
  quantity: "",
  unit: "kg",
  farm_name: "",
  location: "",
  harvest_date: ""
};

const EMPTY_EVENT = {
  event_type: "Processing",
  actor: "",
  location: "",
  note: ""
};

function shortHash(value) {
  if (!value) return "Not available";
  return `${value.slice(0, 12)}...${value.slice(-10)}`;
}

function App() {
  const [page, setPage] = useState("home");
  const [batches, setBatches] = useState([]);
  const [batchForm, setBatchForm] = useState(EMPTY_BATCH);
  const [eventForm, setEventForm] = useState(EMPTY_EVENT);
  const [lookup, setLookup] = useState("");
  const [trace, setTrace] = useState(null);
  const [serverError, setServerError] = useState("");
  const [message, setMessage] = useState("");
  const [busy, setBusy] = useState(false);

  useEffect(() => {
    let ignore = false;
    async function start() {
      try {
        const result = await api.listBatches();
        if (!ignore) {
          setBatches(result.batches);
          setServerError("");
        }
      } catch {
        if (!ignore) {
          setServerError("Backend unavailable. Start the Django server to enable persistence and verification.");
        }
      }

      const traceId = new URLSearchParams(window.location.search).get("trace");
      if (traceId) {
        setPage("verify");
        setLookup(traceId);
        try {
          const result = await api.trace(traceId);
          if (!ignore) setTrace(result);
        } catch (error) {
          if (!ignore) setMessage(error.message);
        }
      }
    }
    start();
    return () => {
      ignore = true;
    };
  }, []);

  async function refreshBatches() {
    const result = await api.listBatches();
    setBatches(result.batches);
    setServerError("");
  }

  async function loadTrace(batchId) {
    const cleanId = batchId.trim();
    if (!cleanId) return;
    setBusy(true);
    setMessage("");
    try {
      const result = await api.trace(cleanId);
      setTrace(result);
      setLookup(cleanId);
      setPage("verify");
    } catch (error) {
      setTrace(null);
      setMessage(error.message);
    } finally {
      setBusy(false);
    }
  }

  async function createBatch(event) {
    event.preventDefault();
    setBusy(true);
    setMessage("");
    try {
      const result = await api.createBatch(batchForm);
      await refreshBatches();
      setBatchForm(EMPTY_BATCH);
      setMessage(`Batch ${result.batch.batch_id} registered.`);
      await loadTrace(result.batch.batch_id);
    } catch (error) {
      setMessage(error.message);
    } finally {
      setBusy(false);
    }
  }

  async function addEvent(event) {
    event.preventDefault();
    if (!trace) return;
    setBusy(true);
    setMessage("");
    try {
      await api.addEvent(trace.batch.batch_id, eventForm);
      setEventForm(EMPTY_EVENT);
      await loadTrace(trace.batch.batch_id);
      await refreshBatches();
    } catch (error) {
      setMessage(error.message);
    } finally {
      setBusy(false);
    }
  }

  const navigation = [
    ["home", "Home"],
    ["explore", "Explore"],
    ["farmer", "Farmer"],
    ["verify", "Verify"]
  ];

  return (
    <div className="app-shell">
      <header className="topbar">
        <button className="brand" onClick={() => setPage("home")}>
          <span className="brand-mark">A</span>
          <span>AyurDAO</span>
        </button>
        <nav aria-label="Primary navigation">
          {navigation.map(([id, label]) => (
            <button key={id} className={page === id ? "nav-active" : ""} onClick={() => setPage(id)}>
              {label}
            </button>
          ))}
        </nav>
      </header>

      {serverError && <div className="system-banner warning">{serverError}</div>}
      {message && <div className="system-banner">{message}</div>}

      <main>
        {page === "home" && <HomePage batches={batches} onExplore={() => setPage("explore")} onVerify={() => setPage("verify")} />}
        {page === "explore" && <ExplorePage />}
        {page === "farmer" && (
          <FarmerPage
            batches={batches}
            form={batchForm}
            setForm={setBatchForm}
            onSubmit={createBatch}
            onView={loadTrace}
            busy={busy}
          />
        )}
        {page === "verify" && (
          <VerifyPage
            lookup={lookup}
            setLookup={setLookup}
            trace={trace}
            onLookup={(event) => {
              event.preventDefault();
              loadTrace(lookup);
            }}
            eventForm={eventForm}
            setEventForm={setEventForm}
            onAddEvent={addEvent}
            busy={busy}
          />
        )}
      </main>

      <footer>
        <strong>AyurDAO prototype</strong>
        <span>Local SHA-256 ledger simulation. Not a decentralized blockchain.</span>
      </footer>
    </div>
  );
}

function HomePage({ batches, onExplore, onVerify }) {
  return (
    <>
      <section className="hero">
        <div>
          <p className="eyebrow">Farm-to-shelf traceability</p>
          <h1>Trust every herb journey.</h1>
          <p className="hero-copy">Register batches, append supply-chain events, and let customers verify linked records from one transparent interface.</p>
          <div className="button-row">
            <button className="primary" onClick={onExplore}>Explore herbs</button>
            <button className="secondary" onClick={onVerify}>Verify a batch</button>
          </div>
        </div>
        <div className="hero-card">
          <span className="hero-glyph">A</span>
          <strong>Verification-ready</strong>
          <p>{batches.length} persistent batch{batches.length === 1 ? "" : "es"} in this local prototype</p>
        </div>
      </section>
      <section className="stats">
        <article><strong>{batches.length}</strong><span>Registered batches</span></article>
        <article><strong>{batches.reduce((total, batch) => total + batch.event_count, 0)}</strong><span>Ledger events</span></article>
        <article><strong>SHA-256</strong><span>Hash algorithm</span></article>
      </section>
      <section className="content-section">
        <p className="eyebrow">Prototype flow</p>
        <h2>Farmer input becomes customer evidence</h2>
        <div className="feature-grid">
          <article><span>01</span><h3>Register</h3><p>Create a batch with farm, quantity, location, and harvest details.</p></article>
          <article><span>02</span><h3>Append</h3><p>Add processing and custody events without editing earlier history.</p></article>
          <article><span>03</span><h3>Verify</h3><p>Recompute the full chain and share the trace through a QR link.</p></article>
        </div>
      </section>
    </>
  );
}

function ExplorePage() {
  return (
    <section className="content-section">
      <p className="eyebrow">Reference catalogue</p>
      <h1>Explore AyurDAO herbs</h1>
      <p className="section-copy">These cards preserve the discovery experience from the supplied design. Transaction records are loaded separately from the backend.</p>
      <div className="herb-grid">
        {HERBS.map((herb, index) => (
          <article className="herb-card" key={herb}>
            <span className="herb-number">0{index + 1}</span>
            <div className="herb-art">{herb.slice(0, 1)}</div>
            <h3>{herb}</h3>
            <p>Reference profile ready for a future verified catalogue service.</p>
          </article>
        ))}
      </div>
    </section>
  );
}

function FarmerPage({ batches, form, setForm, onSubmit, onView, busy }) {
  function update(field, value) {
    setForm((current) => ({ ...current, [field]: value }));
  }

  return (
    <section className="content-section">
      <p className="eyebrow">Farmer workspace</p>
      <h1>Register a harvested batch</h1>
      <div className="workspace-grid">
        <form className="panel form-grid" onSubmit={onSubmit}>
          <label>Batch ID<input value={form.batch_id} onChange={(e) => update("batch_id", e.target.value)} placeholder="ASH-2026-001" required /></label>
          <label>Herb name<input value={form.herb_name} onChange={(e) => update("herb_name", e.target.value)} required /></label>
          <label>Botanical name<input value={form.botanical_name} onChange={(e) => update("botanical_name", e.target.value)} required /></label>
          <label>Quantity<input type="number" min="0.01" step="0.01" value={form.quantity} onChange={(e) => update("quantity", e.target.value)} required /></label>
          <label>Unit<select value={form.unit} onChange={(e) => update("unit", e.target.value)}><option>kg</option><option>g</option><option>units</option></select></label>
          <label>Farm name<input value={form.farm_name} onChange={(e) => update("farm_name", e.target.value)} required /></label>
          <label>Location<input value={form.location} onChange={(e) => update("location", e.target.value)} required /></label>
          <label>Harvest date<input type="date" value={form.harvest_date} onChange={(e) => update("harvest_date", e.target.value)} required /></label>
          <button className="primary span-two" disabled={busy}>{busy ? "Saving..." : "Register batch"}</button>
        </form>

        <div className="panel">
          <div className="panel-heading"><div><p className="eyebrow">Persistent records</p><h2>My batches</h2></div><span className="count-pill">{batches.length}</span></div>
          <div className="batch-list">
            {batches.length === 0 && <p className="empty-state">No batches yet. Register the first one.</p>}
            {batches.map((batch) => (
              <article className="batch-row" key={batch.batch_id}>
                <div><strong>{batch.herb_name}</strong><span>{batch.batch_id}</span><small>{batch.farm_name} | {batch.location}</small></div>
                <div className="batch-actions"><span className="status-pill">{batch.status}</span><button className="text-button" onClick={() => onView(batch.batch_id)}>View ledger</button></div>
              </article>
            ))}
          </div>
        </div>
      </div>
    </section>
  );
}

function VerifyPage({ lookup, setLookup, trace, onLookup, eventForm, setEventForm, onAddEvent, busy }) {
  return (
    <section className="content-section">
      <p className="eyebrow">Customer verification</p>
      <h1>Trace a batch</h1>
      <form className="lookup" onSubmit={onLookup}>
        <input value={lookup} onChange={(e) => setLookup(e.target.value)} placeholder="Enter a batch ID" required />
        <button className="primary" disabled={busy}>{busy ? "Checking..." : "Verify"}</button>
      </form>
      {trace && <TracePanel trace={trace} eventForm={eventForm} setEventForm={setEventForm} onAddEvent={onAddEvent} busy={busy} />}
    </section>
  );
}

function TracePanel({ trace, eventForm, setEventForm, onAddEvent, busy }) {
  const query = new URLSearchParams({ trace: trace.batch.batch_id }).toString();
  const verificationUrl = `${window.location.origin}/?${query}`;

  function update(field, value) {
    setEventForm((current) => ({ ...current, [field]: value }));
  }

  return (
    <div className="trace-layout">
      <article className="panel identity-card">
        <div className={`verification ${trace.verification.valid ? "valid" : "invalid"}`}>
          <strong>{trace.verification.valid ? "Verified" : "Integrity Failure"}</strong>
          <span>{trace.verification.checked_blocks} block{trace.verification.checked_blocks === 1 ? "" : "s"} checked</span>
        </div>
        <h2>{trace.batch.herb_name}</h2>
        <p className="mono">{trace.batch.batch_id}</p>
        <dl>
          <div><dt>Botanical name</dt><dd>{trace.batch.botanical_name}</dd></div>
          <div><dt>Farm</dt><dd>{trace.batch.farm_name}</dd></div>
          <div><dt>Origin</dt><dd>{trace.batch.location}</dd></div>
          <div><dt>Quantity</dt><dd>{trace.batch.quantity} {trace.batch.unit}</dd></div>
          <div><dt>Harvested</dt><dd>{trace.batch.harvest_date}</dd></div>
        </dl>
        {!trace.verification.valid && trace.verification.failed_sequence && <p className="failure-note">Verification first failed at sequence {trace.verification.failed_sequence}.</p>}
        <div className="qr-box">
          <QRCodeSVG value={verificationUrl} size={176} level="M" marginSize={4} title={`Verify ${trace.batch.batch_id}`} />
          <a href={verificationUrl}>Open customer verification link</a>
        </div>
      </article>

      <div>
        <article className="panel">
          <div className="panel-heading"><div><p className="eyebrow">Append-only history</p><h2>Supply-chain timeline</h2></div></div>
          <div className="timeline">
            {trace.blocks.map((block) => (
              <div className="timeline-row" key={block.sequence}>
                <span className="timeline-dot">{block.sequence}</span>
                <div><strong>{block.event_type}</strong><p>{block.actor} | {block.location}</p><small>{new Date(block.timestamp).toLocaleString()}</small>{block.payload.note && <p>{block.payload.note}</p>}</div>
              </div>
            ))}
          </div>
        </article>

        <article className="panel ledger-panel">
          <p className="eyebrow">Hash inspection</p>
          <h2>Ledger blocks</h2>
          {trace.blocks.map((block) => (
            <div className="ledger-row" key={block.sequence}>
              <strong>Block {block.sequence}: {block.event_type}</strong>
              <span>Previous: <code title={block.previous_hash}>{shortHash(block.previous_hash)}</code></span>
              <span>Current: <code title={block.current_hash}>{shortHash(block.current_hash)}</code></span>
            </div>
          ))}
        </article>

        <form className="panel form-grid event-form" onSubmit={onAddEvent}>
          <div className="span-two"><p className="eyebrow">Farmer or partner action</p><h2>Add an event</h2></div>
          <label>Event type<select value={eventForm.event_type} onChange={(e) => update("event_type", e.target.value)}><option>Processing</option><option>Quality Check</option><option>Custody Transfer</option><option>Packaging</option></select></label>
          <label>Actor<input value={eventForm.actor} onChange={(e) => update("actor", e.target.value)} required /></label>
          <label>Location<input value={eventForm.location} onChange={(e) => update("location", e.target.value)} required /></label>
          <label>Note<input value={eventForm.note} onChange={(e) => update("note", e.target.value)} /></label>
          <button className="primary span-two" disabled={busy}>{busy ? "Appending..." : "Append ledger event"}</button>
        </form>
      </div>
    </div>
  );
}

export default App;

How Is the Interface Organised?

  • The App component owns navigation, shared forms, request status, and loaded trace data.
  • The HomePage and ExplorePage components present the traceability journey and herb catalogue.
  • The FarmerPage component collects batch details and lists records returned by the backend.
  • The VerifyPage and TracePanel components prepare the customer lookup, timeline, ledger inspection, event form, and QR destination.

Before you save, do you think the interface can retrieve its batch list while only the frontend server is running?

  • Save frontend/src/App.jsx.
  • Return to the browser tab at http://localhost:5173.

You will see the AyurDAO interface plus the warning Backend unavailable. Start the Django server to enable persistence and verification. This is the intended shortfall.

The request reached the Vite proxy, but no Django server answered it. The visible failure proves that the frontend depends on a real backend instead of silently substituting hardcoded records.

  • Click Home in the top navigation.
  • Click Explore in the top navigation.
  • Click Farmer in the top navigation.
  • Click Verify in the top navigation.

You should see the home journey, four herb cards, the batch registration form, and the batch lookup form. The warning remains visible because every workflow shares the same backend state.

Seeing a Blank Browser?

Check the browser console for a file name and line number. The most common causes here are a missing closing JSX tag or an import path that does not match ./api.js.

Ask for help with the failing component: help me debug the blank AyurDAO React page

Style and verify the responsive interface

The component structure now exposes every workflow. The stylesheet turns those elements into a responsive farm-to-shelf experience with navigation, forms, panels, timelines, ledger rows, verification states, and mobile layouts.

  • Select frontend/src/styles.css in the Explorer sidebar.
  • Select the double-check tab below.
  • Replace the initial shell styles with the complete AyurDAO stylesheet.

✔️ Awesome, I've got everything!

Your styles.css file should style the full desktop experience and include responsive layouts for narrower screens.

ⓧ I'd like to double check the full code

:root {
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  color: #183126;
  background: #f3f6ef;
  font-synthesis: none;
  --green: #1f6b45;
  --dark: #123526;
  --mint: #dcebdc;
  --cream: #fbfaf4;
  --line: #d9e2d7;
  --amber: #b46a1d;
  --danger: #a23636;
}

* { box-sizing: border-box; }
body { margin: 0; min-width: 320px; min-height: 100vh; }
button, input, select { font: inherit; }
button { cursor: pointer; }
button:disabled { cursor: wait; opacity: 0.65; }

.app-shell { min-height: 100vh; display: flex; flex-direction: column; }
.topbar { position: sticky; top: 0; z-index: 10; display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 14px clamp(20px, 5vw, 72px); border-bottom: 1px solid var(--line); background: rgba(251, 250, 244, 0.96); backdrop-filter: blur(12px); }
.brand { display: flex; align-items: center; gap: 10px; border: 0; background: transparent; color: var(--dark); font-size: 1.2rem; font-weight: 800; }
.brand-mark { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 10px; background: var(--green); color: white; }
nav { display: flex; flex-wrap: wrap; gap: 6px; }
nav button { border: 0; border-radius: 999px; padding: 9px 14px; background: transparent; color: #52675c; }
nav button:hover, nav .nav-active { background: var(--mint); color: var(--dark); }
main { flex: 1; }

.system-banner { padding: 10px 5vw; text-align: center; color: var(--dark); background: #e6f1e7; border-bottom: 1px solid #c9ddcc; }
.system-banner.warning { color: #6b4217; background: #fff1d8; border-color: #efd29e; }

.hero { min-height: 470px; display: grid; grid-template-columns: 1.4fr 0.8fr; align-items: center; gap: 50px; padding: 72px clamp(24px, 7vw, 110px); background: linear-gradient(135deg, #edf4e7, #fbfaf4 55%, #e9efe0); }
.eyebrow { margin: 0 0 8px; color: var(--green); font-size: 0.76rem; font-weight: 800; letter-spacing: 0.16em; text-transform: uppercase; }
h1, h2, h3, p { margin-top: 0; }
h1 { max-width: 760px; margin-bottom: 18px; font-family: Georgia, serif; font-size: clamp(2.4rem, 6vw, 5rem); line-height: 0.98; color: var(--dark); }
h2 { margin-bottom: 14px; font-family: Georgia, serif; font-size: clamp(1.6rem, 3vw, 2.3rem); color: var(--dark); }
.hero-copy, .section-copy { max-width: 690px; color: #607268; font-size: 1.05rem; line-height: 1.7; }
.button-row { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 26px; }
.primary, .secondary { border-radius: 12px; padding: 12px 18px; font-weight: 750; }
.primary { border: 1px solid var(--green); background: var(--green); color: white; }
.primary:hover { background: #195b3a; }
.secondary { border: 1px solid #afc5b5; background: white; color: var(--green); }
.hero-card { display: grid; justify-items: center; gap: 12px; padding: 42px; border: 1px solid #cbdcc9; border-radius: 28px; background: rgba(255,255,255,0.72); text-align: center; box-shadow: 0 22px 60px rgba(31, 72, 47, 0.12); }
.hero-glyph { display: grid; place-items: center; width: 120px; height: 120px; border-radius: 36px 36px 55px 55px; background: var(--green); color: white; font-family: Georgia, serif; font-size: 4rem; }
.hero-card p { margin-bottom: 0; color: #64766b; }

.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--line); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.stats article { display: grid; gap: 6px; padding: 26px; background: var(--cream); text-align: center; }
.stats strong { color: var(--green); font-family: Georgia, serif; font-size: 2rem; }
.stats span { color: #6b7c72; }

.content-section { width: min(1180px, calc(100% - 40px)); margin: 0 auto; padding: 58px 0 72px; }
.feature-grid, .herb-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; margin-top: 30px; }
.feature-grid article, .herb-card, .panel { border: 1px solid var(--line); border-radius: 18px; background: white; box-shadow: 0 10px 30px rgba(30, 64, 43, 0.06); }
.feature-grid article { padding: 28px; }
.feature-grid article > span, .herb-number { color: #91a69a; font-weight: 800; }
.feature-grid p, .herb-card p { color: #687a70; line-height: 1.6; }
.herb-grid { grid-template-columns: repeat(4, 1fr); }
.herb-card { padding: 20px; }
.herb-art { display: grid; place-items: center; height: 150px; margin: 14px 0 18px; border-radius: 14px; background: linear-gradient(145deg, #dbe9d2, #f1ead7); color: var(--green); font-family: Georgia, serif; font-size: 4rem; }

.workspace-grid, .trace-layout { display: grid; grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.2fr); gap: 22px; align-items: start; margin-top: 26px; }
.panel { padding: 24px; }
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 15px; }
label { display: grid; gap: 7px; color: #465c50; font-size: 0.86rem; font-weight: 700; }
input, select { width: 100%; border: 1px solid #cbd8ce; border-radius: 10px; padding: 11px 12px; background: #fcfdf9; color: var(--dark); outline: none; }
input:focus, select:focus { border-color: var(--green); box-shadow: 0 0 0 3px rgba(31, 107, 69, 0.12); }
.span-two { grid-column: 1 / -1; }
.panel-heading { display: flex; justify-content: space-between; align-items: start; gap: 18px; }
.count-pill, .status-pill { border-radius: 999px; padding: 5px 10px; background: var(--mint); color: var(--green); font-size: 0.75rem; font-weight: 800; }
.batch-list { display: grid; gap: 10px; }
.batch-row { display: flex; justify-content: space-between; gap: 16px; padding: 15px 0; border-top: 1px solid #edf1eb; }
.batch-row div:first-child { display: grid; gap: 4px; }
.batch-row span, .batch-row small { color: #6c7d73; }
.batch-actions { display: flex; align-items: flex-end; gap: 8px; flex-direction: column; }
.text-button { border: 0; padding: 0; background: transparent; color: var(--green); font-weight: 800; }
.empty-state { padding: 22px; border: 1px dashed #cbd8ce; border-radius: 12px; color: #738278; text-align: center; }

.lookup { display: flex; gap: 10px; max-width: 650px; margin: 24px 0 30px; }
.trace-layout { grid-template-columns: 0.75fr 1.25fr; }
.trace-layout > div { display: grid; gap: 18px; }
.verification { display: flex; justify-content: space-between; gap: 12px; margin: -24px -24px 24px; padding: 15px 20px; border-radius: 18px 18px 0 0; }
.verification.valid { background: #def0df; color: #185a38; }
.verification.invalid { background: #f7dddd; color: var(--danger); }
dl { display: grid; gap: 0; }
dl div { display: flex; justify-content: space-between; gap: 18px; padding: 11px 0; border-bottom: 1px solid #edf1eb; }
dt { color: #738278; }
dd { margin: 0; text-align: right; font-weight: 700; }
.mono, code { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; }
.failure-note { padding: 12px; border-radius: 10px; background: #f9e7e7; color: var(--danger); }
.qr-box { display: grid; justify-items: center; gap: 12px; margin-top: 24px; padding: 20px; border-radius: 14px; background: #f4f7f1; text-align: center; }
.qr-box a { color: var(--green); font-weight: 750; overflow-wrap: anywhere; }
.timeline { display: grid; }
.timeline-row { display: grid; grid-template-columns: 36px 1fr; gap: 12px; padding: 12px 0; }
.timeline-dot { display: grid; place-items: center; width: 30px; height: 30px; border-radius: 50%; background: var(--green); color: white; font-weight: 800; }
.timeline-row p { margin: 4px 0; color: #5d7065; }
.timeline-row small { color: #87958d; }
.ledger-panel { background: #15261e; color: #dbe8df; }
.ledger-panel h2 { color: white; }
.ledger-panel .eyebrow { color: #93c4a2; }
.ledger-row { display: grid; gap: 6px; padding: 13px 0; border-top: 1px solid #30443a; }
.ledger-row span { color: #aec1b5; font-size: 0.83rem; }
.ledger-row code { color: #f2d49f; }
.event-form { margin-top: 0; }

footer { display: flex; justify-content: space-between; gap: 20px; padding: 22px 5vw; background: var(--dark); color: #dce9e0; }
footer span { color: #9db4a7; }

@media (max-width: 900px) {
  .hero, .workspace-grid, .trace-layout { grid-template-columns: 1fr; }
  .herb-grid { grid-template-columns: repeat(2, 1fr); }
  .hero-card { max-width: 520px; }
}

@media (max-width: 620px) {
  .topbar, footer { align-items: flex-start; flex-direction: column; }
  .hero { padding: 48px 22px; }
  .stats, .feature-grid, .herb-grid, .form-grid { grid-template-columns: 1fr; }
  .span-two { grid-column: auto; }
  .lookup, .batch-row { flex-direction: column; }
  .batch-actions { align-items: flex-start; }
}

How Does the Layout Respond?

  • The desktop layout uses grids for the hero, herb catalogue, farmer workspace, and trace display.
  • The verification and ledger selectors give valid records, failed records, and hash details distinct visual states.
  • The media queries collapse the major grids at narrower widths and stack controls on small screens.

Before you save, what visual changes do you expect across the hero, navigation, forms, and warning banner?

  • Save frontend/src/styles.css.
  • Return to http://localhost:5173 in your browser.
  • Resize the browser window below 620px wide.
  • Restore the browser window to its normal width.

You should see a green AyurDAO navigation bar, a large farm-to-shelf hero, three summary cards, and responsive layouts that collapse into one column on a narrow screen. The backend warning stays visible at the top.

  • Select Explore to confirm that four herb cards appear.
  • Select Farmer to confirm that the registration form and empty batch panel appear.
  • Select Verify to confirm that the batch lookup form appears.

That is the designed obstacle complete. Your frontend presents every workflow while clearly showing that persistence and verification still need the backend.

Styles Not Appearing?

Confirm that frontend/src/main.jsx still imports ./styles.css. Check that the stylesheet was saved inside frontend/src.

Ask for help with the missing styles: help me debug the AyurDAO stylesheet

Your complete AyurDAO frontend is now running across Home, Explore, Farmer, and Verify. Next, you will start the backend and move farmer batches into persistent storage.

Persist Farmer Batches

Your React interface now exposes the gap between a polished screen and a working traceability application. The backend unavailable banner proves that the browser has no persistent source for its batch records.

This step resolves that gap with Django and SQLite. A farmer's batch moves into a local database that survives browser refreshes.

In this step, get ready to:
  • Define the relational models for batches and future ledger blocks.
  • Add protected JSON endpoints for creating and listing batches.
  • Register a batch and prove that it survives a browser refresh.
Define the database schema

A database model describes the shape of a stored record. Django uses these models to connect Python objects to tables in SQLite.

  • In the VS Code Explorer sidebar, select the backend/traceability folder.
  • Create models.py inside the backend/traceability folder.
  • Add the batch model by pasting this code into models.py:
from django.db import models


class Batch(models.Model):
    batch_id = models.CharField(max_length=40, primary_key=True)
    herb_name = models.CharField(max_length=120)
    botanical_name = models.CharField(max_length=160)
    quantity = models.DecimalField(max_digits=12, decimal_places=2)
    unit = models.CharField(max_length=20)
    farm_name = models.CharField(max_length=160)
    location = models.CharField(max_length=200)
    harvest_date = models.DateField()
    status = models.CharField(max_length=80, default="Harvested")
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-created_at"]

What does the batch model store?

  • The batch_id field is the primary key used to identify one harvested batch.
  • The farm fields preserve the herb's source details.
  • The status field begins with Harvested so every new batch has a clear starting state.
  • The descending created_at ordering keeps the newest batches at the top of My Batches.
  • Add the ledger block model below the Batch class:
class LedgerBlock(models.Model):
    batch = models.ForeignKey(Batch, on_delete=models.CASCADE, related_name="blocks")
    sequence = models.PositiveIntegerField()
    event_type = models.CharField(max_length=80)
    actor = models.CharField(max_length=160)
    location = models.CharField(max_length=200)
    payload = models.JSONField(default=dict)
    timestamp = models.DateTimeField()
    previous_hash = models.CharField(max_length=64)
    current_hash = models.CharField(max_length=64)

    class Meta:
        ordering = ["sequence"]
        constraints = [
            models.UniqueConstraint(fields=["batch", "sequence"], name="unique_batch_sequence")
        ]

How are ledger blocks related?

Each LedgerBlock belongs to one batch through a foreign key. The related_name="blocks" option lets the API count or retrieve a batch's blocks.

The uniqueness constraint prevents two blocks from sharing one sequence number within the same batch. Step 4 uses that ordering to build the hash chain.

  • Save backend/traceability/models.py.
  • Create 0001_initial.py inside backend/traceability/migrations.
  • Use the double-check tab below to add the included initial migration.

✔️ I've added the schema files

Your model file and initial migration are ready. Save both files before continuing.

ⓧ I'd like to double check the full code

Compare backend/traceability/models.py with this complete reference.

from django.db import models


class Batch(models.Model):
    batch_id = models.CharField(max_length=40, primary_key=True)
    herb_name = models.CharField(max_length=120)
    botanical_name = models.CharField(max_length=160)
    quantity = models.DecimalField(max_digits=12, decimal_places=2)
    unit = models.CharField(max_length=20)
    farm_name = models.CharField(max_length=160)
    location = models.CharField(max_length=200)
    harvest_date = models.DateField()
    status = models.CharField(max_length=80, default="Harvested")
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-created_at"]


class LedgerBlock(models.Model):
    batch = models.ForeignKey(Batch, on_delete=models.CASCADE, related_name="blocks")
    sequence = models.PositiveIntegerField()
    event_type = models.CharField(max_length=80)
    actor = models.CharField(max_length=160)
    location = models.CharField(max_length=200)
    payload = models.JSONField(default=dict)
    timestamp = models.DateTimeField()
    previous_hash = models.CharField(max_length=64)
    current_hash = models.CharField(max_length=64)

    class Meta:
        ordering = ["sequence"]
        constraints = [
            models.UniqueConstraint(fields=["batch", "sequence"], name="unique_batch_sequence")
        ]

What should match?

Check every field name against the reference. The relationship name and uniqueness constraint must also match exactly.

Copy this complete migration into backend/traceability/migrations/0001_initial.py.

from django.db import migrations, models


class Migration(migrations.Migration):
    initial = True
    dependencies = []

    operations = [
        migrations.CreateModel(
            name="Batch",
            fields=[
                ("batch_id", models.CharField(max_length=40, primary_key=True, serialize=False)),
                ("herb_name", models.CharField(max_length=120)),
                ("botanical_name", models.CharField(max_length=160)),
                ("quantity", models.DecimalField(decimal_places=2, max_digits=12)),
                ("unit", models.CharField(max_length=20)),
                ("farm_name", models.CharField(max_length=160)),
                ("location", models.CharField(max_length=200)),
                ("harvest_date", models.DateField()),
                ("status", models.CharField(default="Harvested", max_length=80)),
                ("created_at", models.DateTimeField(auto_now_add=True)),
            ],
            options={"ordering": ["-created_at"]},
        ),
        migrations.CreateModel(
            name="LedgerBlock",
            fields=[
                ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
                ("sequence", models.PositiveIntegerField()),
                ("event_type", models.CharField(max_length=80)),
                ("actor", models.CharField(max_length=160)),
                ("location", models.CharField(max_length=200)),
                ("payload", models.JSONField(default=dict)),
                ("timestamp", models.DateTimeField()),
                ("previous_hash", models.CharField(max_length=64)),
                ("current_hash", models.CharField(max_length=64)),
                ("batch", models.ForeignKey(on_delete=models.CASCADE, related_name="blocks", to="traceability.batch")),
            ],
            options={"ordering": ["sequence"]},
        ),
        migrations.AddConstraint(
            model_name="ledgerblock",
            constraint=models.UniqueConstraint(fields=("batch", "sequence"), name="unique_batch_sequence"),
        ),
    ]

What does the migration create?

The migration turns both Python models into database tables. It also creates the foreign key and the database-enforced sequence constraint.

  • Save backend/traceability/migrations/0001_initial.py.
  • Return to the backend PowerShell terminal from earlier.
  • Activate the existing virtual environment if (.venv) is missing from the prompt by running:
.\.venv\Scripts\Activate.ps1

What does activation change?

Activation points the terminal at the Python environment inside backend/.venv. The following Django commands use the pinned project dependencies from that environment.

  • Apply the initial migration by running:
python manage.py migrate

What does migrate do?

Django reads 0001_initial.py and creates the corresponding tables in backend/db.sqlite3. The migration history prevents the same operation from being applied twice.

You should see the traceability migration finish with OK. That confirms the local database now has tables for batches and ledger blocks.

Migration did not finish with OK?

Confirm that PowerShell is inside the backend folder. The current folder must contain manage.py.

Check the field names in models.py against the double-check tab. A missing quote or bracket stops Django before it can create the database.

Ask for help with the exact terminal output: help me diagnose why my AyurDAO Django migration failed

Connect the persistence API

The browser needs JSON endpoints that translate form submissions into database writes. CSRF protection also checks that unsafe requests originate from the expected application.

  • Create views.py inside the backend/traceability folder.
  • Implement the temporary persistence handlers using the complete reference in the double-check tab below.

✔️ I've added the persistence handlers

Save backend/traceability/views.py before registering its routes.

ⓧ I'd like to double check the full code

Replace the contents of backend/traceability/views.py with this temporary persistence implementation.

import json
from datetime import date
from decimal import Decimal, InvalidOperation
from django.db import transaction
from django.http import JsonResponse
from django.views.decorators.csrf import ensure_csrf_cookie
from .models import Batch


def read_json(request):
    try:
        return json.loads(request.body.decode("utf-8"))
    except (json.JSONDecodeError, UnicodeDecodeError):
        return None


def method_error(allowed):
    return JsonResponse({"error": f"Method not allowed. Use {allowed}."}, status=405)


def batch_data(batch):
    return {
        "batch_id": batch.batch_id,
        "herb_name": batch.herb_name,
        "botanical_name": batch.botanical_name,
        "quantity": str(batch.quantity),
        "unit": batch.unit,
        "farm_name": batch.farm_name,
        "location": batch.location,
        "harvest_date": batch.harvest_date.isoformat(),
        "status": batch.status,
        "created_at": batch.created_at.isoformat(),
        "event_count": batch.blocks.count(),
    }


@ensure_csrf_cookie
def csrf(request):
    if request.method != "GET":
        return method_error("GET")
    return JsonResponse({"csrf": "ready"})


def batches(request):
    if request.method == "GET":
        records = Batch.objects.all()
        return JsonResponse({"batches": [batch_data(batch) for batch in records]})

    if request.method != "POST":
        return method_error("GET or POST")

    data = read_json(request)
    if data is None:
        return JsonResponse({"error": "Request body must be valid JSON."}, status=400)

    required = [
        "batch_id", "herb_name", "botanical_name", "quantity",
        "unit", "farm_name", "location", "harvest_date"
    ]
    missing = [field for field in required if not str(data.get(field, "")).strip()]
    if missing:
        return JsonResponse({"error": f"Missing fields: {', '.join(missing)}"}, status=400)

    batch_id = str(data["batch_id"]).strip().upper()
    if Batch.objects.filter(pk=batch_id).exists():
        return JsonResponse({"error": "That batch ID already exists."}, status=409)

    try:
        quantity = Decimal(str(data["quantity"]))
        harvest_date = date.fromisoformat(str(data["harvest_date"]))
    except (InvalidOperation, ValueError):
        return JsonResponse({"error": "Quantity or harvest date is invalid."}, status=400)

    if quantity <= 0:
        return JsonResponse({"error": "Quantity must be greater than zero."}, status=400)

    with transaction.atomic():
        batch = Batch.objects.create(
            batch_id=batch_id,
            herb_name=str(data["herb_name"]).strip(),
            botanical_name=str(data["botanical_name"]).strip(),
            quantity=quantity,
            unit=str(data["unit"]).strip(),
            farm_name=str(data["farm_name"]).strip(),
            location=str(data["location"]).strip(),
            harvest_date=harvest_date,
        )

    return JsonResponse({"batch": batch_data(batch)}, status=201)


def events(request, batch_id):
    if request.method != "POST":
        return method_error("POST")

    try:
        batch = Batch.objects.get(pk=batch_id.upper())
    except Batch.DoesNotExist:
        return JsonResponse({"error": "Batch not found."}, status=404)

    data = read_json(request)
    if data is None:
        return JsonResponse({"error": "Request body must be valid JSON."}, status=400)

    required = ["event_type", "actor", "location"]
    missing = [field for field in required if not str(data.get(field, "")).strip()]
    if missing:
        return JsonResponse({"error": f"Missing fields: {', '.join(missing)}"}, status=400)

    event_type = str(data["event_type"]).strip()

    with transaction.atomic():
        batch.status = event_type
        batch.save()

    return JsonResponse({"batch": batch_data(batch)}, status=201)


def trace(request, batch_id):
    if request.method != "GET":
        return method_error("GET")

    try:
        batch = Batch.objects.get(pk=batch_id.upper())
    except Batch.DoesNotExist:
        return JsonResponse({"error": "Batch not found."}, status=404)

    return JsonResponse({
        "batch": batch_data(batch),
        "verification": {
            "valid": False,
            "checked_blocks": 0,
            "failed_sequence": None,
        },
        "blocks": [],
    })

How does the persistence API work?

The batches() handler lists records for GET requests. POST requests validate the submitted JSON before creating a Batch inside an atomic transaction.

The temporary event and trace handlers reserve the remaining API paths. Step 4 replaces their persistence-only behavior with deterministic ledger operations.

The csrf() handler ensures the browser receives the cookie used by src/api.js for protected POST requests.

  • Save backend/traceability/views.py.
  • Create urls.py inside the backend/traceability folder.
  • Register the four API routes by pasting:
from django.urls import path
from . import views

urlpatterns = [
    path("csrf/", views.csrf, name="csrf"),
    path("batches/", views.batches, name="batches"),
    path("batches/<str:batch_id>/events/", views.events, name="events"),
    path("trace/<str:batch_id>/", views.trace, name="trace"),
]

How do these routes reach the frontend?

The root URL configuration places these paths under /api/. Vite forwards matching browser requests to the Django server at http://127.0.0.1:8000.

  • Save backend/traceability/urls.py.

Batch creation currently opens the trace view immediately. This persistence checkpoint keeps the learner flow in the Farmer view until Step 4 adds real ledger blocks.

  • In frontend/src/App.jsx, locate the success section inside createBatch().
  • Find these two lines:
setMessage(`Batch ${result.batch.batch_id} registered.`);
await loadTrace(result.batch.batch_id);

Why pause the trace request?

The first line confirms that the database accepted the batch. The second line requests ledger data that becomes meaningful after Step 4 adds deterministic blocks.

  • Delete the await loadTrace(result.batch.batch_id); line.
  • Leave the success line looking like this:
setMessage(`Batch ${result.batch.batch_id} registered.`);

What changes in the browser flow?

A successful submission now stays on the Farmer page. The refreshed batch list provides the visible database result for this step.

  • Save frontend/src/App.jsx.
  • Return to the activated backend PowerShell terminal.
  • Start the Django development server by running:
python manage.py runserver

What does the development server do?

Django starts its local development server at http://127.0.0.1:8000/. The command keeps this terminal occupied while it listens for API requests.

Vite remains active in its existing terminal. Together, the two servers form the local frontend and backend stack.

Before you refresh the app, do you expect the backend unavailable banner to remain or disappear?

  • Switch back to the browser tab running http://localhost:5173.
  • Refresh the AyurDAO page.

The backend unavailable banner should disappear. The Home page should load with zero registered batches if the database is new.

Still seeing the backend unavailable banner?

Check that the backend terminal still shows the Django server running. A Python traceback usually points to a typo in views.py or urls.py.

Confirm that Vite still uses http://localhost:5173. That origin matches the proxy and trusted-origin configuration established earlier.

Ask for help with both terminal outputs: help me debug why my Vite frontend cannot reach my local Django API

Prove the batch survives refresh

A database proves its value when a record outlives the browser state that created it. This final check submits one batch before forcing React to reload its data from the API.

  • Select Farmer in the AyurDAO navigation.
  • Enter ASH-2026-001 in the Batch ID field.
  • Enter Ashwagandha in the Herb name field.
  • Enter Withania somnifera in the Botanical name field.
  • Enter 25 in the Quantity field.
  • Keep kg selected in the Unit field.
  • Enter Green Valley Farm in the Farm name field.
  • Enter Pune in the Location field.
  • Select 2026-09-08 in the Harvest date field.

Before you submit the form, do you think the batch will still appear after the page reloads?

  • Click Register batch.

You should see a confirmation for ASH-2026-001. My Batches should show Ashwagandha with a Harvested status.

  • Refresh the browser page.
  • Select Farmer again if the Home view opens.

You should still see Ashwagandha and ASH-2026-001 in My Batches. That is the hard part complete: the batch now comes from SQLite instead of temporary browser state.

Batch missing after refresh?

Check the Django terminal for the POST request followed by a GET request. A failed POST means the batch never reached SQLite.

If the form reports that the batch ID already exists, the database already contains ASH-2026-001. Use a new unique batch ID for another submission.

Ask for help with the browser message and Django output: help me find why my AyurDAO batch disappears after refresh

Your farmer records now survive refreshes through a protected Django API. Next, you will turn each batch into a deterministic ledger whose integrity can be recomputed.

Build the Verifiable Ledger

Your React frontend now saves farmer batches through Django. SQLite keeps each record available after a refresh.

Persistence alone cannot reveal altered history. This step adds a SHA-256 hash chain that the API can recompute to expose changed records.

In this step, get ready to:
  • Create deterministic SHA-256 hashes from canonical event data.
  • Append sequential blocks inside atomic database transactions.
  • Recompute the chain through the trace endpoint.
Create canonical hashes

Every block needs the same byte representation whenever its hash is recalculated. Canonical JSON provides that stable representation by sorting keys and removing optional spacing.

  • In the VS Code Explorer sidebar, create ledger.py inside backend/traceability.
  • Build canonical serialization and hashing by pasting this code into ledger.py:
import hashlib
import json
from datetime import datetime, timezone
from django.db import transaction
from .models import LedgerBlock

GENESIS_HASH = "0" * 64


def canonical_block(batch_id, sequence, event_type, actor, location, payload, timestamp, previous_hash):
    value = {
        "actor": actor,
        "batch_id": batch_id,
        "event_type": event_type,
        "location": location,
        "payload": payload,
        "previous_hash": previous_hash,
        "sequence": sequence,
        "timestamp": timestamp.isoformat(),
    }
    return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))


def calculate_hash(batch_id, sequence, event_type, actor, location, payload, timestamp, previous_hash):
    content = canonical_block(
        batch_id, sequence, event_type, actor, location, payload, timestamp, previous_hash
    )
    return hashlib.sha256(content.encode("utf-8")).hexdigest()

What does this code do?

  • GENESIS_HASH gives the first block a fixed previous hash.
  • canonical_block() builds one stable JSON string from the event fields.
  • calculate_hash() encodes that string as bytes before calculating its SHA-256 digest.
  • Save backend/traceability/ledger.py.
  • Confirm ledger.py appears inside the traceability folder in the Explorer sidebar.

Is ledger.py in the wrong folder?

Check that ledger.py sits beside models.py inside backend/traceability. The relative model import depends on that location.

Help me place the ledger module correctly.

  • Add sequential block creation below calculate_hash() by pasting this code:
def append_block(batch, event_type, actor, location, payload):
    with transaction.atomic():
        previous = LedgerBlock.objects.filter(batch=batch).order_by("-sequence").first()
        sequence = 1 if previous is None else previous.sequence + 1
        previous_hash = GENESIS_HASH if previous is None else previous.current_hash
        timestamp = datetime.now(timezone.utc)
        current_hash = calculate_hash(
            batch.batch_id,
            sequence,
            event_type,
            actor,
            location,
            payload,
            timestamp,
            previous_hash,
        )
        return LedgerBlock.objects.create(
            batch=batch,
            sequence=sequence,
            event_type=event_type,
            actor=actor,
            location=location,
            payload=payload,
            timestamp=timestamp,
            previous_hash=previous_hash,
            current_hash=current_hash,
        )

How does block appending work?

  • The latest block determines the next sequence number.
  • The first block points to GENESIS_HASH.
  • Every later block points to the previous block's current_hash.
  • transaction.atomic() rolls the database operation back if block creation raises an exception.
  • Save backend/traceability/ledger.py.
  • Switch back to the activated backend terminal from earlier.
  • Press Ctrl+C to stop the current Django development server.
  • Restart the Django development server by running:
python manage.py runserver

What does this command do?

The command starts Django's development server using the project configuration in manage.py. It serves the backend at http://127.0.0.1:8000/.

You should see the backend return to its running state at http://127.0.0.1:8000/.

Does the backend fail to restart?

Check the terminal traceback for a line number in ledger.py. Confirm the parentheses in calculate_hash() and append_block() match the reference code.

Help me debug the ledger module syntax.

  • Switch back to backend/traceability/ledger.py in VS Code.
  • Add full-chain verification below append_block() by pasting this code:
def verify_chain(batch):
    blocks = list(LedgerBlock.objects.filter(batch=batch).order_by("sequence"))
    expected_previous = GENESIS_HASH

    for block in blocks:
        expected_hash = calculate_hash(
            batch.batch_id,
            block.sequence,
            block.event_type,
            block.actor,
            block.location,
            block.payload,
            block.timestamp,
            block.previous_hash,
        )
        if block.previous_hash != expected_previous or block.current_hash != expected_hash:
            return {
                "valid": False,
                "checked_blocks": block.sequence,
                "failed_sequence": block.sequence,
            }
        expected_previous = block.current_hash

    return {
        "valid": len(blocks) > 0,
        "checked_blocks": len(blocks),
        "failed_sequence": None,
    }

How does verification work?

  • verify_chain() reads blocks in sequence order.
  • expected_hash recalculates each block from its stored event data.
  • expected_previous checks that every block points to the hash immediately before it.
  • failed_sequence identifies the first block whose hash or link fails.
Wire atomic writes into the API

A batch and its first ledger block form one operation. The same rule applies to a later event and its matching status update.

  • Open backend/traceability/views.py from the VS Code Explorer sidebar.
  • Replace the import section at the top of views.py with this code:
import json
from datetime import date
from decimal import Decimal, InvalidOperation
from django.db import transaction
from django.http import JsonResponse
from django.views.decorators.csrf import ensure_csrf_cookie
from .ledger import append_block, verify_chain
from .models import Batch, LedgerBlock

What do these imports connect?

  • transaction provides atomic database boundaries for related writes.
  • append_block creates a linked ledger record.
  • verify_chain recalculates hashes before the API returns a trace.
  • Save backend/traceability/views.py.
  • Watch the activated backend terminal for Django's automatic reload.

You should see the server return to its running state without a traceback. This confirms that views.py can import the completed ledger module.

Does the import trigger a traceback?

Confirm ledger.py defines both append_block and verify_chain. Check that the import begins with a single dot because both files share the traceability package.

Help me fix the ledger import.

  • Locate the if quantity <= 0: validation inside batches().
  • Replace the batch creation section after that validation with this atomic block:
    with transaction.atomic():
        batch = Batch.objects.create(
            batch_id=batch_id,
            herb_name=str(data["herb_name"]).strip(),
            botanical_name=str(data["botanical_name"]).strip(),
            quantity=quantity,
            unit=str(data["unit"]).strip(),
            farm_name=str(data["farm_name"]).strip(),
            location=str(data["location"]).strip(),
            harvest_date=harvest_date,
        )
        append_block(
            batch,
            "Harvested",
            batch.farm_name,
            batch.location,
            {"quantity": str(batch.quantity), "unit": batch.unit},
        )

    return JsonResponse({"batch": batch_data(batch)}, status=201)

Why create both records atomically?

A successful registration now creates the Batch record plus its first Harvested block. An exception rolls back both writes so a batch cannot exist without its starting history.

  • Locate the payload = {"note": str(data.get("note", "")).strip()} line inside events().
  • Replace the event persistence section below that line with this atomic block:
    with transaction.atomic():
        block = append_block(batch, event_type, actor, location, payload)
        batch.status = event_type
        batch.save()

    return JsonResponse({"event": block_data(block)}, status=201)

What stays consistent here?

The event block and Batch.status update now share one transaction. A failed write leaves both values at their previous state.

  • Save backend/traceability/views.py.
  • Watch the activated backend terminal complete another automatic reload.

You should see Django return to its running state without a traceback. The API can now import both atomic write paths.

Does views.py report an indentation error?

Keep each with transaction.atomic(): block inside its matching view function. The final return JsonResponse(...) line aligns with the with statement.

Help me correct the atomic block indentation.

Verify a linked batch

The trace response must calculate integrity when it is requested. This keeps the verification badge tied to the stored data that exists now.

  • Locate the temporary trace() function at the bottom of backend/traceability/views.py.
  • Replace the entire temporary function with this final trace handler:
def trace(request, batch_id):
    if request.method != "GET":
        return method_error("GET")

    try:
        batch = Batch.objects.get(pk=batch_id.upper())
    except Batch.DoesNotExist:
        return JsonResponse({"error": "Batch not found."}, status=404)

    blocks = LedgerBlock.objects.filter(batch=batch).order_by("sequence")
    return JsonResponse({
        "batch": batch_data(batch),
        "verification": verify_chain(batch),
        "blocks": [block_data(block) for block in blocks],
    })

What does the trace endpoint return?

  • batch contains the persisted herb and farm details.
  • verification contains valid, checked_blocks, and failed_sequence.
  • blocks contains the ordered timeline plus both hashes for each event.
  • Save backend/traceability/views.py.

✔️ Awesome, I've got everything!

Great. Make sure you have saved ledger.py and views.py.

ⓧ I'd like to double check the full code

Compare your two updated backend files with these final versions.

backend/traceability/ledger.py

import hashlib
import json
from datetime import datetime, timezone
from django.db import transaction
from .models import LedgerBlock

GENESIS_HASH = "0" * 64


def canonical_block(batch_id, sequence, event_type, actor, location, payload, timestamp, previous_hash):
    value = {
        "actor": actor,
        "batch_id": batch_id,
        "event_type": event_type,
        "location": location,
        "payload": payload,
        "previous_hash": previous_hash,
        "sequence": sequence,
        "timestamp": timestamp.isoformat(),
    }
    return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))


def calculate_hash(batch_id, sequence, event_type, actor, location, payload, timestamp, previous_hash):
    content = canonical_block(
        batch_id, sequence, event_type, actor, location, payload, timestamp, previous_hash
    )
    return hashlib.sha256(content.encode("utf-8")).hexdigest()


def append_block(batch, event_type, actor, location, payload):
    with transaction.atomic():
        previous = LedgerBlock.objects.filter(batch=batch).order_by("-sequence").first()
        sequence = 1 if previous is None else previous.sequence + 1
        previous_hash = GENESIS_HASH if previous is None else previous.current_hash
        timestamp = datetime.now(timezone.utc)
        current_hash = calculate_hash(
            batch.batch_id,
            sequence,
            event_type,
            actor,
            location,
            payload,
            timestamp,
            previous_hash,
        )
        return LedgerBlock.objects.create(
            batch=batch,
            sequence=sequence,
            event_type=event_type,
            actor=actor,
            location=location,
            payload=payload,
            timestamp=timestamp,
            previous_hash=previous_hash,
            current_hash=current_hash,
        )


def verify_chain(batch):
    blocks = list(LedgerBlock.objects.filter(batch=batch).order_by("sequence"))
    expected_previous = GENESIS_HASH

    for block in blocks:
        expected_hash = calculate_hash(
            batch.batch_id,
            block.sequence,
            block.event_type,
            block.actor,
            block.location,
            block.payload,
            block.timestamp,
            block.previous_hash,
        )
        if block.previous_hash != expected_previous or block.current_hash != expected_hash:
            return {
                "valid": False,
                "checked_blocks": block.sequence,
                "failed_sequence": block.sequence,
            }
        expected_previous = block.current_hash

    return {
        "valid": len(blocks) > 0,
        "checked_blocks": len(blocks),
        "failed_sequence": None,
    }

backend/traceability/views.py

import json
from datetime import date
from decimal import Decimal, InvalidOperation
from django.db import transaction
from django.http import JsonResponse
from django.views.decorators.csrf import ensure_csrf_cookie
from .ledger import append_block, verify_chain
from .models import Batch, LedgerBlock


def read_json(request):
    try:
        return json.loads(request.body.decode("utf-8"))
    except (json.JSONDecodeError, UnicodeDecodeError):
        return None


def method_error(allowed):
    return JsonResponse({"error": f"Method not allowed. Use {allowed}."}, status=405)


def batch_data(batch):
    return {
        "batch_id": batch.batch_id,
        "herb_name": batch.herb_name,
        "botanical_name": batch.botanical_name,
        "quantity": str(batch.quantity),
        "unit": batch.unit,
        "farm_name": batch.farm_name,
        "location": batch.location,
        "harvest_date": batch.harvest_date.isoformat(),
        "status": batch.status,
        "created_at": batch.created_at.isoformat(),
        "event_count": batch.blocks.count(),
    }


def block_data(block):
    return {
        "sequence": block.sequence,
        "event_type": block.event_type,
        "actor": block.actor,
        "location": block.location,
        "payload": block.payload,
        "timestamp": block.timestamp.isoformat(),
        "previous_hash": block.previous_hash,
        "current_hash": block.current_hash,
    }


@ensure_csrf_cookie
def csrf(request):
    if request.method != "GET":
        return method_error("GET")
    return JsonResponse({"csrf": "ready"})


def batches(request):
    if request.method == "GET":
        records = Batch.objects.all()
        return JsonResponse({"batches": [batch_data(batch) for batch in records]})

    if request.method != "POST":
        return method_error("GET or POST")

    data = read_json(request)
    if data is None:
        return JsonResponse({"error": "Request body must be valid JSON."}, status=400)

    required = [
        "batch_id", "herb_name", "botanical_name", "quantity",
        "unit", "farm_name", "location", "harvest_date"
    ]
    missing = [field for field in required if not str(data.get(field, "")).strip()]
    if missing:
        return JsonResponse({"error": f"Missing fields: {', '.join(missing)}"}, status=400)

    batch_id = str(data["batch_id"]).strip().upper()
    if Batch.objects.filter(pk=batch_id).exists():
        return JsonResponse({"error": "That batch ID already exists."}, status=409)

    try:
        quantity = Decimal(str(data["quantity"]))
        harvest_date = date.fromisoformat(str(data["harvest_date"]))
    except (InvalidOperation, ValueError):
        return JsonResponse({"error": "Quantity or harvest date is invalid."}, status=400)

    if quantity <= 0:
        return JsonResponse({"error": "Quantity must be greater than zero."}, status=400)

    with transaction.atomic():
        batch = Batch.objects.create(
            batch_id=batch_id,
            herb_name=str(data["herb_name"]).strip(),
            botanical_name=str(data["botanical_name"]).strip(),
            quantity=quantity,
            unit=str(data["unit"]).strip(),
            farm_name=str(data["farm_name"]).strip(),
            location=str(data["location"]).strip(),
            harvest_date=harvest_date,
        )
        append_block(
            batch,
            "Harvested",
            batch.farm_name,
            batch.location,
            {"quantity": str(batch.quantity), "unit": batch.unit},
        )

    return JsonResponse({"batch": batch_data(batch)}, status=201)


def events(request, batch_id):
    if request.method != "POST":
        return method_error("POST")

    try:
        batch = Batch.objects.get(pk=batch_id.upper())
    except Batch.DoesNotExist:
        return JsonResponse({"error": "Batch not found."}, status=404)

    data = read_json(request)
    if data is None:
        return JsonResponse({"error": "Request body must be valid JSON."}, status=400)

    required = ["event_type", "actor", "location"]
    missing = [field for field in required if not str(data.get(field, "")).strip()]
    if missing:
        return JsonResponse({"error": f"Missing fields: {', '.join(missing)}"}, status=400)

    event_type = str(data["event_type"]).strip()
    actor = str(data["actor"]).strip()
    location = str(data["location"]).strip()
    payload = {"note": str(data.get("note", "")).strip()}

    with transaction.atomic():
        block = append_block(batch, event_type, actor, location, payload)
        batch.status = event_type
        batch.save()

    return JsonResponse({"event": block_data(block)}, status=201)


def trace(request, batch_id):
    if request.method != "GET":
        return method_error("GET")

    try:
        batch = Batch.objects.get(pk=batch_id.upper())
    except Batch.DoesNotExist:
        return JsonResponse({"error": "Batch not found."}, status=404)

    blocks = LedgerBlock.objects.filter(batch=batch).order_by("sequence")
    return JsonResponse({
        "batch": batch_data(batch),
        "verification": verify_chain(batch),
        "blocks": [block_data(block) for block in blocks],
    })
  • Return to the AyurDAO browser tab at http://localhost:5173.
  • Select Farmer in the navigation.
  • Enter ASH-2026-LEDGER in the Batch ID field.
  • Enter Ashwagandha in the Herb name field.
  • Enter Ashwagandha in the Botanical name field.
  • Enter 10 in the Quantity field.
  • Keep kg in the Unit field.

These values define the event data that becomes part of the first block's hash. The remaining fields identify the actor and origin.

  • Enter AyurDAO Demo Farm in the Farm name field.
  • Enter Pune Demo Plot in the Location field.
  • Select today's date in the Harvest date field.
  • Click Register batch.

Your batch opens in the Verify view with Verified and 1 block checked. That first block now protects the harvested event with a reproducible hash.

  • Keep Processing in the Event type field.
  • Enter AyurDAO Processing Co-op in the Actor field.
  • Enter Pune Processing Centre in the event Location field.
  • Enter Roots washed and dried in the Note field.

Before you append the event, consider whether block 2 will point to block 1's current hash.

  • Click Append ledger event.

You should see Verified with 2 blocks checked. The timeline now includes Processing as block 2.

Excellent work. The second block's previous hash matches the first block's current hash, so your stored history now forms a verifiable chain.

Does the ledger fail to show two blocks?

Check that both development servers are still running. A duplicate batch identifier also prevents a fresh batch from being registered.

If the event form reports a backend problem, inspect the Django terminal for a traceback from append_block() or verify_chain().

Help me debug the missing ledger block.

Your backend now stores deterministic ledger links and verifies them on every trace request. Next up, you will turn each verified batch into a customer-facing QR trace link.

Generate Customer QR Trace Links

Your ledger can prove that its records still link together. A buyer still needs a direct route from a physical batch to that evidence.

You will use the qrcode.react library to give each batch a QR-backed verification URL. React will read the requested batch ID from the URL when the app starts.

In this step, get ready to:
  • Load a requested batch from the trace query parameter.
  • Generate a QR code with a clickable customer verification link.
  • Confirm the customer view shows the complete trace result with an honest simulation label.
Add trace-aware startup

A customer link needs to restore the requested trace after a fresh page load. The app can do this by reading ?trace= during its startup effect.

  • Switch back to frontend/src/App.jsx in Visual Studio Code.
  • Find the existing useEffect(() => { block near the start of App().
  • Replace that effect with this trace-aware startup code:
  useEffect(() => {
    let ignore = false;
    async function start() {
      try {
        const result = await api.listBatches();
        if (!ignore) {
          setBatches(result.batches);
          setServerError("");
        }
      } catch {
        if (!ignore) {
          setServerError("Backend unavailable. Start the Django server to enable persistence and verification.");
        }
      }

      const traceId = new URLSearchParams(window.location.search).get("trace");
      if (traceId) {
        setPage("verify");
        setLookup(traceId);
        try {
          const result = await api.trace(traceId);
          if (!ignore) setTrace(result);
        } catch (error) {
          if (!ignore) setMessage(error.message);
        }
      }
    }
    start();
    return () => {
      ignore = true;
    };
  }, []);

What does this startup effect do?

  • The first request restores the persisted batch list from the backend.
  • URLSearchParams reads the trace value from the current query string.
  • setPage("verify") switches directly to the customer verification view when a batch ID is present.
  • api.trace(traceId) requests the matching batch timeline with its current integrity result.
  • Save frontend/src/App.jsx.

The Vite terminal should complete the update without reporting a compile error.

Seeing a compile error?

Check that the replacement remains inside App(). Confirm that the closing }, []); appears before refreshBatches().

Ask for help with the startup effect.

Render the customer QR link

The QR code needs a URL containing the current batch ID. The visible text link keeps the same customer journey testable from this Windows computer.

  • Replace the import group at the top of frontend/src/App.jsx with these imports:
import { useEffect, useState } from "react";
import { QRCodeSVG } from "qrcode.react";
import { api } from "./api.js";

What do these imports provide?

  • useEffect restores the requested trace during startup.
  • useState continues to manage the application state.
  • QRCodeSVG turns the verification URL into a scalable QR graphic.
  • api loads the matching backend trace.
  • Save frontend/src/App.jsx.

The Vite terminal should update the app without reporting a missing import.

Is QRCodeSVG unavailable?

Confirm that the import uses qrcode.react in lowercase. Confirm that QRCodeSVG uses the same capitalization everywhere.

Ask for help with the QR import.

  • Find the TracePanel() function near the bottom of frontend/src/App.jsx.
  • Add these URL values directly below the function declaration:
  const query = new URLSearchParams({ trace: trace.batch.batch_id }).toString();
  const verificationUrl = `${window.location.origin}/?${query}`;

How is the verification URL built?

  • query safely serializes the current batch ID into the trace query parameter.
  • window.location.origin keeps the link on the same local Vite origin.
  • verificationUrl becomes the shared value for the QR code and text link.
  • Find the failure-note paragraph inside the identity-card article.
  • Add this QR section directly below that paragraph:
        <div className="qr-box">
          <QRCodeSVG value={verificationUrl} size={176} level="M" marginSize={4} title={`Verify ${trace.batch.batch_id}`} />
          <a href={verificationUrl}>Open customer verification link</a>
        </div>

What does the QR section do?

  • QRCodeSVG encodes the customer verification URL as an SVG image.
  • size={176} gives the code a consistent size inside the trace card.
  • level="M" selects the QR component's medium error-correction level.
  • The anchor provides a clickable route to the same customer trace.
  • Save frontend/src/App.jsx.
  • Switch back to the AyurDAO browser tab from earlier.
  • Select Farmer in the navigation.
  • Click View ledger for the batch with the Processing event.

You should see a QR code beneath the batch details. A link labelled Open customer verification link should appear below it.

Don't see the QR code?

Check that the QR markup sits inside the identity-card article. Confirm that verificationUrl is defined at the start of TracePanel().

Ask for help with the QR rendering.

✔️ Awesome, I've got everything!

Great. Your trace card now generates a QR-backed customer link for the selected batch.

ⓧ I'd like to double check the full code

Compare your complete frontend/src/App.jsx file with this final version.

import { useEffect, useState } from "react";
import { QRCodeSVG } from "qrcode.react";
import { api } from "./api.js";

const HERBS = ["Ashwagandha", "Tulsi", "Turmeric", "Amla"];

const EMPTY_BATCH = {
  batch_id: "",
  herb_name: "",
  botanical_name: "",
  quantity: "",
  unit: "kg",
  farm_name: "",
  location: "",
  harvest_date: ""
};

const EMPTY_EVENT = {
  event_type: "Processing",
  actor: "",
  location: "",
  note: ""
};

function shortHash(value) {
  if (!value) return "Not available";
  return `${value.slice(0, 12)}...${value.slice(-10)}`;
}

function App() {
  const [page, setPage] = useState("home");
  const [batches, setBatches] = useState([]);
  const [batchForm, setBatchForm] = useState(EMPTY_BATCH);
  const [eventForm, setEventForm] = useState(EMPTY_EVENT);
  const [lookup, setLookup] = useState("");
  const [trace, setTrace] = useState(null);
  const [serverError, setServerError] = useState("");
  const [message, setMessage] = useState("");
  const [busy, setBusy] = useState(false);

  useEffect(() => {
    let ignore = false;
    async function start() {
      try {
        const result = await api.listBatches();
        if (!ignore) {
          setBatches(result.batches);
          setServerError("");
        }
      } catch {
        if (!ignore) {
          setServerError("Backend unavailable. Start the Django server to enable persistence and verification.");
        }
      }

      const traceId = new URLSearchParams(window.location.search).get("trace");
      if (traceId) {
        setPage("verify");
        setLookup(traceId);
        try {
          const result = await api.trace(traceId);
          if (!ignore) setTrace(result);
        } catch (error) {
          if (!ignore) setMessage(error.message);
        }
      }
    }
    start();
    return () => {
      ignore = true;
    };
  }, []);

  async function refreshBatches() {
    const result = await api.listBatches();
    setBatches(result.batches);
    setServerError("");
  }

  async function loadTrace(batchId) {
    const cleanId = batchId.trim();
    if (!cleanId) return;
    setBusy(true);
    setMessage("");
    try {
      const result = await api.trace(cleanId);
      setTrace(result);
      setLookup(cleanId);
      setPage("verify");
    } catch (error) {
      setTrace(null);
      setMessage(error.message);
    } finally {
      setBusy(false);
    }
  }

  async function createBatch(event) {
    event.preventDefault();
    setBusy(true);
    setMessage("");
    try {
      const result = await api.createBatch(batchForm);
      await refreshBatches();
      setBatchForm(EMPTY_BATCH);
      setMessage(`Batch ${result.batch.batch_id} registered.`);
      await loadTrace(result.batch.batch_id);
    } catch (error) {
      setMessage(error.message);
    } finally {
      setBusy(false);
    }
  }

  async function addEvent(event) {
    event.preventDefault();
    if (!trace) return;
    setBusy(true);
    setMessage("");
    try {
      await api.addEvent(trace.batch.batch_id, eventForm);
      setEventForm(EMPTY_EVENT);
      await loadTrace(trace.batch.batch_id);
      await refreshBatches();
    } catch (error) {
      setMessage(error.message);
    } finally {
      setBusy(false);
    }
  }

  const navigation = [
    ["home", "Home"],
    ["explore", "Explore"],
    ["farmer", "Farmer"],
    ["verify", "Verify"]
  ];

  return (
    <div className="app-shell">
      <header className="topbar">
        <button className="brand" onClick={() => setPage("home")}>
          <span className="brand-mark">A</span>
          <span>AyurDAO</span>
        </button>
        <nav aria-label="Primary navigation">
          {navigation.map(([id, label]) => (
            <button key={id} className={page === id ? "nav-active" : ""} onClick={() => setPage(id)}>
              {label}
            </button>
          ))}
        </nav>
      </header>

      {serverError && <div className="system-banner warning">{serverError}</div>}
      {message && <div className="system-banner">{message}</div>}

      <main>
        {page === "home" && <HomePage batches={batches} onExplore={() => setPage("explore")} onVerify={() => setPage("verify")} />}
        {page === "explore" && <ExplorePage />}
        {page === "farmer" && (
          <FarmerPage
            batches={batches}
            form={batchForm}
            setForm={setBatchForm}
            onSubmit={createBatch}
            onView={loadTrace}
            busy={busy}
          />
        )}
        {page === "verify" && (
          <VerifyPage
            lookup={lookup}
            setLookup={setLookup}
            trace={trace}
            onLookup={(event) => {
              event.preventDefault();
              loadTrace(lookup);
            }}
            eventForm={eventForm}
            setEventForm={setEventForm}
            onAddEvent={addEvent}
            busy={busy}
          />
        )}
      </main>

      <footer>
        <strong>AyurDAO prototype</strong>
        <span>Local SHA-256 ledger simulation. Not a decentralized blockchain.</span>
      </footer>
    </div>
  );
}

function HomePage({ batches, onExplore, onVerify }) {
  return (
    <>
      <section className="hero">
        <div>
          <p className="eyebrow">Farm-to-shelf traceability</p>
          <h1>Trust every herb journey.</h1>
          <p className="hero-copy">Register batches, append supply-chain events, and let customers verify linked records from one transparent interface.</p>
          <div className="button-row">
            <button className="primary" onClick={onExplore}>Explore herbs</button>
            <button className="secondary" onClick={onVerify}>Verify a batch</button>
          </div>
        </div>
        <div className="hero-card">
          <span className="hero-glyph">A</span>
          <strong>Verification-ready</strong>
          <p>{batches.length} persistent batch{batches.length === 1 ? "" : "es"} in this local prototype</p>
        </div>
      </section>
      <section className="stats">
        <article><strong>{batches.length}</strong><span>Registered batches</span></article>
        <article><strong>{batches.reduce((total, batch) => total + batch.event_count, 0)}</strong><span>Ledger events</span></article>
        <article><strong>SHA-256</strong><span>Hash algorithm</span></article>
      </section>
      <section className="content-section">
        <p className="eyebrow">Prototype flow</p>
        <h2>Farmer input becomes customer evidence</h2>
        <div className="feature-grid">
          <article><span>01</span><h3>Register</h3><p>Create a batch with farm, quantity, location, and harvest details.</p></article>
          <article><span>02</span><h3>Append</h3><p>Add processing and custody events without editing earlier history.</p></article>
          <article><span>03</span><h3>Verify</h3><p>Recompute the full chain and share the trace through a QR link.</p></article>
        </div>
      </section>
    </>
  );
}

function ExplorePage() {
  return (
    <section className="content-section">
      <p className="eyebrow">Reference catalogue</p>
      <h1>Explore AyurDAO herbs</h1>
      <p className="section-copy">These cards preserve the discovery experience from the supplied design. Transaction records are loaded separately from the backend.</p>
      <div className="herb-grid">
        {HERBS.map((herb, index) => (
          <article className="herb-card" key={herb}>
            <span className="herb-number">0{index + 1}</span>
            <div className="herb-art">{herb.slice(0, 1)}</div>
            <h3>{herb}</h3>
            <p>Reference profile ready for a future verified catalogue service.</p>
          </article>
        ))}
      </div>
    </section>
  );
}

function FarmerPage({ batches, form, setForm, onSubmit, onView, busy }) {
  function update(field, value) {
    setForm((current) => ({ ...current, [field]: value }));
  }

  return (
    <section className="content-section">
      <p className="eyebrow">Farmer workspace</p>
      <h1>Register a harvested batch</h1>
      <div className="workspace-grid">
        <form className="panel form-grid" onSubmit={onSubmit}>
          <label>Batch ID<input value={form.batch_id} onChange={(e) => update("batch_id", e.target.value)} placeholder="ASH-2026-001" required /></label>
          <label>Herb name<input value={form.herb_name} onChange={(e) => update("herb_name", e.target.value)} required /></label>
          <label>Botanical name<input value={form.botanical_name} onChange={(e) => update("botanical_name", e.target.value)} required /></label>
          <label>Quantity<input type="number" min="0.01" step="0.01" value={form.quantity} onChange={(e) => update("quantity", e.target.value)} required /></label>
          <label>Unit<select value={form.unit} onChange={(e) => update("unit", e.target.value)}><option>kg</option><option>g</option><option>units</option></select></label>
          <label>Farm name<input value={form.farm_name} onChange={(e) => update("farm_name", e.target.value)} required /></label>
          <label>Location<input value={form.location} onChange={(e) => update("location", e.target.value)} required /></label>
          <label>Harvest date<input type="date" value={form.harvest_date} onChange={(e) => update("harvest_date", e.target.value)} required /></label>
          <button className="primary span-two" disabled={busy}>{busy ? "Saving..." : "Register batch"}</button>
        </form>

        <div className="panel">
          <div className="panel-heading"><div><p className="eyebrow">Persistent records</p><h2>My batches</h2></div><span className="count-pill">{batches.length}</span></div>
          <div className="batch-list">
            {batches.length === 0 && <p className="empty-state">No batches yet. Register the first one.</p>}
            {batches.map((batch) => (
              <article className="batch-row" key={batch.batch_id}>
                <div><strong>{batch.herb_name}</strong><span>{batch.batch_id}</span><small>{batch.farm_name} | {batch.location}</small></div>
                <div className="batch-actions"><span className="status-pill">{batch.status}</span><button className="text-button" onClick={() => onView(batch.batch_id)}>View ledger</button></div>
              </article>
            ))}
          </div>
        </div>
      </div>
    </section>
  );
}

function VerifyPage({ lookup, setLookup, trace, onLookup, eventForm, setEventForm, onAddEvent, busy }) {
  return (
    <section className="content-section">
      <p className="eyebrow">Customer verification</p>
      <h1>Trace a batch</h1>
      <form className="lookup" onSubmit={onLookup}>
        <input value={lookup} onChange={(e) => setLookup(e.target.value)} placeholder="Enter a batch ID" required />
        <button className="primary" disabled={busy}>{busy ? "Checking..." : "Verify"}</button>
      </form>
      {trace && <TracePanel trace={trace} eventForm={eventForm} setEventForm={setEventForm} onAddEvent={onAddEvent} busy={busy} />}
    </section>
  );
}

function TracePanel({ trace, eventForm, setEventForm, onAddEvent, busy }) {
  const query = new URLSearchParams({ trace: trace.batch.batch_id }).toString();
  const verificationUrl = `${window.location.origin}/?${query}`;

  function update(field, value) {
    setEventForm((current) => ({ ...current, [field]: value }));
  }

  return (
    <div className="trace-layout">
      <article className="panel identity-card">
        <div className={`verification ${trace.verification.valid ? "valid" : "invalid"}`}>
          <strong>{trace.verification.valid ? "Verified" : "Integrity Failure"}</strong>
          <span>{trace.verification.checked_blocks} block{trace.verification.checked_blocks === 1 ? "" : "s"} checked</span>
        </div>
        <h2>{trace.batch.herb_name}</h2>
        <p className="mono">{trace.batch.batch_id}</p>
        <dl>
          <div><dt>Botanical name</dt><dd>{trace.batch.botanical_name}</dd></div>
          <div><dt>Farm</dt><dd>{trace.batch.farm_name}</dd></div>
          <div><dt>Origin</dt><dd>{trace.batch.location}</dd></div>
          <div><dt>Quantity</dt><dd>{trace.batch.quantity} {trace.batch.unit}</dd></div>
          <div><dt>Harvested</dt><dd>{trace.batch.harvest_date}</dd></div>
        </dl>
        {!trace.verification.valid && trace.verification.failed_sequence && <p className="failure-note">Verification first failed at sequence {trace.verification.failed_sequence}.</p>}
        <div className="qr-box">
          <QRCodeSVG value={verificationUrl} size={176} level="M" marginSize={4} title={`Verify ${trace.batch.batch_id}`} />
          <a href={verificationUrl}>Open customer verification link</a>
        </div>
      </article>

      <div>
        <article className="panel">
          <div className="panel-heading"><div><p className="eyebrow">Append-only history</p><h2>Supply-chain timeline</h2></div></div>
          <div className="timeline">
            {trace.blocks.map((block) => (
              <div className="timeline-row" key={block.sequence}>
                <span className="timeline-dot">{block.sequence}</span>
                <div><strong>{block.event_type}</strong><p>{block.actor} | {block.location}</p><small>{new Date(block.timestamp).toLocaleString()}</small>{block.payload.note && <p>{block.payload.note}</p>}</div>
              </div>
            ))}
          </div>
        </article>

        <article className="panel ledger-panel">
          <p className="eyebrow">Hash inspection</p>
          <h2>Ledger blocks</h2>
          {trace.blocks.map((block) => (
            <div className="ledger-row" key={block.sequence}>
              <strong>Block {block.sequence}: {block.event_type}</strong>
              <span>Previous: <code title={block.previous_hash}>{shortHash(block.previous_hash)}</code></span>
              <span>Current: <code title={block.current_hash}>{shortHash(block.current_hash)}</code></span>
            </div>
          ))}
        </article>

        <form className="panel form-grid event-form" onSubmit={onAddEvent}>
          <div className="span-two"><p className="eyebrow">Farmer or partner action</p><h2>Add an event</h2></div>
          <label>Event type<select value={eventForm.event_type} onChange={(e) => update("event_type", e.target.value)}><option>Processing</option><option>Quality Check</option><option>Custody Transfer</option><option>Packaging</option></select></label>
          <label>Actor<input value={eventForm.actor} onChange={(e) => update("actor", e.target.value)} required /></label>
          <label>Location<input value={eventForm.location} onChange={(e) => update("location", e.target.value)} required /></label>
          <label>Note<input value={eventForm.note} onChange={(e) => update("note", e.target.value)} /></label>
          <button className="primary span-two" disabled={busy}>{busy ? "Appending..." : "Append ledger event"}</button>
        </form>
      </div>
    </div>
  );
}

export default App;
Verify the customer journey

The final check proves that the link survives a fresh page load. It also confirms that customers receive the same backend verification result as the ledger view.

Before you click, think about whether the same batch will load automatically from the URL.

  • Click Open customer verification link beneath the QR code.

You should remain on the Verify view. The address bar should contain ?trace= followed by the selected batch ID.

  • Refresh the browser page.

You should see the same batch ID with its Harvested and Processing events. The verification panel should show Verified with the number of checked blocks.

  • Hover over each shortened value in Ledger blocks.
  • Confirm that the tooltip reveals the complete previous or current hash.
  • Read the message in the application footer.

The footer should say Local SHA-256 ledger simulation. Not a decentralized blockchain. This keeps the customer result accurate about what the prototype proves.

What does Verified mean here?

Verified means the local backend recomputed every stored block. It found matching hashes with an unbroken sequence of previous-hash links.

An altered block changes the result to Integrity Failure. The customer view also displays the first failed sequence.

This local simulation does not provide distributed consensus. It does not claim to run on Hyperledger Fabric or a decentralized blockchain.

Does the link open an empty trace?

Confirm that the URL contains the exact batch ID after ?trace=. Check that both development servers from earlier are still running.

Ask for help with the customer deep link.

That completes the customer path. Every batch now carries a QR-backed route to its timeline and live integrity result.

Secret mission

Prove Tamper Detection

Your verifier has only seen valid history. Change one stored ledger event outside the application API. Then prove the customer view catches the exact break.

Clean Up Your Resources

Clean Up Your Resources

Everything in this project runs locally at no ongoing cost. Choose whether to keep the demo available, pause both servers, or remove its files.

Resources you used:

  • Local Vite development server at http://localhost:5173.
  • Local Django development server at http://127.0.0.1:8000.
  • Local ayurdao-ledger project folder on your Desktop.
  • Installed frontend packages inside frontend/node_modules.
  • Python virtual environment inside backend/.venv.
  • Local SQLite database at backend/db.sqlite3.

Keep everything running

No action is needed. Choose this option if you want to keep the AyurDAO demonstration available in your browser.

  • Keep the frontend terminal running to preserve access to http://localhost:5173.
  • Keep the backend terminal running to preserve access to http://127.0.0.1:8000.
  • Retain the ayurdao-ledger folder to preserve your source files.
  • Retain backend/db.sqlite3 to preserve the tamper-detection demonstration.

Pause - I'll come back to this later

Pausing frees the local ports used by both development servers. Your source files remain ready for another session.

  • Press Ctrl+C in the frontend terminal to stop Vite.
  • Press Ctrl+C in the backend terminal to stop Django.
  • Leave the ayurdao-ledger folder on your Desktop.

Both local URLs stop responding after their servers close. Your batches remain stored in backend/db.sqlite3.

Delete - I don't want to use this again

Remove the entire project when you want a fresh start. Use the data-reset path if you want to keep the code without its existing batches.

Delete the entire project

This path permanently removes the complete local project. Your globally installed tools remain available.

Permanent Local Deletion

The permanent deletion option removes the folder without moving it to the Recycle Bin. Windows asks for confirmation before continuing.

  • Press Ctrl+C in the frontend terminal to stop Vite.
  • Press Ctrl+C in the backend terminal to stop Django.
  • Locate ayurdao-ledger on your Desktop through File Explorer.
  • Select the ayurdao-ledger folder.
  • Hold Shift while clicking Delete in the File Explorer toolbar.
  • Approve the permanent deletion prompt.
  • Confirm that ayurdao-ledger no longer appears on your Desktop.

Reset only the data

This path removes every batch from the local database. The source files plus installed dependencies stay in place.

  • Press Ctrl+C in the activated backend terminal to stop Django.
  • Open the backend folder inside ayurdao-ledger through File Explorer.
  • Select db.sqlite3.
  • Click Delete in the File Explorer toolbar.
  • Switch back to the activated backend terminal from earlier.
  • Recreate the empty database by running:
python manage.py migrate
What Does This Command Do?
  • The migration applies the included schema to a new backend/db.sqlite3 file.
  • The rebuilt database starts with no batches.
  • Confirm that the migration output ends with OK.
  • Leave the Django server stopped until you want to continue developing.
Database Not Recreated?
  • Check that the activated PowerShell prompt is inside the backend folder.
  • Check that the old backend/db.sqlite3 file was deleted before the migration.
  • Ask for help with the database reset.

Nice Work!

Nice Work!

You made it! Your complete AyurDAO traceability application now connects a React interface to a Django API backed by SQLite.

You've learned how to:

  • Built a responsive AyurDAO frontend for herb discovery. Farmers can register batches through a dedicated workspace. Customers can inspect each journey through the Verify view.
  • Connected the frontend to Django endpoints with CSRF protection. SQLite keeps every batch available after a browser refresh. It also preserves each supply-chain event.
  • Generated a deterministic SHA-256 hash chain from canonical event data. Recomputed the chain to display Verified or Integrity Failure. Shared each trace through a QR verification URL.
  • Completed the optional Secret Mission by changing a stored LedgerBlock outside the application API. The Verify view exposed the edit as Integrity Failure at the first failed sequence.

Ready to quiz yourself?