Browse Source

improved overlay manager

Nicole 1 tháng trước cách đây
mục cha
commit
3c1773f791

+ 1 - 0
.gitignore

@@ -139,3 +139,4 @@ base
 
 
 # overlays
 # overlays
 custom_overlays
 custom_overlays
+overlay_db

+ 1 - 0
docker-compose.yml

@@ -47,5 +47,6 @@ services:
     restart: always
     restart: always
     volumes:
     volumes:
       - ./custom_overlays:/srv:rw
       - ./custom_overlays:/srv:rw
+      - ./overlay_db:/app/db   # <-- We mount a whole folder here now
     ports:
     ports:
       - "0.0.0.0:11081:80"
       - "0.0.0.0:11081:80"

+ 346 - 72
overlay_manager/main.py

@@ -1,8 +1,10 @@
 import os
 import os
+import sqlite3
 import shutil
 import shutil
-import logging
 import tempfile
 import tempfile
-from fastapi import FastAPI, Request, File, UploadFile, Form
+import logging
+from urllib.parse import quote
+from fastapi import FastAPI, Request, File, UploadFile, Form, HTTPException
 from fastapi.responses import HTMLResponse, RedirectResponse, FileResponse
 from fastapi.responses import HTMLResponse, RedirectResponse, FileResponse
 from fastapi.templating import Jinja2Templates
 from fastapi.templating import Jinja2Templates
 from fastapi.staticfiles import StaticFiles
 from fastapi.staticfiles import StaticFiles
@@ -11,96 +13,368 @@ from fastapi.staticfiles import StaticFiles
 logging.basicConfig(level=logging.INFO)
 logging.basicConfig(level=logging.INFO)
 logger = logging.getLogger("overlay-manager")
 logger = logging.getLogger("overlay-manager")
 
 
-# root_path ensures FastAPI knows it lives at /patch-manager/
 app = FastAPI(root_path="/patch-manager")
 app = FastAPI(root_path="/patch-manager")
 
 
 os.makedirs("static", exist_ok=True)
 os.makedirs("static", exist_ok=True)
 app.mount("/static", StaticFiles(directory="static"), name="static")
 app.mount("/static", StaticFiles(directory="static"), name="static")
 templates = Jinja2Templates(directory="templates")
 templates = Jinja2Templates(directory="templates")
 
 
-OVERLAY_DIR = "/srv"
+OVERLAY_DIR = os.path.abspath("/srv")
+
+# Use the new mounted directory for the database
+DB_DIR = "/app/db"
+os.makedirs(DB_DIR, exist_ok=True)
+DB_PATH = os.path.join(DB_DIR, "overlays.db")
+
+def init_db():
+    """Initializes the SQLite database and schema."""
+    with sqlite3.connect(DB_PATH) as conn:
+        conn.execute("""
+            CREATE TABLE IF NOT EXISTS profiles (
+                id INTEGER PRIMARY KEY AUTOINCREMENT,
+                name TEXT UNIQUE NOT NULL,
+                is_active BOOLEAN NOT NULL DEFAULT 0
+            )
+        """)
+        conn.execute("""
+            CREATE TABLE IF NOT EXISTS files (
+                id INTEGER PRIMARY KEY AUTOINCREMENT,
+                profile_id INTEGER NOT NULL,
+                filepath TEXT NOT NULL,
+                content BLOB NOT NULL,
+                FOREIGN KEY(profile_id) REFERENCES profiles(id) ON DELETE CASCADE,
+                UNIQUE(profile_id, filepath)
+            )
+        """)
+        
+        # Auto-migrate: Add description and is_readonly columns if they don't exist
+        cursor = conn.cursor()
+        cursor.execute("PRAGMA table_info(profiles)")
+        columns = [info[1] for info in cursor.fetchall()]
+        if "description" not in columns:
+            cursor.execute("ALTER TABLE profiles ADD COLUMN description TEXT DEFAULT ''")
+        if "is_readonly" not in columns:
+            cursor.execute("ALTER TABLE profiles ADD COLUMN is_readonly BOOLEAN NOT NULL DEFAULT 0")
+        
+        # Create default profiles if the DB is empty
+        cursor.execute("SELECT count(*) FROM profiles")
+        if cursor.fetchone()[0] == 0:
+            cursor.execute("INSERT INTO profiles (name, description, is_active, is_readonly) VALUES ('Vanilla (No Overlays)', 'Baseline ArduPilot build without any modifications. Read-only.', 1, 1)")
+            cursor.execute("INSERT INTO profiles (name, description, is_active, is_readonly) VALUES ('Custom Build Alpha', 'Experimental tuning parameters.', 0, 0)")
+        else:
+            # Lock the existing Vanilla profile for users upgrading
+            cursor.execute("UPDATE profiles SET is_readonly = 1 WHERE name = 'Vanilla (No Overlays)'")
+            
+        conn.commit()
+
+init_db()
+
+def get_db_connection():
+    conn = sqlite3.connect(DB_PATH)
+    conn.row_factory = sqlite3.Row
+    return conn
 
 
 @app.get("/", response_class=HTMLResponse)
 @app.get("/", response_class=HTMLResponse)
-async def index(request: Request):
+async def index(request: Request, success_msg: str = None, error_msg: str = None):
+    conn = get_db_connection()
+    
+    profiles = conn.execute("SELECT * FROM profiles ORDER BY name").fetchall()
+    active_profile = conn.execute("SELECT * FROM profiles WHERE is_active = 1").fetchone()
+    
     files = []
     files = []
-    dirs = set([""])
-    if os.path.exists(OVERLAY_DIR):
-        for root, dirnames, filenames in os.walk(OVERLAY_DIR):
-            rel_root = os.path.relpath(root, OVERLAY_DIR)
-            if rel_root != ".":
-                dirs.add(rel_root)
-            for filename in filenames:
-                rel_path = os.path.relpath(os.path.join(root, filename), OVERLAY_DIR)
-                files.append(rel_path)
+    dirs = set()
+    
+    if active_profile:
+        files = conn.execute("SELECT * FROM files WHERE profile_id = ? ORDER BY filepath", (active_profile['id'],)).fetchall()
+        for f in files:
+            dirname = os.path.dirname(f['filepath'])
+            if dirname:
+                dirs.add(dirname)
+                
+    conn.close()
                 
                 
     return templates.TemplateResponse("index.html", {
     return templates.TemplateResponse("index.html", {
         "request": request, 
         "request": request, 
-        "files": sorted(files),
-        "dirs": sorted(list(dirs))
+        "profiles": profiles,
+        "active_profile": active_profile,
+        "files": files,
+        "dirs": sorted(list(dirs)),
+        "success_msg": success_msg,
+        "error_msg": error_msg
     })
     })
 
 
