Full Code of errorcode26/test for AI

main f7944e60f6a3 cached
65 files
874.1 KB
265.9k tokens
319 symbols
1 requests
Download .txt
Showing preview only (910K chars total). Download the full file or copy to clipboard to get everything.
Repository: errorcode26/test
Branch: main
Commit: f7944e60f6a3
Files: 65
Total size: 874.1 KB

Directory structure:
gitextract_tb8zvos5/

├── IndiaLIMS.spec
├── LIMS.spec
├── README.md
├── app.py
├── build_exe.py
├── clean_db.py
├── config.py
├── core/
│   ├── __init__.py
│   ├── database.py
│   ├── logic.py
│   └── security.py
├── gis_processor.py
├── inno_setup.iss
├── render.yaml
├── report_generator.py
├── requirements-dev.txt
├── requirements-windows.txt
├── requirements.txt
├── routes/
│   ├── __init__.py
│   ├── auth.py
│   ├── documents.py
│   ├── feedback.py
│   ├── gis.py
│   ├── pages.py
│   ├── records.py
│   ├── users.py
│   └── utils.py
├── run_server.py
├── scripts/
│   ├── inspect_recovery.py
│   ├── recover_superadmin_auto.py
│   └── recovery_credentials_2026-04-25T050759_400298.txt
├── static/
│   ├── css/
│   │   └── style.css
│   ├── data/
│   │   └── india-boundary.geojson
│   └── js/
│       ├── api.js
│       ├── auth.js
│       ├── map.js
│       ├── map_OLD_BAK.js
│       └── modules/
│           ├── admin.js
│           ├── dashboard.js
│           ├── forms.js
│           ├── gis.js
│           ├── map_engine.js
│           ├── records.js
│           ├── state.js
│           └── utils.js
├── templates/
│   ├── admin_dashboard.html
│   ├── login.html
│   └── public_viewer_v2.html
├── tests/
│   ├── Testing_Report_20260425_211328.md
│   ├── Testing_Report_20260426_013232.md
│   ├── Testing_Report_20260426_095300.md
│   ├── Testing_Report_20260426_125456.md
│   ├── Testing_Report_20260426_132557.md
│   ├── Testing_Report_20260426_132640.md
│   ├── check_users_roles.py
│   ├── generate_test_report.py
│   ├── robust_role_test.py
│   ├── test_advanced_gis.py
│   ├── test_logins.py
│   ├── test_output/
│   │   └── Village_Ledger_Amingaon.xlsx
│   ├── test_record_soft_delete.py
│   ├── test_recovery_flow.py
│   ├── test_report_gen.py
│   └── test_restore_flow.py
└── utils.py

================================================
FILE CONTENTS
================================================

================================================
FILE: IndiaLIMS.spec
================================================
# -*- mode: python ; coding: utf-8 -*-


a = Analysis(
    ['c:\\Users\\user\\Desktop\\zz\\app.py'],
    pathex=[],
    binaries=[],
    datas=[('c:\\Users\\user\\Desktop\\zz\\templates', 'templates'), ('c:\\Users\\user\\Desktop\\zz\\static', 'static'), ('c:\\Users\\user\\Desktop\\zz\\.env', '.')],
    hiddenimports=['flask', 'werkzeug', 'werkzeug.security', 'jinja2', 'shapely', 'shapely.geometry', 'shapely.validation', 'fpdf', 'pandas', 'openpyxl', 'qrcode', 'webview', 'pymongo', 'dnspython', 'certifi', 'dotenv'],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=[],
    noarchive=False,
    optimize=0,
)
pyz = PYZ(a.pure)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.datas,
    [],
    name='IndiaLIMS',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    upx_exclude=[],
    runtime_tmpdir=None,
    console=False,
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
    icon=['c:\\Users\\user\\Desktop\\zz\\logo.ico'],
)


================================================
FILE: LIMS.spec
================================================
# -*- mode: python ; coding: utf-8 -*-


a = Analysis(
    ['c:\\Users\\user\\Desktop\\zz\\app.py'],
    pathex=[],
    binaries=[],
    datas=[('c:\\Users\\user\\Desktop\\zz\\templates', 'templates'), ('c:\\Users\\user\\Desktop\\zz\\static', 'static'), ('c:\\Users\\user\\Desktop\\zz\\.env', '.')],
    hiddenimports=['flask', 'werkzeug', 'werkzeug.security', 'jinja2', 'shapely', 'shapely.geometry', 'shapely.validation', 'fpdf', 'pandas', 'openpyxl', 'qrcode', 'webview', 'pymongo', 'dnspython', 'certifi', 'dotenv'],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=[],
    noarchive=False,
    optimize=0,
)
pyz = PYZ(a.pure)

exe = EXE(
    pyz,
    a.scripts,
    a.binaries,
    a.datas,
    [],
    name='LIMS',
    debug=False,
    bootloader_ignore_signals=False,
    strip=False,
    upx=True,
    upx_exclude=[],
    runtime_tmpdir=None,
    console=False,
    disable_windowed_traceback=False,
    argv_emulation=False,
    target_arch=None,
    codesign_identity=None,
    entitlements_file=None,
    icon=['c:\\Users\\user\\Desktop\\zz\\logo.ico'],
)


================================================
FILE: README.md
================================================
nothing


================================================
FILE: app.py
================================================
import os
import sys
import time
import socket
import threading
import uuid
from datetime import datetime, timedelta
import json
from flask import Flask, jsonify
from werkzeug.security import generate_password_hash

from config import SECRET_KEY, DEBUG, HOST, PORT, MONGO_URI
from routes import (
    pages_bp, auth_bp, records_bp, users_bp, 
    gis_bp, documents_bp, feedback_bp, utils_bp
)
from core import DATA_DIR, load_users, save_users, users_collection

def create_app():
    app = Flask(__name__)
    app.secret_key = SECRET_KEY
    app.permanent_session_lifetime = timedelta(days=30)

    # Register Blueprints
    app.register_blueprint(pages_bp)
    app.register_blueprint(auth_bp)
    app.register_blueprint(records_bp)
    app.register_blueprint(users_bp)
    app.register_blueprint(gis_bp)
    app.register_blueprint(documents_bp)
    app.register_blueprint(feedback_bp)
    app.register_blueprint(utils_bp)

    @app.route('/ping', methods=['GET'])
    def ping():
        return jsonify({"status": "alive"}), 200

    @app.after_request
    def add_header(response):
        response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
        response.headers['Pragma'] = 'no-cache'
        response.headers['Expires'] = '0'
        return response

    return app

app = create_app()

def bootstrap_admin():
    """Bootstrap default admin if no superadmin exists with the standard ID."""
    try:
        if users_collection is not None:
            # Check for the specific bootstrap ID to ensure test consistency
            bootstrap_user = users_collection.find_one({"user_id": "bootstrap-admin-01"})
            if not bootstrap_user:
                default_admin_user = os.environ.get("DEFAULT_ADMIN_USER", "admin")
                default_admin_password = os.environ.get("DEFAULT_ADMIN_PASSWORD", "password123")
                users = load_users()
                new_sa = {
                    "user_id": "bootstrap-admin-01",
                    "username": default_admin_user,
                    "password_hash": generate_password_hash(default_admin_password),
                    "role": "superadmin",
                    "full_name": "System Administrator",
                    "email": "admin@indialims.edu",
                    "phone": "+91-0000000000",
                    "designation": "System Administrator",
                    "department": "Land Records",
                    "office_location": "System Default",
                    "is_active": True,
                    "is_recovery": True, # Grant recovery rights for testing
                    "created_at": datetime.now().isoformat() + "Z",
                    "last_login": datetime.now().isoformat() + "Z"
                }
                # If 'admin' username is taken by a non-bootstrap user, use a unique one
                if any(u.get('username') == default_admin_user for u in users):
                    new_sa['username'] = f"admin_root_{uuid.uuid4().hex[:4]}"
                
                users.append(new_sa)
                save_users(users)
                print(f"[BOOTSTRAP] System Administrator '{new_sa['username']}' created.")
    except Exception as e:
        print(f"Bootstrap error: {e}")

    # If the users collection is empty or missing expected test accounts, seed from user_id_password.json
    try:
        existing = load_users()
        if not existing:
            cred_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'user_id_password.json')
            if os.path.exists(cred_file):
                with open(cred_file, 'r', encoding='utf-8') as f:
                    try:
                        creds = json.load(f)
                    except Exception:
                        creds = []

                seeded = []
                for idx, c in enumerate(creds):
                    uname = c.get('username') or c.get('user_id')
                    pwd = c.get('password')
                    if not uname or not pwd:
                        continue
                    user_id = f"user-{uuid.uuid4().hex[:8]}"
                    role = 'admin'
                    is_recovery = False
                    if idx == 0 or uname == os.environ.get('DEFAULT_ADMIN_USER', 'admin'):
                        role = 'superadmin'
                        user_id = 'bootstrap-admin-01'
                    if isinstance(uname, str) and uname.startswith('recovery_sa_'):
                        is_recovery = True
                        role = 'superadmin'

                    seeded_user = {
                        'user_id': user_id,
                        'username': uname,
                        'password_hash': generate_password_hash(pwd),
                        'role': role,
                        'is_recovery': is_recovery,
                        'created_at': datetime.now().isoformat() + 'Z'
                    }
                    seeded.append(seeded_user)

                if seeded:
                    save_users(seeded)
                    print(f"[BOOTSTRAP] Seeded {len(seeded)} user(s) from {cred_file}")
    except Exception:
        pass

# Ensure a bootstrap superadmin exists when the app is imported (useful for tests)
try:
    bootstrap_admin()
except Exception:
    # Non-fatal: if DB isn't reachable at import time, tests or runtime will still try again when running __main__
    pass

if __name__ == "__main__":
    # Redirect logs safely
    log_path = os.path.join(os.environ.get('APPDATA', os.path.dirname(os.path.abspath(__file__))), "LIMS.log")
    if sys.stdout is None or getattr(sys.stdout, "closed", True):
        sys.stdout = open(log_path, "w", encoding="utf-8")
    if sys.stderr is None or getattr(sys.stderr, "closed", True):
        sys.stderr = open(log_path, "a", encoding="utf-8")

    os.makedirs(DATA_DIR, exist_ok=True)
    bootstrap_admin()

    print("\n" + "=" * 60)
    print("  LIMS - Modular Version")
    print(f"  Server starting on http://{HOST}:{PORT}")
    print("=" * 60 + "\n")

    try:
        import webview
        def get_free_port(start_port):
            for port in range(start_port, 65535):
                with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
                    try:
                        s.bind((HOST, port))
                        return port
                    except OSError: continue
            return start_port

        actual_port = get_free_port(PORT)
        def start_server():
            app.run(host=HOST, port=actual_port, debug=False, use_reloader=False)

        threading.Thread(target=start_server, daemon=True).start()
        time.sleep(2)
        display_host = "127.0.0.1" if HOST == "0.0.0.0" else HOST
        webview.settings['ALLOW_DOWNLOADS'] = True
        webview.create_window("LIMS", url=f"http://{display_host}:{actual_port}/login", width=1400, height=900)
        webview.start()
    except ImportError:
        app.run(host=HOST, port=PORT, debug=DEBUG)


================================================
FILE: build_exe.py
================================================
"""
build_exe.py - PyInstaller & PyWebView Compilation Script for LIMS
Compiles the application into a standalone Windows .exe using PyWebView
as the GUI wrapper and PyInstaller for bundling.

Usage:
    python build_exe.py          # Build the .exe
    python build_exe.py --run    # Run in PyWebView window (for testing)
"""

import os
import sys
import subprocess
import threading
import time

from utils import resource_path


def start_flask():
    """Start the Flask server in a background thread."""
    from app import app
    app.run(host="127.0.0.1", port=5000, debug=False, use_reloader=False)


def run_pywebview():
    """Run the application in a PyWebView window (for testing)."""
    try:
        import webview
    except ImportError:
        print("ERROR: pywebview is not installed. Install with: pip install pywebview")
        sys.exit(1)

    # Start Flask in background thread
    flask_thread = threading.Thread(target=start_flask, daemon=True)
    flask_thread.start()

    # Wait for Flask to be ready
    print("Waiting for Flask server to start...")
    time.sleep(2)

    # Enable downloads in PyWebView
    import webview
    webview.settings['ALLOW_DOWNLOADS'] = True

    # Create PyWebView window
    window = webview.create_window(
        title="LIMS - Land Information Management System",
        url="http://127.0.0.1:5000/login",
        width=1400,
        height=900,
        min_size=(1024, 700),
        resizable=True,
        frameless=False,
        easy_drag=True
    )

    webview.start(debug=False)
    print("Application closed.")


def build_exe():
    """Build the standalone .exe using PyInstaller."""
    try:
        import PyInstaller  # noqa: F401
    except ImportError:
        print("ERROR: PyInstaller is not installed. Install with: pip install pyinstaller")
        sys.exit(1)

    project_dir = os.path.dirname(os.path.abspath(__file__))

    # PyInstaller command
    pyinstaller_args = [
        sys.executable,
        '-m',
        'PyInstaller',
        '--name=LIMS',
        '--onefile',
        '--windowed',  # No console window on Windows
        '--clean',
    ]

    # Add icon if available
    icon_path = os.path.join(project_dir, 'logo.ico')
    if os.path.exists(icon_path):
        pyinstaller_args.append('--icon=' + icon_path)
        print('Using icon: ' + icon_path)
    else:
        print('Warning: Icon file not found at ' + icon_path + ". Please place a 'logo.ico' in the project root to include an icon.")

    pyinstaller_args.extend([
        # Add data files (templates, static)
        '--add-data=' + os.path.join(project_dir, 'templates') + os.pathsep + 'templates',
        '--add-data=' + os.path.join(project_dir, 'static') + os.pathsep + 'static',
        '--add-data=' + os.path.join(project_dir, '.env') + os.pathsep + '.',

        # Hidden imports that PyInstaller might miss
        '--hidden-import=flask',
        '--hidden-import=werkzeug',
        '--hidden-import=werkzeug.security',
        '--hidden-import=jinja2',
        '--hidden-import=shapely',
        '--hidden-import=shapely.geometry',
        '--hidden-import=shapely.validation',
        '--hidden-import=fpdf',
        '--hidden-import=pandas',
        '--hidden-import=openpyxl',
        '--hidden-import=qrcode',
        '--hidden-import=webview',
        '--hidden-import=pymongo',
        '--hidden-import=dnspython',
        '--hidden-import=certifi',
        '--hidden-import=dotenv',

        # Main entry point
        os.path.join(project_dir, 'app.py')
    ])

    print('=' * 60)
    print('  Building LIMS .exe with PyInstaller')
    print('=' * 60)
    print('\nCommand: ' + ' '.join(pyinstaller_args) + '\n')

    result = subprocess.run(pyinstaller_args, cwd=project_dir)

    if result.returncode == 0:
        print('\n' + '=' * 60)
        print('  BUILD SUCCESSFUL!')
        print('  Executable: ' + os.path.join(project_dir, 'dist', 'LIMS.exe'))
        print('=' * 60)
    else:
        print('\n' + '=' * 60)
        print('  BUILD FAILED!')
        print('=' * 60)
        sys.exit(1)


if __name__ == '__main__':
    if len(sys.argv) > 1 and sys.argv[1] == '--run':
        # Run in PyWebView window for testing
        run_pywebview()
    else:
        # Build the .exe
        build_exe()


================================================
FILE: clean_db.py
================================================
from config import MONGO_URI
from pymongo import MongoClient
import certifi
db = MongoClient(MONGO_URI, tlsCAFile=certifi.where()).get_database('indialims')
db.records.delete_many({})
db.users.delete_many({"user_id": {"$ne": "bootstrap-admin-01"}})
print('Cleaned')

================================================
FILE: config.py
================================================
"""
config.py - Application Configuration for India LIMS
Centralized settings for the application.
"""

import os
from dotenv import load_dotenv
from utils import resource_path

# Load variables from .env file
env_path = resource_path(".env")
load_dotenv(env_path)

# --- Application Settings ---
# Use environment variable, or persist a generated key to file so sessions
# survive server restarts.
def _get_or_create_secret_key():
    env_key = os.environ.get("LIMS_SECRET_KEY")
    if env_key:
        return env_key
    key_file = os.path.join(BASE_DIR, ".secret_key")
    if os.path.exists(key_file):
        with open(key_file, "r") as f:
            return f.read().strip()
    new_key = os.urandom(32).hex()
    with open(key_file, "w") as f:
        f.write(new_key)
    return new_key

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
SECRET_KEY = _get_or_create_secret_key()
DEBUG = os.environ.get("LIMS_DEBUG", "false").lower() == "true"

# Render provides 'PORT' env var, standard local uses 'LIMS_PORT'
PORT = int(os.environ.get("PORT", os.environ.get("LIMS_PORT", 5000)))

# On Render/Production, bind to all interfaces; locally use 127.0.0.1 for safety
RENDER = os.environ.get("RENDER")
HOST = os.environ.get("LIMS_HOST", "0.0.0.0" if RENDER else "127.0.0.1")

# --- Database Configuration ---
# The MongoDB Atlas connection string
MONGO_URI = os.environ.get("MONGO_URI")

if not MONGO_URI:
    print("WARNING: MONGO_URI is not set in the environment or .env file.")

# --- Data Paths (relative to project root) ---
# Note: app.py overrides these for PyInstaller using resource_path()
DATA_DIR = os.path.join(BASE_DIR, "data")
RECORDS_FILE = os.path.join(DATA_DIR, "records.json")
USERS_FILE = os.path.join(DATA_DIR, "users.json")
FEEDBACK_FILE = os.path.join(DATA_DIR, "feedback.json")

# --- Land Use Configuration ---
LAND_USE_OPTIONS = [
    "Agricultural",
    "Residential",
    "Commercial",
    "Industrial",
    "Government",
    "Forest",
    "Wasteland"
]

# --- Land Use Color Map ---
LAND_USE_COLORS = {
    "Agricultural": "#22c55e",
    "Residential": "#3b82f6",
    "Commercial": "#f59e0b",
    "Industrial": "#8b5cf6",
    "Government": "#ef4444",
    "Forest": "#065f46",
    "Wasteland": "#9ca3af"
}

# --- Mutation Types ---
MUTATION_TYPES = [
    "Sale Deed",
    "Inheritance",
    "Gift Deed",
    "Partition",
    "Court Order"
]

# --- Pagination ---
DEFAULT_PAGE_SIZE = 50
MAX_PAGE_SIZE = 200

# --- Dashboard Limits ---
DASHBOARD_TOP_DISTRICTS = 6
DASHBOARD_RECENT_MUTATIONS = 6


================================================
FILE: core/__init__.py
================================================
from .database import (
    load_users, save_users, load_records, save_records, 
    load_feedback, save_feedback, _log_audit, audit_collection, 
    users_collection, records_collection, feedback_collection, DATA_DIR
)
from .security import (
    admin_required, viewer_or_admin_required, role_required, 
    generate_captcha, verify_captcha_logic
)
from .logic import (
    _mask_owner_for_viewer, _strip_b64_from_list, 
    _apply_filters_to_records, _generate_ulpin, _update_nested,
    _calculate_estimated_value
)


================================================
FILE: core/database.py
================================================
import os
import json
import uuid
from datetime import datetime
from pymongo import MongoClient, ReplaceOne
import certifi

from config import MONGO_URI
from utils import resource_path

# Override data paths for PyInstaller
DATA_DIR = resource_path("data")
RECORDS_FILE = os.path.join(DATA_DIR, "records.json")
USERS_FILE = os.path.join(DATA_DIR, "users.json")
FEEDBACK_FILE = os.path.join(DATA_DIR, "feedback.json")

# Database Setup (MongoDB)
try:
    mongo_client = MongoClient(MONGO_URI, tlsCAFile=certifi.where(), serverSelectionTimeoutMS=5000)
    db = mongo_client.get_database("indialims")
    users_collection = db.users
    records_collection = db.records
    feedback_collection = db.feedback
    audit_collection = db.audit
    print("Successfully connected to MongoDB Cluster.")
except Exception as e:
    print(f"MongoDB connection error: {e}")
    mongo_client = None
    db = None
    users_collection = None
    records_collection = None
    feedback_collection = None
    audit_collection = None

# --- Data Access Helpers ---

def load_users():
    if users_collection is None: return []
    return list(users_collection.find({}, {"_id": 0}))

def save_users(users):
    if users_collection is None: return
    if not users:
        users_collection.delete_many({})
        return
    requests = [ReplaceOne({"user_id": u["user_id"]}, u, upsert=True) for u in users]
    users_collection.bulk_write(requests)
    users_collection.delete_many({"user_id": {"$nin": [u["user_id"] for u in users]}})

def load_records():
    if records_collection is None: return []
    records = list(records_collection.find({}))
    for r in records:
        if "_id" in r:
            r["_id"] = str(r["_id"])
    return records

def save_records(records):
    if records_collection is None: return
    if not records:
        records_collection.delete_many({})
        return
    requests = [ReplaceOne({"_id": r["_id"]}, r, upsert=True) for r in records]
    records_collection.bulk_write(requests)
    records_collection.delete_many({"_id": {"$nin": [r["_id"] for r in records]}})

def load_feedback():
    if feedback_collection is None: return []
    return list(feedback_collection.find({}, {"_id": 0}))

def save_feedback(feedback_data):
    if feedback_collection is None: return
    if not feedback_data:
        feedback_collection.delete_many({})
        return
    requests = [ReplaceOne({"id": f["id"]}, f, upsert=True) for f in feedback_data]
    feedback_collection.bulk_write(requests)
    feedback_collection.delete_many({"id": {"$nin": [f["id"] for f in feedback_data]}})

def _log_audit(action, performed_by, record_id=None, details=None):
    """Write a simple audit entry to the audit collection/file."""
    try:
        entry = {
            "action": action,
            "performed_by": performed_by,
            "record_id": record_id,
            "details": details or {},
            "timestamp": datetime.now().isoformat() + "Z"
        }
        if audit_collection is not None:
            audit_collection.insert_one(entry)
        else:
            audit_file = os.path.join(DATA_DIR, "audit.json")
            try:
                if os.path.exists(audit_file):
                    with open(audit_file, "r", encoding="utf-8") as f:
                        existing = json.load(f)
                else:
                    existing = []
            except Exception:
                existing = []
            existing.append(entry)
            with open(audit_file, "w", encoding="utf-8") as f:
                json.dump(existing, f, indent=2, ensure_ascii=False)
    except Exception:
        pass

def get_audit_collection():
    return audit_collection

def get_data_dir():
    return DATA_DIR


================================================
FILE: core/logic.py
================================================
import random

def _mask_owner_for_viewer(record):
    if "owner" not in record:
        return record
    record = record.copy()
    record["owner"] = record["owner"].copy()
    record["owner"].pop("aadhaar", None)
    record["owner"].pop("phone", None)
    name = record["owner"].get("name", "")
    parts = name.split()
    if len(parts) > 1:
        record["owner"]["name"] = parts[0] + " " + parts[1][0] + "."
    record["owner"].pop("proof_doc_b64", None)
    for mut in record.get("mutation_history", []):
        mut.pop("proof_doc_b64", None)
    return record

def _strip_b64_from_list(records):
    clean_records = []
    for r in records:
        r_copy = r.copy()
        if "owner" in r_copy:
            r_copy["owner"] = r_copy["owner"].copy()
            r_copy["owner"].pop("proof_doc_b64", None)
        if "mutation_history" in r_copy:
            r_copy["mutation_history"] = [m.copy() for m in r_copy["mutation_history"]]
            for m in r_copy["mutation_history"]:
                m.pop("proof_doc_b64", None)
        clean_records.append(r_copy)
    return clean_records

def _apply_filters_to_records(records, params):
    state = (params.get("state") or "").strip().lower()
    district = (params.get("district") or "").strip().lower()
    village = (params.get("village") or "").strip().lower()
    land_use = (params.get("land_use") or "").strip()
    search = (params.get("search") or "").strip().lower()
    if not any([state, district, village, land_use, search]):
        return records
    filtered = []
    for rec in records:
        loc = rec.get("location", {})
        attrs = rec.get("attributes", {})
        if state and loc.get("state", "").lower() != state: continue
        if district and loc.get("district", "").lower() != district: continue
        if village and loc.get("village", "").lower() != village: continue
        if land_use and attrs.get("land_use") != land_use: continue
        if search:
            search_text = " ".join([
                rec.get("khasra_no", ""),
                rec.get("ulpin", ""),
                loc.get("village", ""),
                loc.get("district", ""),
                rec.get("owner", {}).get("name", "")
            ]).lower()
            if search not in search_text: continue
        filtered.append(rec)
    return filtered

def _generate_ulpin():
    return str(random.randint(10000000000000, 99999999999999))

def _calculate_estimated_value(area_ha, circle_rate_inr, land_use):
    """
    Calculate estimated value using a land-use multiplier for realism.
    """
    multipliers = {
        'Commercial': 2.5,
        'Industrial': 1.8,
        'Residential': 1.5,
        'Agricultural': 1.0,
        'Government': 1.2,
        'Forest': 0.8,
        'Wasteland': 0.5
    }
    multiplier = multipliers.get(land_use, 1.0)
    return float(area_ha) * float(circle_rate_inr) * multiplier

def _update_nested(record, parent_key, child_key, value):
    if parent_key not in record:
        record[parent_key] = {}
    record[parent_key][child_key] = value
    return value


================================================
FILE: core/security.py
================================================
import string
import random
from functools import wraps
from flask import session, jsonify
from itsdangerous import URLSafeTimedSerializer
from config import SECRET_KEY

def admin_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        role = (session.get("role") or "").lower()
        if role not in ("admin", "superadmin"):
            return jsonify({"error": "Unauthorized. Admin access required."}), 403
        return f(*args, **kwargs)
    return decorated_function

def viewer_or_admin_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        role = (session.get("role") or "").lower()
        if role not in ("admin", "superadmin", "viewer", "officer"):
            return jsonify({"error": "Unauthorized. Please log in or pass CAPTCHA."}), 401
        return f(*args, **kwargs)
    return decorated_function

def role_required(*allowed_roles):
    allowed = tuple(r.lower() for r in allowed_roles)
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            role = (session.get("role") or "").lower()
            if role not in allowed:
                return jsonify({"error": "Unauthorized. Insufficient role."}), 403
            return f(*args, **kwargs)
        return decorated_function
    return decorator

def generate_captcha():
    chars = string.ascii_letters
    captcha_text = ''.join(random.choice(chars) for _ in range(6))
    serializer = URLSafeTimedSerializer(SECRET_KEY)
    token = serializer.dumps(captcha_text, salt='captcha-salt')
    return captcha_text, token

def verify_captcha_logic(token, user_answer):
    serializer = URLSafeTimedSerializer(SECRET_KEY)
    try:
        expected = serializer.loads(token, salt='captcha-salt', max_age=300)
        return user_answer == expected
    except Exception:
        return False


================================================
FILE: gis_processor.py
================================================
"""
gis_processor.py - Spatial Calculation Module for India LIMS
Uses Shapely for all GIS heavy lifting: area calculation, validation, and spatial operations.
"""

import math
import os
import json
from shapely.geometry import Polygon, mapping, shape
from shapely.validation import make_valid

_india_boundary_shape = None

def get_india_boundary_shape():
    """Load and cache the India boundary GeoJSON. Loads once at startup."""
    global _india_boundary_shape
    if _india_boundary_shape is None:
        try:
            base_dir = os.path.dirname(os.path.abspath(__file__))
            geojson_path = os.path.join(base_dir, 'static', 'data', 'india-boundary.geojson')
            with open(geojson_path, 'r', encoding='utf-8') as f:
                data = json.load(f)
                if 'features' in data and len(data['features']) > 0:
                    _india_boundary_shape = shape(data['features'][0]['geometry'])
                else:
                    _india_boundary_shape = shape(data)
        except Exception as e:
            print(f"Warning: India boundary not loaded: {e}")
    return _india_boundary_shape

# Preload at module import time
try:
    get_india_boundary_shape()
except Exception:
    pass  # Will load on first request if fails at startup


# --- Constants for Unit Conversion ---
# 1 Hectare = 2.47105 Acres
# 1 Hectare = 100.00065 Guntha (standardized)
# 1 Hectare = 107639.104 Sq. Ft.

HECTARE_TO_ACRE = 2.47105
HECTARE_TO_GUNTHA = 100.00065
HECTARE_TO_SQFT = 107639.104

# Bigha varies significantly by state. Using Madhya Pradesh standard:
# 1 Bigha ≈ 0.2529 Hectares in MP
HECTARE_TO_BIGHA_MP = 3.9537

# Assam (Northeast India) Land Units:
# 1 Assam Bigha = 14,400 sq ft = 1,337.804 sq meters ≈ 0.13378 Ha
# 1 Lecha = 144 sq ft = 1/100 Assam Bigha
HECTARE_TO_BIGHA_ASSAM = 7.4752   # 1 Ha = 7.4752 Assam Bigha
HECTARE_TO_LECHA_ASSAM = 747.52   # 1 Ha = 747.52 Lecha

# WGS84 Authalic Radius (equal-area sphere radius for area calculations)
WGS84_AUTHALIC_RADIUS = 6371007.180918  # meters


def calculate_area(geometry_dict):
    """
    Calculate area of a polygon from a GeoJSON geometry dict.
    Returns area in Hectares along with local unit equivalents.

    Uses geodesic area calculation (WGS84 ellipsoid) for high accuracy.
    This is the proper method for land survey calculations.

    Args:
        geometry_dict: GeoJSON geometry object with type 'Polygon' and coordinates.

    Returns:
        dict with area in hectares, acres, guntha, sqft, and bigha.
    """
    try:
        geom = shape(geometry_dict)
        if not geom.is_valid:
            geom = make_valid(geom)

        # Calculate geodesic area in square meters using WGS84 ellipsoid
        area_sq_meters = _geodesic_area_m2(geom)

        # Convert to hectares (1 hectare = 10,000 sq meters)
        area_ha = area_sq_meters / 10000.0

        return {
            "area_ha": round(area_ha, 4),
            "area_acres": round(area_ha * HECTARE_TO_ACRE, 4),
            "area_guntha": round(area_ha * HECTARE_TO_GUNTHA, 2),
            "area_sqft": round(area_ha * HECTARE_TO_SQFT, 2),
            "area_bigha_mp": round(area_ha * HECTARE_TO_BIGHA_MP, 4),
            "area_bigha_assam": round(area_ha * HECTARE_TO_BIGHA_ASSAM, 2),
            "area_lecha_assam": round(area_ha * HECTARE_TO_LECHA_ASSAM, 0),
            "unit": "hectares"
        }
    except Exception as e:
        return {"error": f"Spatial calculation failed: {str(e)}", "area_ha": 0}