-@app.get("/download-all")
-async def download_all():
-    """Zips the entire /srv directory and serves it as a backup."""
-    with tempfile.TemporaryDirectory() as tmpdir:
-        zip_base = os.path.join(tmpdir, "backup")
-        shutil.make_archive(zip_base, 'zip', OVERLAY_DIR)
-        return FileResponse(
-            path=f"{zip_base}.zip", 
-            filename="ardupilot_overlays_backup.zip",
-            media_type='application/zip'
-        )
+@app.post("/profile/create")
+async def create_profile(name: str = Form(...)):
+    try:
+        with get_db_connection() as conn:
+            conn.execute("INSERT INTO profiles (name, description, is_active, is_readonly) VALUES (?, '', 0, 0)", (name.strip(),))
+            conn.commit()
+        msg = quote(f"Profile '{name}' created.")
+        return RedirectResponse(url=f"/patch-manager/?success_msg={msg}", status_code=303)
+    except sqlite3.IntegrityError:
+        msg = quote("A profile with that name already exists.")
+        return RedirectResponse(url=f"/patch-manager/?error_msg={msg}", status_code=303)
+    except Exception as e:
+        logger.error(f"Profile creation failed: {e}")
+        return RedirectResponse(url=f"/patch-manager/?error_msg=Failed to create profile.", status_code=303)
 
 
-@app.post("/upload")
-async def upload_file(file: UploadFile = File(...), target_path: str = Form("")):
-    save_dir = os.path.join(OVERLAY_DIR, target_path.strip("/"))
-    os.makedirs(save_dir, exist_ok=True)
-    file_location = os.path.join(save_dir, file.filename)
-    with open(file_location, "wb+") as file_object:
-        shutil.copyfileobj(file.file, file_object)
-    return RedirectResponse(url="./", status_code=303)
-
-@app.post("/create_folder")
-async def create_folder(folder_path: str = Form(...)):
-    save_dir = os.path.join(OVERLAY_DIR, folder_path.strip("/"))
-    os.makedirs(save_dir, exist_ok=True)
-    return RedirectResponse(url="./", status_code=303)
-
-@app.post("/delete")
-async def delete_file(filepath: str = Form(...)):
-    target = os.path.join(OVERLAY_DIR, filepath.lstrip("/"))
-    if os.path.exists(target):
-        if os.path.isdir(target):
-            shutil.rmtree(target)
-        else:
-            os.remove(target)
-    return RedirectResponse(url="./", status_code=303)
+@app.post("/profile/rename")
+async def rename_profile(profile_id: int = Form(...), new_name: str = Form(...)):
+    try:
+        with get_db_connection() as conn:
+            prof = conn.execute("SELECT is_readonly FROM profiles WHERE id = ?", (profile_id,)).fetchone()
+            if prof and prof['is_readonly']:
+                return RedirectResponse(url=f"/patch-manager/?error_msg=Cannot rename a read-only profile.", status_code=303)
+                
+            conn.execute("UPDATE profiles SET name = ? WHERE id = ?", (new_name.strip(), profile_id))
+            conn.commit()
+        msg = quote(f"Profile renamed to '{new_name}'.")
+        return RedirectResponse(url=f"/patch-manager/?success_msg={msg}", status_code=303)
+    except sqlite3.IntegrityError:
+        msg = quote("A profile with that name already exists.")
+        return RedirectResponse(url=f"/patch-manager/?error_msg={msg}", status_code=303)
+    except Exception as e:
+        logger.error(f"Profile rename failed: {e}")
+        return RedirectResponse(url=f"/patch-manager/?error_msg=Failed to rename profile.", status_code=303)
 
 
-@app.get("/edit", response_class=HTMLResponse)
-async def edit_file(request: Request, filepath: str):
-    clean_path = filepath.lstrip("/")
-    target = os.path.join(OVERLAY_DIR, clean_path)
-    logger.info(f"Attempting to edit: {target}")
+@app.post("/profile/annotate")
+async def annotate_profile(profile_id: int = Form(...), description: str = Form("")):
+    try:
+        with get_db_connection() as conn:
+            prof = conn.execute("SELECT is_readonly FROM profiles WHERE id = ?", (profile_id,)).fetchone()
+            if prof and prof['is_readonly']:
+                return RedirectResponse(url=f"/patch-manager/?error_msg=Cannot edit notes on a read-only profile.", status_code=303)
 
 
-    if not os.path.exists(target):
-        return HTMLResponse(content=f"<h1>404 Not Found</h1><p>File {target} missing.</p><a href='./'>Back</a>", status_code=404)
+            conn.execute("UPDATE profiles SET description = ? WHERE id = ?", (description, profile_id))
+            conn.commit()
+        msg = quote("Notes saved successfully.")
+        return RedirectResponse(url=f"/patch-manager/?success_msg={msg}", status_code=303)
+    except Exception as e:
+        logger.error(f"Annotation failed: {e}")
+        return RedirectResponse(url=f"/patch-manager/?error_msg=Failed to save notes.", status_code=303)
 
 
+@app.post("/profile/duplicate")
+async def duplicate_profile(profile_id: int = Form(...)):
     try:
     try:
-        with open(target, "r", encoding="utf-8", errors="ignore") as f:
-            content = f.read()
+        with get_db_connection() as conn:
+            cursor = conn.cursor()
+            
+            src = cursor.execute("SELECT name, description FROM profiles WHERE id = ?", (profile_id,)).fetchone()
+            if not src:
+                raise Exception("Source profile not found.")
+            
+            base_name = src['name']
+            desc = src['description'] if src['description'] else ""
+            
+            new_name = f"{base_name} (Copy)"
+            counter = 1
+            while True:
+                exists = cursor.execute("SELECT id FROM profiles WHERE name = ?", (new_name,)).fetchone()
+                if not exists:
+                    break
+                counter += 1
+                new_name = f"{base_name} (Copy {counter})"
+            
+            cursor.execute("UPDATE profiles SET is_active = 0")
+            
+            # Duplicates are NEVER read-only, even if cloned from a read-only profile
+            cursor.execute("INSERT INTO profiles (name, description, is_active, is_readonly) VALUES (?, ?, 1, 0)", (new_name, desc))
+            new_profile_id = cursor.lastrowid
+            
+            files = cursor.execute("SELECT filepath, content FROM files WHERE profile_id = ?", (profile_id,)).fetchall()
+            for f in files:
+                cursor.execute("INSERT INTO files (profile_id, filepath, content) VALUES (?, ?, ?)", 
+                               (new_profile_id, f['filepath'], f['content']))
+            conn.commit()
+            
+        msg = quote(f"Seamlessly cloned and activated '{new_name}'.")
+        return RedirectResponse(url=f"/patch-manager/?success_msg={msg}", status_code=303)
     except Exception as e:
     except Exception as e:
-        return HTMLResponse(content=f"<h1>Error</h1><p>{str(e)}</p>", status_code=500)
+        logger.error(f"Profile duplication failed: {e}")
+        return RedirectResponse(url=f"/patch-manager/?error_msg=Failed to duplicate profile.", status_code=303)
 
 
-    return templates.TemplateResponse("edit.html", {
-        "request": request, 
-        "filepath": filepath, 
-        "content": content
-    })
+@app.post("/profile/activate")
+async def activate_profile(profile_id: int = Form(...)):
+    try:
+        with get_db_connection() as conn:
+            conn.execute("UPDATE profiles SET is_active = 0")
+            conn.execute("UPDATE profiles SET is_active = 1 WHERE id = ?", (profile_id,))
+            conn.commit()
+        return RedirectResponse(url=f"/patch-manager/", status_code=303)
+    except Exception as e:
+        logger.error(f"Profile activation failed: {e}")
+        return RedirectResponse(url=f"/patch-manager/?error_msg=Failed to activate profile.", status_code=303)
+
+@app.post("/profile/delete")
+async def delete_profile(profile_id: int = Form(...)):
+    try:
+        with get_db_connection() as conn:
+            prof = conn.execute("SELECT is_readonly, is_active FROM profiles WHERE id = ?").fetchone()
+            
+            if prof and prof['is_readonly']:
+                msg = quote("Cannot delete a read-only profile.")
+                return RedirectResponse(url=f"/patch-manager/?error_msg={msg}", status_code=303)
+                
+            if prof and prof['is_active']:
+                msg = quote("Cannot delete the currently active profile. Switch to another first.")
+                return RedirectResponse(url=f"/patch-manager/?error_msg={msg}", status_code=303)
+                
+            conn.execute("DELETE FROM profiles WHERE id = ?", (profile_id,))
+            conn.commit()
+        msg = quote("Profile deleted successfully.")
+        return RedirectResponse(url=f"/patch-manager/?success_msg={msg}", status_code=303)
+    except Exception as e:
+        logger.error(f"Profile deletion failed: {e}")
+        return RedirectResponse(url=f"/patch-manager/?error_msg=Failed to delete profile.", status_code=303)
+
+@app.post("/deploy")
+async def deploy_active_profile():
+    try:
+        with get_db_connection() as conn:
+            active = conn.execute("SELECT id, name FROM profiles WHERE is_active = 1").fetchone()
+            if not active:
+                raise Exception("No active profile selected.")
+                
+            files = conn.execute("SELECT filepath, content FROM files WHERE profile_id = ?", (active['id'],)).fetchall()
+            
+        if os.path.exists(OVERLAY_DIR):
+            for item in os.listdir(OVERLAY_DIR):
+                item_path = os.path.join(OVERLAY_DIR, item)
+                if os.path.isfile(item_path):
+                    os.remove(item_path)
+                elif os.path.isdir(item_path):
+                    shutil.rmtree(item_path)
+        os.makedirs(OVERLAY_DIR, exist_ok=True)
+        
+        count = 0
+        for f in files:
+            target = os.path.abspath(os.path.join(OVERLAY_DIR, f['filepath'].lstrip("/")))
+            if target.startswith(OVERLAY_DIR):
+                os.makedirs(os.path.dirname(target), exist_ok=True)
+                with open(target, "wb") as out_file:
+                    out_file.write(f['content'])
+                count += 1
+                
+        return RedirectResponse(url="/", status_code=303)
+    except Exception as e:
+        logger.error(f"Deployment failed: {e}")
+        msg = quote("Failed to deploy files to staging directory.")
+        return RedirectResponse(url=f"/patch-manager/?error_msg={msg}", status_code=303)
+
+@app.post("/file/upload")
+async def upload_file(
+    file: UploadFile = File(...), 
+    existing_path: str = Form(""), 
+    new_path: str = Form(""), 
+    profile_id: int = Form(...)
+):
+    try:
+        with get_db_connection() as conn:
+            prof = conn.execute("SELECT is_readonly FROM profiles WHERE id = ?", (profile_id,)).fetchone()
+            if prof and prof['is_readonly']:
+                msg = quote("Cannot upload files to a read-only profile.")
+                return RedirectResponse(url=f"/patch-manager/?error_msg={msg}", status_code=303)
+
+            target_path = new_path.strip() if new_path.strip() else existing_path.strip()
+            clean_path = target_path.strip("/")
+            
+            safe_filename = os.path.basename(file.filename)
+            full_filepath = os.path.join(clean_path, safe_filename) if clean_path else safe_filename
+            
+            content = await file.read()
+
+            conn.execute("""
+                INSERT INTO files (profile_id, filepath, content) 
+                VALUES (?, ?, ?)
+                ON CONFLICT(profile_id, filepath) 
+                DO UPDATE SET content=excluded.content
+            """, (profile_id, full_filepath, content))
+            conn.commit()
+            
+        msg = quote(f"Successfully saved {safe_filename} to profile.")
+        return RedirectResponse(url=f"/patch-manager/?success_msg={msg}", status_code=303)
+    except Exception as e:
+        logger.error(f"Upload failed: {e}")
+        msg = quote("Failed to upload file to database.")
+        return RedirectResponse(url=f"/patch-manager/?error_msg={msg}", status_code=303)
+
+@app.post("/file/delete")
+async def delete_file(file_id: int = Form(...)):
+    try:
+        with get_db_connection() as conn:
+            conn.execute("DELETE FROM files WHERE id = ?", (file_id,))
+            conn.commit()
+        return RedirectResponse(url=f"/patch-manager/", status_code=303)
+    except Exception as e:
+        logger.error(f"File deletion failed: {e}")
+        return RedirectResponse(url=f"/patch-manager/?error_msg=Failed to delete file.", status_code=303)
+
+@app.get("/edit", response_class=HTMLResponse)
+async def edit_file(request: Request, file_id: int, error_msg: str = None):
+    try:
+        with get_db_connection() as conn:
+            file_record = conn.execute("SELECT * FROM files WHERE id = ?", (file_id,)).fetchone()
+            active_profile = conn.execute("SELECT * FROM profiles WHERE is_active = 1").fetchone()
+            profiles = conn.execute("SELECT * FROM profiles ORDER BY name").fetchall()
+            
+        if not file_record:
+            msg = quote("File not found in database.")
+            return RedirectResponse(url=f"/patch-manager/?error_msg={msg}", status_code=303)
+
+        try:
+            content_text = file_record['content'].decode('utf-8')
+        except UnicodeDecodeError:
+            msg = quote(f"Safety Lock: {os.path.basename(file_record['filepath'])} appears to be a binary file and cannot be opened in the text editor.")
+            return RedirectResponse(url=f"/patch-manager/?error_msg={msg}", status_code=303)
+
+        return templates.TemplateResponse("edit.html", {
+            "request": request, 
+            "file_id": file_id,
+            "filepath": file_record['filepath'], 
+            "content": content_text,
+            "active_profile": active_profile,
+            "profiles": profiles,
+            "error_msg": error_msg
+        })
+    except Exception as e:
+        logger.error(f"Edit GET failed: {e}")
+        msg = quote(f"System Error loading file: {str(e)}")
+        return RedirectResponse(url=f"/patch-manager/?error_msg={msg}", status_code=303)
 
 
 @app.post("/edit")
 @app.post("/edit")