def _geodesic_area_m2(geom):
    """
    Calculate the geodesic area of a polygon on a sphere (WGS84 authalic radius).
    Uses the spherical excess formula: Area = R² * |Σ(Δλ * (2 + sin(φ1) + sin(φ2)))| / 2
    
    This algorithm is based on "Some algorithms for polygons on a sphere" 
    by Robert G. Chamberlain and William H. Duquette (NASA JPL).
    
    Accuracy: Within 0.1% for typical land parcels in India.
    For survey-grade accuracy (< 0.01%), use pyproj with proper UTM zones.

    Args:
        geom: A Shapely geometry object (Polygon).

    Returns:
        float: Area in square meters.
    """
    # WGS84 authalic radius (equal-area sphere radius)
    # This ensures area calculations are consistent with the WGS84 ellipsoid
    R = WGS84_AUTHALIC_RADIUS  # 6371007.180918 meters

    coords = list(geom.exterior.coords)

    if len(coords) < 4:
        return 0.0

    # Calculate signed area using the spherical trapezoid method
    # Formula: A = R² * Σ[(λ2 - λ1) * (2 + sin(φ1) + sin(φ2))] / 2
    # where λ = longitude (radians), φ = latitude (radians)
    
    signed_area = 0.0
    
    for i in range(len(coords) - 1):
        lng1, lat1 = coords[i]
        lng2, lat2 = coords[i + 1]
        
        # Convert to radians
        lng1_rad = math.radians(lng1)
        lat1_rad = math.radians(lat1)
        lng2_rad = math.radians(lng2)
        lat2_rad = math.radians(lat2)
        
        # Add contribution from this edge
        signed_area += (lng2_rad - lng1_rad) * (2.0 + math.sin(lat1_rad) + math.sin(lat2_rad))
    
    # Multiply by R²/2 to get area
    area = abs(signed_area) * (R ** 2) / 2.0
    
    # Handle polygons with holes (subtract hole areas)
    for interior in geom.interiors:
        hole_coords = list(interior.coords)
        hole_area = 0.0
        
        for i in range(len(hole_coords) - 1):
            lng1, lat1 = hole_coords[i]
            lng2, lat2 = hole_coords[i + 1]
            
            lng1_rad = math.radians(lng1)
            lat1_rad = math.radians(lat1)
            lng2_rad = math.radians(lng2)
            lat2_rad = math.radians(lat2)
            
            hole_area += (lng2_rad - lng1_rad) * (2.0 + math.sin(lat1_rad) + math.sin(lat2_rad))
        
        area -= abs(hole_area) * (R ** 2) / 2.0
    
    return max(0.0, area)  # Ensure non-negative


def validate_polygon(geometry_dict):
    """
    Validate a GeoJSON polygon geometry.
    Checks for: valid structure, sufficient vertices, no self-intersection,
    and ensures the polygon is within India's bounding box.

    Args:
        geometry_dict: GeoJSON geometry object.

    Returns:
        dict with 'valid' (bool) and 'errors' (list of strings).
    """
    errors = []

    # Check structure
    if not isinstance(geometry_dict, dict):
        return {"valid": False, "errors": ["Geometry must be a dictionary."]}

    if geometry_dict.get("type") != "Polygon":
        errors.append("Geometry type must be 'Polygon'.")

    coords = geometry_dict.get("coordinates", [])
    if not coords or not coords[0]:
        errors.append("Polygon must have at least one ring with coordinates.")
        return {"valid": False, "errors": errors}

    ring = coords[0]

    # Minimum 4 points for a closed polygon (triangle + closing point)
    if len(ring) < 4:
        errors.append(f"Polygon ring must have at least 4 points (got {len(ring)}). A valid polygon requires at least 3 distinct vertices plus the closing point.")

    # Check that ring is closed (first point == last point)
    if ring[0] != ring[-1]:
        errors.append("Polygon ring must be closed (first coordinate must equal last coordinate).")

    # Try to create Shapely geometry and check validity
    try:
        geom = shape(geometry_dict)
        if not geom.is_valid:
            # Get specific reason
            from shapely.validation import explain_validity
            reason = explain_validity(geom)
            errors.append(f"Invalid polygon geometry: {reason}")
        
        # Check against actual official India GeoJSON boundary
        india_shape = get_india_boundary_shape()
        if india_shape is not None:
            # Check if the polygon is strictly within the Indian boundary
            # Using buffer to allow small edge/coastal tolerance (0.01 deg is ~1km)
            if not india_shape.buffer(0.01).contains(geom):
                errors.append("Polygon is located outside the borders of India. Data creation is restricted to Indian territories only.")
                
    except Exception as e:
        errors.append(f"Could not parse geometry or bounds: {str(e)}")
        return {"valid": False, "errors": errors}

    # Fallback/Backward compatibility checking
    # India bbox: lat ~6.5 to ~37.5, lng ~68.0 to ~97.5
    for point in ring:
        lng, lat = point[0], point[1]
        if not (6.5 <= lat <= 37.5 and 68.0 <= lng <= 97.5):
            errors.append(
                f"Coordinate ({lng}, {lat}) is outside India's general boundaries. "
                "All coordinates must be within India (Lat: 6.5-37.5, Lng: 68.0-97.5)."
            )
            break

    return {"valid": len(errors) == 0, "errors": errors}


def check_overlap(new_geometry_dict, existing_records):
    """
    Check if a new polygon overlaps with any existing land record polygons.

    Args:
        new_geometry_dict: GeoJSON geometry for the new parcel.
        existing_records: List of existing record dicts, each with 'geometry' key.

    Returns:
        dict with 'overlaps' (bool) and 'conflicting_records' (list of record IDs).
    """
    try:
        new_geom = shape(new_geometry_dict)
        if not new_geom.is_valid:
            new_geom = make_valid(new_geom)

        conflicting = []
        for record in existing_records:
            try:
                existing_geom = shape(record["geometry"])
                if not existing_geom.is_valid:
                    existing_geom = make_valid(existing_geom)

                if new_geom.intersects(existing_geom):
                    # Check for actual area overlap, not just touching boundaries
                    intersection = new_geom.intersection(existing_geom)
                    if intersection.area > 1e-7:  # Precise threshold for real-world land parcels
                        conflicting.append({
                            "record_id": record.get("_id", "unknown"),
                            "khasra_no": record.get("khasra_no", "unknown"),
                            "overlap_area_ha": round(intersection.area, 6)
                        })
            except Exception:
                continue

        return {
            "overlaps": len(conflicting) > 0,
            "conflicting_records": conflicting
        }
    except Exception as e:
        return {"error": f"Overlap check failed: {str(e)}", "overlaps": False, "conflicting_records": []}


def get_centroid(geometry_dict):
    """
    Calculate the centroid of a polygon.

    Args:
        geometry_dict: GeoJSON geometry object.

    Returns:
        dict with 'lat' and 'lng', or error dict.
    """
    try:
        geom = shape(geometry_dict)
        if not geom.is_valid:
            geom = make_valid(geom)
        centroid = geom.centroid
        return {"lat": round(centroid.y, 6), "lng": round(centroid.x, 6)}
    except Exception as e:
        return {"error": f"Centroid calculation failed: {str(e)}"}


def geojson_to_wkt(geometry_dict):
    """
    Convert a GeoJSON geometry to Well-Known Text (WKT) format.
    Useful for interoperability with other GIS systems.

    Args:
        geometry_dict: GeoJSON geometry object.

    Returns:
        str: WKT representation of the geometry.
    """
    try:
        geom = shape(geometry_dict)
        return geom.wkt
    except Exception as e:
        return f"ERROR: {str(e)}"


def calculate_perimeter(geometry_dict):
    """
    Calculate the perimeter of a polygon in meters.

    Args:
        geometry_dict: GeoJSON geometry object.

    Returns:
        dict with perimeter in meters and kilometers.
    """
    try:
        geom = shape(geometry_dict)
        if not geom.is_valid:
            geom = make_valid(geom)

        def haversine_distance(p1, p2):
            """True spherical distance between two points in meters."""
            lon1, lat1 = math.radians(p1[0]), math.radians(p1[1])
            lon2, lat2 = math.radians(p2[0]), math.radians(p2[1])
            dlon, dlat = lon2 - lon1, lat2 - lat1
            a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2
            c = 2 * math.asin(math.sqrt(a))
            return c * WGS84_AUTHALIC_RADIUS

        coords = list(geom.exterior.coords)
        perimeter_m = 0.0
        for i in range(len(coords) - 1):
            perimeter_m += haversine_distance(coords[i], coords[i + 1])

        # Add perimeters of any holes
        for interior in geom.interiors:
            hole_coords = list(interior.coords)
            for i in range(len(hole_coords) - 1):
                perimeter_m += haversine_distance(hole_coords[i], hole_coords[i + 1])

        return {
            "perimeter_m": round(perimeter_m, 2),
            "perimeter_km": round(perimeter_m / 1000, 4)
        }
    except Exception as e:
        return {"error": f"Perimeter calculation failed: {str(e)}"}


================================================
FILE: inno_setup.iss
================================================
; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!

#define MyAppName "LIMS"
#define MyAppVersion "1.0"
#define MyAppPublisher "Premithiews"
#define MyAppExeName "LIMS.exe"

[Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={{5A1B2C3D-4E5F-6A7B-8C9D-0E1F2A3B4C5D}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
DefaultDirName={autopf}\{#MyAppName}
DisableProgramGroupPage=yes
; Uncomment the following line to run in non administrative install mode (install for current user only.)
;PrivilegesRequired=lowest
OutputDir=installer_output
OutputBaseFilename=LIMS_Setup
Compression=lzma
SolidCompression=yes
WizardStyle=modern

[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"

[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked

[Files]
; The actual executable
Source: "dist\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion

; NOTE: Don't use "Flags: ignoreversion" on any shared system files

[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon

[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent


================================================
FILE: render.yaml
================================================
services:
  - type: web
    name: lims-website
    env: python
    buildCommand: pip install -r requirements.txt
    startCommand: PYTHONPATH=. gunicorn app:app
    envVars:
      - key: PYTHON_VERSION
        value: 3.11
      - key: LIMS_MODE
        value: production


================================================
FILE: report_generator.py
================================================
"""
report_generator.py - Document Generation Module for India LIMS
Generates single-page PDF Property Cards and Excel Village Ledgers.
"""

import os
import io
import json
import uuid
import struct
import base64
import tempfile
from datetime import datetime

from utils import resource_path

try:
    from fpdf import FPDF
except ImportError:
    FPDF = None

try:
    import pandas as pd
except ImportError:
    pd = None

try:
    import qrcode
except ImportError:
    qrcode = None

# ── Unit Conversion Constants ─────────────────────────────────────────────────
HA_TO_ACRE        = 2.47105
HA_TO_BIGHA_ASSAM = 7.4752    # 1 Assam Bigha = 14,400 sq ft
HA_TO_LECHA_ASSAM = 747.52    # 1 Lecha = 1/100 Assam Bigha


def _fmt_inr(value):
    """Format a number as Indian Rupees with commas."""
    try:
        v = int(float(value))
        s = str(v)
        if len(s) > 3:
            last3 = s[-3:]
            rest = s[:-3]
            parts = []
            while len(rest) > 2:
                parts.append(rest[-2:])
                rest = rest[:-2]
            if rest:
                parts.append(rest)
            parts.reverse()
            return ','.join(parts) + ',' + last3
        return s
    except Exception:
        return str(value)


def generate_property_card_pdf(record, map_image_base64=None):
    """
    Generate a clean, single-page A4 PDF Property Card.

    Layout (all on one page):
      • Header with QR code
      • 2-column property info table
      • Prominent map section (90mm)
      • Polygon coordinates table
      • Footer
    """
    if FPDF is None:
        raise ImportError("fpdf2 is required. Install with: pip install fpdf2")

    pdf = PropertyCardPDF()
    pdf.set_auto_page_break(auto=False)   # We handle layout manually
    pdf.add_page()

    # ── Extract Data ─────────────────────────────────────────────────────────
    loc      = record.get("location", {})
    attrs    = record.get("attributes", {})
    owner    = record.get("owner", {})
    mutations = record.get("mutation_history", [])
    geometry = record.get("geometry", {})

    area_ha    = float(attrs.get("area_ha", 0) or 0)
    area_acres = round(area_ha * HA_TO_ACRE, 2)
    area_bigha = round(area_ha * HA_TO_BIGHA_ASSAM, 2)
    area_lecha = int(round(area_ha * HA_TO_LECHA_ASSAM))

    try:
        circle_rate = float(attrs.get("circle_rate_inr", 0) or 0)
        land_use = attrs.get("land_use", "Agricultural")
        
        multipliers = {
            'Commercial': 2.5, 'Industrial': 1.8, 'Residential': 1.5,
            'Agricultural': 1.0, 'Government': 1.2, 'Forest': 0.8, 'Wasteland': 0.5
        }
        multiplier = multipliers.get(land_use, 1.0)
        estimated_value = area_ha * circle_rate * multiplier
    except Exception:
        circle_rate = 0
        estimated_value = 0

    state    = loc.get("state",    "N/A")
    district = loc.get("district", "N/A")
    village  = loc.get("village",  "N/A")

    # ── QR Code (top-right) ──────────────────────────────────────────────────
    qr_path = None
    if qrcode is not None:
        try:
            qr = qrcode.QRCode(version=1, box_size=4, border=1)
            qr.add_data(record.get("ulpin", "N/A"))
            qr.make(fit=True)
            qr_img = qr.make_image(fill_color="black", back_color="white")
            buf = io.BytesIO()
            qr_img.save(buf, format="PNG")
            buf.seek(0)
            qr_path = os.path.join(tempfile.gettempdir(), f"qr_{uuid.uuid4().hex[:8]}.png")
            with open(qr_path, "wb") as f:
                f.write(buf.getvalue())
            pdf.image(qr_path, x=183, y=10, w=18, h=18)
        except Exception:
            pass

    # ── Header ───────────────────────────────────────────────────────────────
    pdf.set_y(10)
    pdf.set_font("Helvetica", "B", 14)
    pdf.cell(170, 7, "LIMS - Property Card (Khasra Patta)", new_x="LMARGIN", new_y="NEXT", align="C")
    pdf.set_font("Helvetica", "", 8)
    pdf.cell(170, 4, "Land Information Management System | Academic Prototype", new_x="LMARGIN", new_y="NEXT", align="C")
    pdf.set_font("Helvetica", "B", 9)
    pdf.cell(170, 5, f"{village}, {district}, {state}", new_x="LMARGIN", new_y="NEXT", align="C")
    pdf.ln(1)

    pdf.set_draw_color(30, 64, 150)
    pdf.set_line_width(0.6)
    y_div = pdf.get_y()
    pdf.line(10, y_div, 200, y_div)
    pdf.ln(3)

    # ── Two-Column Info Table ────────────────────────────────────────────────
    Y_TABLE = pdf.get_y()
    RH = 6.5          # row height mm
    LW = 38           # label cell width
    VW = 54           # value cell width  (total col = 92mm)
    GAP = 6           # gap between cols

    def draw_row(x, y, label, value, fill=True):
        pdf.set_xy(x, y)
        pdf.set_font("Helvetica", "B", 7.5)
        pdf.set_fill_color(230, 237, 255)
        pdf.cell(LW, RH, f"  {label}", border=1, fill=fill)
        pdf.set_font("Helvetica", "", 7.5)
        pdf.set_fill_color(255, 255, 255)
        val_str = str(value)[:42]
        pdf.cell(VW, RH, f"  {val_str}", border=1, fill=True, new_x="RIGHT", new_y="TOP")

    left = [
        ("ULPIN",        record.get("ulpin", "N/A")),
        ("Khasra No.",   record.get("khasra_no", "N/A")),
        ("Khata No.",    record.get("khata_no", "N/A")),
        ("Land Use",     attrs.get("land_use", "N/A")),
        ("State",        state),
        ("District",     district),
        ("Village/Ward", village),
        ("Share (%)",    f"{owner.get('share_pct', 'N/A')}%"),
    ]
    right = [
        ("Area (Ha)",    f"{area_ha} Ha"),
        ("Area (Acres)", f"{area_acres} Ac"),
        ("Area (Bigha)", f"{area_bigha} Bigha"),
        ("Circle Rate",  f"Rs. {_fmt_inr(int(circle_rate))}/Ha" if circle_rate else "N/A"),
        ("Est. Value",   f"Rs. {_fmt_inr(int(estimated_value))}" if estimated_value else "N/A"),
        ("Centroid",     f"{attrs.get('centroid', {}).get('lat', 'N/A')}, {attrs.get('centroid', {}).get('lng', 'N/A')}"),
        ("Perimeter",    f"{int(attrs.get('perimeter_m', 0))} meters"),
        ("Owner",        owner.get("name", "N/A")),
    ]

    for i, (lbl, val) in enumerate(left):
        draw_row(10, Y_TABLE + i * RH, lbl, val)
    for i, (lbl, val) in enumerate(right):
        draw_row(10 + LW + VW + GAP, Y_TABLE + i * RH, lbl, val)

    # Extra row for Mutations (full width below the two columns)
    y_mut = Y_TABLE + max(len(left), len(right)) * RH
    draw_row(10, y_mut, "Mutation History", f"{len(mutations)} transaction(s) recorded on this parcel")
    
    pdf.ln(0)
    y_after_table = y_mut + RH + 3

    # ── Divider ───────────────────────────────────────────────────────────────
    pdf.set_draw_color(30, 64, 150)
    pdf.set_line_width(0.4)
    pdf.line(10, y_after_table, 200, y_after_table)

    # ── Map Section ───────────────────────────────────────────────────────────
    MAP_LABEL_Y = y_after_table + 2
    MAP_Y       = MAP_LABEL_Y + 6
    MAP_H       = 118   # mm — generous height now that coords table is removed

    pdf.set_font("Helvetica", "B", 9)
    pdf.set_xy(10, MAP_LABEL_Y)
    pdf.cell(0, 5, "PARCEL MAP", new_x="LMARGIN", new_y="NEXT")

    if map_image_base64:
        try:
            if "," in map_image_base64:
                map_image_base64 = map_image_base64.split(",")[1]
            img_data = base64.b64decode(map_image_base64)

            tmp_map = os.path.join(tempfile.gettempdir(), f"map_{record.get('ulpin','x')}_{uuid.uuid4().hex[:6]}.png")
            with open(tmp_map, "wb") as f:
                f.write(img_data)

            # Read actual PNG dimensions for proportional placement
            img_w, img_h = 800, 450  # fallback defaults
            try:
                with open(tmp_map, "rb") as f:
                    f.read(8)
                    chunk = f.read(17)
                    if len(chunk) == 17:
                        img_w = struct.unpack(">I", chunk[8:12])[0]
                        img_h = struct.unpack(">I", chunk[12:16])[0]
            except Exception:
                pass

            # Scale to fill 190mm width, but cap at MAP_H height
            max_w = 190.0
            scale = min(max_w / img_w, MAP_H / (img_h * (210 / img_w)) if img_w else 1)
            draw_w = min(max_w, img_w * (max_w / img_w))
            draw_h = img_h * (draw_w / img_w)
            if draw_h > MAP_H:
                draw_h = MAP_H
                draw_w = img_w * (draw_h / img_h)

            x_pos = (210 - draw_w) / 2
            pdf.image(tmp_map, x=x_pos, y=MAP_Y, w=draw_w, h=draw_h)

            try:
                os.remove(tmp_map)
            except Exception:
                pass

        except Exception as e:
            pdf.set_font("Helvetica", "I", 9)
            pdf.set_xy(10, MAP_Y + 5)
            pdf.cell(0, 6, f"Map not available: {str(e)[:60]}", new_x="LMARGIN", new_y="NEXT")
    else:
        pdf.set_font("Helvetica", "I", 9)
        pdf.set_xy(10, MAP_Y + 5)
        pdf.cell(0, 6, "Map image not captured.", new_x="LMARGIN", new_y="NEXT")

    y_after_map = MAP_Y + MAP_H + 3

    # ── Page 2: Mutation Ledger (History) ──────────────────────────────────
    if mutations:
        pdf.add_page()
        pdf.set_y(15)
        pdf.set_font("Helvetica", "B", 12)
        pdf.cell(0, 7, "Ownership Mutation Ledger (History of Transactions)", new_x="LMARGIN", new_y="NEXT", align="C")
        pdf.set_font("Helvetica", "I", 8)
        pdf.cell(0, 4, f"Detailed record of ownership changes for ULPIN: {record.get('ulpin', 'N/A')}", new_x="LMARGIN", new_y="NEXT", align="C")
        pdf.ln(5)

        # Table Header
        pdf.set_font("Helvetica", "B", 8)
        pdf.set_fill_color(240, 240, 240)
        col_widths = [35, 60, 30, 30, 35]
        headers = ["Date", "Previous Owner", "Type", "Share", "Reference"]
        for i, h in enumerate(headers):
            pdf.cell(col_widths[i], 8, h, border=1, fill=True, align="C")
        pdf.ln(8)

        # Table Rows
        pdf.set_font("Helvetica", "", 7.5)
        for mut in mutations:
            pdf.cell(col_widths[0], 7, f" {mut.get('mutation_date', 'N/A')}", border=1)
            pdf.cell(col_widths[1], 7, f" {mut.get('previous_owner', 'N/A')[:38]}", border=1)
            pdf.cell(col_widths[2], 7, f" {mut.get('mutation_type', 'Sale Deed')}", border=1)
            pdf.cell(col_widths[3], 7, f" {mut.get('previous_share_pct', 100)}%", border=1, align="C")
            pdf.cell(col_widths[4], 7, f" {mut.get('mutation_ref', 'N/A')[:22]}", border=1)
            pdf.ln(7)
            
        pdf.ln(10)
        pdf.set_font("Helvetica", "I", 8)
        pdf.multi_cell(0, 5, "Note: This ledger is an archived record of past ownership. The first page of this document reflects the current state of the land record as per the digital database.")


    # ── Footer ───────────────────────────────────────────────────────────────
    pdf.set_y(281)   # 297 - 16mm from bottom
    pdf.set_draw_color(30, 64, 150)
    pdf.set_line_width(0.3)
    pdf.line(10, pdf.get_y(), 200, pdf.get_y())
    pdf.ln(2)
    gen_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    pdf.set_font("Helvetica", "I", 7)
    doc_id = f"PC-{record.get('ulpin','N/A')}-{datetime.now().strftime('%Y%m%d%H%M')}"
    pdf.cell(0, 4, f"Generated: {gen_time}  |  Document ID: {doc_id}  |  LIMS Academic Prototype", new_x="LMARGIN", new_y="NEXT", align="C")
    pdf.set_font("Helvetica", "B", 7)
    pdf.cell(0, 4, "Computer-generated document. Scan QR code for digital verification.", new_x="LMARGIN", new_y="NEXT", align="C")

    # Cleanup QR
    if qr_path:
        try:
            os.remove(qr_path)
        except Exception:
            pass

    # ── Output ────────────────────────────────────────────────────────────────
    try:
        return bytes(pdf.output())
    except Exception:
        output = pdf.output()
        if isinstance(output, (bytes, bytearray)):
            return bytes(output)
        return output.encode("latin-1") if isinstance(output, str) else bytes(output)


class PropertyCardPDF(FPDF):
    """Custom FPDF with thin blue top border."""

    def header(self):
        self.set_draw_color(30, 64, 150)
        self.set_line_width(1.2)
        self.line(5, 5, 205, 5)
        self.set_line_width(0.3)
        self.set_draw_color(0, 0, 0)

    def footer(self):
        pass   # Footer handled manually above


def generate_village_excel(records, village_name="All Villages"):
    """
    Generate a formatted Excel village ledger from land records.
    Includes Bigha and Lecha columns.
    """
    if pd is None:
        raise ImportError("pandas and openpyxl are required.")

    flat_rows = []
    for rec in records:
        loc   = rec.get("location", {})
        attrs = rec.get("attributes", {})
        owner = rec.get("owner", {})
        muts  = rec.get("mutation_history", [])
        last_mut = muts[-1] if muts else {}

        area_ha = float(attrs.get("area_ha", 0) or 0)
        circle_rate = float(attrs.get("circle_rate_inr", 0) or 0)
        land_use = attrs.get("land_use", "Agricultural")
        
        multipliers = {
            'Commercial': 2.5, 'Industrial': 1.8, 'Residential': 1.5,
            'Agricultural': 1.0, 'Government': 1.2, 'Forest': 0.8, 'Wasteland': 0.5
        }
        multiplier = multipliers.get(land_use, 1.0)
        estimated_value = area_ha * circle_rate * multiplier

        flat_rows.append({
            "ULPIN":                   rec.get("ulpin", ""),
            "Khasra No.":              rec.get("khasra_no", ""),
            "Khata No.":               rec.get("khata_no", ""),
            "State":                   loc.get("state", ""),
            "District":                loc.get("district", ""),
            "Village":                 loc.get("village", ""),
            "Area (Ha)":               area_ha,
            "Area (Bigha - Assam)":    round(area_ha * HA_TO_BIGHA_ASSAM, 2),
            "Area (Lecha - Assam)":    int(round(area_ha * HA_TO_LECHA_ASSAM)),
            "Area (Acres)":            round(area_ha * HA_TO_ACRE, 2),
            "Land Use":                attrs.get("land_use", ""),
            "Circle Rate (INR/Ha)":    circle_rate,
            "Multiplier":              multiplier,
            "Estimated Value (INR)":   estimated_value,
            "Owner Name":              owner.get("name", ""),
            "Share (%)":               owner.get("share_pct", 0),
            "Aadhaar (Masked)":        owner.get("aadhaar_mask", ""),
            "Total Mutations":         len(muts),
            "Last Mutation Date":      last_mut.get("mutation_date", ""),
            "Last Mutation Type":      last_mut.get("mutation_type", ""),
            "Record ID":               rec.get("_id", ""),
        })

    df = pd.DataFrame(flat_rows)
    output = io.BytesIO()
    with pd.ExcelWriter(output, engine="openpyxl") as writer:
        df.to_excel(writer, index=False, sheet_name="Village Ledger")
        try:
            ws = writer.sheets["Village Ledger"]
            for idx, col in enumerate(df.columns):
                max_len = max(
                    df[col].astype(str).map(len).max() if len(df) > 0 else 0,
                    len(col)
                )
                col_letter = chr(65 + idx) if idx < 26 else chr(64 + idx // 26) + chr(65 + idx % 26)
                ws.column_dimensions[col_letter].width = min(max_len + 3, 40)
        except Exception:
            pass

    output.seek(0)
    return output.getvalue()


if __name__ == "__main__":
    sample = {
        "_id": "test-001",
        "ulpin": "18011010001001",
        "khasra_no": "42/B",
        "khata_no": "KH-07",
        "location": {"state": "Assam", "district": "Kamrup Metropolitan", "village": "Guwahati Ward 12"},
        "attributes": {"area_ha": 1.34, "land_use": "Agricultural", "circle_rate_inr": 85000},
        "owner": {"name": "Ramesh Kumar", "share_pct": 100, "aadhaar_mask": "XXXX-XXXX-7890"},
        "geometry": {"type": "Polygon", "coordinates": [[[91.76, 26.12], [91.765, 26.12], [91.765, 26.125], [91.76, 26.125], [91.76, 26.12]]]},
        "mutation_history": []
    }
    b = generate_property_card_pdf(sample)
    print(f"PDF generated: {len(b)} bytes")


================================================
FILE: requirements-dev.txt
================================================
pytest>=7.0
flask>=3.0
pymongo>=4.0
certifi
python-dotenv


================================================
FILE: requirements-windows.txt
================================================
# Base Web Requirements
-r requirements.txt

# Windows Desktop Specific
pywebview==4.4.1
pythonnet==3.0.3
pyinstaller==6.3.0
pillow==10.1.0


================================================
FILE: requirements.txt
================================================
Flask==3.0.0
pymongo[srv]==4.6.1
python-dotenv==1.0.0
reportlab==4.0.8
pandas
openpyxl==3.1.2
gunicorn==21.2.0
werkzeug==3.0.1
jinja2==3.1.2
itsdangerous==2.1.2
click==8.1.7
blinker==1.7.0
certifi==2023.11.17
fpdf2==2.7.7
qrcode==7.4.2


================================================
FILE: routes/__init__.py
================================================
from .pages import pages_bp
from .auth import auth_bp
from .records import records_bp
from .users import users_bp
from .gis import gis_bp
from .documents import documents_bp
from .feedback import feedback_bp
from .utils import utils_bp


================================================
FILE: routes/auth.py
================================================
from datetime import datetime
from flask import Blueprint, request, jsonify, session
from werkzeug.security import check_password_hash
from core import generate_captcha, verify_captcha_logic, load_users, save_users

auth_bp = Blueprint('auth', __name__)

@auth_bp.route("/api/captcha", methods=["GET"])
def get_captcha():
    question, token = generate_captcha()
    return jsonify({"question": question, "token": token})

@auth_bp.route("/api/verify-captcha", methods=["POST"])
def verify_captcha():
    data = request.get_json() or {}
    user_answer = str(data.get("answer", "")).strip()
    token = str(data.get("token", "")).strip()

    if verify_captcha_logic(token, user_answer):
        session.permanent = True
        session["role"] = "viewer"
        session["username"] = "Viewer"
        return jsonify({"success": True, "redirect": "/viewer"})
    else:
        new_question, new_token = generate_captcha()
        return jsonify({"success": False, "message": "Incorrect answer or expired. Please try again.", "new_question": new_question, "new_token": new_token}), 400

@auth_bp.route("/api/login", methods=["POST"])
def admin_login():
    data = request.get_json() or {}
    username = data.get("username", "").strip()
    password = data.get("password", "")

    if not username or not password:
        return jsonify({"error": "Username and password are required."}), 400

    users = load_users()
    user = next((u for u in users if u["username"] == username), None)

    if user and check_password_hash(user["password_hash"], password):
        session.permanent = True
        role = (user.get("role") or "Officer").lower()
        session["role"] = role
        session["username"] = username
        session["admin_id"] = user.get("user_id", "")

        user["last_login"] = datetime.now().isoformat() + "Z"
        save_users(users)

        redirect_url = "/admin" if role in ("admin", "superadmin") else "/viewer"
        return jsonify({"success": True, "redirect": redirect_url})
    else:
        return jsonify({"error": "Invalid username or password."}), 401

@auth_bp.route("/api/logout", methods=["POST"])
def logout():
    session.clear()
    return jsonify({"success": True, "redirect": "/login"})

@auth_bp.route("/api/session-info", methods=["GET"])
def session_info():
    return jsonify({
        "role": session.get("role", None),
        "username": session.get("username", None),
        "is_authenticated": session.get("role") is not None
    })

@auth_bp.route("/api/forgot", methods=["GET", "POST"])
def forgot_password():
    """Handle password recovery instructions."""
    return jsonify({
        "success": True, 
        "instructions": "To recover your password, please contact the District Revenue Officer or System Administrator at support@india-lims.gov.in. Provide your Employee ID and Office Location for verification."
    })


================================================
FILE: routes/documents.py
================================================
import io
from datetime import datetime
from flask import Blueprint, request, jsonify, send_file
from core import load_records, viewer_or_admin_required, admin_required

documents_bp = Blueprint('documents', __name__)

@documents_bp.route("/api/print-card/<ulpin>", methods=["GET", "POST"])
@viewer_or_admin_required
def print_property_card(ulpin):
    records = load_records()
    record = next((r for r in records if r.get("ulpin") == ulpin), None)
    if not record: return jsonify({"error": "Not found."}), 404
    map_image = None
    if request.method == "POST":
        map_image = (request.get_json() or {}).get("map_image")
    try:
        from report_generator import generate_property_card_pdf
        pdf_bytes = generate_property_card_pdf(record, map_image_base64=map_image)
        return send_file(io.BytesIO(pdf_bytes), mimetype="application/pdf", as_attachment=True, download_name=f"Card_{ulpin}.pdf")
    except Exception as e:
        return jsonify({"error": str(e)}), 500

@documents_bp.route("/api/export-village", methods=["GET"])
@admin_required
def export_village_ledger():
    records = load_records()
    village = request.args.get("village", "").strip()
    if village:
        records = [r for r in records if r.get("location", {}).get("village", "").lower() == village.lower()]
    if not records: return jsonify({"error": "No records."}), 404
    try:
        from report_generator import generate_village_excel
        excel_bytes = generate_village_excel(records, village_name=village or "All")
        return send_file(io.BytesIO(excel_bytes), mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", as_attachment=True, download_name="Ledger.xlsx")
    except Exception as e:
        return jsonify({"error": str(e)}), 500


================================================
FILE: routes/feedback.py
================================================
import uuid
import os
import json
from datetime import datetime
from flask import Blueprint, request, jsonify, session
from core import (
    load_feedback, save_feedback, load_records, admin_required, 
    viewer_or_admin_required, _apply_filters_to_records, audit_collection, DATA_DIR,
    _calculate_estimated_value
)

feedback_bp = Blueprint('feedback', __name__)

@feedback_bp.route("/api/feedback", methods=["GET"])
@admin_required
def get_feedback():
    """Admin-only: Fetch all feedback submissions."""
    return jsonify(load_feedback())

@feedback_bp.route("/api/feedback", methods=["POST"])
@viewer_or_admin_required
def submit_feedback():
    """Submit feedback or issue reports."""
    data = request.get_json() or {}
    email = data.get("email", "").strip()
    message = data.get("message", "").strip()
    if not email or not message:
        return jsonify({"error": "Required fields missing."}), 400
        
    entry = {
        "id": str(uuid.uuid4()),
        "email": email,
        "type": data.get("type", "General"),
        "message": message,
        "timestamp": datetime.now().isoformat(),
        "status": "New"
    }
    fb = load_feedback()
    fb.append(entry)
    save_feedback(fb)
    return jsonify({"success": True})

@feedback_bp.route("/api/dashboard", methods=["GET"])
@admin_required
def dashboard_analytics():
    """Compute heavy aggregation for the admin dashboard charts and KPIs."""
    records = load_records()
    params = {k: request.args.get(k, "") for k in ["state", "district", "village", "land_use", "search"]}
    filtered = _apply_filters_to_records(records, params)
    
    total_area = 0.0
    total_value = 0.0
    total_mutations = 0
    land_use_stats = {}
    district_stats = {}
    top_parcel = None
    top_parcel_value = -1
    
    for rec in filtered:
        loc = rec.get("location", {})
        attrs = rec.get("attributes", {})
        area = float(attrs.get("area_ha", 0) or 0)
        rate = float(attrs.get("circle_rate_inr", 0) or 0)
        lu = attrs.get("land_use", "Agricultural")
        value = _calculate_estimated_value(area, rate, lu)
        
        total_area += area
        total_value += value
        muts = rec.get("mutation_history", []) or []
        total_mutations += len(muts)
        
        # Land use distribution
        lu = attrs.get("land_use", "Unknown")
        if lu not in land_use_stats: land_use_stats[lu] = {"count": 0, "area": 0.0}
        land_use_stats[lu]["count"] += 1
        land_use_stats[lu]["area"] += area
        
        # District stats
        d = loc.get("district", "Unknown")
        if d not in district_stats: district_stats[d] = {"count": 0, "area": 0.0, "value": 0.0}
        district_stats[d]["count"] += 1
        district_stats[d]["area"] += area
        district_stats[d]["value"] += value
        
        if value > top_parcel_value:
            top_parcel_value = value
            top_parcel = {
                "khasra_no": rec.get("khasra_no"), 
                "ulpin": rec.get("ulpin"),
                "village": loc.get("village"), 
                "district": d, 
                "land_use": attrs.get("land_use", "N/A"),
                "area_ha": round(area, 2),
                "estimated_value": round(value, 0)
            }
            
    sorted_districts = sorted(district_stats.items(), key=lambda x: x[1]["value"], reverse=True)[:6]
    
    all_mutations = []
    for rec in filtered:
        for m in rec.get("mutation_history", []) or []:
            all_mutations.append({
                "khasra_no": rec.get("khasra_no"), 
                "district": rec.get("location", {}).get("district"),
                "previous_owner": m.get("previous_owner"), 
                "mutation_date": m.get("mutation_date"),
                "mutation_type": m.get("mutation_type", "Mutation")
            })
    all_mutations.sort(key=lambda x: str(x.get("mutation_date", "")), reverse=True)
    
    return jsonify({
        "kpis": {
            "total_parcels": len(filtered),
            "total_area": round(total_area, 2),
            "estimated_value": round(total_value, 0),
            "total_mutations": total_mutations
        },
        "land_use_distribution": land_use_stats,
        "district_overview": [{"name": d, **s} for d, s in sorted_districts],
        "top_parcel": top_parcel,
        "recent_mutations": all_mutations[:6]
    })

@feedback_bp.route('/api/audit', methods=['GET'])
@admin_required
def list_audit():
    """Fetch system audit logs."""
    limit = min(100, max(1, int(request.args.get('limit', 50))))
    if audit_collection is not None:
        entries = list(audit_collection.find({}, {'_id': 0}).sort('timestamp', -1).limit(limit))
    else:
        audit_file = os.path.join(DATA_DIR, 'audit.json')
        if os.path.exists(audit_file):
            with open(audit_file, 'r', encoding='utf-8') as f:
                entries = json.load(f)
                entries = sorted(entries, key=lambda x: x.get('timestamp', ''), reverse=True)[:limit]
        else:
            entries = []
    return jsonify(entries)

@feedback_bp.route("/api/feedback/<feedback_id>", methods=["DELETE"])
@admin_required
def delete_feedback(feedback_id):
    fb = load_feedback()
    new_fb = [entry for entry in fb if entry.get("id") != feedback_id]
    if len(new_fb) == len(fb):
        return jsonify({"error": "Feedback not found."}), 404
    save_feedback(new_fb)
    return jsonify({"success": True})

@feedback_bp.route("/api/feedback/<feedback_id>/status", methods=["PUT"])
@admin_required
def update_feedback_status(feedback_id):
    """Mark feedback as reviewed or resolved."""
    data = request.get_json() or {}
    new_status = data.get("status", "Reviewed")
    
    fb = load_feedback()
    entry = next((e for e in fb if e.get("id") == feedback_id), None)
    if not entry:
        return jsonify({"error": "Feedback not found."}), 404
        
    entry["status"] = new_status
    entry["reviewed_at"] = datetime.now().isoformat()
    entry["reviewed_by"] = session.get("username", "admin")
    
    save_feedback(fb)
    return jsonify({"success": True, "status": new_status})


================================================
FILE: routes/gis.py
================================================
import os
import json
from flask import Blueprint, request, jsonify, current_app
from urllib.parse import urlencode
from urllib.request import Request, urlopen
from core import role_required

gis_bp = Blueprint('gis', __name__)

@gis_bp.route("/api/boundary", methods=["GET"])
def get_india_boundary():
    geojson_path = os.path.join(current_app.root_path, "static", "data", "india-boundary.geojson")
    try:
        with open(geojson_path, "r", encoding="utf-8") as f:
            data = json.load(f)
        return jsonify(data)
    except FileNotFoundError:
        return jsonify({"error": "Boundary data not found."}), 404

@gis_bp.route("/api/calculate-area", methods=["POST"])
@role_required("admin", "superadmin", "officer")
def api_calculate_area():
    data = request.get_json() or {}
    geometry = data.get("geometry")
    if not geometry: return jsonify({"error": "Geometry required."}), 400
    from gis_processor import calculate_area, calculate_perimeter, get_centroid
    return jsonify({
        "area": calculate_area(geometry),
        "perimeter": calculate_perimeter(geometry),
        "centroid": get_centroid(geometry)
    })

@gis_bp.route("/api/validate-geometry", methods=["POST"])
@role_required("admin", "superadmin", "officer")
def api_validate_geometry():
    """Validate a GeoJSON polygon geometry."""
    data = request.get_json() or {}
    geometry = data.get("geometry")
    if not geometry: return jsonify({"error": "Geometry is required."}), 400
    from gis_processor import validate_polygon
    return jsonify(validate_polygon(geometry))

@gis_bp.route("/api/location-from-coords", methods=["GET"])
@role_required("admin", "superadmin", "officer")
def location_from_coordinates():
    lat = request.args.get("lat", type=float)
    lng = request.args.get("lng", type=float)
    if lat is None or lng is None: return jsonify({"error": "lat and lng required."}), 400
    query = urlencode({"lat": f"{lat:.6f}", "lon": f"{lng:.6f}", "format": "jsonv2", "addressdetails": 1})
    url = f"https://nominatim.openstreetmap.org/reverse?{query}"
    try:
        req = Request(url, headers={"User-Agent": "LIMS/1.0"})
        with urlopen(req, timeout=8) as response:
            data = json.loads(response.read().decode("utf-8"))
        addr = data.get("address", {})
        
        # Robust detection for Indian administrative levels
        state = addr.get("state", "")
        
        # District can be in several fields
        district = addr.get("state_district") or addr.get("district") or addr.get("county") or addr.get("city") or ""
        
        # Clean up district names (e.g. "Indore District" -> "Indore")
        district = district.replace(" District", "").replace(" Zila", "").replace(" Dist.", "").strip()
        
        # Village/Ward/Locality can be in many fields in India
        village = (
            addr.get("village") or 
            addr.get("suburb") or 
            addr.get("neighbourhood") or 
            addr.get("hamlet") or 
            addr.get("town") or 
            addr.get("city_district") or 
            addr.get("locality") or 
            addr.get("residential") or
            ""
        )
        
        return jsonify({
            "success": True,
            "state": state,
            "district": district,
            "village": village,
            "display_name": data.get("display_name", "")
        })
    except Exception as e:
        return jsonify({"error": str(e)}), 502

@gis_bp.route("/api/location-catalog", methods=["GET"])
def get_location_catalog():
    """Return the master hierarchy of States, Districts, and Villages."""
    # This would typically come from a DB, but we'll use a robust static catalog for India
    catalog = {
        "Madhya Pradesh": {
            "Indore": ["Bicholi Mardana", "Kanadia", "Hatod", "Rau", "Mhow"],
            "Bhopal": ["Bairagarh", "Huzur", "Berasia", "Misrod", "Arera"],
            "Jabalpur": ["Panagar", "Sihora", "Patan", "Shahpura"],
            "Gwalior": ["Dabra", "Bhitarwar", "Chinore"],
            "Ujjain": ["Nagda", "Mahidpur", "Tarana", "Khachrod"]
        },
        "Maharashtra": {
            "Mumbai": ["Colaba", "Dadar", "Andheri", "Borivali", "Kurla"],
            "Pune": ["Haveli", "Khed", "Shirur", "Baramati", "Indapur"],
            "Nagpur": ["Kamptee", "Ramtek", "Katol", "Saoner"],
            "Nashik": ["Malegaon", "Sinnar", "Yeola", "Igatpuri"]
        },
        "Uttar Pradesh": {
            "Lucknow": ["Bakshi Ka Talab", "Malihabad", "Mohanlalganj"],
            "Kanpur": ["Bilhaur", "Ghatampur"],
            "Varanasi": ["Pindra", "Rajatalab"],
            "Agra": ["Etmadpur", "Fatehabad", "Kheragarh"]
        },
        "Delhi": {
            "New Delhi": ["Connaught Place", "Chanakyapuri"],
            "South Delhi": ["Saket", "Hauz Khas", "Mehrauli"],
            "North Delhi": ["Model Town", "Narela"],
            "East Delhi": ["Preet Vihar", "Mayur Vihar"]
        }
    }
    return jsonify(catalog)


================================================
FILE: routes/pages.py
================================================
from flask import Blueprint, render_template, redirect, url_for, session, request, jsonify
from core import generate_captcha

pages_bp = Blueprint('pages', __name__)

@pages_bp.route("/")
def index():
    return redirect(url_for("pages.login_page"))

@pages_bp.route("/login")
def login_page():
    role = (session.get("role") or "").lower()
    if role in ("admin", "superadmin"):
        return redirect(url_for("pages.admin_dashboard"))
    elif role == "viewer":
        return redirect(url_for("pages.viewer_page"))
    captcha_question, captcha_token = generate_captcha()
    return render_template("login.html", captcha_question=captcha_question, captcha_token=captcha_token)

@pages_bp.route("/admin")
def admin_dashboard():
    role = (session.get("role") or "").lower()
    if role not in ("admin", "superadmin"):
        return redirect(url_for("pages.login_page"))
    return render_template("admin_dashboard.html", username=session.get("username", "Admin"))

@pages_bp.route("/viewer")
def viewer_page():
    role = (session.get("role") or "").lower()
    if role not in ("admin", "superadmin", "viewer"):
        return redirect(url_for("pages.login_page"))
    return render_template("public_viewer_v2.html")


================================================
FILE: routes/records.py
================================================
import uuid
import random
from datetime import datetime
from flask import Blueprint, request, jsonify, session
from core import (
    load_records, save_records, viewer_or_admin_required, role_required,
    _strip_b64_from_list, _mask_owner_for_viewer, _log_audit,
    _generate_ulpin, _update_nested, _apply_filters_to_records
)

records_bp = Blueprint('records', __name__)

def generate_ulpin(state_name, district_name):
    """Generate a unique 14-digit ULPIN."""
    state_code = str(abs(hash(state_name)) % 90 + 10)
    dist_code = str(abs(hash(district_name)) % 90 + 10)
    parcel_code = ''.join([str(random.randint(0, 9)) for _ in range(10)])
    return f"{state_code}{dist_code}{parcel_code}"

@records_bp.route("/api/records", methods=["GET"])
@viewer_or_admin_required
def get_records():
    """Fetch all land records with role-based masking and filtering."""
    records = load_records()
    records = _strip_b64_from_list(records)
    role = (session.get("role") or "").lower()
    
    # Exclude soft-deleted records for non-admins
    if role not in ("admin", "superadmin"):
        records = [r for r in records if not r.get("deleted")]
    
    # Mask owner details for public viewers
    if role == "viewer":
        records = [_mask_owner_for_viewer(rec) for rec in records]
        
    return jsonify(records)

@records_bp.route("/api/records/<record_id>", methods=["GET"])
@viewer_or_admin_required
def get_record(record_id):
    """Fetch a single land record by its ID."""
    records = load_records()
    record = next((r for r in records if r["_id"] == record_id), None)
    
    if not record:
        return jsonify({"error": "Record not found."}), 404
        
    role = (session.get("role") or "").lower()
    if record.get("deleted") and role not in ("admin", "superadmin"):
        return jsonify({"error": "Record not found."}), 404
        
    if role == "viewer":
        record = _mask_owner_for_viewer(record)
        
    return jsonify(record)

@records_bp.route("/api/records/search", methods=["GET"])
@viewer_or_admin_required
def search_records():
    """Search records using a global text query."""
    query = request.args.get("q", "").strip().lower()
    if not query:
        return jsonify({"error": "Search query parameter 'q' is required."}), 400
        
    records = load_records()
    results = _apply_filters_to_records(records, {"search": query})
    
    role = (session.get("role") or "").lower()
    if role not in ("admin", "superadmin"):
        results = [r for r in results if not r.get("deleted")]
    if role == "viewer":
        results = [_mask_owner_for_viewer(rec) for rec in results]
        
    return jsonify(results)

@records_bp.route("/api/records/filter", methods=["GET"])
@viewer_or_admin_required
def filter_records():
    """Advanced filtering of records by location and attributes."""
    records = load_records()
    params = {k: request.args.get(k, "") for k in ["state", "district", "village", "land_use", "search"]}
    filtered = _apply_filters_to_records(records, params)
    
    role = (session.get("role") or "").lower()
    if role not in ("admin", "superadmin"):
        filtered = [r for r in filtered if not r.get("deleted")]
    
    filtered = _strip_b64_from_list(filtered)
    return jsonify(filtered)

@records_bp.route("/api/location-catalog", methods=["GET"])
@viewer_or_admin_required
def location_catalog():
    """Return the hierarchy of state > district > village for dropdowns."""
    records = load_records()
    catalog = {}
    for rec in records:
        loc = rec.get("location", {})
        state, dist, vill = loc.get("state"), loc.get("district"), loc.get("village")
        if not all([state, dist, vill]): continue
        
        if state not in catalog: catalog[state] = {}
        if dist not in catalog[state]: catalog[state][dist] = set()
        catalog[state][dist].add(vill)
    
    # Format for JSON
    result = {}
    for s in sorted(catalog.keys()):
        result[s] = {d: sorted(list(v)) for d, v in sorted(catalog[s].items())}
    return jsonify(result)

@records_bp.route("/api/records", methods=["POST"])
@role_required("admin", "superadmin", "officer")
def create_record():
    """Create a new land record with geometry validation."""
    data = request.get_json() or {}
    required_fields = ["khasra_no", "khata_no", "location", "geometry", "land_use", "owner_name"]
    missing = [f for f in required_fields if not data.get(f)]
    if missing:
        return jsonify({"error": f"Missing fields: {', '.join(missing)}"}), 400
    
    username = session.get("username", "admin")
    
    # 1. Geometry Validation & Metrics
    geometry = data.get("geometry")
    if not geometry:
        return jsonify({"error": "Geometry is required."}), 400
        
    from gis_processor import validate_polygon, calculate_area, check_overlap, get_centroid, calculate_perimeter
    validation = validate_polygon(geometry)
    if not validation["valid"]:
        return jsonify({"error": "Invalid Geometry", "details": validation["errors"]}), 400
        
    # 2. Overlap Check
    records = load_records()
    overlap = check_overlap(geometry, [r for r in records if not r.get("deleted")])
    if overlap["overlaps"]:
        return jsonify({
            "error": "Spatial Overlap Detected", 
            "conflicting_records": overlap["conflicting_records"]
        }), 409

    # 3. Auto-fill Data
    area_data = calculate_area(geometry)
    centroid = get_centroid(geometry)
    
    loc = data.get("location", {})
    state = loc.get("state", "Unknown")
    district = loc.get("district", "Unknown")
    
    ulpin = data.get("ulpin")
    if not ulpin or len(str(ulpin)) < 10:
        ulpin = generate_ulpin(state, district)
        # Ensure uniqueness
        while any(r.get("ulpin") == ulpin for r in records):
            ulpin = generate_ulpin(state, district)

    new_record = {
        "_id": str(uuid.uuid4()),
        "ulpin": ulpin,
        "khasra_no": data.get("khasra_no", "N/A"),
        "khata_no": data.get("khata_no", "N/A"),
        "location": {
            "state": state,
            "district": district,
            "village": loc.get("village", "Unknown")
        },
        "owner": {
            "name": data.get("owner_name", "N/A"),
            "share_pct": data.get("share_pct", 100),
            "aadhaar_mask": data.get("aadhaar_mask", "XXXX-XXXX-XXXX"),
            "proof_doc_b64": data.get("owner_proof_doc_b64")
        },
        "attributes": {
            "area_ha": area_data.get("area_ha", 0),
            "land_use": data.get("land_use", "Other"),
            "circle_rate_inr": data.get("circle_rate_inr", 0),
            "centroid": centroid,
            "perimeter_m": calculate_perimeter(geometry).get("perimeter_m", 0)
        },
        "geometry": geometry,
        "mutation_history": [],
        "deleted": False
    }
    
    records.append(new_record)
    save_records(records)
    
    _log_audit('create', username, new_record["_id"], {'ulpin': ulpin, 'khasra_no': new_record["khasra_no"]})
    return jsonify({"success": True, "record": new_record}), 201

@records_bp.route("/api/records/<record_id>", methods=["PUT"])
@role_required("admin", "superadmin", "officer")
def update_record(record_id):
    """Update a record or perform an ownership mutation."""
    data = request.get_json() or {}
    records = load_records()
    record = next((r for r in records if r["_id"] == record_id), None)
    
    if not record:
        return jsonify({"error": "Record not found."}), 404

    from gis_processor import validate_polygon, calculate_area, get_centroid, calculate_perimeter

    # --- Scenario A: Ownership Mutation ---
    if data.get("mutation") and data.get("new_owner_name"):
        old_owner = record.get("owner", {})
        mutation_entry = {
            "previous_owner": old_owner.get("name", "Unknown"),
            "previous_share_pct": old_owner.get("share_pct", 0),
            "previous_aadhaar": old_owner.get("aadhaar_mask", "XXXX-XXXX-XXXX"),
            "previous_proof_doc": old_owner.get("proof_doc_b64"),
            "mutation_date": data.get("mutation_date", datetime.now().strftime("%Y-%m-%d")),
            "mutation_type": data.get("mutation_type", "Sale Deed"),
            "mutation_ref": data.get("mutation_ref", f"MUT-{datetime.now().strftime('%Y')}-{random.randint(10000, 99999)}"),
            "proof_doc_b64": data.get("mutation_proof_doc_b64")
        }
        if "mutation_history" not in record: record["mutation_history"] = []
        record["mutation_history"].append(mutation_entry)
        record["owner"] = {
            "name": data["new_owner_name"],
            "share_pct": data.get("new_share_pct", 100),
            "aadhaar_mask": data.get("new_aadhaar_mask", "XXXX-XXXX-XXXX")
        }
        
        # Mutations can also update location/geometry if provided
        if "location" in data: record["location"] = data["location"]
        if "geometry" in data:
            val = validate_polygon(data["geometry"])
            if val["valid"]:
                record["geometry"] = data["geometry"]
                record["attributes"]["area_ha"] = calculate_area(data["geometry"]).get("area_ha", 0)
                record["attributes"]["centroid"] = get_centroid(data["geometry"])
                record["attributes"]["perimeter_m"] = calculate_perimeter(data["geometry"]).get("perimeter_m", 0)

    # --- Scenario B: Regular Field Updates ---
    else:
        for field in ["khasra_no", "khata_no"]:
            if field in data: record[field] = data[field]
        
        if "land_use" in data: _update_nested(record, "attributes", "land_use", data["land_use"])
        if "circle_rate_inr" in data: _update_nested(record, "attributes", "circle_rate_inr", data["circle_rate_inr"])
        if "share_pct" in data: _update_nested(record, "owner", "share_pct", data["share_pct"])
        if "aadhaar_mask" in data: _update_nested(record, "owner", "aadhaar_mask", data["aadhaar_mask"])
        if "location" in data: record["location"] = data["location"]
        if "owner_proof_doc_b64" in data: _update_nested(record, "owner", "proof_doc_b64", data["owner_proof_doc_b64"])
        
        if "geometry" in data:
            val = validate_polygon(data["geometry"])
            if not val["valid"]:
                return jsonify({"error": "Invalid geometry.", "details": val["errors"]}), 400
            record["geometry"] = data["geometry"]
            
            # Check for overlaps with other records
            from gis_processor import check_overlap
            others = [r for r in records if r["_id"] != record_id and not r.get("deleted")]
            overlap_result = check_overlap(data["geometry"], others)
            if overlap_result.get("overlaps"):
                return jsonify({"error": "Parcel overlap detected.", "conflicting_records": overlap_result["conflicting_records"]}), 409
                
            record["attributes"]["area_ha"] = calculate_area(data["geometry"]).get("area_ha", 0)
            record["attributes"]["centroid"] = get_centroid(data["geometry"])
            record["attributes"]["perimeter_m"] = calculate_perimeter(data["geometry"]).get("perimeter_m", 0)

    save_records(records)
    
    username = session.get("username", "admin")
    _log_audit('update', username, record_id, {'ulpin': record.get('ulpin'), 'khasra_no': record.get('khasra_no')})
    
    return jsonify({"success": True, "record": record})

@records_bp.route("/api/records/<record_id>", methods=["DELETE"])
@role_required("admin", "superadmin", "officer")
def delete_record(record_id):
    """
    Safely delete a record.
    - If active: Soft-delete (move to trash).
    - If already in trash: Hard-delete (permanent) if user is Admin/Superadmin.
    """
    records = load_records()
    record_index = next((i for i, r in enumerate(records) if r["_id"] == record_id), None)
    
    if record_index is None:
        return jsonify({"error": "Record not found."}), 404
        
    record = records[record_index]
    role = (session.get("role") or "").lower()
    username = session.get("username", "admin")
    
    if record.get("deleted"):
        # Record is already in trash. Only Admins can permanently remove it.
        if role in ("admin", "superadmin"):
            records.pop(record_index)
            save_records(records)
            _log_audit('hard_delete', username, record_id, {'khasra_no': record.get('khasra_no')})
            return jsonify({"success": True, "message": "Record permanently deleted from database."})
        else:
            return jsonify({"error": "Only administrators can permanently delete records."}), 403
    else:
        # Soft delete the record
        record["deleted"] = True
        record["deleted_at"] = datetime.now().isoformat() + "Z"
        record["deleted_by"] = username
        save_records(records)
        _log_audit('soft_delete', username, record_id, {'khasra_no': record.get('khasra_no')})
        return jsonify({"success": True, "message": "Record moved to trash."})

@records_bp.route("/api/records/<record_id>/restore", methods=["POST"])
@role_required("admin", "superadmin")
def restore_record(record_id):
    """Restore a soft-deleted record."""
    records = load_records()
    record = next((r for r in records if r.get("_id") == record_id), None)
    if not record: return jsonify({"error": "Not found."}), 404
    record["deleted"] = False
    record.pop("deleted_by", None)
    record.pop("deleted_at", None)
    save_records(records)
    return jsonify({"success": True})

import io
import base64
from fpdf import FPDF
import qrcode
from flask import send_file

@records_bp.route("/api/records/<ulpin>/card", methods=["POST"])
@viewer_or_admin_required
def generate_property_card(ulpin):
    """Generate a PDF Property Card for a given ULPIN."""
    records = load_records()
    record = next((r for r in records if r.get("ulpin") == ulpin), None)
    if not record:
        return jsonify({"error": "Record not found."}), 404
        
    data = request.get_json() or {}
    map_image_b64 = data.get("map_image")
    
    # PDF Configuration
    pdf = FPDF()
    pdf.add_page()
    pdf.set_font("helvetica", "B", 18)
    
    # Header
    pdf.set_text_color(234, 88, 12) # Orange-600
    pdf.cell(0, 10, "GOVERNMENT OF INDIA", ln=True, align="C")
    pdf.set_font("helvetica", "B", 14)
    pdf.set_text_color(31, 41, 55) # Gray-800
    pdf.cell(0, 8, "BHOOMI-LIMS PROPERTY CARD", ln=True, align="C")
    pdf.ln(5)
    
    # Horizontal Line
    pdf.set_draw_color(209, 213, 219)
    pdf.line(10, pdf.get_y(), 200, pdf.get_y())
    pdf.ln(10)
    
    # Main Info Grid
    pdf.set_font("helvetica", "B", 10)
    pdf.set_fill_color(249, 250, 251)
    
    col_width = 45
    def add_info_row(label, value):
        pdf.set_font("helvetica", "B", 9)
        pdf.set_text_color(107, 114, 128)
        pdf.cell(col_width, 8, f"{label}:", border=0)
        pdf.set_font("helvetica", "", 10)
        pdf.set_text_color(0, 0, 0)
        pdf.cell(0, 8, str(value), ln=True)

    add_info_row("ULPIN", record.get("ulpin", "N/A"))
    add_info_row("Khasra No", record.get("khasra_no", "N/A"))
    add_info_row("Khata No", record.get("khata_no", "N/A"))
    add_info_row("Area (Hectares)", f"{record.get('attributes', {}).get('area_ha', 0):.4f}")
    add_info_row("Village", record.get("location", {}).get("village", "N/A"))
    add_info_row("District", record.get("location", {}).get("district", "N/A"))
    add_info_row("State", record.get("location", {}).get("state", "N/A"))
    
    # Owner Info
    pdf.ln(5)
    pdf.set_font("helvetica", "B", 11)
    pdf.cell(0, 10, "OWNERSHIP DETAILS", ln=True)
    pdf.set_font("helvetica", "", 10)
    owner = record.get("owner", {})
    add_info_row("Primary Owner", owner.get("name", "N/A"))
    add_info_row("Share Percentage", f"{owner.get('share_pct', 100)}%")
    
    # Map Image
    if map_image_b64:
        try:
            img_data = base64.b64decode(map_image_b64.split(",")[1])
            img_io = io.BytesIO(img_data)
            # Position at bottom right or below text
            y_pos = pdf.get_y() + 10
            if y_pos > 180: # Start new page if no space
                pdf.add_page()
                y_pos = 20
            pdf.image(img_io, x=10, y=y_pos, w=120)
            pdf.set_y(y_pos + 70)
        except Exception as e:
            print(f"PDF Map Error: {e}")

    # QR Code for Verification
    qr_data = f"https://lims-india.gov.in/verify/{ulpin}"
    qr = qrcode.QRCode(version=1, box_size=10, border=1)
    qr.add_data(qr_data)
    qr.make(fit=True)
    qr_img = qr.make_image(fill_color="black", back_color="white")
    
    qr_io = io.BytesIO()
    qr_img.save(qr_io, format="PNG")
    qr_io.seek(0)
    
    # Place QR code in top right
    pdf.image(qr_io, x=165, y=30, w=30)
    pdf.set_font("helvetica", "I", 7)
    pdf.set_xy(165, 60)
    pdf.cell(30, 5, "Scan to Verify", align="C")

    # Mutation History Table
    pdf.set_xy(10, pdf.get_y() + 10)
    pdf.set_font("helvetica", "B", 11)
    pdf.cell(0, 10, "MUTATION HISTORY", ln=True)
    
    mutations = record.get("mutation_history", [])
    if not mutations:
        pdf.set_font("helvetica", "I", 9)
        pdf.cell(0, 8, "No prior mutations recorded.", ln=True)
    else:
        pdf.set_font("helvetica", "B", 8)
        pdf.set_fill_color(243, 244, 246)
        pdf.cell(30, 8, "Date", 1, 0, "C", True)
        pdf.cell(30, 8, "Type", 1, 0, "C", True)
        pdf.cell(80, 8, "Previous Owner", 1, 0, "C", True)
        pdf.cell(50, 8, "Reference", 1, 1, "C", True)
        
        pdf.set_font("helvetica", "", 8)
        for m in mutations:
            pdf.cell(30, 7, str(m.get("mutation_date", "N/A")), 1)
            pdf.cell(30, 7, str(m.get("mutation_type", "N/A")), 1)
            pdf.cell(80, 7, str(m.get("previous_owner", "N/A")), 1)
            pdf.cell(50, 7, str(m.get("mutation_ref", "N/A")), 1, 1)

    # Footer
    pdf.set_y(-25)
    pdf.set_font("helvetica", "I", 8)
    pdf.set_text_color(156, 163, 175)
    pdf.cell(0, 10, f"Document Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", align="C")
    pdf.ln(5)
    pdf.cell(0, 10, "This is a computer-generated document and does not require a physical signature.", align="C")

    # Return PDF
    pdf_output = pdf.output()
    return send_file(
        io.BytesIO(pdf_output),
        mimetype="application/pdf",
        as_attachment=True,
        download_name=f"Property_Card_{ulpin}.pdf"
    )


================================================
FILE: routes/users.py
================================================
import uuid
from datetime import datetime
from flask import Blueprint, request, jsonify, session
from werkzeug.security import generate_password_hash, check_password_hash
from core import load_users, save_users, admin_required, viewer_or_admin_required

users_bp = Blueprint('users', __name__)

def _get_current_user(users):
    current_username = session.get("username", "")
    return next((u for u in users if u.get("username") == current_username), None)

@users_bp.route("/api/profile", methods=["GET"])
@viewer_or_admin_required
def get_profile():
    users = load_users()
    user = _get_current_user(users)
    if not user:
        return jsonify({"error": "Profile not found."}), 404
    profile = {k: v for k, v in user.items() if k != "password_hash"}
    return jsonify(profile)

@users_bp.route("/api/profile", methods=["PUT"])
@viewer_or_admin_required
def update_profile():
    data = request.get_json() or {}
    users = load_users()
    user = _get_current_user(users)
    if not user:
        return jsonify({"error": "Profile not found."}), 404
    
    allowed_fields = ["full_name", "email", "phone", "designation", "department", "office_location"]
    for field in allowed_fields:
        if field in data:
            user[field] = data[field]
    
    if data.get("current_password") and data.get("new_password"):
        if not check_password_hash(user.get("password_hash", ""), data["current_password"]):
            return jsonify({"error": "Current password is incorrect."}), 403
        user["password_hash"] = generate_password_hash(data["new_password"])
    
    save_users(users)
    profile = {k: v for k, v in user.items() if k != "password_hash"}
    return jsonify({"success": True, "profile": profile})

@users_bp.route("/api/users", methods=["GET"])
@admin_required
def list_users():
    """List all users (admin only). Recovery accounts are hidden."""
    users = load_users()
    # Ghost Mode: Recovery accounts are hidden from standard admins
    current_user = _get_current_user(users)
    is_rec = current_user.get("is_recovery") if current_user else False
    
    result = [{k: v for k, v in u.items() if k != "password_hash"} 
              for u in users if is_rec or not u.get("is_recovery")]
    return jsonify(result)

@users_bp.route("/api/users", methods=["POST"])
@admin_required
def create_user():
    data = request.get_json() or {}
    role = (data.get("role") or "officer").strip().lower()
    current_role = (session.get("role") or "").lower()
    
    users = load_users()
    current_user = _get_current_user(users)

    # 1. SECURITY CHECK FIRST
    if role == "superadmin" and current_role != "superadmin":
        return jsonify({"error": "Unauthorized. Only SuperAdmins can create other SuperAdmins."}), 403
    
    # Define role hierarchy
    if current_role == "superadmin":
        valid_roles = ["superadmin", "admin", "officer", "viewer"]
    elif current_role == "admin":
        valid_roles = ["admin", "officer", "viewer"]
    else:
        valid_roles = []

    if role not in valid_roles:
        return jsonify({
            "error": f"Unauthorized role assignment. Your current role '{current_role}' is not permitted to create a '{role}' account.",
            "allowed_roles_for_you": valid_roles
        }), 403

    # 2. VALIDATION CHECKS
    required = ["username", "password", "full_name"]
    missing = [f for f in required if not data.get(f)]
    if missing:
        return jsonify({"error": f"Missing: {', '.join(missing)}"}), 400
    
    if any(u.get("username") == data["username"] for u in users):
        return jsonify({"error": "Username exists."}), 409
    
    # 3. EXECUTION
    new_user = {
        "user_id": str(uuid.uuid4()),
        "username": data["username"],
        "password_hash": generate_password_hash(data["password"]),
        "role": role,
        "full_name": data["full_name"],
        "email": data.get("email", ""),
        "phone": data.get("phone", ""),
        "designation": data.get("designation", ""),
        "department": data.get("department", ""),
        "office_location": data.get("office_location", ""),
        "is_active": data.get("is_active", True),
        "is_recovery": data.get("is_recovery", False) if current_user and current_user.get("is_recovery") else False,
        "created_at": datetime.now().isoformat() + "Z",
        "last_login": None
    }
    users.append(new_user)
    save_users(users)
    return jsonify({"success": True, "user": {k:v for k,v in new_user.items() if k != "password_hash"}}), 201

@users_bp.route("/api/users/<user_id>", methods=["GET"])
@admin_required
def get_user(user_id):
    """Get a specific user's profile (admin only). Recovery accounts are invisible."""
    users = load_users()
    user = next((u for u in users if u.get("user_id") == user_id), None)
    # Hide the existence of recovery accounts from standard admins
    current_user = _get_current_user(users)
    is_rec = current_user.get("is_recovery") if current_user else False
    
    if not user or (user.get("is_recovery") and not is_rec):
        return jsonify({"error": "Not found."}), 404
    return jsonify({k: v for k, v in user.items() if k != "password_hash"})

@users_bp.route("/api/users/<user_id>", methods=["PUT"])
@admin_required
def update_user(user_id):
    """Update any user's profile (admin only)."""
    data = request.get_json() or {}
    users = load_users()
    target_user = next((u for u in users if u.get("user_id") == user_id), None)
    
    # Standard admins can't even "see" that a recovery account exists to update it
    current_user = _get_current_user(users)
    is_rec = current_user.get("is_recovery") if current_user else False
    
    if not target_user or (target_user.get("is_recovery") and not is_rec):
        return jsonify({"error": "Not found."}), 404

    current_user = _get_current_user(users)
    current_role = (session.get("role") or "").lower()

    # 1. SECURITY CHECK
    target_role = target_user.get("role", "").lower()
    
    # Only superadmins can modify other superadmins
    if target_role == "superadmin" and current_role != "superadmin":
        return jsonify({"error": "Unauthorized. Only SuperAdmins can modify SuperAdmin accounts."}), 403
        
    # Admins cannot modify superadmins, but can modify anyone else
    if current_role == "admin" and target_role == "superadmin":
         return jsonify({"error": "Unauthorized. Admins cannot modify SuperAdmin accounts."}), 403

    if "role" in data:
        new_role = data["role"].lower()
        if current_role == "superadmin":
            valid_roles = ["superadmin", "admin", "officer", "viewer"]
        elif current_role == "admin":
            valid_roles = ["admin", "officer", "viewer"]
        else:
            valid_roles = []

        if new_role not in valid_roles:
            return jsonify({"error": f"Invalid role assignment: '{new_role}' is not allowed for your role."}), 403
        target_user["role"] = new_role

    # 2. EXECUTION
    allowed_fields = ["full_name", "email", "phone", "designation", "department", "office_location", "is_active"]
    for field in allowed_fields:
        if field in data:
            target_user[field] = data[field]
    
    if "is_recovery" in data and current_user and current_user.get("is_recovery"):
        target_user["is_recovery"] = data["is_recovery"]
    
    if data.get("new_password"):
        target_user["password_hash"] = generate_password_hash(data["new_password"])
    
    save_users(users)
    return jsonify({"success": True, "user": {k:v for k,v in target_user.items() if k != "password_hash"}})

@users_bp.route("/api/users/<user_id>", methods=["DELETE"])
@admin_required
def delete_user(user_id):
    """Delete a user (admin only)."""
    users = load_users()
    target_user = next((u for u in users if u.get("user_id") == user_id), None)
    
    # Hide the existence of recovery accounts from standard admins
    current_user = _get_current_user(users)
    is_rec = current_user.get("is_recovery") if current_user else False
    
    if not target_user or (target_user.get("is_recovery") and not is_rec):
        return jsonify({"error": "Not found."}), 404

    current_user = _get_current_user(users)
    current_role = (session.get("role") or "").lower()

    if target_user.get("username") == session.get("username"):
        return jsonify({"error": "Cannot delete self."}), 403

    target_role = target_user.get("role", "").lower()
    if target_role == "superadmin":
        if current_role != "superadmin":
            return jsonify({"error": "Unauthorized. Only SuperAdmins can delete other SuperAdmins."}), 403
    
    if current_role != "superadmin" and target_role == "admin":
        return jsonify({"error": "Unauthorized to delete admins."}), 403
    
    # 2. EXECUTION
    users = [u for u in users if u.get("user_id") != user_id]
    save_users(users)
    return jsonify({"success": True})


================================================
FILE: routes/utils.py
================================================
from flask import Blueprint, jsonify
from config import LAND_USE_OPTIONS, LAND_USE_COLORS, MUTATION_TYPES

utils_bp = Blueprint('utils', __name__)

@utils_bp.route("/api/config", methods=["GET"])
def app_config():
    return jsonify({
        "land_use_options": LAND_USE_OPTIONS,
        "land_use_colors": LAND_USE_COLORS,
        "mutation_types": MUTATION_TYPES
    })


================================================
FILE: run_server.py
================================================

from app import create_app
import os

if __name__ == '__main__':
    app = create_app()
    # Disable debug mode and reloader to avoid issues in background
    app.run(host='127.0.0.1', port=5000, debug=False, use_reloader=False)


================================================
FILE: scripts/inspect_recovery.py
================================================
#!/usr/bin/env python3
"""Inspect superadmin and recovery accounts in the database."""
from dotenv import load_dotenv
import os
load_dotenv()
from pymongo import MongoClient
try:
    from config import MONGO_URI
except Exception:
    MONGO_URI = os.environ.get('MONGO_URI')

def main():
    uri = MONGO_URI or os.environ.get('MONGO_URI')
    if not uri:
        raise SystemExit('MONGO_URI not found in config or environment')
    client = MongoClient(uri)
    db = client.get_database('indialims')
    users = list(db.users.find({}, {'password_hash': 0}))
    print('Total users:', len(users))
    print('\nSuperadmin users:')
    for u in users:
        if u.get('role') == 'superadmin':
            print('-', u.get('username'), '| is_recovery=', u.get('is_recovery', False))

    print('\nRecovery-flagged users:')
    for u in users:
        if u.get('is_recovery'):
            print('-', u.get('username'), '| role=', u.get('role'))

if __name__ == '__main__':
    main()


================================================
FILE: scripts/recover_superadmin_auto.py
================================================
#!/usr/bin/env python3
"""Server-only recovery script to upsert or reset a superadmin account.

Run this from the server/host with access to the MongoDB instance (SSH).
It prints the generated password once — copy it securely and rotate after first login.
Supports `--dry-run` to show actions without modifying the database.
"""
import secrets
import argparse
import os
from datetime import datetime
from dotenv import load_dotenv

# Load .env into environment so MONGO_URI is available when running the script
load_dotenv()


def random_password(length=20):
    alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+"
    return ''.join(secrets.choice(alphabet) for _ in range(length))


def upsert_superadmin(username=None, password=None, dry_run=False):
    now = datetime.utcnow().isoformat() + "Z"
    username = username or f"recovery_sa_{now.replace(':','').replace('-','').replace('T','_')}"
    password = password or random_password()

    if dry_run:
        print("[DRY-RUN] Would ensure a superadmin exists or be updated with the following:")
        print("USERNAME:", username)
        print("PASSWORD:", password)
        print("[DRY-RUN] No database connections or writes were performed.")
        return username, password, False

    # Perform actual DB operations
    # Import heavy dependencies only when performing real DB operations so
    # `--dry-run` remains usable in minimal environments.
    from werkzeug.security import generate_password_hash
    from pymongo import MongoClient

    # Load MONGO_URI from project config if available, otherwise from env
    try:
        from config import MONGO_URI
    except Exception:
        MONGO_URI = os.environ.get("MONGO_URI")
    if not MONGO_URI:
        raise RuntimeError("MONGO_URI is not set in config.py or environment.")

    client = MongoClient(MONGO_URI)
    db = client.get_database("indialims")
    users = db.users

    sa = users.find_one({"role": "superadmin"})
    if sa:
        users.update_one({"_id": sa["_id"]}, {"$set": {
            "username": username,
            "password_hash": generate_password_hash(password),
            "is_active": True,
            "last_login": now,
            "is_recovery": True
        }})
        created = False
    else:
        users.insert_one({
            "user_id": f"recovery-sa-{now}",
            "username": username,
            "password_hash": generate_password_hash(password),
            "role": "superadmin",
            "full_name": "Recovery SuperAdmin",
            "email": "",
            "phone": "",
            "is_active": True,
            "is_recovery": True,
            "created_at": now,
            "last_login": now
        })
        created = True
    print("SuperAdmin created" if created else "SuperAdmin updated")
    print("USERNAME:", username)
    print("PASSWORD:", password)
    print("IMPORTANT: copy the password now and rotate on first login.")
    return username, password, created


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Upsert/reset superadmin (server-only).")
    parser.add_argument("username", nargs="?", help="Optional username to set")
    parser.add_argument("password", nargs="?", help="Optional password to set")
    parser.add_argument("--dry-run", action="store_true", help="Show actions without writing to DB")
    parser.add_argument("--dump", action="store_true", help="Dump credentials to a file in the scripts folder")
    parser.add_argument("--dump-file", help="Optional path for dumped credentials (overrides default)")
    args = parser.parse_args()

    username, password, created = upsert_superadmin(args.username, args.password, dry_run=args.dry_run)

    # If requested, write the last generated credentials to a file for operator convenience.
    # Note: `upsert_superadmin` prints credentials; to capture them here we re-run a quiet
    # generation when --dump is requested but do not modify DB again.
    if args.dump and not args.dry_run:
        # Use the exact credentials returned from the upsert operation
        # (so the dump matches what was written to the DB)
        dump_username = username
        dump_password = password

        # If user provided --dump-file use it; otherwise place in scripts folder
        dump_path = args.dump_file
        if not dump_path:
            scripts_dir = os.path.dirname(__file__)
            safe_now = datetime.utcnow().isoformat().replace(':', '').replace('.', '_')
            dump_path = os.path.join(scripts_dir, f"recovery_credentials_{safe_now}.txt")

        with open(dump_path, 'w', encoding='utf-8') as f:
            f.write(f"# Recovery credentials generated: {datetime.utcnow().isoformat()}Z\n")
            f.write(f"USERNAME: {dump_username}\n")
            f.write(f"PASSWORD: {dump_password}\n")
            f.write("# IMPORTANT: rotate this password immediately after first login.\n")

        print(f"Credentials dumped to: {dump_path}")


================================================
FILE: scripts/recovery_credentials_2026-04-25T050759_400298.txt
================================================
SuperAdmin updated
USERNAME: recovery_sa_20260425_154817.302101Z
PASSWORD: WwI9v*V_J=MpH_SHi3+^
IMPORTANT: copy the password now and rotate on first login. 

================================================
FILE: static/css/style.css
================================================
/*
 * style.css - Custom Styling for India LIMS
 * Tailwind CSS is loaded via CDN; this file adds Leaflet & custom overrides.
 */

/* ─── Base Overrides ──────────────────────────────────────────────────────── */
* {
    box-sizing: border-box;
}

body {
    font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
    margin: 0;
    padding: 0;
}

/* ─── Leaflet Map Overrides ───────────────────────────────────────────────── */
#map,
#add-record-map,
#view-record-map {
    width: 100% !important;
    height: 100% !important;
    z-index: 1;
}

/* Ensure Leaflet controls don't conflict with our UI */
.leaflet-control-container {
    z-index: 400;
}

.leaflet-control-zoom {
    border: none !important;
    box-shadow: 0 2px 8px rgba(0,0,0,0.15) !important;
    border-radius: 8px !important;
    overflow: hidden;
}

.leaflet-control-zoom a {
    width: 36px !important;
    height: 36px !important;
    line-height: 36px !important;
    font-size: 18px !important;
    color: #374151 !important;
    background: white !important;
    border-bottom: 1px solid #e5e7eb !important;
}

.leaflet-control-zoom a:hover {
    background: #f3f4f6 !important;
    color: #ea580c !important;
}

/* ─── Leaflet-Geoman Toolbar Styling ──────────────────────────────────────── */
.leaflet-pm-toolbar {
    z-index: 500 !important;
}

.pm-toolbar {
    border-radius: 8px !important;
    box-shadow: 0 2px 10px rgba(0,0,0,0.15) !important;
    border: none !important;
    overflow: hidden;
}

.pm-toolbar .button-container .pm-btn {
    width: 40px !important;
    height: 40px !important;
    background: white !important;
    border: none !important;
    border-bottom: 1px solid #e5e7eb !important;
    transition: background 0.15s;
}

.pm-toolbar .button-container .pm-btn:hover {
    background: #fff7ed !important;
}

.pm-toolbar .button-container .pm-btn.active {
    background: #fed7aa !important;
    color: #ea580c !important;
}

.pm-toolbar .button-container:last-child .pm-btn {
    border-bottom: none !important;
}

/* ─── Map Popup Styling ───────────────────────────────────────────────────── */
.leaflet-popup-content-wrapper {
    border-radius: 10px !important;
    box-shadow: 0 4px 20px rgba(0,0,0,0.12) !important;
    padding: 0 !important;
}

.leaflet-popup-content {
    margin: 0 !important;
    font-family: 'Segoe UI', system-ui, sans-serif;
    font-size: 13px;
    min-width: 220px;
}

.leaflet-popup-tip {
    box-shadow: 0 4px 20px rgba(0,0,0,0.08) !important;
}

/* ─── Parcel Tooltip Styling ──────────────────────────────────────────────── */
.parcel-tooltip {
    background: white !important;
    border: none !important;
    border-radius: 10px !important;
    box-shadow: 0 6px 20px rgba(0,0,0,0.15) !important;
    padding: 0 !important;
    font-family: 'Segoe UI', system-ui, sans-serif !important;
    font-size: 13px !important;
    color: #374151 !important;
    max-width: 300px !important;
    min-width: 250px !important;
}

.parcel-tooltip::before {
    border-top-color: white !important;
}

.parcel-tooltip .tooltip-content {
    padding: 14px 16px;
}

.parcel-tooltip .tooltip-title {
    font-size: 15px;
    font-weight: 700;
    color: #1f2937;
    margin-bottom: 10px;
    border-bottom: 2px solid #ea580c;
    padding-bottom: 8px;
}

.parcel-tooltip .tooltip-row {
    display: flex;
    justify-content: space-between;
    margin-bottom: 6px;
    line-height: 1.6;
}

.parcel-tooltip .tooltip-label {
    font-weight: 600;
    color: #6b7280;
    margin-right: 10px;
    min-width: 80px;
}

.parcel-tooltip .tooltip-value {
    font-weight: 500;
    color: #1f2937;
    text-align: right;
}

.parcel-tooltip .tooltip-divider {
    height: 1px;
    background: #e5e7eb;
    margin: 10px 0;
}

.parcel-tooltip .tooltip-hint {
    margin-top: 10px;
    padding-top: 8px;
    border-top: 1px solid #e5e7eb;
    font-size: 12px;
    color: #ea580c;
    font-weight: 600;
    text-align: center;
}

/* ─── Custom Parcel Styling ───────────────────────────────────────────────── */
.parcel-polygon {
    stroke: #ea580c;
    stroke-width: 2.5;
    fill-opacity: 0.2;
    stroke-opacity: 0.9;
}

.parcel-polygon:hover {
    fill-opacity: 0.35;
    stroke-width: 3.5;
}

.parcel-polygon-selected {
    stroke: #dc2626;
    stroke-width: 3.5;
    fill-opacity: 0.35;
    stroke-opacity: 1;
}

/* Land use color coding */
.parcel-agricultural { fill: #22c55e; }
.parcel-residential { fill: #3b82f6; }
.parcel-commercial { fill: #f59e0b; }
.parcel-industrial { fill: #8b5cf6; }
.parcel-government { fill: #ef4444; }
.parcel-forest { fill: #065f46; }
.parcel-wasteland { fill: #9ca3af; }

/* ─── Map Layer Switcher ─────────────────────────────────────────────────── */
input[type="radio"][name="basemap"],
input[type="radio"][name="add-basemap"],
input[type="radio"][name="view-basemap"] {
    -webkit-appearance: none;
    appearance: none;
    width: 14px;
    height: 14px;
    border: 2px solid #d1d5db;
    border-radius: 50%;
    background: white;
    cursor: pointer;
    position: relative;
    flex-shrink: 0;
}

input[type="radio"][name="basemap"]:checked,
input[type="radio"][name="add-basemap"]:checked,
input[type="radio"][name="view-basemap"]:checked {
    border-color: #ea580c;
    border-width: 2px;
}

input[type="radio"][name="basemap"]:checked::after,
input[type="radio"][name="add-basemap"]:checked::after,
input[type="radio"][name="view-basemap"]:checked::after {
    content: '';
    position: absolute;
    top: 2px;
    left: 2px;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: #ea580c;
}

input[type="radio"][name="basemap"]:hover,
input[type="radio"][name="add-basemap"]:hover,
input[type="radio"][name="view-basemap"]:hover {
    border-color: #ea580c;
}

/* ─── Public Viewer Map Layer Switcher (green theme) ─────────────────────── */
.viewer-radio {
    -webkit-appearance: none;
    appearance: none;
    width: 14px;
    height: 14px;
    border: 2px solid #d1d5db;
    border-radius: 50%;
    background: white;
    cursor: pointer;
    position: relative;
    flex-shrink: 0;
}

.viewer-radio:checked {
    border-color: #16a34a;
    border-width: 2px;
}

.viewer-radio:checked::after {
    content: '';
    position: absolute;
    top: 2px;
    left: 2px;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: #16a34a;
}

.viewer-radio:hover {
    border-color: #16a34a;
}

/* ─── Public Viewer Filter Selects (green theme) ────────────────────────── */
#filter-state:focus,
#filter-district:focus,
#filter-village:focus,
#filter-land-use:focus {
    --tw-ring-color: #16a34a;
    border-color: #16a34a;
}

/* ─── Main Tab Navigation (Left Sidebar) ─────────────────────────────────── */
.main-tab-btn {
    width: 100%;
    border: 0;
    background: transparent;
    color: #6b7280;
    font-size: 0.9rem;
    font-weight: 500;
    padding: 0.875rem 1rem;
    cursor: pointer;
    transition: all 0.2s;
    display: flex;
    align-items: center;
    gap: 0.75rem;
    position: relative;
    border-left: 4px solid transparent;
    border-radius: 0.5rem;
    margin-bottom: 0.25rem;
}

.main-tab-btn:hover {
    color: #374151;
    background: #f9fafb;
}

.main-tab-btn.active {
    color: #ea580c;
    background: #fff7ed;
    border-left-color: #ea580c;
    font-weight: 600;
}

.main-tab-btn svg {
    flex-shrink: 0;
}

.main-tab-btn span {
    flex: 1;
    text-align: left;
}

/* Logout button styling */
#btn-logout {
    margin-top: 0.25rem;
    border-radius: 0.5rem;
}

#btn-logout:hover {
    background: #fef2f2;
    border-left-color: #dc2626;
    color: #dc2626;
}

.main-tab-panel {
    animation: fadeIn 0.25s ease-in;
}

/* ─── Sidebar Styling (Legacy Support) ────────────────────────────────────── */
.sidebar-tab.active {
    color: #ea580c;
    border-bottom-color: #ea580c;
}

.sidebar-tab {
    transition: color 0.15s, border-color 0.15s;
    border-bottom: 2px solid transparent;
    cursor: pointer;
    background: none;
    border-top: none;
    border-left: none;
    border-right: none;
}

.sidebar-tab:hover {
    color: #374151;
}

/* ─── Form Sub-Tabs ───────────────────────────────────────────────────────── */
.form-tab-btn {
    flex: 1;
    border: 0;
    background: transparent;
    color: #6b7280;
    font-size: 0.875rem;
    font-weight: 500;
    padding: 0.75rem 1rem;
    cursor: pointer;
    transition: all 0.2s;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    border-bottom: 2px solid transparent;
}

.form-tab-btn:hover {
    color: #374151;
    background: #f3f4f6;
}

.form-tab-btn.active {
    color: #ea580c;
    background: #fff7ed;
    border-bottom-color: #ea580c;
}

.form-tab-panel {
    animation: fadeIn 0.2s ease-in;
}

/* ─── Draw Settings Panel ─────────────────────────────────────────────────── */
.draw-settings-btn {
    border: 0;
    border-radius: 0.375rem;
    font-size: 0.75rem;
    font-weight: 600;
    padding: 0.5rem 0.75rem;
    transition: background 0.15s;
}

#map-autofill-status {
    min-height: 1rem;
}

/* ─── Dashboard KPI Cards ─────────────────────────────────────────────────── */
.dashboard-kpi-card {
    border: 1px solid #e5e7eb;
    background: #f9fafb;
    border-radius: 0.5rem;
    padding: 0.55rem 0.7rem;
}

.dashboard-kpi-label {
    font-size: 0.68rem;
    color: #6b7280;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.dashboard-kpi-value {
    font-size: 1rem;
    font-weight: 700;
    color: #1f2937;
    margin-top: 0.15rem;
}

.dashboard-row {
    border: 1px solid #e5e7eb;
    border-radius: 0.5rem;
    background: #ffffff;
    padding: 0.5rem 0.65rem;
}

.dashboard-row-bar {
    height: 0.4rem;
    background: #ffedd5;
    border-radius: 9999px;
    overflow: hidden;
    margin-top: 0.35rem;
}

.dashboard-row-bar-fill {
    height: 100%;
    background: #ea580c;
}

/* ─── Dashboard Content Improvements ──────────────────────────────────────── */
#main-tab-dashboard .bg-white.rounded-lg.shadow-md {
    transition: box-shadow 0.2s ease;
}

#main-tab-dashboard .bg-white.rounded-lg.shadow-md:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

#dashboard-land-use > div,
#dashboard-districts > div {
    transition: transform 0.15s ease;
}

#dashboard-land-use > div:hover,
#dashboard-districts > div:hover {
    transform: translateX(2px);
}

/* ─── Records List Styling ────────────────────────────────────────────────── */
.records-header {
    position: sticky;
    top: 0;
    z-index: 5;
    background: #ffffff;
    border-bottom: 1px solid #e5e7eb;
    padding: 0.65rem 0.75rem;
    display: flex;
    align-items: center;
    justify-content: space-between;
}

.records-view-tabs {
    display: inline-flex;
    border: 1px solid #e5e7eb;
    border-radius: 0.5rem;
    overflow: hidden;
    margin-bottom: 0;
    width: auto;
    min-width: 180px;
    flex-shrink: 0;
}

.records-view-tab {
    border: 0;
    background: #ffffff;
    color: #4b5563;
    font-size: 0.75rem;
    font-weight: 600;
    padding: 0.5rem 0.875rem;
    cursor: pointer;
    transition: background 0.15s, color 0.15s;
    flex: 1;
    white-space: nowrap;
    min-width: 90px;
}

.records-view-tab:hover {
    background: #f3f4f6;
}

.records-view-tab.active {
    background: #fff7ed;
    color: #c2410c;
}

.record-table-row {
    border: 1px solid #e5e7eb;
    border-radius: 0.5rem;
    background: #ffffff;
    padding: 0.55rem 0.65rem;
    margin-bottom: 0.5rem;
    transition: all 0.15s;
    cursor: pointer;
}

.record-table-row:hover {
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}

.record-table-row.active {
    border-color: #fdba74;
    background: #fff7ed;
}

.view-record-btn-table {
    cursor: pointer;
    transition: all 0.15s;
    font-weight: 500;
}

.view-record-btn-table:hover {
    background: #ea580c !important;
    transform: translateY(-1px);
    box-shadow: 0 2px 4px rgba(234, 88, 12, 0.3);
}

/* ─── Record Card in Sidebar ──────────────────────────────────────────────── */
.record-card {
    padding: 12px 16px;
    cursor: pointer;
    transition: background 0.15s;
    border-left: 3px solid transparent;
    border-radius: 0.5rem;
    margin-bottom: 0.5rem;
    background: #ffffff;
    border: 1px solid #e5e7eb;
}

.record-card:hover {
    background: #f9fafb;
    border-left-color: #ea580c;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}

.record-card.active {
    background: #fff7ed;
    border-left-color: #ea580c;
    box-shadow: 0 2px 8px rgba(234, 88, 12, 0.1);
}

.view-record-btn {
    cursor: pointer;
    transition: all 0.15s;
}

.view-record-btn:hover {
    transform: translateY(-1px);
    box-shadow: 0 2px 4px rgba(234, 88, 12, 0.3);
}

.record-card .land-use-badge {
    display: inline-block;
    padding: 2px 10px;
    border-radius: 9999px;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

.badge-agricultural { background: #dcfce7; color: #166534; }
.badge-residential { background: #dbeafe; color: #1e40af; }
.badge-commercial { background: #fef3c7; color: #92400e; }
.badge-industrial { background: #ede9fe; color: #5b21b6; }
.badge-government { background: #fee2e2; color: #991b1b; }
.badge-forest { background: #d1fae5; color: #065f46; }
.badge-wasteland { background: #f3f4f6; color: #4b5563; }

/* Soft-deleted badge */
.badge-deleted { background: #fee2e2; color: #9f1239; padding: 2px 8px; border-radius: 9999px; font-size: 11px; font-weight: 700; text-transform: none; }

/* Muted appearance for deleted records in lists */
.record-card.deleted { opacity: 0.65; filter: grayscale(0.08); }
.record-table-row.deleted { opacity: 0.6; filter: grayscale(0.08); }

/* ─── Form Field Group Styling ────────────────────────────────────────────── */
fieldset {
    border-color: #e5e7eb;
}

fieldset legend {
    font-size: 0.75rem;
    font-weight: 600;
    color: #6b7280;
    padding: 0 0.5rem;
}

/* ─── Toast Notification ──────────────────────────────────────────────────── */
#toast {
    position: fixed;
    bottom: 20px;
    right: 20px;
    z-index: 10000;
    transform: translateY(20px);
    opacity: 0;
    transition: all 0.3s ease;
    max-width: 400px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

#toast.show {
    display: block !important;
    transform: translateY(0);
    opacity: 1;
}

#toast .toast-inner {
    padding: 12px 20px;
    border-radius: 8px;
    color: white;
    font-size: 14px;
    font-weight: 500;
}

#toast.success .toast-inner { background: #166534; }
#toast.error .toast-inner { background: #991b1b; }
#toast.info .toast-inner { background: #1e40af; }
#toast.warning .toast-inner { background: #92400e; }

/* ─── Form Input Improvements ─────────────────────────────────────────────── */
input[type="text"],
input[type="number"],
input[type="email"],
input[type="date"],
input[type="password"],
select,
textarea {
    transition: all 0.2s ease;
}

input[type="text"]:focus,
input[type="number"]:focus,
input[type="email"]:focus,
input[type="date"]:focus,
input[type="password"]:focus,
select:focus,
textarea:focus {
    outline: none;
}

input[readonly] {
    cursor: not-allowed;
}

/* ─── Login Page Styling ──────────────────────────────────────────────────── */
#captcha-question {
    font-variant-numeric: tabular-nums;
    letter-spacing: 2px;
    user-select: none;
}

/* ─── Scrollbar Styling ───────────────────────────────────────────────────── */
::-webkit-scrollbar {
    width: 8px;
}

::-webkit-scrollbar-track {
    background: #f1f1f1;
}

::-webkit-scrollbar-thumb {
    background: #c4c4c4;
    border-radius: 4px;
}

::-webkit-scrollbar-thumb:hover {
    background: #a0a0a0;
}

/* ─── Loading Spinner ─────────────────────────────────────────────────────── */
.spinner {
    display: inline-block;
    width: 20px;
    height: 20px;
    border: 2px solid rgba(255,255,255,0.3);
    border-radius: 50%;
    border-top-color: #fff;
    animation: spin 0.6s linear infinite;
}

@keyframes spin {
    to { transform: rotate(360deg); }
}

/* ─── Print Styles (for Property Cards) ───────────────────────────────────── */
@media print {
    body * {
        visibility: hidden;
    }
    #print-area, #print-area * {
        visibility: visible;
    }
    #print-area {
        position: absolute;
        left: 0;
        top: 0;
    }
}

/* ─── Responsive Adjustments ──────────────────────────────────────────────── */
@media (max-width: 1024px) {
    /* Adjust dashboard grid for tablets */
    #main-tab-dashboard .grid {
        gap: 1rem;
    }
    
    /* Reduce padding in records toolbar */
    .bg-white.border-b.px-4.py-3 {
        padding-left: 0.75rem;
        padding-right: 0.75rem;
    }
    
    /* Adjust search input width */
    #search-input {
        width: 12rem !important;
    }
    
    /* Hide department column on tablets */
    #users-table-body td:nth-child(4),
    #users-table-body th:nth-child(4) {
        display: none;
    }
}

@media (max-width: 1024px) and (min-width: 768px) {
    /* Make sidebar slightly smaller on tablet only */
    .w-56 {
        width: 64px !important;
    }
    
    .main-tab-btn span,
    #btn-logout span {
        display: none;
    }

    .main-tab-btn {
        padding: 0.75rem;
        justify-content: center;
    }
    
    #btn-logout {
        padding: 0.75rem;
        justify-content: center;
    }
}

@media (max-width: 768px) {
    /* Stack dashboard cards on mobile */
    #main-tab-dashboard .grid {
        grid-template-columns: 1fr !important;
    }
    
    /* Hide some filters on mobile */
    .records-header .records-view-tabs {
        display: none;
    }
    
    /* Hide contact and department columns on mobile */
    #users-table-body td:nth-child(3),
    #users-table-body th:nth-child(3),
    #users-table-body td:nth-child(4),
    #users-table-body th:nth-child(4) {
        display: none;
    }
}

@media (max-width: 767px) {
    /* Ensure map takes full viewport on mobile */
    #map {
        height: calc(100vh - 60px) !important;
    }

    /* Parcel panel on mobile - full width with top border */
    #parcel-panel {
        max-width: 100vw !important;
        border-radius: 0 !important;
    }

    /* Make filter sidebar touch-friendly */
    #filter-sidebar {
        -webkit-overflow-scrolling: touch;
    }

    /* Larger touch targets for filter inputs */
    #filter-state,
    #filter-district,
    #filter-village,
    #filter-land-use,
    #search-input {
        min-height: 44px;
        font-size: 16px !important; /* Prevents iOS zoom on focus */
    }

    #btn-apply-filters,
    #btn-clear-filters {
        min-height: 44px;
    }

    /* Ensure overlay is clickable */
    #filter-overlay {
        z-index: 35;
    }
}


================================================
FILE: static/data/india-boundary.geojson
================================================
{"type": "FeatureCollection", "features": [{"type": "Feature", "properties": {"name": "India", "ISO3166-1-Alpha-3": "IND", "ISO3166-1-Alpha-2": "IN"}, "geometry": {"type": "MultiPolygon", "coordinates": [[[[77.800346, 35.495406], [77.815332, 35.47334], [77.834246, 35.452152], [77.857087, 35.436598], [77.883132, 35.431068], [77.912691, 35.44099], [77.957856, 35.482073], [77.985865, 35.494165], [78.044259, 35.491633], [78.055628, 35.452876], [78.038058, 35.398099], [78.009843, 35.347456], [78.001161, 35.268908], [78.036404, 35.194235], [78.130042, 35.055432], [78.13924, 35.018949], [78.148129, 34.94314], [78.162185, 34.908826], [78.202492, 34.865728], [78.211381, 34.848262], [78.273186, 34.658867], [78.296027, 34.624658], [78.335301, 34.594375], [78.379432, 34.578666], [78.607325, 34.546471], [78.664583, 34.526421], [78.750676, 34.471282], [78.801732, 34.415006], [78.839249, 34.396894], [78.885861, 34.385758], [78.922138, 34.372296], [78.95118, 34.349171], [78.95694, 34.339961], [78.976192, 34.309173], [78.98539, 34.286358], [78.988491, 34.263129], [78.985494, 34.239901], [78.976192, 34.217163], [78.903225, 34.158123], [78.806073, 34.122983], [78.730109, 34.079265], [78.721117, 33.994386], [78.770313, 33.872352], [78.787263, 33.808428], [78.794084, 33.74391], [78.781372, 33.552785], [78.800905, 33.494236], [78.824263, 33.461059], [78.915948, 33.387656], [78.917694, 33.386258], [78.964986, 33.344254], [78.993449, 33.330782], [79.023041, 33.31798], [79.053278, 33.307873], [79.075048, 33.295743], [79.098028, 33.287655], [79.125038, 33.290203], [79.150213, 33.292555], [79.260061, 33.291776], [79.340307, 33.275132], [79.420084, 33.275169], [79.456096, 33.250399], [79.423475, 33.157825], [79.37317, 33.11214], [79.381063, 33.061768], [79.399273, 33.026878], [79.339787, 33.013257], [79.333586, 32.994292], [79.378462, 32.978881], [79.414089, 32.972053], [79.471089, 32.908959], [79.564093, 32.793928], [79.613913, 32.765035], [79.620673, 32.728725], [79.57617, 32.670646], [79.531201, 32.648317], [79.515741, 32.60812], [79.472113, 32.565472], [79.443497, 32.534773], [79.40682, 32.529683], [79.379477, 32.57645], [79.320466, 32.592267], [79.287412, 32.532579], [79.235608, 32.504296], [79.215247, 32.507914], [79.191166, 32.50427], [79.1614, 32.496467], [79.132875, 32.48546], [79.112928, 32.472386], [79.102902, 32.449855], [79.09174, 32.390556], [79.075307, 32.37079], [79.066316, 32.370067], [79.039651, 32.375984], [79.028178, 32.377095], [79.014742, 32.373994], [78.989834, 32.364098], [78.976192, 32.361618], [78.943429, 32.346373], [78.911286, 32.354745], [78.881521, 32.376268], [78.855682, 32.400452], [78.77145, 32.461663], [78.754707, 32.493806], [78.73693, 32.545792], [78.734966, 32.560649], [78.740651, 32.578684], [78.749126, 32.595867], [78.750986, 32.612791], [78.736827, 32.629844], [78.713262, 32.637518], [78.694452, 32.629663], [78.664893, 32.599019], [78.647736, 32.585971], [78.630166, 32.578013], [78.589859, 32.569925], [78.447542, 32.566256], [78.408165, 32.558376], [78.384807, 32.547549], [78.380983, 32.528041], [78.409198, 32.476623], [78.448782, 32.426782], [78.452193, 32.416782], [78.448162, 32.410658], [78.441444, 32.404948], [78.43638, 32.396163], [78.434519, 32.383761], [78.435966, 32.377973], [78.43886, 32.373296], [78.462735, 32.281803], [78.461391, 32.260151], [78.456327, 32.242116], [78.457981, 32.229662], [78.468541, 32.226668], [78.469017, 32.226533], [78.476481, 32.224417], [78.513068, 32.20757], [78.534978, 32.181344], [78.551825, 32.15039], [78.649287, 32.036444], [78.678329, 32.016755], [78.722151, 31.994586], [78.733829, 31.984664], [78.744578, 31.9642], [78.741994, 31.945338], [78.720187, 31.902912], [78.714192, 31.883016], [78.706648, 31.840797], [78.700343, 31.821676], [78.674091, 31.78695], [78.670887, 31.770517], [78.695589, 31.746694], [78.695175, 31.737961], [78.692592, 31.7284], [78.693212, 31.71853], [78.698069, 31.711037], [78.710472, 31.69817], [78.71843, 31.6806], [78.724011, 31.673313], [78.731349, 31.668042], [78.739824, 31.666027], [78.758117, 31.666595], [78.795324, 31.633781], [78.818889, 31.607374], [78.814031, 31.595023], [78.796978, 31.578849], [78.699516, 31.510016], [78.695382, 31.488105], [78.733003, 31.467744], [78.762561, 31.44511], [78.760081, 31.412037], [78.745508, 31.373538], [78.73848, 31.334988], [78.740857, 31.317728], [78.745095, 31.308116], [78.753156, 31.301966], [78.784472, 31.288272], [78.796048, 31.288117], [78.821989, 31.293698], [78.848654, 31.290908], [78.861987, 31.291476], [78.886972, 31.284171], [78.921008, 31.269685], [78.940282, 31.301576], [78.959552, 31.325117], [78.988801, 31.343256], [79.015529, 31.383397], [79.049937, 31.422953], [79.07021, 31.461381], [79.07799, 31.461664], [79.131397, 31.438433], [79.157693, 31.414926], [79.179218, 31.393796], [79.192442, 31.369789], [79.207628, 31.351373], [79.238319, 31.329656], [79.239239, 31.311724], [79.25411, 31.299006], [79.262024, 31.278513], [79.24843, 31.256704], [79.25286, 31.245437], [79.278316, 31.240044], [79.288321, 31.225474], [79.298078, 31.203242], [79.303209, 31.183104], [79.302162, 31.16345], [79.319417, 31.14228], [79.367048, 31.111559], [79.372343, 31.094072], [79.382593, 31.078335], [79.388363, 31.069474], [79.394978, 31.036711], [79.401592, 31.023637], [79.414408, 31.020382], [79.430738, 31.023172], [79.446344, 31.023792], [79.46164, 31.021105], [79.47642, 31.014025], [79.480761, 31.010046], [79.484275, 31.005447], [79.486858, 31.000228], [79.488202, 30.994492], [79.497607, 30.981211], [79.512697, 30.966173], [79.529646, 30.953564], [79.544219, 30.947414], [79.565923, 30.942454], [79.577809, 30.938371], [79.589488, 30.940231], [79.630312, 30.962504], [79.648192, 30.96576], [79.6885, 30.967413], [79.739039, 30.979247], [79.76064, 30.977232], [79.833504, 30.961522], [79.85035, 30.954494], [79.862856, 30.941523], [79.880426, 30.912843], [79.903164, 30.890209], [79.914635, 30.883819], [79.91937, 30.881182], [79.934893, 30.872535], [80.005586, 30.847266], [80.028634, 30.830781], [80.044447, 30.8067], [80.062534, 30.784789], [80.092196, 30.774505], [80.109249, 30.778122], [80.140669, 30.793212], [80.157102, 30.79316], [80.169297, 30.785254], [80.196066, 30.748098], [80.224591, 30.734094], [80.231412, 30.724947], [80.207021, 30.686138], [80.19989, 30.680712], [80.191725, 30.679679], [80.18201, 30.680041], [80.172915, 30.679162], [80.16661, 30.674408], [80.16692, 30.661902], [80.177255, 30.649862], [80.190588, 30.637976], [80.19989, 30.625884], [80.2033, 30.60635], [80.197823, 30.591364], [80.188418, 30.576998], [80.179839, 30.559479], [80.21622, 30.566818], [80.252806, 30.565009], [80.325877, 30.546509], [80.424992, 30.497829], [80.475429, 30.480466], [80.508295, 30.462328], [80.525141, 30.458607], [80.545088, 30.461036], [80.559971, 30.465118], [80.575267, 30.466255], [80.596351, 30.459796], [80.694743, 30.411737], [80.723062, 30.392048], [80.735361, 30.378199], [80.754998, 30.345797], [80.767194, 30.331431], [80.781663, 30.320993], [80.866981, 30.288488], [80.943772, 30.270143], [80.976018, 30.255209], [80.996275, 30.22689], [81.003303, 30.212731], [80.996017, 30.196969], [80.988162, 30.196556], [80.920827, 30.17666], [80.903309, 30.180433], [80.893647, 30.198104], [80.883879, 30.210405], [80.883684, 30.21028], [80.867911, 30.200173], [80.850393, 30.181931], [80.83644, 30.170046], [80.849669, 30.143381], [80.829825, 30.117129], [80.769674, 30.077287], [80.755721, 30.064574], [80.725749, 30.022768], [80.715827, 30.013311], [80.680171, 29.992072], [80.654126, 29.970575], [80.641413, 29.963444], [80.622293, 29.958173], [80.586223, 29.954142], [80.57134, 29.946855], [80.562865, 29.929699], [80.549533, 29.89368], [80.527312, 29.862416], [80.476152, 29.80614], [80.454861, 29.790586], [80.395227, 29.776582], [80.368975, 29.757926], [80.354299, 29.730279], [80.354402, 29.70488], [80.363911, 29.679714], [80.377553, 29.652946], [80.386752, 29.626823], [80.385201, 29.604835], [80.373316, 29.58419], [80.350785, 29.562073], [80.345101, 29.558352], [80.332698, 29.552461], [80.327221, 29.548585], [80.323603, 29.54166], [80.32505, 29.535511], [80.327634, 29.52993], [80.327221, 29.52484], [80.320089, 29.515822], [80.311511, 29.508122], [80.29146, 29.494738], [80.29115, 29.494609], [80.282055, 29.4843], [80.273064, 29.478667], [80.266242, 29.47213], [80.263452, 29.459237], [80.257457, 29.450064], [80.228312, 29.441718], [80.217666, 29.434639], [80.213739, 29.416888], [80.220974, 29.400119], [80.242161, 29.367408], [80.249499, 29.326997], [80.254977, 29.316635], [80.263555, 29.315318], [80.272754, 29.315705], [80.280092, 29.31015], [80.282779, 29.291314], [80.278748, 29.268085], [80.258181, 29.202456], [80.248672, 29.204394], [80.236063, 29.213076], [80.218803, 29.211086], [80.213739, 29.196565], [80.230482, 29.154733], [80.233066, 29.139308], [80.220664, 29.126079], [80.20144, 29.121169], [80.180563, 29.121324], [80.16351, 29.123366], [80.132607, 29.110214], [80.113073, 29.072206], [80.104702, 29.027609], [80.107906, 28.994769], [80.107906, 28.994588], [80.099431, 28.977276], [80.085168, 28.967484], [80.068425, 28.959939], [80.052922, 28.949113], [80.04052, 28.932809], [80.033905, 28.915936], [80.031115, 28.897798], [80.030288, 28.87767], [80.036386, 28.837026], [80.054782, 28.824185], [80.073054, 28.820925], [80.081861, 28.819353], [80.11421, 28.802584], [80.147593, 28.76331], [80.162063, 28.753285], [80.181183, 28.74729], [80.216426, 28.741942], [80.233273, 28.732718], [80.238957, 28.726258], [80.249396, 28.710213], [80.257044, 28.702745], [80.265209, 28.699283], [80.283399, 28.695356], [80.291047, 28.689671], [80.318332, 28.640113], [80.329701, 28.627479], [80.349751, 28.620218], [80.369078, 28.622647], [80.388302, 28.627246], [80.408352, 28.626342], [80.426543, 28.616807], [80.468814, 28.571849], [80.488451, 28.562444], [80.493412, 28.575647], [80.484937, 28.639287], [80.487624, 28.656469], [80.497546, 28.670137], [80.517596, 28.680033], [80.534444, 28.679555], [80.555837, 28.678948], [80.559041, 28.673031], [80.557387, 28.66435], [80.556871, 28.655022], [80.563589, 28.647038], [80.581365, 28.638951], [80.598522, 28.633783], [80.635625, 28.627866], [80.648338, 28.619753], [80.668182, 28.586344], [80.67862, 28.574717], [80.695777, 28.567508], [80.727093, 28.559679], [80.743526, 28.549964], [80.781663, 28.514746], [80.798406, 28.505703], [80.81701, 28.502551], [80.857989, 28.502447], [80.872252, 28.496866], [80.880365, 28.48188], [80.881864, 28.466842], [80.88667, 28.452786], [80.905273, 28.440849], [80.921344, 28.436198], [80.941188, 28.432684], [80.960515, 28.432271], [80.976018, 28.436922], [80.9878, 28.430979], [80.991314, 28.420256], [80.993175, 28.407828], [81.000409, 28.397002], [81.047066, 28.389088], [81.146344, 28.372249], [81.169701, 28.361319], [81.190372, 28.338116], [81.210732, 28.278637], [81.224323, 28.250783], [81.282356, 28.191588], [81.295947, 28.167455], [81.280754, 28.161977], [81.276775, 28.153787], [81.283079, 28.146087], [81.299409, 28.142521], [81.296205, 28.12831], [81.307057, 28.123814], [81.323697, 28.126243], [81.338115, 28.132728], [81.347262, 28.144433], [81.351447, 28.15681], [81.357907, 28.165982], [81.373203, 28.167843], [81.396044, 28.160634], [81.417335, 28.147017], [81.435267, 28.129783], [81.448237, 28.111412], [81.45294, 28.097046], [81.454025, 28.086607], [81.458211, 28.07728], [81.473455, 28.066402], [81.561925, 28.025991], [81.582183, 28.013278], [81.595102, 27.994881], [81.59536, 27.994778], [81.615049, 27.981239], [81.665433, 27.9708], [81.688946, 27.963204], [81.710392, 27.94752], [81.750131, 27.909667], [81.800154, 27.884087], [81.827852, 27.865639], [81.855654, 27.850937], [81.883198, 27.849051], [81.906091, 27.863107], [81.946398, 27.905352], [81.975905, 27.916953], [82.02722, 27.912406], [82.050212, 27.905586], [82.051611, 27.905171], [82.071662, 27.89003], [82.090369, 27.872331], [82.107422, 27.863572], [82.151037, 27.848275], [82.270409, 27.760477], [82.347924, 27.726009], [82.401926, 27.677175], [82.440683, 27.666426], [82.526156, 27.675211], [82.652143, 27.70415], [82.673891, 27.696458], [82.679687, 27.694409], [82.69705, 27.669397], [82.709246, 27.630924], [82.718857, 27.556123], [82.72971, 27.518166], [82.752137, 27.494964], [82.876264, 27.487548], [82.901275, 27.480365], [82.947164, 27.457317], [83.010468, 27.443391], [83.132786, 27.444217], [83.169631, 27.431195], [83.21924, 27.393781], [83.231591, 27.381224], [83.243115, 27.362155], [83.249419, 27.348099], [83.259496, 27.338126], [83.282441, 27.330943], [83.304868, 27.331925], [83.32435, 27.341691], [83.341145, 27.356936], [83.362746, 27.385616], [83.369567, 27.398174], [83.370962, 27.410214], [83.355821, 27.428663], [83.353857, 27.44029], [83.355925, 27.452486], [83.360989, 27.462201], [83.387034, 27.470469], [83.480981, 27.469746], [83.590432, 27.45662], [83.663399, 27.43228], [83.801995, 27.365928], [83.847988, 27.351045], [83.85334, 27.346194], [83.854602, 27.34505], [83.861733, 27.354507], [83.867521, 27.358383], [83.877391, 27.361742], [83.87765, 27.369907], [83.873671, 27.380087], [83.87119, 27.38944], [83.842717, 27.418069], [83.834087, 27.434037], [83.853517, 27.440962], [83.899871, 27.443856], [83.922043, 27.449696], [83.923022, 27.449954], [83.935941, 27.446078], [83.975835, 27.439722], [84.007668, 27.440807], [84.028907, 27.453726], [84.079601, 27.509536], [84.099548, 27.516874], [84.11717, 27.513283], [84.121563, 27.494964], [84.130968, 27.486411], [84.141716, 27.480753], [84.165746, 27.472174], [84.175099, 27.462976], [84.185848, 27.438636], [84.195253, 27.436053], [84.225536, 27.440393], [84.239023, 27.431143], [84.248893, 27.412436], [84.267703, 27.388562], [84.289408, 27.376108], [84.577039, 27.329031], [84.606546, 27.310479], [84.631919, 27.277044], [84.648197, 27.240716], [84.657654, 27.203405], [84.659824, 27.165113], [84.654398, 27.125374], [84.644528, 27.103721], [84.630369, 27.080829], [84.62148, 27.057988], [84.627268, 27.03649], [84.640239, 27.028377], [84.760697, 26.999025], [84.771962, 26.99918], [84.785501, 27.008482], [84.801934, 27.013753], [84.817489, 27.0106], [84.828134, 26.994943], [84.828289, 26.994839], [84.851751, 26.982127], [84.901567, 26.967192], [84.924046, 26.955668], [84.938567, 26.9366], [84.944251, 26.915723], [84.952726, 26.897636], [84.975774, 26.886887], [84.988176, 26.883787], [85.000837, 26.882236], [85.018045, 26.874433], [85.016857, 26.85924], [85.018562, 26.845804], [85.043987, 26.843737], [85.100211, 26.863529], [85.122845, 26.8657], [85.162016, 26.851024], [85.16553, 26.820793], [85.165685, 26.786273], [85.194934, 26.758885], [85.28728, 26.736974], [85.302369, 26.737336], [85.337044, 26.746792], [85.353219, 26.757593], [85.355352, 26.759912], [85.369238, 26.775008], [85.385878, 26.788444], [85.40319, 26.787617], [85.421535, 26.782656], [85.439622, 26.78772], [85.475692, 26.805238], [85.519462, 26.826426], [85.598578, 26.854383], [85.609379, 26.851024], [85.687772, 26.811853], [85.701828, 26.796608], [85.709631, 26.76245], [85.702241, 26.68845], [85.712887, 26.653206], [85.727046, 26.6376], [85.780996, 26.599204], [85.789988, 26.597034], [85.800065, 26.600703], [85.809728, 26.603028], [85.817635, 26.596724], [85.819547, 26.588301], [85.819753, 26.579516], [85.821614, 26.571713], [85.828538, 26.566131], [85.844765, 26.568509], [85.866366, 26.579929], [85.934682, 26.632897], [85.952148, 26.642044], [85.975713, 26.64437], [86.01137, 26.654447], [86.041704, 26.645455], [86.110692, 26.606749], [86.115859, 26.602563], [86.122061, 26.60029], [86.144798, 26.60153], [86.146355, 26.601356], [86.152653, 26.600651], [86.167794, 26.596621], [86.17446, 26.593313], [86.179266, 26.588611], [86.185364, 26.584425], [86.195855, 26.582668], [86.202883, 26.58458], [86.225155, 26.597396], [86.263086, 26.609075], [86.284428, 26.61202], [86.301378, 26.609023], [86.308613, 26.60215], [86.309026, 26.587991], [86.316002, 26.580963], [86.323185, 26.580084], [86.344838, 26.582616], [86.353674, 26.582616], [86.383647, 26.572849], [86.444832, 26.543084], [86.475476, 26.531973], [86.494906, 26.527839], [86.510616, 26.520139], [86.52431, 26.509081], [86.537746, 26.49487], [86.537953, 26.49487], [86.5579, 26.484018], [86.625337, 26.456319], [86.695204, 26.418234], [86.713601, 26.414565], [86.724091, 26.421954], [86.730706, 26.433788], [86.738199, 26.44371], [86.751893, 26.445519], [86.78724, 26.433065], [86.796438, 26.431514], [86.821863, 26.438129], [86.846357, 26.45265], [86.865684, 26.472442], [86.875968, 26.494921], [86.907129, 26.51151], [86.935153, 26.520289], [86.972448, 26.531973], [86.985212, 26.540655], [87.015443, 26.568974], [87.029912, 26.579774], [87.041333, 26.580187], [87.044433, 26.561171], [87.045002, 26.544272], [87.056474, 26.494921], [87.056681, 26.49487], [87.066706, 26.465621], [87.083294, 26.432083], [87.10629, 26.404746], [87.135022, 26.394204], [87.188455, 26.399578], [87.2191, 26.408105], [87.229435, 26.398752], [87.236308, 26.3833], [87.2453, 26.370226], [87.258425, 26.36294], [87.30049, 26.34599], [87.314029, 26.343768], [87.326225, 26.353328], [87.344932, 26.389346], [87.356404, 26.403712], [87.384206, 26.418647], [87.416452, 26.426967], [87.449628, 26.428621], [87.480324, 26.423401], [87.552051, 26.386711], [87.586984, 26.378029], [87.623984, 26.392912], [87.648745, 26.409993], [87.659641, 26.41751], [87.68119, 26.424228], [87.697572, 26.41627], [87.710904, 26.405676], [87.727544, 26.403764], [87.74222, 26.410482], [87.749455, 26.425727], [87.756173, 26.446914], [87.768988, 26.451461], [87.785938, 26.445984], [87.804439, 26.437354], [87.821595, 26.437509], [87.852084, 26.46066], [87.869809, 26.464639], [87.870688, 26.460712], [87.894769, 26.442935], [87.897301, 26.443452], [87.902004, 26.435287], [87.908515, 26.41782], [87.914768, 26.40826], [87.928979, 26.396426], [87.961225, 26.378959], [87.975488, 26.366557], [88.006648, 26.369864], [88.044321, 26.405676], [88.074189, 26.453942], [88.082251, 26.49487], [88.07915, 26.507375], [88.079822, 26.517556], [88.087136, 26.5391], [88.101681, 26.581944], [88.14664, 26.661216], [88.163383, 26.705141], [88.167517, 26.725037], [88.169067, 26.744002], [88.167827, 26.762915], [88.159042, 26.802551], [88.155321, 26.845598], [88.151394, 26.862806], [88.142816, 26.878412], [88.12044, 26.90885], [88.11181, 26.924301], [88.096824, 26.959337], [88.076877, 26.99179], [88.055948, 27.018042], [88.042822, 27.028946], [88.027371, 27.035353], [88.009491, 27.045637], [87.9913, 27.081501], [87.975488, 27.095143], [87.970733, 27.10274], [87.969183, 27.110801], [87.970837, 27.119224], [87.985461, 27.14837], [87.989337, 27.218391], [88.004663, 27.249163], [88.029024, 27.298076], [88.035122, 27.322571], [88.032538, 27.333888], [88.020136, 27.35337], [88.015795, 27.364222], [88.014762, 27.378072], [88.017242, 27.389234], [88.02582, 27.412075], [88.029593, 27.418741], [88.034812, 27.423805], [88.039308, 27.42949], [88.041633, 27.438068], [88.039773, 27.442357], [88.030781, 27.450729], [88.028094, 27.455121], [88.02334, 27.474655], [88.0221, 27.484241], [88.02334, 27.494912], [88.048765, 27.545348], [88.11088, 27.639503], [88.134858, 27.723167], [88.149327, 27.748772], [88.159662, 27.77412], [88.154598, 27.815151], [88.166587, 27.833599], [88.164623, 27.845356], [88.156407, 27.851273], [88.143023, 27.855717], [88.118218, 27.860885], [88.104885, 27.879721], [88.097496, 27.904008], [88.099924, 27.928322], [88.115737, 27.947262], [88.126693, 27.95044], [88.151187, 27.94721], [88.16297, 27.9469], [88.174855, 27.949768], [88.197851, 27.958295], [88.37877, 27.982634], [88.399751, 27.994726], [88.399854, 27.994726], [88.399906, 27.994881], [88.455768, 28.03152], [88.475405, 28.036248], [88.50207, 28.028885], [88.51716, 28.039582], [88.530802, 28.059012], [88.552817, 28.078055], [88.594003, 28.106606], [88.610488, 28.105831], [88.631675, 28.083352], [88.652035, 28.069399], [88.71012, 28.061983], [88.735648, 28.055265], [88.780141, 28.028342], [88.802931, 28.011005], [88.817503, 27.994881], [88.81771, 27.994881], [88.81771, 27.994726], [88.819674, 27.977337], [88.809752, 27.944497], [88.810682, 27.927857], [88.818899, 27.915196], [88.842773, 27.892407], [88.851558, 27.877163], [88.855072, 27.859179], [88.853884, 27.843676], [88.805669, 27.655109], [88.783087, 27.622036], [88.767377, 27.586198], [88.748154, 27.560128], [88.741022, 27.54571], [88.741642, 27.53168], [88.757559, 27.511448], [88.757197, 27.494964], [88.754355, 27.464216], [88.759781, 27.435071], [88.773578, 27.408509], [88.795386, 27.385926], [88.808202, 27.378382], [88.819829, 27.373627], [88.830836, 27.367323], [88.852023, 27.342363], [88.864632, 27.33239], [88.892331, 27.315543], [88.884631, 27.286346], [88.876116, 27.280486], [88.861428, 27.270378], [88.801174, 27.256425], [88.775852, 27.240871], [88.754355, 27.212604], [88.738438, 27.179789], [88.730067, 27.150954], [88.733082, 27.148972], [88.742572, 27.142737], [88.805256, 27.113023], [88.827632, 27.097882], [88.840809, 27.075248], [88.845615, 27.049565], [88.845615, 26.994943], [88.845615, 26.994839], [88.851713, 26.945437], [88.867113, 26.964247], [88.88644, 26.978975], [88.906645, 26.981093], [88.924474, 26.961663], [88.926954, 26.949416], [88.925404, 26.939029], [88.9253, 26.929365], [88.932328, 26.919237], [88.942767, 26.913707], [88.954549, 26.912622], [88.965815, 26.915464], [88.975323, 26.921717], [88.996614, 26.922751], [89.021574, 26.912674], [89.04488, 26.897119], [89.060693, 26.881461], [89.074335, 26.856398], [89.082448, 26.836037], [89.096504, 26.821413], [89.128079, 26.813455], [89.184613, 26.810561], [89.212828, 26.813041], [89.240837, 26.819759], [89.252619, 26.826787], [89.262954, 26.836348], [89.273599, 26.843892], [89.286364, 26.844978], [89.300161, 26.844409], [89.341657, 26.854331], [89.368895, 26.837359], [89.407338, 26.813403], [89.442891, 26.797022], [89.50542, 26.803688], [89.546503, 26.797539], [89.586087, 26.784051], [89.611357, 26.765964], [89.613475, 26.748549], [89.597507, 26.720954], [89.609806, 26.712221], [89.628307, 26.712531], [89.66324, 26.725502], [89.685151, 26.724571], [89.738739, 26.703281], [89.75467, 26.700989], [89.760288, 26.70018], [89.800079, 26.70049], [89.804058, 26.699767], [89.81367, 26.696149], [89.817287, 26.696201], [89.822093, 26.701007], [89.825245, 26.707673], [89.826796, 26.713513], [89.826744, 26.715683], [89.827261, 26.717905], [89.827674, 26.722453], [89.829741, 26.72762], [89.834961, 26.731548], [89.839043, 26.731238], [89.847001, 26.72483], [89.850774, 26.723176], [89.858701, 26.722057], [89.85992, 26.721884], [89.880281, 26.716252], [89.890409, 26.714856], [89.912113, 26.716717], [89.975262, 26.731858], [90.089157, 26.741728], [90.127191, 26.751392], [90.152202, 26.771752], [90.17742, 26.832058], [90.210907, 26.851489], [90.229148, 26.852884], [90.265994, 26.851592], [90.284442, 26.85707], [90.300875, 26.868284], [90.314208, 26.880376], [90.328574, 26.890659], [90.348831, 26.896654], [90.382627, 26.891796], [90.475232, 26.83242], [90.587783, 26.780072], [90.717077, 26.767049], [90.944493, 26.77887], [91.007395, 26.782139], [91.012781, 26.784348], [91.062069, 26.804567], [91.091938, 26.804773], [91.127078, 26.800898], [91.198185, 26.802344], [91.232498, 26.795161], [91.261954, 26.779038], [91.27601, 26.774077], [91.296577, 26.774594], [91.313217, 26.778677], [91.330063, 26.785498], [91.345566, 26.794696], [91.358382, 26.806169], [91.370061, 26.824669], [91.378122, 26.842807], [91.388457, 26.858465], [91.406338, 26.869524], [91.420187, 26.871539], [91.460598, 26.869369], [91.475067, 26.865545], [91.484472, 26.852729], [91.507417, 26.808029], [91.520646, 26.79759], [91.539353, 26.79883], [91.57563, 26.810148], [91.593716, 26.810768], [91.637951, 26.798985], [91.653764, 26.797952], [91.702133, 26.803481], [91.731589, 26.816194], [91.795254, 26.853504], [91.825123, 26.858465], [91.843934, 26.84937], [91.849515, 26.834746], [91.852408, 26.818519], [91.863054, 26.804773], [91.87866, 26.803016], [91.886928, 26.814437], [91.885895, 26.83087], [91.874009, 26.844306], [91.895507, 26.853504], [91.895507, 26.868284], [91.89313, 26.881358], [91.908219, 26.885182], [91.925686, 26.878671], [91.957415, 26.854279], [91.975088, 26.846631], [92.03586, 26.854848], [92.072757, 26.887766], [92.080371, 26.921571], [92.083919, 26.937323], [92.066969, 26.994839], [92.066969, 26.994943], [92.049709, 27.026827], [91.999893, 27.071527], [91.987697, 27.104031], [91.987904, 27.122273], [91.990798, 27.140773], [91.995862, 27.158033], [92.002994, 27.172606], [92.005788, 27.176112], [92.008471, 27.179479], [92.021494, 27.19183], [92.027075, 27.19984], [92.029969, 27.20847], [92.033173, 27.227435], [92.03679, 27.236478], [92.050639, 27.251413], [92.081852, 27.275132], [92.088776, 27.29234], [92.084849, 27.304226], [92.023598, 27.44678], [91.975088, 27.472433], [91.963306, 27.468919], [91.900591, 27.45973], [91.856578, 27.466239], [91.816232, 27.461357], [91.779555, 27.456475], [91.727092, 27.459942], [91.727041, 27.460005], [91.704924, 27.468764], [91.680223, 27.472846], [91.657382, 27.479151], [91.641672, 27.494964], [91.632887, 27.511655], [91.604465, 27.532145], [91.59506, 27.546382], [91.573252, 27.619711], [91.579867, 27.657977], [91.626686, 27.716423], [91.634957, 27.746571], [91.63577, 27.782525], [91.605336, 27.810658], [91.566023, 27.821074], [91.564355, 27.851372], [91.628339, 27.852694], [91.680131, 27.849823], [91.729871, 27.804136], [91.826589, 27.807627], [91.856373, 27.764539], [91.864914, 27.729988], [91.908736, 27.7319], [91.952247, 27.72482], [91.968001, 27.746278], [91.988898, 27.769398], [92.064878, 27.761721], [92.10616, 27.786081], [92.126294, 27.812722], [92.239568, 27.865277], [92.24918, 27.862642], [92.258275, 27.848198], [92.275225, 27.811689], [92.288764, 27.793085], [92.303854, 27.78616], [92.31729, 27.803679], [92.329175, 27.832928], [92.334653, 27.831274], [92.339821, 27.815564], [92.350053, 27.802542], [92.367933, 27.803834], [92.375581, 27.821119], [92.381058, 27.842462], [92.392841, 27.856027], [92.404416, 27.853831], [92.417335, 27.828948], [92.427567, 27.821171], [92.439039, 27.82329], [92.475109, 27.846596], [92.518151, 27.839468], [92.574977, 27.847806], [92.592912, 27.868193], [92.636468, 27.89537], [92.683674, 27.91045], [92.71077, 27.949702], [92.736392, 27.985909], [92.701349, 28.025241], [92.701349, 28.037825], [92.689257, 28.048315], [92.65422, 28.052087], [92.638924, 28.057487], [92.63727, 28.071983], [92.65484, 28.105831], [92.678611, 28.133064], [92.70786, 28.155414], [92.770079, 28.192182], [92.779174, 28.195903], [92.782894, 28.190942], [92.785375, 28.183294], [92.790232, 28.178901], [92.805322, 28.178023], [92.819895, 28.179263], [92.834777, 28.183526], [92.85028, 28.19195], [92.86537, 28.205205], [92.889554, 28.235823], [92.903817, 28.249698], [92.922007, 28.258638], [92.960351, 28.270214], [92.974924, 28.282306], [93.046399, 28.302097], [93.129828, 28.325962], [93.207825, 28.340539], [93.218073, 28.394647], [93.186286, 28.431995], [93.218838, 28.457436], [93.259068, 28.496023], [93.286972, 28.520517], [93.302843, 28.556921], [93.31717, 28.603463], [93.355432, 28.624065], [93.446213, 28.671894], [93.551736, 28.678948], [93.607133, 28.672204], [93.62553, 28.67236], [93.64341, 28.68024], [93.664908, 28.690239], [93.705525, 28.691893], [93.722475, 28.696647], [93.781744, 28.680504], [93.86308, 28.704871], [93.927786, 28.682752], [93.961093, 28.729943], [93.981591, 28.768131], [93.993136, 28.801887], [93.992019, 28.844649], [94.01145, 28.85302], [94.026849, 28.864182], [94.079474, 28.883072], [94.135396, 28.897788], [94.173658, 28.930164], [94.250183, 28.933107], [94.291389, 28.977256], [94.347311, 29.024349], [94.309048, 29.059668], [94.270786, 29.09793], [94.287505, 29.147938], [94.323782, 29.146026], [94.335771, 29.149617], [94.346313, 29.159074], [94.36254, 29.185326], [94.372875, 29.196307], [94.396439, 29.207081], [94.422381, 29.210492], [94.474987, 29.210802], [94.498242, 29.21522], [94.514778, 29.22106], [94.528628, 29.23124], [94.582681, 29.302915], [94.599941, 29.316635], [94.63043, 29.319452], [94.668464, 29.30661], [94.704224, 29.284699], [94.756417, 29.230517], [94.767683, 29.213851], [94.761482, 29.174706], [94.776571, 29.166696], [94.798792, 29.166438], [94.815432, 29.168867], [94.854086, 29.170004], [94.891293, 29.160495], [94.989065, 29.153984], [95.047666, 29.140806], [95.099032, 29.11378], [95.116912, 29.108095], [95.191533, 29.09683], [95.199698, 29.094246], [95.207863, 29.089595], [95.212824, 29.081869], [95.214064, 29.073084], [95.216338, 29.064997], [95.224916, 29.059364], [95.281553, 29.052672], [95.367406, 29.036496], [95.429824, 29.046525], [95.487577, 29.068994], [95.511307, 29.131789], [95.521435, 29.137861], [95.522675, 29.161193], [95.511927, 29.197547], [95.515337, 29.209433], [95.550167, 29.212482], [95.552028, 29.22013], [95.550684, 29.230284], [95.553785, 29.239043], [95.56443, 29.245606], [95.572905, 29.24726], [95.58169, 29.247467], [95.592335, 29.249689], [95.646697, 29.22835], [95.717165, 29.218284], [95.747365, 29.273651], [95.744781, 29.340432], [95.776407, 29.345548], [95.79594, 29.352886], [95.863134, 29.323985], [95.953735, 29.359219], [96.014135, 29.364252], [96.074536, 29.369286], [96.141966, 29.368467], [96.150957, 29.354075], [96.166047, 29.303174], [96.176279, 29.286456], [96.205424, 29.256949], [96.235293, 29.241214], [96.26785, 29.24173], [96.304126, 29.261212], [96.323247, 29.27501], [96.337096, 29.279713], [96.349912, 29.274235], [96.366241, 29.257233], [96.366862, 29.244211], [96.342367, 29.210647], [96.327484, 29.180442], [96.316425, 29.171864], [96.210075, 29.145767], [96.193642, 29.136931], [96.183514, 29.123779], [96.174625, 29.108845], [96.175954, 29.017354], [96.195323, 28.941106], [96.248588, 28.945343], [96.294655, 28.992324], [96.366568, 29.036537], [96.457605, 28.994588], [96.451198, 28.981385], [96.452128, 28.970378], [96.460086, 28.96335], [96.474659, 28.962058], [96.492229, 28.948002], [96.500394, 28.929243], [96.510832, 28.885525], [96.523751, 28.864415], [96.576668, 28.808527], [96.592584, 28.757884], [96.59801, 28.70991], [96.501044, 28.687631], [96.502375, 28.644476], [96.409019, 28.558929], [96.336459, 28.479667], [96.301852, 28.420711], [96.369644, 28.378116], [96.441127, 28.395416], [96.495226, 28.42147], [96.512382, 28.428808], [96.561518, 28.447293], [96.604426, 28.444534], [96.716924, 28.436539], [96.897889, 28.354704], [96.933959, 28.336618], [96.965791, 28.330468], [96.997417, 28.337083], [97.032971, 28.35703], [97.078446, 28.375143], [97.11555, 28.366564], [97.182729, 28.314991], [97.193271, 28.3114], [97.20278, 28.311555], [97.210531, 28.308092], [97.220763, 28.283391], [97.229651, 28.274606], [97.285255, 28.235668], [97.323496, 28.217478], [97.328663, 28.190012], [97.298898, 28.12924], [97.292903, 28.095082], [97.306546, 28.069657], [97.355122, 28.023045], [97.362253, 27.994881], [97.356775, 27.980567], [97.340032, 27.951809], [97.335691, 27.935609], [97.336311, 27.913181], [97.338172, 27.901554], [97.333521, 27.894164], [97.314917, 27.884604], [97.292903, 27.87755], [97.288666, 27.884346], [97.288046, 27.897756], [97.276367, 27.910339], [97.260657, 27.912044], [97.241227, 27.907858], [97.222933, 27.899719], [97.210944, 27.889513], [97.198025, 27.873261], [97.182109, 27.857267], [97.131776, 27.816856], [97.123198, 27.812154], [97.112449, 27.809208], [97.106041, 27.805539], [97.098186, 27.791018], [97.092192, 27.785489], [97.072348, 27.779184], [97.06532, 27.773991], [97.062013, 27.763345], [97.060359, 27.750452], [97.056742, 27.746783], [97.050541, 27.746783], [97.036175, 27.74292], [97.004032, 27.734277], [96.990906, 27.726629], [96.957006, 27.688079], [96.93954, 27.674023], [96.899749, 27.649476], [96.883523, 27.636325], [96.87019, 27.61909], [96.862129, 27.599298], [96.862025, 27.578369], [96.866159, 27.568448], [96.877321, 27.553074], [96.880319, 27.54385], [96.881042, 27.534393], [96.884659, 27.515841], [96.888484, 27.507185], [96.890241, 27.492483], [96.877011, 27.463079], [96.881352, 27.444114], [96.905123, 27.408406], [97.067904, 27.2171], [97.095499, 27.191261], [97.101287, 27.183303], [97.1017, 27.165268], [97.104388, 27.156018], [97.111726, 27.148835], [97.120821, 27.143926], [97.129296, 27.13788], [97.134773, 27.127338], [97.133016, 27.119586], [97.122888, 27.093696], [97.119167, 27.087288], [97.106041, 27.082586], [97.09798, 27.085893], [97.089918, 27.092198], [97.077206, 27.096487], [97.066974, 27.095246], [97.058827, 27.092076], [97.047854, 27.087805], [97.038035, 27.08703], [97.02677, 27.091474], [97.018915, 27.098709], [97.011887, 27.107029], [97.002998, 27.114522], [96.865332, 27.171624], [96.842698, 27.188522], [96.841768, 27.204387], [96.851276, 27.221182], [96.859648, 27.240716], [96.853757, 27.248725], [96.821718, 27.273065], [96.809935, 27.285777], [96.789161, 27.317972], [96.776346, 27.330633], [96.758879, 27.341743], [96.724152, 27.356264], [96.705239, 27.36076], [96.687255, 27.361742], [96.675887, 27.357659], [96.659247, 27.341433], [96.649015, 27.336214], [96.636406, 27.335542], [96.629275, 27.338694], [96.623177, 27.343655], [96.614185, 27.348409], [96.5869, 27.353577], [96.576048, 27.345412], [96.56747, 27.328307], [96.547316, 27.306448], [96.531813, 27.298542], [96.510936, 27.292495], [96.490058, 27.290738], [96.474659, 27.295389], [96.407789, 27.29818], [96.142586, 27.25751], [96.074166, 27.229864], [96.013188, 27.190796], [95.974844, 27.142892], [95.938361, 27.079899], [95.91645, 27.051373], [95.888855, 27.027034], [95.861466, 27.014218], [95.804932, 27.004761], [95.777957, 26.995149], [95.734962, 26.947297], [95.709227, 26.907454], [95.699305, 26.896189], [95.686076, 26.89128], [95.639981, 26.886422], [95.624271, 26.880738], [95.613316, 26.867922], [95.603394, 26.844254], [95.591302, 26.823739], [95.574765, 26.816142], [95.554405, 26.816607], [95.531357, 26.820276], [95.489809, 26.810716], [95.462731, 26.777178], [95.439993, 26.735372], [95.410641, 26.701627], [95.392658, 26.691757], [95.31597, 26.669071], [95.278969, 26.652948], [95.260573, 26.647625], [95.246933, 26.648904], [95.239075, 26.649641], [95.223262, 26.670363], [95.204865, 26.667572], [95.195564, 26.660751], [95.181611, 26.641682], [95.173239, 26.633828], [95.163421, 26.629745], [95.141407, 26.624991], [95.132622, 26.620805], [95.122803, 26.611607], [95.119082, 26.604217], [95.115879, 26.57564], [95.116189, 26.569335], [95.114948, 26.562566], [95.109574, 26.5513], [95.099239, 26.536107], [95.069887, 26.506239], [95.054384, 26.494921], [95.040638, 26.475594], [95.042601, 26.459678], [95.061722, 26.426605], [95.066062, 26.407433], [95.064202, 26.392602], [95.050456, 26.359064], [95.043635, 26.327696], [95.041775, 26.287596], [95.046322, 26.24796], [95.058724, 26.217677], [95.066373, 26.211011], [95.074537, 26.208324], [95.082806, 26.206619], [95.090764, 26.202691], [95.094484, 26.195043], [95.08973, 26.187602], [95.082392, 26.179798], [95.078361, 26.171324], [95.08818, 26.104093], [95.101719, 26.093395], [95.137479, 26.082957], [95.150088, 26.070968], [95.151225, 26.049936], [95.139546, 26.029937], [95.10823, 25.995107], [95.065856, 25.953817], [95.049423, 25.943741], [95.012009, 25.931442], [95.00064, 25.922036], [94.994026, 25.900436], [94.993199, 25.880954], [95.013456, 25.777704], [95.015316, 25.755122], [95.008805, 25.737138], [94.999917, 25.731557], [94.978626, 25.729077], [94.969014, 25.725614], [94.961469, 25.717966], [94.871656, 25.598025], [94.868038, 25.594718], [94.857496, 25.589344], [94.853466, 25.586036], [94.852949, 25.580765], [94.86101, 25.571412], [94.860287, 25.567174], [94.850778, 25.562782], [94.831038, 25.562472], [94.822356, 25.559113], [94.80799, 25.542628], [94.782359, 25.504594], [94.764375, 25.491675], [94.745772, 25.486611], [94.711149, 25.482787], [94.692028, 25.476327], [94.659472, 25.455502], [94.65036, 25.44665], [94.630533, 25.42739], [94.608003, 25.394627], [94.550125, 25.245282], [94.553329, 25.204147], [94.576893, 25.173607], [94.612964, 25.161359], [94.653271, 25.154125], [94.689135, 25.138518], [94.706808, 25.109941], [94.713836, 25.068264], [94.708565, 25.02589], [94.689755, 24.99509], [94.673528, 24.974807], [94.654821, 24.889154], [94.639112, 24.862799], [94.599011, 24.813474], [94.593223, 24.783786], [94.593223, 24.765931], [94.589502, 24.74725], [94.581544, 24.73012], [94.569452, 24.716399], [94.555706, 24.708855], [94.543407, 24.70733], [94.531005, 24.707485], [94.516329, 24.704953], [94.507957, 24.689011], [94.48708, 24.620462], [94.474987, 24.597389], [94.430236, 24.570904], [94.418453, 24.560311], [94.410805, 24.545945], [94.397059, 24.495069], [94.381866, 24.48631], [94.370394, 24.477732], [94.363676, 24.46582], [94.361196, 24.425409], [94.352204, 24.413937], [94.340009, 24.406005], [94.328743, 24.394481], [94.32833, 24.388487], [94.33174, 24.371098], [94.330707, 24.362855], [94.323679, 24.354664], [94.302802, 24.341022], [94.29536, 24.332599], [94.289986, 24.31676], [94.286885, 24.283067], [94.282751, 24.266427], [94.233969, 24.160335], [94.222186, 24.122637], [94.218466, 24.08667], [94.214125, 24.069178], [94.203686, 24.057757], [94.199139, 24.048094], [94.193868, 24.009673], [94.19025, 23.995281], [94.144878, 23.938876], [94.13506, 23.919006], [94.133613, 23.899162], [94.13506, 23.877303], [94.131856, 23.856969], [94.128859, 23.853905], [94.11687, 23.841647], [94.099713, 23.842628], [94.085347, 23.857511], [94.071084, 23.876115], [94.055065, 23.887949], [93.997911, 23.916965], [93.97507, 23.920918], [93.94055, 23.928876], [93.912334, 23.939005], [93.883189, 23.94531], [93.803711, 23.936059], [93.782833, 23.945465], [93.743042, 23.995281], [93.711106, 24.005461], [93.660464, 24.011223], [93.612404, 24.009052], [93.588013, 23.995229], [93.585636, 23.987297], [93.581812, 23.979597], [93.576954, 23.973008], [93.571063, 23.968021], [93.561348, 23.965696], [93.55525, 23.970993], [93.549979, 23.978021], [93.543468, 23.980889], [93.526828, 23.975385], [93.495719, 23.959133], [93.474842, 23.957505], [93.456652, 23.95996], [93.449004, 23.968926], [93.445593, 23.981612], [93.439185, 23.995229], [93.406526, 24.025847], [93.3965, 24.037526], [93.368285, 24.07848], [93.348545, 24.088893], [93.32219, 24.080056], [93.309684, 24.063984], [93.302966, 24.041143], [93.302559, 24.035775], [93.301106, 24.016571], [93.302966, 23.995281], [93.308134, 23.977504], [93.316092, 23.961097], [93.337589, 23.928411], [93.345961, 23.918696], [93.351852, 23.913916], [93.355986, 23.906991], [93.35919, 23.890946], [93.35826, 23.855263], [93.374796, 23.739017], [93.406319, 23.719122], [93.412313, 23.708709], [93.425956, 23.690803], [93.430297, 23.680339], [93.432364, 23.647317], [93.400221, 23.454022], [93.399808, 23.426091], [93.405905, 23.36767], [93.407766, 23.356379], [93.407249, 23.347258], [93.403735, 23.338757], [93.384201, 23.313126], [93.377897, 23.296822], [93.37645, 23.279485], [93.380791, 23.237265], [93.372729, 23.169827], [93.373039, 23.154221], [93.37521, 23.142154], [93.3749, 23.129726], [93.333352, 23.052289], [93.318262, 23.03366], [93.298832, 23.01795], [93.272787, 23.00591], [93.246535, 23.004488], [93.225555, 23.020379], [93.212119, 23.039887], [93.196512, 23.053839], [93.177082, 23.058439], [93.152381, 23.049938], [93.140082, 23.031903], [93.138221, 23.005264], [93.143182, 22.977513], [93.151141, 22.956223], [93.171088, 22.920437], [93.177392, 22.900205], [93.175532, 22.881705], [93.161062, 22.860983], [93.125302, 22.821554], [93.115897, 22.798403], [93.112383, 22.799953], [93.079517, 22.77272], [93.070835, 22.69288], [93.073936, 22.671899], [93.081894, 22.65283], [93.104425, 22.615933], [93.110523, 22.596348], [93.109076, 22.574696], [93.098224, 22.535266], [93.099154, 22.51196], [93.109593, 22.479042], [93.117861, 22.462868], [93.153518, 22.427521], [93.16592, 22.386593], [93.174292, 22.269391], [93.173672, 22.259418], [93.169021, 22.246912], [93.161269, 22.242519], [93.151451, 22.240969], [93.129747, 22.231616], [93.124786, 22.232287], [93.123545, 22.230686], [93.123132, 22.218387], [93.141735, 22.187277], [93.119515, 22.180973], [93.104322, 22.187174], [93.090576, 22.197148], [93.072489, 22.20247], [93.045824, 22.206863], [93.036419, 22.206243], [93.024533, 22.201902], [93.022363, 22.196217], [93.023913, 22.188828], [93.02257, 22.120976], [93.025464, 22.112915], [93.00748, 22.105939], [92.995181, 22.103251], [92.985466, 22.095965], [92.967172, 22.062685], [92.965312, 22.04956], [92.968413, 22.036175], [92.974924, 22.023308], [92.977921, 22.016487], [92.979575, 22.009355], [92.979885, 22.002276], [92.978955, 21.995351], [92.977094, 21.993026], [92.974924, 21.990907], [92.968103, 21.987238], [92.961695, 21.986876], [92.955597, 21.989615], [92.950326, 21.995351], [92.937407, 22.013024], [92.920974, 22.021809], [92.906194, 22.017314], [92.89865, 21.995403], [92.889244, 21.966722], [92.877462, 21.957007], [92.867024, 21.966516], [92.862063, 21.995403], [92.853898, 22.024135], [92.836121, 22.046511], [92.771629, 22.104182], [92.708067, 22.147952], [92.683779, 22.154308], [92.67396, 22.147073], [92.671067, 22.133896], [92.674684, 22.105577], [92.67396, 22.095242], [92.669516, 22.092658], [92.664555, 22.093846], [92.662075, 22.094777], [92.656184, 22.075294], [92.65608, 22.060567], [92.658354, 22.046562], [92.658354, 22.032816], [92.650913, 22.019174], [92.639854, 22.013386], [92.627555, 22.012353], [92.615256, 22.008787], [92.603887, 21.995403], [92.597996, 21.989305], [92.591278, 21.984241], [92.583837, 21.980313], [92.575879, 21.977574], [92.575155, 21.986566], [92.573088, 21.995403], [92.559962, 22.061342], [92.537328, 22.128625], [92.549317, 22.138495], [92.5642, 22.13834], [92.575568, 22.143301], [92.577015, 22.168674], [92.518931, 22.495372], [92.517381, 22.51258], [92.504565, 22.543793], [92.500844, 22.560536], [92.502911, 22.618621], [92.495883, 22.695618], [92.491543, 22.71107], [92.480691, 22.726159], [92.453715, 22.747967], [92.442037, 22.762927], [92.435009, 22.793287], [92.426327, 22.87106], [92.411858, 22.887932], [92.378681, 22.900438], [92.359871, 22.926612], [92.352223, 22.960331], [92.35305, 23.029913], [92.327625, 23.171429], [92.328555, 23.211194], [92.332689, 23.226258], [92.350673, 23.260984], [92.357494, 23.278942], [92.356874, 23.289122], [92.352223, 23.298837], [92.347055, 23.315426], [92.346745, 23.325296], [92.349536, 23.344959], [92.348606, 23.354105], [92.344885, 23.359118], [92.331656, 23.368652], [92.326695, 23.375267], [92.325351, 23.382166], [92.326901, 23.396015], [92.325351, 23.403508], [92.31977, 23.412164], [92.306748, 23.423869], [92.301477, 23.431801], [92.296826, 23.44658], [92.291038, 23.495285], [92.252797, 23.609309], [92.250007, 23.642615], [92.261686, 23.685119], [92.259737, 23.702641], [92.259309, 23.706487], [92.238741, 23.716848], [92.222102, 23.707779], [92.209389, 23.661606], [92.1943, 23.648144], [92.180761, 23.665378], [92.170632, 23.703645], [92.150788, 23.731653], [92.108414, 23.718243], [92.092187, 23.701345], [92.059631, 23.659487], [92.042371, 23.64556], [92.019013, 23.640057], [92.00351, 23.647421], [91.961652, 23.691139], [91.949147, 23.709768], [91.936641, 23.72354], [91.922688, 23.722972], [91.916487, 23.709872], [91.91442, 23.688323], [91.915971, 23.65016], [91.942325, 23.53443], [91.940775, 23.495182], [91.918141, 23.459939], [91.905118, 23.446632], [91.889099, 23.435935], [91.833805, 23.413895], [91.817682, 23.400924], [91.791637, 23.368342], [91.762595, 23.321549], [91.743681, 23.272327], [91.749159, 23.232407], [91.763938, 23.200626], [91.779028, 23.131767], [91.791947, 23.101149], [91.795564, 23.089496], [91.791327, 23.080375], [91.776237, 23.06526], [91.773757, 23.064898], [91.764248, 23.065647], [91.760941, 23.065492], [91.75598, 23.061539], [91.755567, 23.056837], [91.75629, 23.05216], [91.75505, 23.048233], [91.738307, 23.024668], [91.731279, 23.01733], [91.714432, 23.003119], [91.706991, 22.995368], [91.694692, 22.988133], [91.667614, 22.982629], [91.654074, 22.978056], [91.646943, 22.976583], [91.639915, 22.978004], [91.632784, 22.980304], [91.625756, 22.981544], [91.616661, 22.978056], [91.61201, 22.969839], [91.608289, 22.960331], [91.602398, 22.952967], [91.586068, 22.944466], [91.582968, 22.947903], [91.583071, 22.957799], [91.576766, 22.968806], [91.563124, 22.974981], [91.549791, 22.977229], [91.536562, 22.981854], [91.523643, 22.995213], [91.519406, 23.005625], [91.518165, 23.035081], [91.515582, 23.038802], [91.506073, 23.041179], [91.503283, 23.044408], [91.498115, 23.069962], [91.475067, 23.143085], [91.470416, 23.153291], [91.463595, 23.184116], [91.461218, 23.184426], [91.446438, 23.197887], [91.445405, 23.203029], [91.446748, 23.208791], [91.446438, 23.215535], [91.439927, 23.223519], [91.439617, 23.22264], [91.436207, 23.215716], [91.436517, 23.214863], [91.428145, 23.229513], [91.413572, 23.248634], [91.394349, 23.262664], [91.372128, 23.261889], [91.360966, 23.247523], [91.355695, 23.224139], [91.355901, 23.179646], [91.374608, 23.095723], [91.370887, 23.062779], [91.337401, 23.071074], [91.313527, 23.101046], [91.300504, 23.142309], [91.27725, 23.303256], [91.272909, 23.313488], [91.267535, 23.321549], [91.265778, 23.329352], [91.272909, 23.338499], [91.282934, 23.347646], [91.287275, 23.355552], [91.283968, 23.361443], [91.270945, 23.364596], [91.25813, 23.373639], [91.247898, 23.393767], [91.234462, 23.436271], [91.229707, 23.461179], [91.22733, 23.468775], [91.220819, 23.477896], [91.203352, 23.487921], [91.195084, 23.495182], [91.185886, 23.511563], [91.177514, 23.544275], [91.169866, 23.561974], [91.140824, 23.6121], [91.136276, 23.629515], [91.139687, 23.65357], [91.152296, 23.655095], [91.166456, 23.654087], [91.175241, 23.670313], [91.168936, 23.685377], [91.1524, 23.69101], [91.136587, 23.69889], [91.132349, 23.720646], [91.141754, 23.739896], [91.157567, 23.744986], [91.176584, 23.746536], [91.194981, 23.755166], [91.205213, 23.771573], [91.225263, 23.826144], [91.228157, 23.84498], [91.224023, 23.857485], [91.212758, 23.88074], [91.211931, 23.89446], [91.216272, 23.908387], [91.250378, 23.964042], [91.26092, 23.977271], [91.274149, 23.988227], [91.291616, 23.994816], [91.304948, 23.99386], [91.308433, 23.992501], [91.316214, 23.989467], [91.327066, 23.987865], [91.339055, 23.995229], [91.349287, 24.036053], [91.350837, 24.074242], [91.363033, 24.099848], [91.404581, 24.103129], [91.480442, 24.088014], [91.517545, 24.085198], [91.55868, 24.089668], [91.581831, 24.096101], [91.596507, 24.10499], [91.606015, 24.118865], [91.613767, 24.140104], [91.624515, 24.203459], [91.629063, 24.215319], [91.639502, 24.211702], [91.657175, 24.171368], [91.666167, 24.155917], [91.68415, 24.145478], [91.703684, 24.142946], [91.72115, 24.148992], [91.732623, 24.164314], [91.732002, 24.181678], [91.725801, 24.202064], [91.723011, 24.220977], [91.732933, 24.234284], [91.796598, 24.221598], [91.807037, 24.221133], [91.809724, 24.214544], [91.811377, 24.192039], [91.813755, 24.183124], [91.844244, 24.155323], [91.864501, 24.150594], [91.87711, 24.158036], [91.884345, 24.175554], [91.905945, 24.260639], [91.906875, 24.280354], [91.896437, 24.320119], [91.899847, 24.33805], [91.921862, 24.34451], [91.930957, 24.336965], [91.953074, 24.322987], [91.972194, 24.316062], [91.971368, 24.329679], [91.948837, 24.363527], [91.949974, 24.375387], [92.033379, 24.36885], [92.062732, 24.37102], [92.08826, 24.38164], [92.107587, 24.405979], [92.111204, 24.434298], [92.105933, 24.49525], [92.110894, 24.514293], [92.123193, 24.525378], [92.138593, 24.533723], [92.14998, 24.542183], [92.153165, 24.544549], [92.163604, 24.5592], [92.168875, 24.574315], [92.173422, 24.608396], [92.182931, 24.647256], [92.233781, 24.777533], [92.236468, 24.793036], [92.236881, 24.80859], [92.235021, 24.824765], [92.234928, 24.824991], [92.221792, 24.856908], [92.221759, 24.857047], [92.217658, 24.874581], [92.223032, 24.891376], [92.252177, 24.902977], [92.290245, 24.891351], [92.290418, 24.891298], [92.358146, 24.853749], [92.358321, 24.853652], [92.359321, 24.846428], [92.359354, 24.846185], [92.363507, 24.84112], [92.363592, 24.841017], [92.37093, 24.837917], [92.380748, 24.836702], [92.442037, 24.855409], [92.477693, 24.86391], [92.491543, 24.883728], [92.49454, 24.894864], [92.491336, 24.912279], [92.491267, 24.912424], [92.483791, 24.928221], [92.474386, 24.93667], [92.474272, 24.936715], [92.449788, 24.946437], [92.450028, 24.946636], [92.458056, 24.953284], [92.457828, 24.953558], [92.454026, 24.958116], [92.453769, 24.958231], [92.444517, 24.962379], [92.444446, 24.962427], [92.412478, 24.984083], [92.41246, 24.984361], [92.412271, 24.987391], [92.41214, 24.987434], [92.41186, 24.987527], [92.394701, 24.99323], [92.38695, 24.99509], [92.386926, 24.995202], [92.386876, 24.995444], [92.384937, 25.004773], [92.381058, 25.023435], [92.356254, 25.037], [92.328142, 25.048007], [92.312122, 25.0686], [92.303544, 25.074285], [92.251557, 25.080796], [92.233677, 25.084956], [92.220138, 25.090382], [92.208046, 25.098107], [92.194093, 25.109166], [92.17921, 25.126684], [92.172699, 25.131955], [92.164741, 25.133557], [92.150995, 25.129733], [92.145517, 25.131697], [92.138593, 25.1364], [92.124123, 25.136968], [92.116165, 25.139449], [92.110377, 25.144875], [92.100455, 25.159086], [92.092911, 25.164718], [92.066039, 25.174537], [92.033999, 25.181668], [92.001753, 25.18296], [91.975088, 25.175364], [91.956278, 25.169783], [91.900571, 25.177586], [91.793704, 25.165338], [91.745335, 25.169369], [91.730142, 25.167457], [91.723734, 25.161928], [91.71774, 25.149836], [91.710712, 25.144668], [91.701823, 25.147148], [91.688904, 25.152988], [91.677432, 25.152626], [91.672264, 25.136503], [91.662033, 25.127201], [91.638675, 25.124049], [91.613147, 25.125134], [91.596197, 25.128907], [91.580177, 25.140895], [91.573459, 25.152264], [91.566121, 25.159964], [91.548758, 25.161101], [91.540179, 25.157639], [91.523436, 25.145185], [91.511654, 25.142497], [91.500905, 25.141154], [91.480752, 25.135263], [91.471036, 25.133919], [91.431866, 25.13795], [91.283968, 25.178981], [91.235702, 25.201925], [91.224953, 25.201615], [91.203146, 25.191332], [91.190847, 25.189471], [91.135346, 25.191228], [90.975149, 25.167819], [90.94342, 25.157432], [90.934821, 25.15636], [90.82198, 25.142291], [90.793972, 25.145391], [90.779399, 25.151954], [90.766893, 25.160533], [90.753974, 25.167561], [90.737955, 25.169421], [90.732684, 25.166889], [90.722038, 25.156605], [90.716354, 25.153556], [90.707672, 25.152988], [90.670568, 25.158776], [90.648968, 25.167819], [90.637599, 25.171075], [90.62344, 25.171436], [90.583339, 25.162031], [90.50138, 25.168801], [90.399784, 25.149009], [90.364644, 25.149991], [90.286923, 25.180066], [90.130085, 25.211589], [89.908134, 25.296907], [89.870204, 25.295563], [89.834599, 25.282437], [89.819044, 25.284969], [89.807365, 25.304503], [89.798529, 25.340056], [89.795015, 25.374163], [89.799234, 25.402893], [89.800699, 25.412869], [89.808568, 25.439507], [89.82323, 25.489143], [89.82509, 25.564487], [89.834392, 25.634767], [89.824212, 25.674093], [89.801526, 25.724684], [89.783129, 25.814446], [89.786953, 25.839147], [89.830051, 25.90798], [89.834392, 25.931752], [89.826434, 25.937488], [89.810724, 25.93878], [89.792017, 25.949115], [89.811551, 25.955213], [89.825607, 25.965703], [89.828656, 25.979397], [89.815272, 25.995107], [89.814135, 25.99614], [89.812688, 25.996347], [89.811034, 25.996037], [89.809329, 25.99521], [89.809174, 25.995107], [89.808864, 25.995107], [89.802043, 25.988647], [89.795531, 25.98596], [89.789434, 25.987976], [89.783956, 25.995107], [89.776411, 26.008233], [89.755844, 26.032417], [89.750211, 26.041564], [89.748713, 26.059031], [89.751141, 26.071536], [89.749849, 26.083887], [89.736724, 26.100579], [89.728766, 26.114169], [89.724321, 26.130654], [89.718482, 26.145537], [89.706545, 26.154167], [89.697656, 26.154787], [89.677709, 26.153133], [89.670371, 26.154115], [89.658175, 26.159231], [89.655798, 26.161298], [89.657245, 26.165381], [89.657829, 26.187174], [89.658382, 26.207859], [89.653008, 26.22269], [89.634921, 26.225842], [89.61332, 26.219383], [89.606396, 26.211011], [89.608463, 26.180677], [89.615284, 26.169101], [89.614406, 26.164192], [89.593993, 26.169722], [89.586759, 26.169205], [89.579937, 26.166104], [89.573426, 26.16073], [89.562781, 26.142591], [89.569395, 26.130086], [89.585777, 26.122903], [89.604225, 26.120732], [89.592546, 26.113963], [89.57787, 26.107245], [89.566915, 26.098977], [89.567018, 26.087091], [89.57694, 26.082647], [89.590789, 26.085024], [89.603864, 26.084662], [89.611357, 26.072208], [89.602882, 26.055258], [89.559163, 26.022237], [89.550689, 25.995055], [89.551825, 25.981516], [89.549087, 25.968855], [89.541697, 25.959709], [89.528416, 25.957125], [89.518184, 25.962086], [89.509451, 25.972628], [89.495343, 25.995055], [89.490641, 25.997329], [89.485628, 25.998724], [89.480615, 25.999241], [89.426407, 25.995055], [89.426355, 25.994952], [89.421187, 25.993195], [89.416123, 25.992626], [89.411266, 25.993195], [89.406615, 25.995055], [89.391422, 26.015467], [89.379743, 26.015312], [89.367754, 26.005442], [89.351476, 25.996864], [89.335301, 25.998569], [89.315147, 26.006786], [89.296906, 26.018155], [89.272979, 26.043476], [89.237633, 26.0581], [89.229261, 26.067196], [89.22306, 26.086574], [89.212621, 26.098977], [89.197842, 26.10771], [89.131593, 26.133755], [89.11919, 26.146622], [89.111697, 26.164502], [89.103377, 26.22517], [89.095884, 26.241035], [89.075885, 26.271059], [89.069426, 26.287389], [89.068857, 26.314261], [89.08033, 26.315604], [89.094799, 26.309506], [89.103532, 26.313692], [89.102912, 26.327335], [89.095729, 26.334518], [89.071855, 26.340616], [89.055215, 26.347023], [89.054078, 26.353948], [89.057282, 26.36263], [89.052631, 26.374619], [89.043743, 26.381078], [89.02178, 26.386246], [89.010877, 26.390173], [89.001781, 26.39715], [88.986537, 26.412704], [88.975323, 26.417872], [88.959303, 26.428569], [88.950157, 26.43694], [88.94101, 26.439421], [88.924577, 26.432135], [88.911968, 26.421231], [88.899462, 26.404488], [88.891246, 26.385781], [88.891866, 26.368779], [88.912433, 26.348935], [88.941217, 26.340099], [88.966021, 26.328472], [88.975323, 26.299946], [88.981731, 26.291833], [88.989689, 26.289404], [88.998629, 26.291523], [89.007983, 26.297156], [89.004365, 26.27571], [89.0116, 26.269199], [89.023537, 26.268837], [89.034338, 26.265788], [89.039712, 26.247185], [89.019455, 26.234472], [88.975323, 26.224344], [88.946849, 26.232922], [88.902718, 26.272919], [88.875794, 26.277364], [88.855641, 26.265116], [88.840654, 26.232043], [88.824841, 26.226566], [88.807995, 26.233439], [88.798486, 26.24765], [88.792027, 26.2646], [88.784017, 26.279947], [88.747947, 26.292505], [88.663404, 26.264444], [88.645834, 26.27602], [88.653276, 26.2831], [88.690121, 26.299843], [88.702988, 26.309661], [88.712238, 26.328265], [88.709706, 26.340616], [88.69627, 26.350279], [88.673378, 26.360718], [88.667435, 26.368159], [88.66852, 26.385729], [88.661079, 26.388881], [88.652242, 26.391517], [88.649865, 26.39839], [88.651932, 26.407071], [88.656273, 26.415133], [88.633018, 26.420094], [88.622373, 26.424538], [88.611521, 26.430843], [88.603563, 26.440196], [88.597878, 26.451461], [88.591161, 26.461228], [88.579172, 26.466034], [88.557984, 26.465724], [88.549768, 26.466654], [88.526823, 26.475388], [88.506825, 26.487635], [88.49866, 26.49487], [88.487911, 26.505773], [88.475405, 26.514972], [88.462383, 26.528821], [88.446467, 26.535539], [88.411016, 26.545099], [88.397891, 26.55869], [88.394997, 26.578379], [88.395927, 26.599566], [88.394377, 26.618015], [88.385282, 26.623544], [88.372518, 26.611607], [88.36027, 26.593107], [88.352829, 26.578689], [88.332365, 26.514662], [88.319342, 26.494921], [88.313865, 26.481537], [88.315105, 26.465052], [88.322546, 26.451772], [88.335621, 26.448206], [88.34022, 26.454149], [88.34301, 26.477196], [88.346524, 26.484018], [88.353552, 26.484586], [88.37691, 26.475336], [88.434581, 26.465156], [88.461143, 26.45389], [88.475405, 26.432031], [88.495456, 26.378081], [88.497626, 26.352708], [88.475405, 26.355808], [88.467912, 26.354051], [88.45799, 26.353018], [88.448637, 26.353431], [88.442022, 26.356118], [88.431687, 26.362216], [88.426003, 26.357927], [88.421197, 26.348729], [88.413807, 26.340202], [88.366781, 26.313434], [88.351692, 26.30098], [88.336602, 26.281808], [88.332933, 26.267235], [88.334535, 26.230803], [88.326267, 26.21592], [88.308697, 26.206102], [88.250303, 26.188015], [88.229787, 26.184036], [88.218884, 26.18016], [88.211959, 26.173752], [88.205654, 26.166414], [88.196921, 26.159645], [88.163796, 26.140473], [88.155632, 26.128484], [88.144469, 26.095876], [88.141369, 26.090502], [88.139922, 26.085334], [88.141162, 26.075464], [88.14478, 26.070503], [88.150981, 26.066989], [88.156407, 26.061408], [88.157905, 26.050452], [88.151032, 26.031332], [88.125866, 26.0134], [88.106539, 25.979966], [88.088039, 25.923225], [88.082664, 25.915939], [88.077393, 25.912786], [88.074396, 25.908135], [88.082044, 25.863849], [88.088969, 25.848656], [88.087574, 25.839974], [88.087574, 25.830983], [88.103335, 25.814808], [88.108038, 25.806281], [88.114911, 25.787264], [88.127726, 25.774914], [88.146433, 25.77574], [88.166225, 25.783647], [88.18271, 25.792432], [88.227462, 25.804266], [88.259915, 25.789021], [88.320273, 25.725253], [88.392981, 25.680759], [88.41944, 25.653784], [88.423781, 25.592237], [88.437785, 25.579835], [88.475405, 25.562575], [88.485586, 25.542628], [88.512767, 25.523714], [88.520467, 25.509297], [88.530286, 25.500202], [88.550956, 25.496481], [88.593589, 25.495086], [88.613175, 25.486404], [88.645421, 25.467336], [88.66635, 25.462891], [88.67808, 25.465889], [88.687072, 25.473898], [88.69565, 25.483614], [88.706192, 25.491675], [88.71415, 25.492812], [88.723142, 25.491417], [88.732651, 25.491003], [88.742572, 25.495189], [88.741952, 25.512759], [88.772338, 25.501959], [88.780968, 25.495189], [88.793112, 25.48196], [88.805824, 25.471676], [88.814919, 25.458602], [88.816315, 25.437208], [88.799313, 25.401706], [88.799623, 25.395609], [88.810372, 25.388426], [88.813266, 25.380054], [88.814041, 25.370391], [88.81833, 25.359642], [88.841998, 25.332822], [88.849026, 25.327189], [88.859258, 25.324709], [88.880187, 25.323468], [88.88613, 25.319076], [88.902873, 25.303418], [88.926489, 25.300472], [88.975323, 25.302643], [88.982713, 25.294478], [88.984935, 25.284969], [88.982351, 25.274892], [88.975323, 25.265229], [88.937754, 25.240166], [88.929073, 25.229417], [88.927988, 25.21593], [88.930778, 25.202649], [88.927884, 25.192727], [88.909642, 25.189161], [88.924163, 25.167871], [88.898635, 25.169318], [88.830216, 25.191332], [88.823601, 25.194846], [88.81709, 25.196809], [88.809132, 25.195259], [88.802517, 25.188128], [88.792337, 25.166579], [88.784224, 25.160946], [88.724537, 25.166475], [88.664593, 25.186888], [88.634259, 25.192262], [88.599119, 25.19314], [88.590127, 25.190143], [88.577725, 25.177482], [88.570697, 25.173245], [88.55938, 25.170764], [88.554677, 25.171126], [88.550026, 25.173142], [88.523154, 25.179808], [88.491838, 25.191125], [88.475405, 25.195104], [88.441299, 25.18973], [88.43148, 25.173038], [88.434168, 25.116659], [88.425796, 25.051056], [88.414117, 25.021523], [88.391018, 24.99509], [88.387865, 24.968761], [88.37567, 24.94561], [88.340892, 24.90414], [88.322753, 24.874684], [88.313606, 24.868302], [88.296812, 24.869698], [88.25485, 24.879955], [88.242758, 24.880601], [88.24188, 24.898766], [88.232836, 24.918273], [88.21878, 24.935146], [88.20245, 24.9453], [88.18302, 24.94698], [88.16452, 24.941114], [88.114911, 24.916413], [88.114911, 24.912021], [88.120337, 24.906775], [88.124936, 24.898145], [88.138062, 24.863496], [88.140955, 24.844583], [88.13124, 24.831767], [88.117391, 24.819261], [88.107779, 24.801097], [88.094808, 24.803112], [88.088245, 24.796136], [88.085352, 24.784018], [88.084008, 24.770453], [88.07698, 24.761177], [88.064268, 24.759162], [88.053209, 24.754976], [88.048455, 24.711206], [88.035432, 24.693817], [88.027164, 24.67573], [88.02179, 24.645603], [88.04091, 24.640435], [88.05724, 24.634105], [88.067265, 24.613667], [88.081838, 24.599869], [88.08499, 24.593875], [88.08499, 24.541733], [88.086023, 24.53672], [88.090002, 24.533], [88.098787, 24.522484], [88.107934, 24.507963], [88.10995, 24.500986], [88.138785, 24.49525], [88.401301, 24.369418], [88.475405, 24.315468], [88.49835, 24.310972], [88.5631, 24.308207], [88.612451, 24.292885], [88.634517, 24.294125], [88.649503, 24.315364], [88.660149, 24.338257], [88.681801, 24.33389], [88.714564, 24.315416], [88.737508, 24.287097], [88.743038, 24.247539], [88.74495, 24.243379], [88.747482, 24.226972], [88.749807, 24.223251], [88.746397, 24.214079], [88.734821, 24.198421], [88.731617, 24.189842], [88.731456, 24.189771], [88.715907, 24.182918], [88.69379, 24.175321], [88.683765, 24.166536], [88.67808, 24.154289], [88.678287, 24.144651], [88.68299, 24.124291], [88.681801, 24.112664], [88.675393, 24.091838], [88.674101, 24.08264], [88.67901, 24.071219], [88.697821, 24.056259], [88.700921, 24.04365], [88.725002, 24.038379], [88.725468, 24.028896], [88.71384, 24.014685], [88.701593, 23.995281], [88.708363, 23.98355], [88.714357, 23.964404], [88.717148, 23.943139], [88.71415, 23.925156], [88.704435, 23.913709], [88.666505, 23.8795], [88.653121, 23.869784], [88.633949, 23.866141], [88.613175, 23.867847], [88.593589, 23.866916], [88.577621, 23.85516], [88.574366, 23.845626], [88.575451, 23.836556], [88.577621, 23.827616], [88.577518, 23.818521], [88.57235, 23.806894], [88.557674, 23.785681], [88.552713, 23.774984], [88.552507, 23.765424], [88.559741, 23.750283], [88.561188, 23.741007], [88.540104, 23.649953], [88.541551, 23.638765], [88.550956, 23.634424], [88.56124, 23.632486], [88.566356, 23.62874], [88.564392, 23.616208], [88.561085, 23.608612], [88.561705, 23.602023], [88.57142, 23.592334], [88.581549, 23.588329], [88.601289, 23.590344], [88.609247, 23.588794], [88.624233, 23.573859], [88.647643, 23.535076], [88.664489, 23.518307], [88.69379, 23.499807], [88.704642, 23.495182], [88.719421, 23.468104], [88.731927, 23.472625], [88.743709, 23.489317], [88.756835, 23.498799], [88.770168, 23.488826], [88.767894, 23.467277], [88.719266, 23.348292], [88.69503, 23.312609], [88.685935, 23.293308], [88.686607, 23.271604], [88.69658, 23.252354], [88.710016, 23.241218], [88.775697, 23.221633], [88.786187, 23.221529], [88.794197, 23.224966], [88.800967, 23.230754], [88.809545, 23.236051], [88.822671, 23.237963], [88.839621, 23.23481], [88.909642, 23.212434], [88.918066, 23.208584], [88.926954, 23.206517], [88.954239, 23.207422], [88.959975, 23.202693], [88.95982, 23.194451], [88.954704, 23.183806], [88.945868, 23.174349], [88.919099, 23.153859], [88.864787, 23.100271], [88.8513, 23.07513], [88.848716, 23.023841], [88.839414, 22.995368], [88.838794, 22.975808], [88.842773, 22.964568], [88.861738, 22.942399], [88.872177, 22.926767], [88.893468, 22.879638], [88.905147, 22.866435], [88.933103, 22.858115], [88.944524, 22.848012], [88.946798, 22.834525], [88.945144, 22.815094], [88.941217, 22.795664], [88.936876, 22.782331], [88.926231, 22.767035], [88.914965, 22.7544], [88.908144, 22.740784], [88.910573, 22.722593], [88.92251, 22.693706], [88.92592, 22.680839], [88.930003, 22.658618], [88.942354, 22.637482], [88.952017, 22.611903], [88.95641, 22.584979], [88.950312, 22.561983], [88.927988, 22.548134], [88.959045, 22.536817], [88.970982, 22.527928], [88.979974, 22.490928], [88.998371, 22.451241], [89.002505, 22.427986], [89.000748, 22.420131], [88.99217, 22.406954], [88.989482, 22.39667], [88.989586, 22.386903], [89.001523, 22.333677], [89.006949, 22.321429], [89.023537, 22.294816], [89.00912, 22.285618], [89.018628, 22.264689], [89.062295, 22.211565], [89.070718, 22.194822], [89.076196, 22.17503], [89.078159, 22.150845], [89.060693, 22.130485], [89.060395, 22.129869], [89.060313, 22.130601], [89.043712, 22.137397], [89.048595, 22.122952], [89.045665, 22.112047], [89.039887, 22.100898], [89.036876, 22.085598], [89.04005, 22.069525], [89.054047, 22.043647], [89.057384, 22.031236], [89.065603, 21.967108], [89.060883, 21.938463], [89.036876, 21.925116], [89.023123, 21.931098], [89.013031, 21.929429], [89.008067, 21.920559], [89.009532, 21.904608], [88.993012, 21.912665], [88.96046, 21.934312], [88.944672, 21.938788], [88.937836, 21.948717], [88.934581, 21.969306], [88.928722, 21.986558], [88.91391, 21.986558], [88.90504, 21.969428], [88.910818, 21.946479], [88.924083, 21.926337], [88.937836, 21.91767], [88.951915, 21.913886], [88.968272, 21.904731], [88.99586, 21.884182], [89.01588, 21.853339], [89.030935, 21.770453], [89.043712, 21.733344], [89.047862, 21.728013], [89.067882, 21.709133], [89.082774, 21.678209], [89.085216, 21.674709], [89.088227, 21.633734], [89.082693, 21.617336], [89.06422, 21.609198], [89.053233, 21.609809], [89.044688, 21.613511], [89.038829, 21.619452], [89.036876, 21.626899], [89.033946, 21.633246], [89.027599, 21.624579], [89.021658, 21.611802], [89.019786, 21.606106], [89.000255, 21.60342], [88.975759, 21.615912], [88.933849, 21.643948], [88.896983, 21.650133], [88.891612, 21.65351], [88.886078, 21.661811], [88.881602, 21.672024], [88.872081, 21.74901], [88.866059, 21.766832], [88.858653, 21.766832], [88.855724, 21.748847], [88.841563, 21.716254], [88.838227, 21.701972], [88.842947, 21.684149], [88.859874, 21.652574], [88.858653, 21.636542], [88.844249, 21.622219], [88.823985, 21.617255], [88.805837, 21.62287], [88.797862, 21.640204], [88.814789, 21.653876], [88.81837, 21.65762], [88.818533, 21.665839], [88.816661, 21.675035], [88.810883, 21.691718], [88.804942, 21.671861], [88.800466, 21.661851], [88.7942, 21.65762], [88.784028, 21.655951], [88.779063, 21.65119], [88.77711, 21.643541], [88.776866, 21.63345], [88.781749, 21.614244], [88.791026, 21.598863], [88.796641, 21.584418], [88.790538, 21.568264], [88.762462, 21.555854], [88.727875, 21.559882], [88.70753, 21.578315], [88.722179, 21.609198], [88.687999, 21.678127], [88.684906, 21.697211], [88.703624, 21.810614], [88.698009, 21.825507], [88.694509, 21.83983], [88.714692, 21.897854], [88.71697, 21.951972], [88.722179, 21.966132], [88.733165, 21.9772], [88.746837, 21.987128], [88.758311, 21.999457], [88.763031, 22.017279], [88.754649, 22.037584], [88.717052, 22.058051], [88.708507, 22.072211], [88.714854, 22.086982], [88.730154, 22.103583], [88.763031, 22.130561], [88.743907, 22.12934], [88.726573, 22.121161], [88.712169, 22.109036], [88.701671, 22.095852], [88.694835, 22.081529], [88.692882, 22.065863], [88.699474, 22.053209], [88.718516, 22.048041], [88.730724, 22.037177], [88.723318, 22.013088], [88.701671, 21.97602], [88.698416, 21.962592], [88.689952, 21.94599], [88.678722, 21.936672], [88.667003, 21.945014], [88.656261, 21.941067], [88.642589, 21.940904], [88.63087, 21.945461], [88.625987, 21.955512], [88.627452, 21.970404], [88.631684, 21.979804], [88.646332, 22.00023], [88.653494, 22.018012], [88.652599, 22.031806], [88.648774, 22.046047], [88.64503, 22.075141], [88.639496, 22.097235], [88.639496, 22.109524], [88.656993, 22.151068], [88.674001, 22.162055], [88.683116, 22.184963], [88.679942, 22.204413], [88.660167, 22.205024], [88.661143, 22.198961], [88.656016, 22.183254], [88.649669, 22.169094], [88.646332, 22.167792], [88.644298, 22.162299], [88.634939, 22.144965], [88.63087, 22.1258], [88.622081, 22.116034], [88.619151, 22.109524], [88.618337, 22.102973], [88.619965, 22.078315], [88.625499, 22.059272], [88.625987, 22.048041], [88.620372, 22.029975], [88.60255, 22.006415], [88.598643, 21.990302], [88.600271, 21.947211], [88.598399, 21.929348], [88.567068, 21.850816], [88.564626, 21.832343], [88.56544, 21.771186], [88.564626, 21.766832], [88.581309, 21.769436], [88.604177, 21.7787
Download .txt
gitextract_tb8zvos5/

├── IndiaLIMS.spec
├── LIMS.spec
├── README.md
├── app.py
├── build_exe.py
├── clean_db.py
├── config.py
├── core/
│   ├── __init__.py
│   ├── database.py
│   ├── logic.py
│   └── security.py
├── gis_processor.py
├── inno_setup.iss
├── render.yaml
├── report_generator.py
├── requirements-dev.txt
├── requirements-windows.txt
├── requirements.txt
├── routes/
│   ├── __init__.py
│   ├── auth.py
│   ├── documents.py
│   ├── feedback.py
│   ├── gis.py
│   ├── pages.py
│   ├── records.py
│   ├── users.py
│   └── utils.py
├── run_server.py
├── scripts/
│   ├── inspect_recovery.py
│   ├── recover_superadmin_auto.py
│   └── recovery_credentials_2026-04-25T050759_400298.txt
├── static/
│   ├── css/
│   │   └── style.css
│   ├── data/
│   │   └── india-boundary.geojson
│   └── js/
│       ├── api.js
│       ├── auth.js
│       ├── map.js
│       ├── map_OLD_BAK.js
│       └── modules/
│           ├── admin.js
│           ├── dashboard.js
│           ├── forms.js
│           ├── gis.js
│           ├── map_engine.js
│           ├── records.js
│           ├── state.js
│           └── utils.js
├── templates/
│   ├── admin_dashboard.html
│   ├── login.html
│   └── public_viewer_v2.html
├── tests/
│   ├── Testing_Report_20260425_211328.md
│   ├── Testing_Report_20260426_013232.md
│   ├── Testing_Report_20260426_095300.md
│   ├── Testing_Report_20260426_125456.md
│   ├── Testing_Report_20260426_132557.md
│   ├── Testing_Report_20260426_132640.md
│   ├── check_users_roles.py
│   ├── generate_test_report.py
│   ├── robust_role_test.py
│   ├── test_advanced_gis.py
│   ├── test_logins.py
│   ├── test_output/
│   │   └── Village_Ledger_Amingaon.xlsx
│   ├── test_record_soft_delete.py
│   ├── test_recovery_flow.py
│   ├── test_report_gen.py
│   └── test_restore_flow.py
└── utils.py
Download .txt
SYMBOL INDEX (319 symbols across 40 files)

FILE: app.py
  function create_app (line 19) | def create_app():
  function bootstrap_admin (line 49) | def bootstrap_admin():
  function get_free_port (line 154) | def get_free_port(start_port):
  function start_server (line 164) | def start_server():

FILE: build_exe.py
  function start_flask (line 20) | def start_flask():
  function run_pywebview (line 26) | def run_pywebview():
  function build_exe (line 62) | def build_exe():

FILE: config.py
  function _get_or_create_secret_key (line 17) | def _get_or_create_secret_key():

FILE: core/database.py
  function load_users (line 37) | def load_users():
  function save_users (line 41) | def save_users(users):
  function load_records (line 50) | def load_records():
  function save_records (line 58) | def save_records(records):
  function load_feedback (line 67) | def load_feedback():
  function save_feedback (line 71) | def save_feedback(feedback_data):
  function _log_audit (line 80) | def _log_audit(action, performed_by, record_id=None, details=None):
  function get_audit_collection (line 108) | def get_audit_collection():
  function get_data_dir (line 111) | def get_data_dir():

FILE: core/logic.py
  function _mask_owner_for_viewer (line 3) | def _mask_owner_for_viewer(record):
  function _strip_b64_from_list (line 19) | def _strip_b64_from_list(records):
  function _apply_filters_to_records (line 33) | def _apply_filters_to_records(records, params):
  function _generate_ulpin (line 61) | def _generate_ulpin():
  function _calculate_estimated_value (line 64) | def _calculate_estimated_value(area_ha, circle_rate_inr, land_use):
  function _update_nested (line 80) | def _update_nested(record, parent_key, child_key, value):

FILE: core/security.py
  function admin_required (line 8) | def admin_required(f):
  function viewer_or_admin_required (line 17) | def viewer_or_admin_required(f):
  function role_required (line 26) | def role_required(*allowed_roles):
  function generate_captcha (line 38) | def generate_captcha():
  function verify_captcha_logic (line 45) | def verify_captcha_logic(token, user_answer):

FILE: gis_processor.py
  function get_india_boundary_shape (line 14) | def get_india_boundary_shape():
  function calculate_area (line 61) | def calculate_area(geometry_dict):
  function _geodesic_area_m2 (line 100) | def _geodesic_area_m2(geom):
  function validate_polygon (line 169) | def validate_polygon(geometry_dict):
  function check_overlap (line 240) | def check_overlap(new_geometry_dict, existing_records):
  function get_centroid (line 283) | def get_centroid(geometry_dict):
  function geojson_to_wkt (line 303) | def geojson_to_wkt(geometry_dict):
  function calculate_perimeter (line 321) | def calculate_perimeter(geometry_dict):

FILE: report_generator.py
  function _fmt_inr (line 38) | def _fmt_inr(value):
  function generate_property_card_pdf (line 59) | def generate_property_card_pdf(record, map_image_base64=None):
  class PropertyCardPDF (line 319) | class PropertyCardPDF(FPDF):
    method header (line 322) | def header(self):
    method footer (line 329) | def footer(self):
  function generate_village_excel (line 333) | def generate_village_excel(records, village_name="All Villages"):

FILE: routes/auth.py
  function get_captcha (line 9) | def get_captcha():
  function verify_captcha (line 14) | def verify_captcha():
  function admin_login (line 29) | def admin_login():
  function logout (line 56) | def logout():
  function session_info (line 61) | def session_info():
  function forgot_password (line 69) | def forgot_password():

FILE: routes/documents.py
  function print_property_card (line 10) | def print_property_card(ulpin):
  function export_village_ledger (line 26) | def export_village_ledger():

FILE: routes/feedback.py
  function get_feedback (line 16) | def get_feedback():
  function submit_feedback (line 22) | def submit_feedback():
  function dashboard_analytics (line 45) | def dashboard_analytics():
  function list_audit (line 126) | def list_audit():
  function delete_feedback (line 143) | def delete_feedback(feedback_id):
  function update_feedback_status (line 153) | def update_feedback_status(feedback_id):

FILE: routes/gis.py
  function get_india_boundary (line 11) | def get_india_boundary():
  function api_calculate_area (line 22) | def api_calculate_area():
  function api_validate_geometry (line 35) | def api_validate_geometry():
  function location_from_coordinates (line 45) | def location_from_coordinates():
  function get_location_catalog (line 90) | def get_location_catalog():

FILE: routes/pages.py
  function index (line 7) | def index():
  function login_page (line 11) | def login_page():
  function admin_dashboard (line 21) | def admin_dashboard():
  function viewer_page (line 28) | def viewer_page():

FILE: routes/records.py
  function generate_ulpin (line 13) | def generate_ulpin(state_name, district_name):
  function get_records (line 22) | def get_records():
  function get_record (line 40) | def get_record(record_id):
  function search_records (line 59) | def search_records():
  function filter_records (line 78) | def filter_records():
  function location_catalog (line 93) | def location_catalog():
  function create_record (line 114) | def create_record():
  function update_record (line 194) | def update_record(record_id):
  function delete_record (line 274) | def delete_record(record_id):
  function restore_record (line 310) | def restore_record(record_id):
  function generate_property_card (line 329) | def generate_property_card(ulpin):

FILE: routes/users.py
  function _get_current_user (line 9) | def _get_current_user(users):
  function get_profile (line 15) | def get_profile():
  function update_profile (line 25) | def update_profile():
  function list_users (line 48) | def list_users():
  function create_user (line 61) | def create_user():
  function get_user (line 119) | def get_user(user_id):
  function update_user (line 133) | def update_user(user_id):
  function delete_user (line 190) | def delete_user(user_id):

FILE: routes/utils.py
  function app_config (line 7) | def app_config():

FILE: scripts/inspect_recovery.py
  function main (line 12) | def main():

FILE: scripts/recover_superadmin_auto.py
  function random_password (line 18) | def random_password(length=20):
  function upsert_superadmin (line 23) | def upsert_superadmin(username=None, password=None, dry_run=False):

FILE: static/js/api.js
  constant API_BASE (line 6) | const API_BASE = '';
  constant FETCH_OPTS (line 9) | const FETCH_OPTS = {
  function verifyCaptcha (line 14) | async function verifyCaptcha(answer, token) {
  function getCaptcha (line 24) | async function getCaptcha() {
  function adminLogin (line 32) | async function adminLogin(username, password) {
  function logout (line 42) | async function logout() {
  function getSessionInfo (line 51) | async function getSessionInfo() {
  function forgotPassword (line 59) | async function forgotPassword() {
  function fetchRecords (line 68) | async function fetchRecords() {
  function fetchRecord (line 80) | async function fetchRecord(recordId) {
  function searchRecords (line 92) | async function searchRecords(query) {
  function fetchLocationCatalog (line 104) | async function fetchLocationCatalog() {
  function createRecord (line 110) | async function createRecord(recordData) {
  function updateRecord (line 124) | async function updateRecord(recordId, updateData) {
  function deleteRecord (line 138) | async function deleteRecord(recordId) {
  function restoreRecord (line 150) | async function restoreRecord(recordId) {
  function calculateArea (line 163) | async function calculateArea(geometry) {
  function validateGeometry (line 177) | async function validateGeometry(geometry) {
  function fetchLocationFromCoordinates (line 191) | async function fetchLocationFromCoordinates(lat, lng) {
  function fetchFilteredRecords (line 211) | async function fetchFilteredRecords(filters = {}) {
  function fetchDashboardAnalytics (line 235) | async function fetchDashboardAnalytics(filters = {}) {
  function fetchAppConfig (line 259) | async function fetchAppConfig() {
  function fetchAudit (line 271) | async function fetchAudit(limit = 50) {
  function getPropertyCardUrl (line 284) | function getPropertyCardUrl(ulpin) { return `${API_BASE}/api/print-card/...
  function getVillageExcelUrl (line 286) | function getVillageExcelUrl(village) {
  function downloadFile (line 291) | function downloadFile(url, filename) {
  function showToast (line 301) | function showToast(message, type = 'info', duration = 3000) {
  function updateRealTimeClock (line 328) | function updateRealTimeClock() {

FILE: static/js/auth.js
  function showCaptchaError (line 127) | function showCaptchaError(msg) {
  function showLoginError (line 192) | function showLoginError(msg) {
  function checkSessionAndRedirect (line 205) | async function checkSessionAndRedirect() {

FILE: static/js/map.js
  function setupTabSwitching (line 75) | function setupTabSwitching() {
  function switchMainTab (line 93) | function switchMainTab(tabId) {
  function switchFormTab (line 136) | function switchFormTab(tabId) {
  function performAdminSearch (line 163) | async function performAdminSearch() {
  function viewRecordDetails (line 167) | async function viewRecordDetails(recordId) {

FILE: static/js/map_OLD_BAK.js
  constant DEFAULT_SNAP_DISTANCE (line 27) | const DEFAULT_SNAP_DISTANCE = 20;
  function showConfirmModal (line 30) | function showConfirmModal(message, onConfirm) {
  function sortedValues (line 85) | function sortedValues(values) {
  function asNumber (line 89) | function asNumber(value) {
  function formatInr (line 94) | function formatInr(value) {
  function escapeHtml (line 98) | function escapeHtml(value) {
  function debounce (line 108) | function debounce(fn, wait = 200) {
  function ensureLocationInCatalog (line 117) | function ensureLocationInCatalog(state, district, village) {
  function syncLocationCatalogWithRecords (line 132) | function syncLocationCatalogWithRecords(records) {
  function populateStateFilter (line 139) | function populateStateFilter(records) {
  function populateDistrictFilter (line 161) | function populateDistrictFilter(records) {
  function getAdminFilterState (line 183) | function getAdminFilterState() {
  function filterRecordsByState (line 197) | function filterRecordsByState(records, filterState) {
  function renderKpiCards (line 222) | function renderKpiCards(records) {
  function buildDonutChart (line 250) | function buildDonutChart(slices, colors) {
  function buildRankedList (line 288) | function buildRankedList(items, colors) {
  function renderLandUseDistribution (line 307) | function renderLandUseDistribution(records) {
  function renderDistrictOverview (line 324) | function renderDistrictOverview(records) {
  function renderTopValueParcel (line 347) | function renderTopValueParcel(records) {
  function renderRecentMutations (line 383) | function renderRecentMutations(records) {
  function renderDashboardAnalytics (line 417) | function renderDashboardAnalytics(filteredRecords) {
  function applyAdminFilters (line 425) | function applyAdminFilters(notify) {
  function renderKpiCardsFromServer (line 480) | function renderKpiCardsFromServer(kpis) {
  function renderLandUseDistributionFromServer (line 492) | function renderLandUseDistributionFromServer(stats) {
  function renderDistrictOverviewFromServer (line 503) | function renderDistrictOverviewFromServer(districts) {
  function renderTopValueParcelFromServer (line 516) | function renderTopValueParcelFromServer(parcel) {
  function renderRecentMutationsFromServer (line 532) | function renderRecentMutationsFromServer(mutations) {
  function getFormElements (line 549) | function getFormElements() {
  function populateSelect (line 563) | function populateSelect(selectEl, values, placeholder, selectedValue) {
  function refreshStateOptions (line 591) | function refreshStateOptions(selectedState) {
  function refreshDistrictOptions (line 598) | function refreshDistrictOptions(selectedDistrict) {
  function refreshVillageOptions (line 607) | function refreshVillageOptions(selectedVillage) {
  function setLocationValues (line 620) | function setLocationValues(state, district, village) {
  function setLocationSource (line 639) | function setLocationSource(text) {
  function isManualLocationOverrideEnabled (line 646) | function isManualLocationOverrideEnabled() {
  function toggleManualLocationOverride (line 651) | function toggleManualLocationOverride(enabled) {
  function getEffectiveLocationValues (line 689) | function getEffectiveLocationValues() {
  function initializeLocationFilters (line 714) | function initializeLocationFilters() {
  function updateAutofillStatus (line 768) | function updateAutofillStatus(message, isError) {
  function isMapAutofillEnabled (line 777) | function isMapAutofillEnabled() {
  function shouldApplyLocationUpdate (line 782) | function shouldApplyLocationUpdate(nextLocation, forceUpdate) {
  function applyResolvedLocation (line 806) | function applyResolvedLocation(locationData, forceUpdate) {
  function scheduleMapLocationLookup (line 824) | function scheduleMapLocationLookup(lat, lng, forceUpdate) {
  function initializeFormTabs (line 847) | function initializeFormTabs() {
  function switchFormTab (line 860) | function switchFormTab(tabName) {
  function setMutationMode (line 874) | function setMutationMode(enabled) {
  function updateSnapDistanceLabel (line 884) | function updateSnapDistanceLabel() {
  function getDrawSettings (line 892) | function getDrawSettings() {
  function clearMetricsUI (line 904) | function clearMetricsUI() {
  function clearGeometrySelection (line 925) | function clearGeometrySelection(alsoDisableDraw) {
  function startPolygonDraw (line 943) | function startPolygonDraw() {
  function finishPolygonDraw (line 953) | function finishPolygonDraw() {
  function cancelPolygonSelection (line 959) | function cancelPolygonSelection() {
  function clearPolygonSelection (line 965) | function clearPolygonSelection() {
  function initializeDrawSettingsPanel (line 971) | function initializeDrawSettingsPanel() {
  function updateGeometryMetrics (line 1015) | async function updateGeometryMetrics(geometry, options) {
  function switchBaseLayer (line 1088) | function switchBaseLayer(layerName) {
  function addIndiaMask (line 1097) | async function addIndiaMask(mapInstance) {
  function initMap (line 1165) | function initMap(adminMode) {
  function loadRecordsOnMap (line 1315) | async function loadRecordsOnMap() {
  function clearMapLayers (line 1346) | function clearMapLayers() {
  function addRecordsToMap (line 1353) | function addRecordsToMap(records) {
  function getLandUseColor (line 1427) | function getLandUseColor(landUse) {
  function onParcelClick (line 1440) | function onParcelClick(record) {
  function flyToRecord (line 1449) | function flyToRecord(record) {
  function switchRecordsView (line 1456) | function switchRecordsView(mode) {
  function renderRecordsList (line 1472) | function renderRecordsList(records) {
  function selectRecord (line 1576) | async function selectRecord(recordId) {
  function viewRecordDetails (line 1597) | async function viewRecordDetails(recordId) {
  function initViewRecordMap (line 1671) | function initViewRecordMap(record) {
  function showAuditModal (line 1764) | function showAuditModal(entries) {
  function showAdminDetails (line 2001) | function showAdminDetails(record) {
  function editRecord (line 2172) | async function editRecord(recordId) {
  function capturePolygonMapForPdf (line 2266) | async function capturePolygonMapForPdf(geometry) {
  function printCard (line 2306) | async function printCard(ulpin) {
  function confirmDelete (line 2342) | function confirmDelete(recordId, khasraNo) {
  function initAddRecordMap (line 2364) | function initAddRecordMap() {
  function lookupLocationForAddRecord (line 2548) | function lookupLocationForAddRecord(lat, lng) {
  function updateGeometryMetricsForAddRecord (line 2614) | async function updateGeometryMetricsForAddRecord(geometry) {
  function switchSidebarTab (line 2663) | function switchSidebarTab(tabName) {
  function switchMainTab (line 2668) | function switchMainTab(tabName) {
  function showProfileEditForm (line 2964) | function showProfileEditForm() {
  function hideCreateUserForm (line 3018) | function hideCreateUserForm() { document.getElementById('create-user-for...
  function performAdminSearch (line 3040) | async function performAdminSearch() {
  function fileToBase64 (line 3053) | function fileToBase64(file) {
  function handleFormSubmit (line 3062) | async function handleFormSubmit() {
  function resetForm (line 3264) | function resetForm() {
  function loadProfile (line 3329) | async function loadProfile() {
  function displayProfile (line 3338) | function displayProfile(profile) {
  function loadFeedback (line 3367) | async function loadFeedback() {
  function deleteFeedback (line 3413) | function deleteFeedback(feedbackId) {
  function loadUsers (line 3432) | async function loadUsers() {
  function renderUsersTable (line 3444) | function renderUsersTable() {
  function getSessionUsername (line 3518) | async function getSessionUsername() {
  function editUser (line 3527) | async function editUser(userId) {
  function closeEditUserModal (line 3549) | function closeEditUserModal() {
  function deleteUser (line 3616) | function deleteUser(userId, username) {

FILE: static/js/modules/admin.js
  function showAuditModal (line 12) | function showAuditModal(entries) {
  function showAdminDetails (line 179) | function showAdminDetails(record) {
  function capturePolygonMapForPdf (line 242) | async function capturePolygonMapForPdf(geometry) {
  function printCard (line 265) | async function printCard(ulpin) {
  function confirmDelete (line 279) | function confirmDelete(recordId, khasraNo) {
  function loadProfile (line 289) | async function loadProfile() {
  function displayProfile (line 298) | function displayProfile(profile) {
  function loadFeedback (line 401) | async function loadFeedback() {
  function loadUsers (line 478) | async function loadUsers() {
  function renderUsersTable (line 490) | function renderUsersTable() {
  function getSessionUsername (line 569) | async function getSessionUsername() {
  function editUser (line 578) | async function editUser(userId) {
  function closeEditUserModal (line 635) | function closeEditUserModal() {
  function deleteUser (line 642) | function deleteUser(userId, username) {
  function hideCreateUserForm (line 730) | function hideCreateUserForm() {

FILE: static/js/modules/dashboard.js
  function renderKpiCards (line 5) | function renderKpiCards(records) {
  function buildDonutChart (line 33) | function buildDonutChart(slices, colors) {
  function buildRankedList (line 70) | function buildRankedList(items, colors) {
  function renderLandUseDistribution (line 88) | function renderLandUseDistribution(records) {
  function renderDistrictOverview (line 105) | function renderDistrictOverview(records) {
  function renderTopValueParcel (line 128) | function renderTopValueParcel(records) {
  function renderRecentMutations (line 164) | function renderRecentMutations(records) {
  function renderDashboardAnalytics (line 198) | function renderDashboardAnalytics(filteredRecords) {
  function renderKpiCardsFromServer (line 207) | function renderKpiCardsFromServer(kpis) {
  function renderLandUseDistributionFromServer (line 219) | function renderLandUseDistributionFromServer(stats) {
  function renderDistrictOverviewFromServer (line 230) | function renderDistrictOverviewFromServer(districts) {
  function renderTopValueParcelFromServer (line 243) | function renderTopValueParcelFromServer(parcel) {
  function renderRecentMutationsFromServer (line 259) | function renderRecentMutationsFromServer(mutations) {

FILE: static/js/modules/forms.js
  function getFormElements (line 5) | function getFormElements() {
  function populateSelect (line 19) | function populateSelect(selectEl, values, placeholder, selectedValue) {
  function refreshStateOptions (line 47) | function refreshStateOptions(selectedState) {
  function refreshDistrictOptions (line 54) | function refreshDistrictOptions(selectedDistrict) {
  function refreshVillageOptions (line 63) | function refreshVillageOptions(selectedVillage) {
  function setLocationValues (line 76) | function setLocationValues(state, district, village) {
  function setLocationSource (line 95) | function setLocationSource(text) {
  function isManualLocationOverrideEnabled (line 102) | function isManualLocationOverrideEnabled() {
  function toggleManualLocationOverride (line 107) | function toggleManualLocationOverride(enabled) {
  function getEffectiveLocationValues (line 145) | function getEffectiveLocationValues() {
  function initializeLocationFilters (line 170) | function initializeLocationFilters() {
  function isMapAutofillEnabled (line 261) | function isMapAutofillEnabled() {
  function updateGpsBanner (line 266) | function updateGpsBanner(locationData) {
  function shouldApplyLocationUpdate (line 318) | function shouldApplyLocationUpdate(nextLocation, forceUpdate) {
  function applyResolvedLocation (line 344) | function applyResolvedLocation(locationData, forceUpdate) {
  function scheduleMapLocationLookup (line 373) | function scheduleMapLocationLookup(lat, lng, forceUpdate) {
  function setMutationMode (line 407) | function setMutationMode(enabled) {
  function editRecord (line 417) | async function editRecord(recordId) {
  function handleFormSubmit (line 485) | async function handleFormSubmit() {
  function resetForm (line 674) | function resetForm() {
  function ensureLocationInCatalog (line 709) | function ensureLocationInCatalog(state, district, village) {

FILE: static/js/modules/gis.js
  function updateGeometryMetrics (line 5) | async function updateGeometryMetrics(geometry, options) {
  function updateGeometryMetricsForAddRecord (line 68) | async function updateGeometryMetricsForAddRecord(geometry) {
  function clearMetricsUI (line 126) | function clearMetricsUI() {
  function clearGeometrySelection (line 143) | function clearGeometrySelection(clearAddRecordMap) {

FILE: static/js/modules/map_engine.js
  function switchBaseLayer (line 5) | function switchBaseLayer(layerName) {
  function addIndiaMask (line 13) | async function addIndiaMask(mapInstance) {
  function initMap (line 46) | function initMap(adminMode) {
  function loadRecordsOnMap (line 114) | async function loadRecordsOnMap() {
  function clearMapLayers (line 137) | function clearMapLayers() {
  function addRecordsToMap (line 142) | function addRecordsToMap(records) {
  function getLandUseColor (line 167) | function getLandUseColor(landUse) {
  function onParcelClick (line 172) | function onParcelClick(record) {
  function flyToRecord (line 177) | function flyToRecord(record) {
  function initViewRecordMap (line 183) | function initViewRecordMap(record) {
  function initAddRecordMap (line 223) | function initAddRecordMap() {

FILE: static/js/modules/records.js
  function ensureLocationInCatalog (line 5) | function ensureLocationInCatalog(state, district, village) {
  function syncLocationCatalogWithRecords (line 20) | function syncLocationCatalogWithRecords(records) {
  function populateStateFilter (line 27) | function populateStateFilter(records) {
  function populateDistrictFilter (line 49) | function populateDistrictFilter(records) {
  function populateVillageFilter (line 71) | function populateVillageFilter(records) {
  function getAdminFilterState (line 93) | function getAdminFilterState() {
  function initializeRecordFilters (line 109) | function initializeRecordFilters() {
  function filterRecordsByState (line 134) | function filterRecordsByState(records, filterState) {
  function switchRecordsView (line 159) | function switchRecordsView(mode) {
  function renderRecordsList (line 175) | function renderRecordsList(records) {
  function applyAdminFilters (line 279) | function applyAdminFilters(notify) {

FILE: static/js/modules/state.js
  constant DEFAULT_SNAP_DISTANCE (line 29) | const DEFAULT_SNAP_DISTANCE = 20;

FILE: static/js/modules/utils.js
  function showConfirmModal (line 5) | function showConfirmModal(message, onConfirm) {
  function sortedValues (line 60) | function sortedValues(values) {
  function asNumber (line 64) | function asNumber(value) {
  function formatInr (line 69) | function formatInr(value) {
  function escapeHtml (line 73) | function escapeHtml(value) {
  function debounce (line 82) | function debounce(fn, wait = 200) {
  function updateAutofillStatus (line 91) | function updateAutofillStatus(message, isError) {
  function fileToBase64 (line 100) | function fileToBase64(file) {
  function calculateValuation (line 108) | function calculateValuation(areaHa, circleRateInr, landUse) {

FILE: tests/check_users_roles.py
  function report_case (line 13) | def report_case(scenario, action, expected, actual, passed):
  function check (line 17) | def check():

FILE: tests/generate_test_report.py
  function main (line 15) | def main():

FILE: tests/robust_role_test.py
  function report_case (line 23) | def report_case(scenario, action, expected, actual, passed):
  function run_tests (line 27) | def run_tests():

FILE: tests/test_advanced_gis.py
  function report_case (line 11) | def report_case(scenario, action, expected, actual, passed):

FILE: tests/test_logins.py
  function report_case (line 35) | def report_case(scenario, action, expected, actual, passed):

FILE: tests/test_record_soft_delete.py
  function report_case (line 30) | def report_case(scenario, action, expected, actual, passed):

FILE: tests/test_recovery_flow.py
  function report_case (line 30) | def report_case(scenario, action, expected, actual, passed):

FILE: tests/test_report_gen.py
  function test_report_generation (line 11) | def test_report_generation():

FILE: tests/test_restore_flow.py
  function report_case (line 24) | def report_case(scenario, action, expected, actual, passed):

FILE: utils.py
  function resource_path (line 10) | def resource_path(relative_path):
  function safe_divide (line 20) | def safe_divide(numerator, denominator, default=0):
Condensed preview — 65 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (936K chars).
[
  {
    "path": "IndiaLIMS.spec",
    "chars": 1135,
    "preview": "# -*- mode: python ; coding: utf-8 -*-\r\n\r\n\r\na = Analysis(\r\n    ['c:\\\\Users\\\\user\\\\Desktop\\\\zz\\\\app.py'],\r\n    pathex=[],"
  },
  {
    "path": "LIMS.spec",
    "chars": 1130,
    "preview": "# -*- mode: python ; coding: utf-8 -*-\r\n\r\n\r\na = Analysis(\r\n    ['c:\\\\Users\\\\user\\\\Desktop\\\\zz\\\\app.py'],\r\n    pathex=[],"
  },
  {
    "path": "README.md",
    "chars": 8,
    "preview": "nothing\n"
  },
  {
    "path": "app.py",
    "chars": 6981,
    "preview": "import os\nimport sys\nimport time\nimport socket\nimport threading\nimport uuid\nfrom datetime import datetime, timedelta\nimp"
  },
  {
    "path": "build_exe.py",
    "chars": 4434,
    "preview": "\"\"\"\r\nbuild_exe.py - PyInstaller & PyWebView Compilation Script for LIMS\r\nCompiles the application into a standalone Wind"
  },
  {
    "path": "clean_db.py",
    "chars": 271,
    "preview": "from config import MONGO_URI\r\nfrom pymongo import MongoClient\r\nimport certifi\r\ndb = MongoClient(MONGO_URI, tlsCAFile=cer"
  },
  {
    "path": "config.py",
    "chars": 2526,
    "preview": "\"\"\"\nconfig.py - Application Configuration for India LIMS\nCentralized settings for the application.\n\"\"\"\n\nimport os\nfrom d"
  },
  {
    "path": "core/__init__.py",
    "chars": 520,
    "preview": "from .database import (\n    load_users, save_users, load_records, save_records, \n    load_feedback, save_feedback, _log_"
  },
  {
    "path": "core/database.py",
    "chars": 3731,
    "preview": "import os\nimport json\nimport uuid\nfrom datetime import datetime\nfrom pymongo import MongoClient, ReplaceOne\nimport certi"
  },
  {
    "path": "core/logic.py",
    "chars": 3079,
    "preview": "import random\n\ndef _mask_owner_for_viewer(record):\n    if \"owner\" not in record:\n        return record\n    record = reco"
  },
  {
    "path": "core/security.py",
    "chars": 1851,
    "preview": "import string\nimport random\nfrom functools import wraps\nfrom flask import session, jsonify\nfrom itsdangerous import URLS"
  },
  {
    "path": "gis_processor.py",
    "chars": 13193,
    "preview": "\"\"\"\r\ngis_processor.py - Spatial Calculation Module for India LIMS\r\nUses Shapely for all GIS heavy lifting: area calculat"
  },
  {
    "path": "inno_setup.iss",
    "chars": 1635,
    "preview": "; Script generated by the Inno Setup Script Wizard.\r\n; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT F"
  },
  {
    "path": "render.yaml",
    "chars": 271,
    "preview": "services:\n  - type: web\n    name: lims-website\n    env: python\n    buildCommand: pip install -r requirements.txt\n    sta"
  },
  {
    "path": "report_generator.py",
    "chars": 16610,
    "preview": "\"\"\"\r\nreport_generator.py - Document Generation Module for India LIMS\r\nGenerates single-page PDF Property Cards and Excel"
  },
  {
    "path": "requirements-dev.txt",
    "chars": 63,
    "preview": "pytest>=7.0\r\nflask>=3.0\r\npymongo>=4.0\r\ncertifi\r\npython-dotenv\r\n"
  },
  {
    "path": "requirements-windows.txt",
    "chars": 140,
    "preview": "# Base Web Requirements\n-r requirements.txt\n\n# Windows Desktop Specific\npywebview==4.4.1\npythonnet==3.0.3\npyinstaller==6"
  },
  {
    "path": "requirements.txt",
    "chars": 236,
    "preview": "Flask==3.0.0\npymongo[srv]==4.6.1\npython-dotenv==1.0.0\nreportlab==4.0.8\npandas\nopenpyxl==3.1.2\ngunicorn==21.2.0\nwerkzeug="
  },
  {
    "path": "routes/__init__.py",
    "chars": 236,
    "preview": "from .pages import pages_bp\nfrom .auth import auth_bp\nfrom .records import records_bp\nfrom .users import users_bp\nfrom ."
  },
  {
    "path": "routes/auth.py",
    "chars": 2889,
    "preview": "from datetime import datetime\nfrom flask import Blueprint, request, jsonify, session\nfrom werkzeug.security import check"
  },
  {
    "path": "routes/documents.py",
    "chars": 1780,
    "preview": "import io\nfrom datetime import datetime\nfrom flask import Blueprint, request, jsonify, send_file\nfrom core import load_r"
  },
  {
    "path": "routes/feedback.py",
    "chars": 6168,
    "preview": "import uuid\nimport os\nimport json\nfrom datetime import datetime\nfrom flask import Blueprint, request, jsonify, session\nf"
  },
  {
    "path": "routes/gis.py",
    "chars": 5008,
    "preview": "import os\nimport json\nfrom flask import Blueprint, request, jsonify, current_app\nfrom urllib.parse import urlencode\nfrom"
  },
  {
    "path": "routes/pages.py",
    "chars": 1224,
    "preview": "from flask import Blueprint, render_template, redirect, url_for, session, request, jsonify\nfrom core import generate_cap"
  },
  {
    "path": "routes/records.py",
    "chars": 18567,
    "preview": "import uuid\nimport random\nfrom datetime import datetime\nfrom flask import Blueprint, request, jsonify, session\nfrom core"
  },
  {
    "path": "routes/users.py",
    "chars": 8956,
    "preview": "import uuid\nfrom datetime import datetime\nfrom flask import Blueprint, request, jsonify, session\nfrom werkzeug.security "
  },
  {
    "path": "routes/utils.py",
    "chars": 373,
    "preview": "from flask import Blueprint, jsonify\nfrom config import LAND_USE_OPTIONS, LAND_USE_COLORS, MUTATION_TYPES\n\nutils_bp = Bl"
  },
  {
    "path": "run_server.py",
    "chars": 231,
    "preview": "\nfrom app import create_app\nimport os\n\nif __name__ == '__main__':\n    app = create_app()\n    # Disable debug mode and re"
  },
  {
    "path": "scripts/inspect_recovery.py",
    "chars": 1010,
    "preview": "#!/usr/bin/env python3\r\n\"\"\"Inspect superadmin and recovery accounts in the database.\"\"\"\r\nfrom dotenv import load_dotenv\r"
  },
  {
    "path": "scripts/recover_superadmin_auto.py",
    "chars": 5111,
    "preview": "#!/usr/bin/env python3\r\n\"\"\"Server-only recovery script to upsert or reset a superadmin account.\r\n\r\nRun this from the ser"
  },
  {
    "path": "scripts/recovery_credentials_2026-04-25T050759_400298.txt",
    "chars": 159,
    "preview": "SuperAdmin updated\r\nUSERNAME: recovery_sa_20260425_154817.302101Z\r\nPASSWORD: WwI9v*V_J=MpH_SHi3+^\r\nIMPORTANT: copy the p"
  },
  {
    "path": "static/css/style.css",
    "chars": 18739,
    "preview": "/*\n * style.css - Custom Styling for India LIMS\n * Tailwind CSS is loaded via CDN; this file adds Leaflet & custom overr"
  },
  {
    "path": "static/data/india-boundary.geojson",
    "chars": 183708,
    "preview": "{\"type\": \"FeatureCollection\", \"features\": [{\"type\": \"Feature\", \"properties\": {\"name\": \"India\", \"ISO3166-1-Alpha-3\": \"IND"
  },
  {
    "path": "static/js/api.js",
    "chars": 11986,
    "preview": "/**\r\n * api.js - Backend API Communication Module for India LIMS\r\n * All fetch calls to the Flask REST API are centraliz"
  },
  {
    "path": "static/js/auth.js",
    "chars": 10510,
    "preview": "/**\r\n * auth.js - Login and CAPTCHA Handling for India LIMS\r\n * Handles the login page forms: CAPTCHA verification and A"
  },
  {
    "path": "static/js/map.js",
    "chars": 11162,
    "preview": "/**\n * map.js - Entry point for India LIMS client-side logic.\n * Loads modules and initializes the application.\n */\n\n// "
  },
  {
    "path": "static/js/map_OLD_BAK.js",
    "chars": 160478,
    "preview": "/**\r\n * map.js - Leaflet map and parcel workflow logic for India LIMS.\r\n * Handles map initialization, polygon drawing, "
  },
  {
    "path": "static/js/modules/admin.js",
    "chars": 48607,
    "preview": "/**\n * admin.js - Admin UI, User Management, and Audit Logs logic\n */\n\nconst roleColors = {\n    'superadmin': 'bg-red-10"
  },
  {
    "path": "static/js/modules/dashboard.js",
    "chars": 13002,
    "preview": "/**\n * dashboard.js - Dashboard rendering and analytics logic\n */\n\nfunction renderKpiCards(records) {\n    const totalPar"
  },
  {
    "path": "static/js/modules/forms.js",
    "chars": 30102,
    "preview": "/**\n * forms.js - Form handling and location auto-fill logic\n */\n\nfunction getFormElements() {\n    return {\n        stat"
  },
  {
    "path": "static/js/modules/gis.js",
    "chars": 5993,
    "preview": "/**\n * gis.js - GIS-related UI updates (area, perimeter, centroid)\n */\n\nasync function updateGeometryMetrics(geometry, o"
  },
  {
    "path": "static/js/modules/map_engine.js",
    "chars": 16059,
    "preview": "/**\n * map_engine.js - Leaflet map initialization, layers, and drawing logic\n */\n\nfunction switchBaseLayer(layerName) {\n"
  },
  {
    "path": "static/js/modules/records.js",
    "chars": 12269,
    "preview": "/**\n * records.js - Records list and filtering logic\n */\n\nfunction ensureLocationInCatalog(state, district, village) {\n "
  },
  {
    "path": "static/js/modules/state.js",
    "chars": 769,
    "preview": "/**\n * state.js - Global state for India LIMS\n */\n\nlet map = null;\nlet viewRecordMap = null;\nlet addRecordMap = null;\nle"
  },
  {
    "path": "static/js/modules/utils.js",
    "chars": 4781,
    "preview": "/**\n * utils.js - General utility functions for India LIMS\n */\n\nfunction showConfirmModal(message, onConfirm) {\n    cons"
  },
  {
    "path": "templates/admin_dashboard.html",
    "chars": 112248,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width"
  },
  {
    "path": "templates/login.html",
    "chars": 12225,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width"
  },
  {
    "path": "templates/public_viewer_v2.html",
    "chars": 73054,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width"
  },
  {
    "path": "tests/Testing_Report_20260425_211328.md",
    "chars": 4918,
    "preview": "# INDIA LIMS - AUTOMATED TESTING REPORT\r\n\r\n**Date/Time:** 2026-04-25 21:13:58\r\n\r\n## 1. System Test Cases Summary\r\n\r\n### "
  },
  {
    "path": "tests/Testing_Report_20260426_013232.md",
    "chars": 3938,
    "preview": "# INDIA LIMS - AUTOMATED TESTING REPORT\r\n\r\n**Date/Time:** 2026-04-26 01:32:54\r\n\r\n## 1. System Test Cases Summary\r\n\r\n### "
  },
  {
    "path": "tests/Testing_Report_20260426_095300.md",
    "chars": 5496,
    "preview": "# INDIA LIMS - AUTOMATED TESTING REPORT\r\n\r\n**Date/Time:** 2026-04-26 09:53:35\r\n\r\n## 1. System Test Cases Summary\r\n\r\n### "
  },
  {
    "path": "tests/Testing_Report_20260426_125456.md",
    "chars": 5595,
    "preview": "# LIMS - AUTOMATED TESTING REPORT\r\n\r\n**Date/Time:** 2026-04-26 12:55:31\r\n\r\n## 1. System Test Cases Summary\r\n\r\n### Test T"
  },
  {
    "path": "tests/Testing_Report_20260426_132557.md",
    "chars": 3263,
    "preview": "# LIMS - AUTOMATED TESTING REPORT\r\n\r\n**Date/Time:** 2026-04-26 13:25:59\r\n\r\n## 1. System Test Cases Summary\r\n\r\n*No test c"
  },
  {
    "path": "tests/Testing_Report_20260426_132640.md",
    "chars": 5700,
    "preview": "# LIMS - AUTOMATED TESTING REPORT\r\n\r\n**Date/Time:** 2026-04-26 13:27:17\r\n\r\n## 1. System Test Cases Summary\r\n\r\n### Test T"
  },
  {
    "path": "tests/check_users_roles.py",
    "chars": 1317,
    "preview": "r\"\"\"\r\nSimple verifier: list users and their stored roles\r\n\"\"\"\r\nimport os, sys, json\r\ntry:\r\n    from config import MONGO_"
  },
  {
    "path": "tests/generate_test_report.py",
    "chars": 3478,
    "preview": "import os\r\nimport sys\r\nimport subprocess\r\nimport datetime\r\n\r\nTEST_SCRIPTS = [\r\n    \"tests/test_logins.py\",\r\n    \"tests/r"
  },
  {
    "path": "tests/robust_role_test.py",
    "chars": 6285,
    "preview": "r\"\"\"\nrobust_role_test.py - Improved Role-Based Access Testing\nFollows the user's logic:\n1. Scan user_id_password.json to"
  },
  {
    "path": "tests/test_advanced_gis.py",
    "chars": 3868,
    "preview": "import os\nimport sys\nimport json\nimport uuid\nfrom datetime import datetime\n\n# Ensure project root is importable\nsys.path"
  },
  {
    "path": "tests/test_logins.py",
    "chars": 2868,
    "preview": "r\"\"\"\r\nTest login flow using Flask test client and credentials in user_id_password.json\r\nUsage: .venv\\Scripts\\python test"
  },
  {
    "path": "tests/test_record_soft_delete.py",
    "chars": 3994,
    "preview": "r\"\"\"\r\nTest soft-delete flow for records:\r\n- Login as admin, create a record\r\n- Login as officer, soft-delete the record\r"
  },
  {
    "path": "tests/test_recovery_flow.py",
    "chars": 2431,
    "preview": "r\"\"\"\r\nRecovery account integration smoke test (create + delete superadmin)\r\nRun: .venv\\Scripts\\python tests\\test_recover"
  },
  {
    "path": "tests/test_report_gen.py",
    "chars": 3000,
    "preview": "\nimport os\nimport sys\nimport json\n\n# Add project root to path\nsys.path.append(os.path.abspath(os.path.join(os.path.dirna"
  },
  {
    "path": "tests/test_restore_flow.py",
    "chars": 2990,
    "preview": "r\"\"\"\r\nTest restore flow:\r\n- Login as admin, create temp officer and a record\r\n- Login as officer, soft-delete the record"
  },
  {
    "path": "utils.py",
    "chars": 730,
    "preview": "\"\"\"\nutils.py - Shared Utilities for India LIMS\nCommon functions used across the application.\n\"\"\"\n\nimport os\nimport sys\n\n"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the errorcode26/test GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 65 files (874.1 KB), approximately 265.9k tokens, and a symbol index with 319 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!