-async def save_file(filepath: str = Form(...), content: str = Form(...)):
-    target = os.path.join(OVERLAY_DIR, filepath.lstrip("/"))
-    if os.path.exists(target):
-        with open(target, "w", encoding="utf-8") as f:
-            f.write(content)
-    return RedirectResponse(url="./", status_code=303)
+async def save_file(file_id: int = Form(...), content: str = Form(...)):
+    try:
+        content_bytes = content.encode('utf-8')
+        with get_db_connection() as conn:
+            conn.execute("UPDATE files SET content = ? WHERE id = ?", (content_bytes, file_id))
+            conn.commit()
+            
+        msg = quote("Successfully saved changes.")
+        return RedirectResponse(url=f"/patch-manager/?success_msg={msg}", status_code=303)
+    except Exception as e:
+        logger.error(f"Edit POST failed: {e}")
+        msg = quote("Failed to save file.")
+        return RedirectResponse(url=f"/patch-manager/edit?file_id={file_id}&error_msg={msg}", status_code=303)
+
+@app.get("/download")
+async def download_single_file(file_id: int):
+    try:
+        with get_db_connection() as conn:
+            file_record = conn.execute("SELECT * FROM files WHERE id = ?", (file_id,)).fetchone()
+            
+        if not file_record:
+            raise HTTPException(status_code=404, detail="File not found")
+            
+        with tempfile.NamedTemporaryFile(delete=False) as tmp:
+            tmp.write(file_record['content'])
+            tmp_path = tmp.name
+            
+        return FileResponse(
+            path=tmp_path, 
+            filename=os.path.basename(file_record['filepath']),
+            media_type='application/octet-stream'
+        )
+    except Exception as e:
+        logger.error(f"Download single file failed: {e}")
+        msg = quote("Failed to download file.")
+        return RedirectResponse(url=f"/patch-manager/?error_msg={msg}", status_code=303)

+ 132 - 20
overlay_manager/templates/edit.html

@@ -2,35 +2,147 @@
 <html lang="en">
 <html lang="en">
 <head>
 <head>
     <meta charset="UTF-8">
     <meta charset="UTF-8">
-    <title>Edit - {{ filepath }}</title>
+    <title>ArduPilot Overlay Manager - Edit</title>
     <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
     <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
+    
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/codemirror.min.css">
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/theme/monokai.min.css">
+    
+    <style>
+        html, body { height: 100%; }
+        body {
+            display: flex;
+            flex-direction: column;
+            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+            background-color: #f4f6f9;
+        }
+        .content-wrapper { flex: 1 0 auto; }
+        .navbar-brand { font-size: 25px; }
+        .card { border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); border: none;}
+        footer { flex-shrink: 0; }
+        
+        .CodeMirror {
+            height: 600px;
+            font-size: 14px;
+            font-family: 'Courier New', Courier, monospace;
+            border-radius: 0;
+        }
+        
+        .profile-list-item.active {
+            background-color: #0d6efd;
+            border-color: #0d6efd;
+            color: white;
+        }
+        .profile-list-item.active .text-muted {
+            color: rgba(255,255,255,0.9) !important;
+        }
+    </style>
 </head>
 </head>
-<body class="bg-light">
-    <nav class="navbar navbar-dark shadow-sm py-2 mb-4" style="background-color: #0B61A4;">
-        <div class="container">
-            <span class="navbar-brand mb-0 h4"><i class="bi bi-pencil-square me-2"></i>Overlay Editor</span>
-            <a href="./" class="btn btn-outline-light btn-sm"><i class="bi bi-arrow-left me-1"></i>Back to Manager</a>
-        </div>
-    </nav>
+<body>
+    <div class="content-wrapper">
+        <nav class="navbar navbar-dark bg-dark shadow-sm py-2">
+            <div class="container-fluid px-4">
+                <div class="d-flex align-items-center">
+                    <a class="navbar-brand d-flex align-items-center" href="/">
+                        <img src="/static/images/ardupilot_logo.png" alt="ArduPilot" height="24" class="d-inline-block align-text-top me-2">
+                        <span class="text-white">Overlay Manager</span>
+                    </a>
+                </div>
+                <div class="ms-auto d-flex align-items-center">
+                    <a href="javascript:void(0);" onclick="window.location.href = window.location.protocol + '//' + window.location.hostname;" class="btn btn-outline-light">
+                        <i class="bi bi-arrow-left me-2"></i>Back to Main App
+                    </a>
+                </div>
+            </div>
+        </nav>
 
 
-    <div class="container">
-        <div class="card shadow border-0 rounded-3 mb-5">
-            <div class="card-header bg-white pt-3 pb-2 d-flex justify-content-between align-items-center">
-                <span class="font-monospace fw-bold text-primary">{{ filepath }}</span>
+        <div class="container-fluid px-4 mt-4">
+            {% if error_msg %}
+            <div class="alert alert-danger alert-dismissible fade show shadow-sm" role="alert">
+                <i class="bi bi-exclamation-triangle-fill me-2"></i>{{ error_msg }}
+                <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
             </div>
             </div>
-            <div class="card-body p-0">
-                <form action="edit" method="post">
-                    <input type="hidden" name="filepath" value="{{ filepath }}">
-                    <textarea name="content" class="form-control border-0 font-monospace text-bg-dark rounded-0 p-3" rows="25" style="resize: vertical; background-color: #1e1e1e; color: #d4d4d4;">{{ content }}</textarea>
+            {% endif %}
+
+            <div class="row g-4">
+                <div class="col-md-3">
+                    <div class="card mb-4">
+                        <div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
+                            <h6 class="mb-0 fw-bold text-dark"><i class="bi bi-diagram-3-fill me-2 text-primary"></i>Profiles / Loadouts</h6>
+                        </div>
+                        <div class="list-group list-group-flush">
+                            {% for p in profiles %}
+                            <div class="list-group-item d-flex justify-content-between align-items-center profile-list-item {% if p.id == active_profile.id %}active{% endif %}">
+                                <div class="w-100 fw-bold text-muted {% if p.id == active_profile.id %}text-white{% endif %}">
+                                    {{ p.name }}
+                                    {% if p.id == active_profile.id %}<i class="bi bi-check-circle-fill ms-2"></i>{% endif %}
+                                </div>
+                            </div>
+                            {% endfor %}
+                        </div>
+                        <div class="card-footer bg-light p-3 text-center">
+                            <small class="text-muted"><i class="bi bi-info-circle me-1"></i> Return to the dashboard to switch profiles.</small>
+                        </div>
+                    </div>
+                </div>
 
 
-                    <div class="p-3 bg-light text-end rounded-bottom-3">
-                        <a href="./" class="btn btn-secondary me-2">Cancel</a>
-                        <button type="submit" class="btn btn-success fw-bold"><i class="bi bi-save2-fill me-2"></i>Commit Changes</button>
+                <div class="col-md-9">
+                    <div class="card h-100 mb-5 border-0 shadow-sm">
+                        <div class="card-header bg-white py-3 d-flex justify-content-between align-items-center border-bottom-0">
+                            <h5 class="card-title mb-0 fw-bold text-dark">
+                                <i class="bi bi-pencil-square me-2 text-primary"></i>Editing: <span class="text-primary font-monospace">{{ filepath }}</span>
+                            </h5>
+                            <a href="./" class="btn btn-sm btn-outline-secondary"><i class="bi bi-arrow-left me-1"></i> Back to Loadout</a>
+                        </div>
+                        
+                        <div class="card-body p-0">
+                            <form action="edit" method="post" id="editForm">
+                                <input type="hidden" name="file_id" value="{{ file_id }}">
+                                
+                                <textarea id="code-editor" name="content">{{ content }}</textarea>
+
+                                <div class="p-3 bg-light text-end rounded-bottom border-top">
+                                    <a href="./" class="btn btn-secondary me-2">Cancel</a>
+                                    <button type="submit" class="btn btn-success fw-bold"><i class="bi bi-save2-fill me-2"></i> Save Changes to DB</button>
+                                </div>
+                            </form>
+                        </div>
                     </div>
                     </div>
-                </form>
+                </div>
             </div>
             </div>
         </div>
         </div>
     </div>
     </div>
+
+    <footer class="py-3 bg-dark text-white-50 mt-auto">
+        <div class="container-fluid px-4 d-flex justify-content-between">
+            <div>
+                <span class="mx-2">Credits: <a href="https://github.com/ArduPilot/CustomBuild/graphs/contributors" style="text-decoration: underline; color: white;">See Contributors</a></span>|
+                <span class="mx-2">Source: <a href="https://git.equalmass.com/Equalmass/ArdupilotCustomFirmwareBuilder" style="text-decoration: underline; color: white;">Ardupilot/CustomBuild</a></span>
+            </div>
+            <span>ArduPilot Overlay Manager</span>
+        </div>
+    </footer>
+    
+    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
+    
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/codemirror.min.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/mode/clike/clike.min.js"></script>
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/mode/python/python.min.js"></script>
+    
+    <script>
+        var editor = CodeMirror.fromTextArea(document.getElementById("code-editor"), {
+            lineNumbers: true,
+            mode: "text/x-c++src", 
+            theme: "monokai",
+            matchBrackets: true,
+            indentUnit: 4,
+            indentWithTabs: false
+        });
+        
+        document.getElementById('editForm').addEventListener('submit', function() {
+            editor.save();
+        });
+    </script>
 </body>
 </body>
 </html>
 </html>

+ 229 - 80
overlay_manager/templates/index.html

@@ -6,27 +6,30 @@
     <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
     <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
     <style>
     <style>
-        /* Ensures the body takes up at least the full height of the viewport */
-        html, body {
-            height: 100%;
-        }
+        html, body { height: 100%; }
         body {
         body {
             display: flex;
             display: flex;
             flex-direction: column;
             flex-direction: column;
             font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
             font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+            background-color: #f4f6f9;
         }
         }
-        .content-wrapper {
-            flex: 1 0 auto; /* Pushes the footer down */
-        }
+        .content-wrapper { flex: 1 0 auto; }
         .navbar-brand { font-size: 25px; }
         .navbar-brand { font-size: 25px; }
-        .card { border-radius: 8px; }
+        .card { border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); border: none;}
         .table-light { background-color: #f8f9fa; }
         .table-light { background-color: #f8f9fa; }
-        footer {
-            flex-shrink: 0; /* Prevents footer from shrinking */
+        footer { flex-shrink: 0; }
+        .profile-list-item.active {
+            background-color: #0d6efd;
+            border-color: #0d6efd;
+            color: white;
         }
         }
+        .profile-list-item.active .text-muted, .profile-list-item.active .text-secondary {
+            color: rgba(255,255,255,0.8) !important;
+        }
+        .dropdown-item i { width: 20px; text-align: center; }
     </style>
     </style>
 </head>
 </head>
-<body class="bg-light">
+<body>
     <div class="content-wrapper">
     <div class="content-wrapper">
         <nav class="navbar navbar-dark bg-dark shadow-sm py-2">
         <nav class="navbar navbar-dark bg-dark shadow-sm py-2">
             <div class="container-fluid px-4">
             <div class="container-fluid px-4">
@@ -36,7 +39,7 @@
                         <span class="text-white">Overlay Manager</span>
                         <span class="text-white">Overlay Manager</span>
                     </a>
                     </a>
                 </div>
                 </div>
-                <div class="ms-auto">
+                <div class="ms-auto d-flex align-items-center">
                     <a href="javascript:void(0);" onclick="window.location.href = window.location.protocol + '//' + window.location.hostname;" class="btn btn-outline-light">
                     <a href="javascript:void(0);" onclick="window.location.href = window.location.protocol + '//' + window.location.hostname;" class="btn btn-outline-light">
                         <i class="bi bi-arrow-left me-2"></i>Back to Main App
                         <i class="bi bi-arrow-left me-2"></i>Back to Main App
                     </a>
                     </a>
@@ -45,98 +48,235 @@
         </nav>
         </nav>
 
 
         <div class="container-fluid px-4 mt-4">
         <div class="container-fluid px-4 mt-4">
-            <div class="row mb-4 g-4">
-                <div class="col-md-7">
-                    <div class="card shadow-sm border-0 h-100">
-                        <div class="card-header bg-white py-3">
-                            <h5 class="card-title mb-0 fw-bold text-dark">
-                                <i class="bi bi-cloud-arrow-up-fill me-2 text-info"></i>Upload to Folder
-                            </h5>
+            {% if success_msg %}
+            <div class="alert alert-success alert-dismissible fade show shadow-sm" role="alert">
+                <i class="bi bi-check-circle-fill me-2"></i>{{ success_msg }}
+                <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
+            </div>
+            {% endif %}
+            
+            {% if error_msg %}
+            <div class="alert alert-danger alert-dismissible fade show shadow-sm" role="alert">
+                <i class="bi bi-exclamation-triangle-fill me-2"></i>{{ error_msg }}
+                <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
+            </div>
+            {% endif %}
+
+            <div class="row g-4">
+                <div class="col-md-3">
+                    <div class="card mb-4">
+                        <div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
+                            <h6 class="mb-0 fw-bold text-dark"><i class="bi bi-diagram-3-fill me-2 text-primary"></i>Profiles / Loadouts</h6>
                         </div>
                         </div>
-                        <div class="card-body p-4">
-                            <form action="/patch-manager/upload" method="post" enctype="multipart/form-data" class="d-flex flex-column gap-3">
-                                <label class="form-label small fw-bold text-muted mb-0">Select File</label>
-                                <input class="form-control" type="file" name="file" required>
+                        <div class="list-group list-group-flush">
+                            {% for p in profiles %}
+                            <div class="list-group-item d-flex justify-content-between align-items-center profile-list-item {% if p.is_active %}active{% endif %}">
+                                <form action="/patch-manager/profile/activate" method="post" class="m-0 p-0 flex-grow-1 cursor-pointer" style="cursor: pointer;">
+                                    <input type="hidden" name="profile_id" value="{{ p.id }}">
+                                    <div onclick="this.parentNode.submit();" class="w-100 fw-bold">
+                                        {{ p.name }}
+                                        {% if p.is_readonly %}<i class="bi bi-lock-fill ms-1" title="Read-Only"></i>{% endif %}
+                                        {% if p.is_active %}<i class="bi bi-check-circle-fill ms-2"></i>{% endif %}
+                                    </div>
+                                </form>
                                 
                                 
-                                <label class="form-label small fw-bold text-muted mb-0">Target Directory</label>
-                                <div class="input-group">
-                                    <span class="input-group-text bg-light"><i class="bi bi-folder2-open"></i></span>
-                                    <select class="form-select" name="target_path">
-                                        {% for d in dirs %}
-                                            <option value="{{ d }}">{% if d == "" %}/ (Root Directory){% else %}{{ d }}{% endif %}</option>
-                                        {% endfor %}
-                                    </select>
+                                <div class="dropdown">
+                                    <button class="btn btn-sm btn-link p-0 text-decoration-none {% if p.is_active %}text-white{% else %}text-secondary{% endif %}" type="button" data-bs-toggle="dropdown" aria-expanded="false" title="Profile Options">
+                                        <i class="bi bi-three-dots-vertical fs-5"></i>
+                                    </button>
+                                    <ul class="dropdown-menu shadow-sm dropdown-menu-end">
+                                        {% if not p.is_readonly %}
+                                        <li>
+                                            <a class="dropdown-item" href="javascript:void(0)" onclick="openRenameModal({{ p.id }}, '{{ p.name|escape }}')">
+                                                <i class="bi bi-pencil-square me-2 text-primary"></i>Rename Profile
+                                            </a>
+                                        </li>
+                                        {% endif %}
+                                        <li>
+                                            <form action="/patch-manager/profile/duplicate" method="post" class="m-0 p-0">
+                                                <input type="hidden" name="profile_id" value="{{ p.id }}">
+                                                <button type="submit" class="dropdown-item"><i class="bi bi-copy me-2 text-success"></i>Clone Profile</button>
+                                            </form>
+                                        </li>
+                                        {% if not p.is_active and not p.is_readonly %}
+                                        <li><hr class="dropdown-divider"></li>
+                                        <li>
+                                            <form action="/patch-manager/profile/delete" method="post" class="m-0 p-0" onsubmit="return confirm('Delete this profile completely?');">
+                                                <input type="hidden" name="profile_id" value="{{ p.id }}">
+                                                <button type="submit" class="dropdown-item text-danger"><i class="bi bi-trash me-2"></i>Delete Profile</button>
+                                            </form>
+                                        </li>
+                                        {% endif %}
+                                    </ul>
                                 </div>
                                 </div>
+                                
+                            </div>
+                            {% endfor %}
+                        </div>
+                        <div class="card-footer bg-light p-3">
+                            <form action="/patch-manager/profile/create" method="post" class="input-group">
+                                <input type="text" class="form-control form-control-sm" name="name" placeholder="New profile name..." required>
+                                <button class="btn btn-primary btn-sm" type="submit"><i class="bi bi-plus"></i> Add</button>
+                            </form>
+                        </div>
+                    </div>
 
 
-                                <button type="submit" class="btn btn-primary fw-bold mt-2">
-                                    <i class="bi bi-upload me-2"></i>Upload File
+                    <div class="card bg-dark text-white shadow">
+                        <div class="card-body text-center p-4">
+                            <i class="bi bi-rocket-takeoff-fill display-4 text-warning mb-3 d-block"></i>
+                            <h5 class="fw-bold">Ready to Compile?</h5>
+                            <p class="small text-white-50">Pushing to the builder will wipe the staging directory and write the currently active profile.</p>
+                            <form action="/patch-manager/deploy" method="post" onsubmit="return confirm('This will deploy {{ active_profile.name }} to the builder. Are you sure?');">
+                                <button type="submit" class="btn btn-warning fw-bold w-100 py-2 fs-5">
+                                    DEPLOY TO BUILDER
                                 </button>
                                 </button>
                             </form>
                             </form>
                         </div>
                         </div>
                     </div>
                     </div>
                 </div>
                 </div>
 
 
-                <div class="col-md-5">
-                    <div class="card shadow-sm border-0 h-100">
+                <div class="col-md-9">
+
+                    <div class="card shadow-sm border-0 mb-4">
                         <div class="card-header bg-white py-3">
                         <div class="card-header bg-white py-3">
+                            <h6 class="card-title mb-0 fw-bold text-dark"><i class="bi bi-card-text text-warning me-2"></i>Profile Notes & Annotations</h6>
+                        </div>
+                        <div class="card-body p-3 bg-light">
+                            <form action="/patch-manager/profile/annotate" method="post" class="d-flex flex-column gap-2">
+                                <input type="hidden" name="profile_id" value="{{ active_profile.id }}">
+                                <textarea class="form-control" name="description" rows="3" {% if active_profile.is_readonly %}disabled{% endif %} placeholder="Add notes about this loadout (e.g., 'Aggressive PID tune, experimental VTX settings')">{{ active_profile.description }}</textarea>
+                                {% if not active_profile.is_readonly %}
+                                <div class="text-end">
+                                    <button type="submit" class="btn btn-warning btn-sm fw-bold"><i class="bi bi-save me-1"></i> Save Notes</button>
+                                </div>
+                                {% endif %}
+                            </form>
+                        </div>
+                    </div>
+                    
+                    <div class="card shadow-sm border-0 mb-4">
+                        <div class="card-header bg-white py-3">
+                            <h6 class="card-title mb-0 fw-bold text-dark"><i class="bi bi-cloud-arrow-up text-success me-2"></i>Upload File to Profile</h6>
+                        </div>
+                        
+                        {% if active_profile.is_readonly %}
+                        <div class="card-body p-4 bg-light text-center text-muted">
+                            <i class="bi bi-lock-fill fs-3 d-block mb-2"></i>
+                            <p class="mb-0">This is a locked, unmodified base profile. You cannot upload files here.<br><strong>Clone this profile</strong> from the menu on the left to start making changes.</p>
+                        </div>
+                        {% else %}
+                        <div class="card-body p-3 bg-light">
+                            <form action="/patch-manager/file/upload" method="post" enctype="multipart/form-data" class="row g-3 align-items-end">
+                                <input type="hidden" name="profile_id" value="{{ active_profile.id }}">
+                                
+                                <div class="col-md-4">
+                                    <label class="form-label small fw-bold text-muted mb-1">1. Select File</label>
+                                    <input class="form-control form-control-sm" type="file" name="file" required>
+                                </div>
+                                
+                                <div class="col-md-3">
+                                    <label class="form-label small fw-bold text-muted mb-1">2. Existing Folder</label>
+                                    <select class="form-select form-select-sm" name="existing_path" onchange="document.getElementById('new_path').value=''">
+                                        <option value="">/ (Root Directory)</option>
+                                        {% for d in dirs %}
+                                            <option value="{{ d }}">{{ d }}</option>
+                                        {% endfor %}
+                                    </select>
+                                </div>
+                                
+                                <div class="col-md-3">
+                                    <label class="form-label small fw-bold text-muted mb-1">...Or Create New Folder</label>
+                                    <input type="text" class="form-control form-control-sm font-monospace" name="new_path" id="new_path" placeholder="e.g. libraries/AP_HAL" oninput="document.getElementsByName('existing_path')[0].value=''">
+                                </div>
+                                
+                                <div class="col-md-2">
+                                    <button type="submit" class="btn btn-success btn-sm fw-bold w-100"><i class="bi bi-upload me-1"></i> Upload</button>
+                                </div>
+                            </form>
+                        </div>
+                        {% endif %}
+                    </div>
+
+                    <div class="card mb-5 border-0 shadow-sm">
+                        <div class="card-header bg-white py-3 d-flex justify-content-between align-items-center border-bottom-0">
                             <h5 class="card-title mb-0 fw-bold text-dark">
                             <h5 class="card-title mb-0 fw-bold text-dark">
-                                <i class="bi bi-folder-plus me-2 text-success"></i>Create New Folder
+                                <i class="bi bi-folder2-open me-2 text-info"></i>Files for: <span class="text-primary">{{ active_profile.name }}</span>
                             </h5>
                             </h5>
                         </div>
                         </div>
-                        <div class="card-body p-4">
-                            <form action="/patch-manager/create_folder" method="post" class="d-flex flex-column gap-3">
-                                <label class="form-label small fw-bold text-muted mb-0">Folder Path</label>
-                                <input type="text" class="form-control" name="folder_path" placeholder="e.g., libraries/AP_HAL" required>
-                                <button type="submit" class="btn btn-success fw-bold mt-2">
-                                    <i class="bi bi-plus-lg me-2"></i>Create Folder
-                                </button>
-                            </form>
+                        <div class="card-body p-0">
+                            <div class="table-responsive">
+                                <table class="table table-hover align-middle mb-0">
+                                    <thead class="table-light">
+                                        <tr>
+                                            <th class="ps-4 py-3">Target Path</th>
+                                            <th class="text-end pe-4 py-3">Actions</th>
+                                        </tr>
+                                    </thead>
+                                    <tbody>
+                                        {% for file in files %}
+                                        <tr>
+                                            <td class="ps-4 font-monospace text-dark" style="font-size: 0.95rem;">
+                                                <i class="bi bi-file-code me-2 text-secondary"></i>{{ file.filepath }}
+                                            </td>
+                                            <td class="text-end pe-4">
+                                                <a href="/patch-manager/download?file_id={{ file.id }}" class="btn btn-sm btn-outline-info me-2" title="Download">
+                                                    <i class="bi bi-download"></i>
+                                                </a>
+                                                <a href="/patch-manager/edit?file_id={{ file.id }}" class="btn btn-sm btn-outline-primary me-2">
+                                                    <i class="bi bi-pencil me-1"></i> Edit
+                                                </a>
+                                                <form action="/patch-manager/file/delete" method="post" class="d-inline" onsubmit="return confirm('Remove this file from the profile?');">
+                                                    <input type="hidden" name="file_id" value="{{ file.id }}">
+                                                    <button type="submit" class="btn btn-sm btn-outline-danger">
+                                                        <i class="bi bi-trash"></i>
+                                                    </button>
+                                                </form>
+                                            </td>
+                                        </tr>
+                                        {% else %}
+                                        <tr>
+                                            <td colspan="2" class="text-center py-5 text-muted">
+                                                <i class="bi bi-inbox fs-1 d-block mb-3 text-black-50"></i>
+                                                <h5>This profile is empty.</h5>
+                                                <p>Deploying this profile will result in a clean, vanilla build.<br>Upload a file above to start creating overlays.</p>
+                                            </td>
+                                        </tr>
+                                        {% endfor %}
+                                    </tbody>
+                                </table>
+                            </div>
                         </div>
                         </div>
                     </div>
                     </div>
                 </div>
                 </div>
             </div>
             </div>
+        </div>
+    </div>
 
 
-            <div class="card shadow-sm border-0 mb-5">
-                <div class="card-header bg-white py-3">
-                    <h5 class="card-title mb-0 fw-bold text-dark">
-                        <i class="bi bi-files me-2 text-primary"></i>Managed Files
-                    </h5>
-                </div>
-                <div class="card-body p-0">
-                    <div class="table-responsive">
-                        <table class="table table-hover align-middle mb-0">
-                            <thead class="table-light">
-                                <tr>
-                                    <th class="ps-4 py-3">Source Path</th>
-                                    <th class="text-end pe-4 py-3">Actions</th>
-                                </tr>
-                            </thead>
-                            <tbody>
-                                {% for file in files %}
-                                <tr>
-                                    <td class="ps-4 font-monospace text-primary" style="font-size: 0.9rem;">{{ file }}</td>
-                                    <td class="text-end pe-4">
-                                        <a href="/patch-manager/edit?filepath={{ file }}" class="btn btn-sm btn-outline-primary me-2">
-                                            <i class="bi bi-pencil me-1"></i> Edit
-                                        </a>
-                                        <form action="/patch-manager/delete" method="post" class="d-inline" onsubmit="return confirm('Are you sure you want to delete this file?');">
-                                            <input type="hidden" name="filepath" value="{{ file }}">
-                                            <button type="submit" class="btn btn-sm btn-outline-danger">
-                                                <i class="bi bi-trash me-1"></i> Delete
-                                            </button>
-                                        </form>
-                                    </td>
-                                </tr>
-                                {% endfor %}
-                            </tbody>
-                        </table>
+    <div class="modal fade" id="renameModal" tabindex="-1" aria-hidden="true">
+        <div class="modal-dialog">
+            <div class="modal-content">
+                <form action="/patch-manager/profile/rename" method="post">
+                    <div class="modal-header bg-light">
+                        <h5 class="modal-title fw-bold text-dark"><i class="bi bi-pencil-square me-2"></i>Rename Profile</h5>
+                        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                     </div>
                     </div>
-                </div>
+                    <div class="modal-body p-4">
+                        <input type="hidden" name="profile_id" id="rename-profile-id">
+                        <div class="mb-3">
+                            <label class="form-label text-muted small fw-bold">Profile Name</label>
+                            <input type="text" class="form-control" name="new_name" id="rename-new-name" required>
+                        </div>
+                    </div>
+                    <div class="modal-footer bg-light d-flex justify-content-between">
+                        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
+                        <button type="submit" class="btn btn-primary fw-bold"><i class="bi bi-check-lg me-1"></i>Save Name</button>
+                    </div>
+                </form>
             </div>
             </div>
         </div>
         </div>
     </div>
     </div>
 
 
-    <footer class="py-3 bg-dark text-white-50">
+    <footer class="py-3 bg-dark text-white-50 mt-auto">
         <div class="container-fluid px-4 d-flex justify-content-between">
         <div class="container-fluid px-4 d-flex justify-content-between">
             <div>
             <div>
                 <span class="mx-2">Credits: 
                 <span class="mx-2">Credits: 
@@ -149,5 +289,14 @@
             <span>ArduPilot Overlay Manager</span>
             <span>ArduPilot Overlay Manager</span>
         </div>
         </div>
     </footer>
     </footer>
+    
+    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
+    <script>
+        function openRenameModal(profileId, profileName) {
+            document.getElementById('rename-profile-id').value = profileId;
+            document.getElementById('rename-new-name').value = profileName;
+            new bootstrap.Modal(document.getElementById('renameModal')).show();
+        }
+    </script>
 </body>
 </body>
 </html>
 </html>