diff --git a/README.md b/README.md index 83496e1..58fab68 100644 --- a/README.md +++ b/README.md @@ -10,16 +10,15 @@ A simple SQLite-based system for managing tablet lending and returns. - **Return Management**: Record when tablets are returned - **History Tracking**: Complete history of all loans and returns - **User Loans View**: See all tablets loaned to each user (demonstrates one-to-many relationship) -- **Non-Loanable Devices**: Track inventory devices that cannot be loaned (e.g., projectors, monitors) - **Project Notes**: Markdown editor for development documentation ## Files -- `minimal_app.py` - Interactive command-line application (includes non-loanable device management) +- `minimal_app.py` - Interactive command-line application - `test_app.py` - Test script that demonstrates functionality -- `tablets.db` - SQLite database (created automatically, includes non_loanable_devices table) -- `simple_app.py` - Web-based version (requires Flask, includes non-loanable device management) -- `app.py` - Alternative web version (requires Flask, includes non-loanable device management) +- `tablets.db` - SQLite database (created automatically) +- `simple_app.py` - Web-based version (requires Flask) +- `app.py` - Alternative web version (requires Flask) ## Quick Start @@ -145,7 +144,6 @@ python3 app.py - **Loan Tablet**: Loan a tablet to a user (with search for large datasets) - **User Loans**: View all tablets loaned to each user - demonstrates the **one-to-many relationship** (one user, multiple tablets) - **Loan History**: Complete history of all loan and return transactions -- **Non-Loanable Devices**: Manage inventory of devices that cannot be loaned (add, edit, delete, view) - **Project Management**: Markdown editor for development notes (saved to `notes_development/project_notes.md`) ## Database Backup @@ -155,48 +153,6 @@ To backup your data: cp tablets.db tablets_backup_$(date +%Y%m%d).db ``` -## Testing - -The project includes a comprehensive unit test suite with **46 tests** covering core functionality and edge cases. - -### Running Tests - -```bash -# Install test dependencies (if not already installed) -source .venv/bin/activate -uv pip install pytest pytest-cov - -# Run all tests -python3 -m pytest tests/ -v - -# Run with coverage report -python3 -m pytest tests/ --cov=./ --cov-report=term-missing -``` - -### Test Structure - -| File | Tests | Coverage | -|------|-------|----------| -| `tests/test_core.py` | 21 | Core operations (tablet, user, loan CRUD) | -| `tests/test_edge_cases.py` | 14 | Edge cases (duplicates, invalid IDs, etc.) | -| `tests/test_minimal_app.py` | 11 | Application function tests | - -### Key Edge Cases Covered - -- Loan a device that's already loaned (prevents double-loaning) -- Return a non-existent loan (handles gracefully) -- Duplicate serial numbers (rejected at database level) -- Duplicate user identifications (rejected at database level) -- Invalid tablet/user IDs (validated before operations) -- Multiple loans per user (one-to-many relationship) -- Loan-return-loan sequence (device lifecycle) - -### Test Configuration - -Tests use isolated temporary databases and automatically clean up after each test run. No impact on production data. - - - ## License This is a simple educational project. Feel free to use and modify it as needed. \ No newline at end of file diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md deleted file mode 100644 index f0c6929..0000000 --- a/REQUIREMENTS.md +++ /dev/null @@ -1,221 +0,0 @@ -# Requisitos del Sistema de Gestión de Préstamos de Tablets - -## Descripción General - -El sistema debe permitir la gestión del préstamo y devolución de dispositivos tablets entre un inventario y un conjunto de usuarios. El enfoque principal es el seguimiento del estado de cada dispositivo y su relación con los usuarios que los han tomado prestados. - ---- -## Requisitos Faltantes - -### Gestión de dispositivos no prestables - -✅ **IMPLEMENTADO** - El sistema ahora permite: -- Registrar nuevos dispositivos no prestables en un inventario separado (tabla `non_loanable_devices`) -- Gestionar dispositivos no prestables a través de: - - Interfaz web (app.py): /non_loanable_devices, /add_non_loanable_device, /edit_non_loanable_device, /delete_non_loanable_device - - Interfaz CLI (minimal_app.py): Opciones 8, 9, 10 del menú -- La implementación utiliza una tabla separada en la misma base de datos con los siguientes campos: - - brand, model, serial_number (único), device_type, location, status, notes, purchase_date, purchase_cost - -## Requisitos Funcionales - -### Gestión de Dispositivos - -El sistema debe permitir: -- Registrar nuevos dispositivos en el inventario -- Identificar cada dispositivo de manera única -- Almacenar información descriptiva de cada dispositivo (marca, modelo, número de serie) -- Consultar el estado actual de cada dispositivo (disponible, prestado) - -### Gestión de Usuarios - -El sistema debe permitir: -- Registrar nuevos usuarios -- Identificar cada usuario de manera única -- Almacenar información de contacto básica (nombre, identificación) -- Consultar la información de los usuarios registrados - -### Gestión de Préstamos - -El sistema debe permitir: -- Asignar un dispositivo disponible a un usuario -- Registrar la fecha y hora del préstamo -- Registrar la fecha y hora de la devolución -- Consultar el historial de préstamos de un dispositivo -- Consultar el historial de préstamos de un usuario -- Visualizar todos los dispositivos actualmente prestados - -### Consulta y Reportes - -El sistema debe permitir: -- Buscar dispositivos por cualquier atributo (marca, modelo, número de serie) -- Buscar usuarios por cualquier atributo (nombre, identificación) -- Filtrar préstamos por estado (activos, finalizados) -- Filtrar préstamos por dispositivo -- Filtrar préstamos por usuario -- Ver el historial completo de préstamos y devoluciones - ---- - -## Modelo de Datos - -### Entidades Principales - -1. **Dispositivo** - - Identificador único - - Marca - - Modelo - - Número de serie (único) - - Estado (disponible, prestado) - -2. **Usuario** - - Identificador único - - Nombre - - Identificación (única) - -3. **Préstamo** - - Identificador único - - Dispositivo asociado - - Usuario asociado - - Fecha de préstamo - - Fecha de devolución (opcional) - - Estado (activo, finalizado) - -### Relaciones - -- Un **usuario** puede tener **múltiples préstamos** (relación uno a muchos) -- Un **dispositivo** solo puede estar en **un préstamo activo** a la vez -- Un **préstamo** relaciona exactamente un dispositivo con un usuario -- El historial de préstamos permite rastrear todos los movimientos de cada dispositivo - -### Reglas de Negocio - -1. Un dispositivo en estado "prestado" no puede ser prestado a otro usuario -2. Un préstamo finalizado debe registrar fecha de devolución -3. La identificación de dispositivos y usuarios debe ser única -4. Al devolver un dispositivo, este vuelve al estado "disponible" - ---- - -## Flujos de Trabajo - -### Registro de un Nuevo Dispositivo - -1. El sistema solicita la información del dispositivo (marca, modelo, número de serie) -2. El sistema valida que el número de serie no esté registrado -3. El sistema registra el dispositivo con estado "disponible" -4. El sistema confirma el registro - -### Registro de un Nuevo Usuario - -1. El sistema solicita la información del usuario (nombre, identificación) -2. El sistema valida que la identificación no esté registrada -3. El sistema registra el usuario -4. El sistema confirma el registro - -### Préstamo de un Dispositivo - -1. El sistema muestra los dispositivos disponibles -2. El usuario selecciona un dispositivo -3. El sistema muestra los usuarios registrados -4. El usuario selecciona un usuario -5. El sistema registra el préstamo con: - - Dispositivo seleccionado - - Usuario seleccionado - - Fecha y hora actual - - Estado "activo" -6. El sistema actualiza el estado del dispositivo a "prestado" -7. El sistema confirma el préstamo - -### Devolución de un Dispositivo - -1. El sistema muestra los préstamos activos -2. El usuario selecciona un préstamo -3. El sistema registra la fecha y hora de devolución -4. El sistema actualiza el estado del préstamo a "finalizado" -5. El sistema actualiza el estado del dispositivo a "disponible" -6. El sistema confirma la devolución - -### Consulta de Préstamos por Usuario - -1. El sistema muestra la lista de usuarios -2. El usuario selecciona un usuario -3. El sistema muestra: - - Dispositivos actualmente prestados (préstamos activos) - - Historial de préstamos pasados (opcional: expandible) -4. El sistema permite buscar por cualquier campo - ---- - -## Requisitos No Funcionales - -### Usabilidad - -- La interfaz debe permitir búsqueda eficiente en listas grandes (500+ elementos) -- La visualización de información debe ser clara y organizada -- Los estados deben ser fácilmente identificables -- Las acciones principales deben ser accesibles - -### Escalabilidad - -- El diseño debe soportar un crecimiento en el número de dispositivos y usuarios -- La búsqueda debe ser eficiente -- El historial debe ser consultable sin afectar el rendimiento - -### Mantenibilidad - -- La estructura de datos debe ser clara -- Las relaciones entre entidades deben estar bien definidas -- Los flujos de trabajo deben estar documentados - ---- - -## Diseño de Interfaz (Conceptual) - -### Vistas Principales - -1. **Vista de Inventario** - - Lista de dispositivos - - Filtros por estado - - Búsqueda - -2. **Vista de Usuarios** - - Lista de usuarios - - Búsqueda - -3. **Vista de Préstamos Activos** - - Lista de préstamos en curso - - Filtros por dispositivo/usuario - - Acciones: devolver dispositivo - -4. **Vista de Historial** - - Lista completa de préstamos - - Filtros por fecha, usuario, dispositivo, estado - - Detalles de cada préstamo - -5. **Vista de Préstamos por Usuario** - - Lista de usuarios - - Para cada usuario: dispositivos actualmente prestados - - Opcional: historial de préstamos pasados (expandible) - -### Elementos de Interacción - -- Campos de búsqueda en todas las listas -- Botones de acción claros (Prestar, Devolver, Registrar) -- Indicadores visuales de estado (colores, iconos) -- Confirmación de acciones críticas - ---- - -## Notas de Implementación - -Este documento describe los requisitos a nivel de diseño. La implementación específica (lenguaje de programación, base de datos, framework, arquitectura) queda a criterio del equipo de desarrollo y puede variar según las necesidades técnicas. - -Los detalles técnicos como: -- Tecnologías específicas -- Estructura de base de datos física -- APIs -- Autenticación -- Despliegue - -Deberán ser definidos en la fase de diseño técnico y pueden evolucionar sin afectar los requisitos funcionales descritos aquí. diff --git a/RUNNING.md b/RUNNING.md index 02c80c4..9c37f3d 100644 --- a/RUNNING.md +++ b/RUNNING.md @@ -48,22 +48,10 @@ pkill -f "python app.py" - **Port**: 5000 (configurable in app.py) - **Virtual Environment**: `.venv/` (created with uv) -## Running Tests +## Troubleshooting +If you have issues: +1. Check if the server is running: `ps aux | grep app.py` +2. Check the database: `sqlite3 tablets.db` +3. Restart the server: `source .venv/bin/activate && python app.py` -The project includes 46 unit tests for verifying functionality: - -```bash -# Run all tests -python3 -m pytest tests/ -v - -# Run with coverage -python3 -m pytest tests/ --cov=./ --cov-report=term-missing -``` - -Tests cover: -- Core CRUD operations for tablets, users, and loans -- Edge cases (duplicates, invalid IDs, already loaned devices) -- Application function testing -- Non-loanable device management - -All tests use isolated temporary databases and do not affect production data. +Enjoy managing your tablets! 📱💻 \ No newline at end of file diff --git a/TECHNICAL_SPECIFICATIONS.md b/TECHNICAL_SPECIFICATIONS.md deleted file mode 100644 index a6995c2..0000000 --- a/TECHNICAL_SPECIFICATIONS.md +++ /dev/null @@ -1,200 +0,0 @@ -# Especificaciones Técnicas - -Documento complementario a `REQUIREMENTS.md` con propuestas concretas de implementación técnica. - ---- - -## 1. Concurrencia en Base de Datos - -| Opción | Descripción | Ventajas | Desventajas | -|--------|-------------|---------|-------------| -| **SQLite + WAL** | Modo Write-Ahead Logging | Zero-config, embebido, buena para <100 conexiones | Limitado a 1 escritor simultáneo | -| **PostgreSQL** | Base de datos relacional | Concurrencia completa, transacciones ACID | Requiere servidor | -| **Transacciones** | Aislamiento READ COMMITTED | Previene lecturas sucias | Bloqueos temporales | -| **Locking optimista** | Versión de fila en actualizaciones | Sin bloqueos | Conflictos requieren reintento | - -**Recomendación**: SQLite con WAL para prototipos, PostgreSQL para producción. Usar transacciones para operaciones críticas (préstamo/devolución). - ---- - -## 2. Autenticación Básica - -| Opción | Descripción | Complejidad | -|--------|-------------|-------------| -| **Sesiones (Flask)** | Cookies firmadas | Baja | -| **JWT** | Tokens sin estado | Media | -| **OAuth 2.0** | Delegación a proveedores | Alta | -| **Basic Auth** | HTTP Basic Authentication | Mínima | - -**Flujo mínimo**: -``` -Login (user/pass) → Session/JWT → Middleware verifica rol → Acceso a endpoints -``` - -**Roles sugeridos**: -- `admin`: CRUD completo, gestión de usuarios -- `staff`: Préstamos/devoluciones -- `viewer`: Solo consulta - ---- - -## 3. Separación Frontend/Backend - -| Arquitectura | Descripción | Stack Ejemplo | -|--------------|-------------|---------------| -| **Monolítica + Templates** | Servidor renderiza HTML | Flask+Jinja2, Django | -| **API + SPA** | Backend API, frontend dinámico | FastAPI + React/Vue | -| **API + SSR** | Backend API + renderizado servidor | Next.js + API | -| **Microservicios** | Servicios independientes | Backend API + Frontend estático | - -**Para este proyecto**: -- **Opción A**: Flask + Jinja2 (simple, suficiente para requisitos) -- **Opción B**: FastAPI + React (escalable, separable) - ---- - -## 4. Búsqueda Eficiente (500+ elementos) - -| Opción | Descripción | Implementación | -|--------|-------------|----------------| -| **Índices DB** | B-tree en columnas buscadas | `CREATE INDEX idx_brand ON tablets(brand)` | -| **Búsqueda full-text** | Búsqueda en texto | SQLite FTS5, PostgreSQL tsvector | -| **Filtrado cliente** | JavaScript filtra datos | Ideal para <2000 elementos | -| **Paginación** | Dividir resultados | `LIMIT 50 OFFSET 0` | - -**Recomendación**: Índices en `serial_number`, `brand`, `model`, `user.name`, `user.identification`. - ---- - -## 5. Estructura de Proyecto Propuesta - -``` -project/ -├── backend/ -│ ├── models/ # Entidades (Device, User, Loan) -│ ├── services/ # Lógica de negocio -│ ├── repositories/ # Acceso a datos -│ ├── routes/ # Endpoints/API -│ └── app.py # Configuración -├── frontend/ # Opcional para opción B -│ ├── public/ -│ └── src/ -├── migrations/ # Control de cambios DB -├── tests/ -└── docs/ -``` - ---- - -## 6. Decisiones Clave por Tomar - -| Decisión | Opciones | Impacto | -|----------|----------|---------| -| Base de datos | SQLite / PostgreSQL | Concurrencia, despliegue | -| Autenticación | Sesiones / JWT / Ninguna | Seguridad, complejidad | -| Frontend | Templates / SPA / Ninguno | Experiencia usuario | -| Despliegue | Local / Docker / Cloud | Escalabilidad | - ---- - -## 7. Ejemplo de Implementación Mínima (Pseudocódigo) - -```python -# modelos.py -class Device: - id: int (PK) - serial_number: str (unique) - status: str (available/loaned) - loans: List[Loan] (one-to-many) - -class User: - id: int (PK) - identification: str (unique) - loans: List[Loan] (one-to-many) - -class Loan: - id: int (PK) - device_id: int (FK) - user_id: int (FK) - loan_date: datetime - return_date: datetime (nullable) - status: str (active/returned) - -# servicios/loan_service.py -def loan_device(device_id: int, user_id: int) -> Loan: - with transaction(): # Atomicidad - device = get_device_for_update(device_id) - if device.status != 'available': - raise DeviceNotAvailableError() - - loan = Loan.create( - device_id=device_id, - user_id=user_id, - loan_date=now(), - status='active' - ) - device.status = 'loaned' - return loan -``` - ---- - -## 8. Consideraciones Adicionales - -- **Validaciones**: Únicas (serial_number, identification) a nivel de base de datos -- **Auditoría**: Campos `created_at`, `updated_at` en todas las entidades -- **Backups**: Script para exportar base de datos periódicamente -- **Logging**: Registrar préstamos/devoluciones para auditoría -- **Versionado API**: `/api/v1/...` para compatibilidad futura -- **Pruebas**: Cubrir flujos críticos (préstamo con dispositivo no disponible, devolución de préstamo inexistente) - ---- - -## 9. Migración a PostgreSQL (Futuro) - -### Contexto - -El sistema actualmente utiliza **SQLite** como base de datos embebida, ideal para prototipos y aplicaciones pequeñas. Sin embargo, SQLite tiene limitaciones para escalar. - -### Criterios para Migración - -Considerar migrar a **PostgreSQL** cuando: -- Tamaño DB > 1GB -- Más de 50 conexiones simultáneas -- Más de 200 usuarios activos -- Más de 500 transacciones por minuto -- Necesidad de múltiples servidores - -### Arquitectura Propuesta - -Usar un patrón de **Repository** para abstraer la base de datos: -``` -backend/ -├── repositories/ -│ ├── base_repository.py # Interfaz abstracta -│ ├── sqlite_repository.py # Implementación SQLite -│ └── postgres_repository.py # Implementación PostgreSQL -└── config/ - └── database.py # Fábrica de repositorios -``` - -### Schema PostgreSQL - -El schema es similar al de SQLite pero con tipos de datos más específicos y índices adicionales para rendimiento. - -### Pasos para Migración - -1. Instalar dependencias: `pip install psycopg2-binary sqlalchemy alembic` -2. Configurar PostgreSQL y crear base de datos -3. Ejecutar script de migración: `python scripts/migrate_to_postgres.py` -4. Cambiar configuración: `DB_TYPE=postgres` -5. Iniciar aplicación - -### Beneficios - -- Concurrencia ilimitada (múltiples escritores) -- Escalabilidad a miles de conexiones -- Mejor rendimiento con índices -- Seguridad integrada (autenticación, roles) -- Backups automáticos -- Replicación y clustering diff --git a/app.py b/app.py index 7dfba4d..a5332b7 100644 --- a/app.py +++ b/app.py @@ -63,22 +63,6 @@ def init_db(): ) ''') - # Create non_loanable_devices table for devices that cannot be loaned - cursor.execute(''' - CREATE TABLE IF NOT EXISTS non_loanable_devices ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - brand TEXT NOT NULL, - model TEXT NOT NULL, - serial_number TEXT UNIQUE NOT NULL, - device_type TEXT NOT NULL, - location TEXT, - status TEXT DEFAULT 'available', - notes TEXT, - purchase_date TEXT, - purchase_cost REAL - ) - ''') - conn.commit() @app.route('/') @@ -335,99 +319,6 @@ def user_loans(): return render_template('user_loans.html', users_with_loans=user_loans_list) -@app.route('/non_loanable_devices') -def non_loanable_devices(): - """Show all non-loanable devices""" - with get_db() as conn: - cursor = conn.cursor() - cursor.execute("SELECT * FROM non_loanable_devices ORDER BY device_type, brand, model") - devices = cursor.fetchall() - - return render_template('non_loanable_devices.html', devices=devices) - - -@app.route('/add_non_loanable_device', methods=['GET', 'POST']) -def add_non_loanable_device(): - """Add a new non-loanable device to inventory""" - if request.method == 'POST': - brand = request.form['brand'] - model = request.form['model'] - serial_number = request.form['serial_number'] - device_type = request.form['device_type'] - location = request.form['location'] - notes = request.form['notes'] - purchase_date = request.form.get('purchase_date', '') - purchase_cost = request.form.get('purchase_cost', '') - - try: - with get_db() as conn: - cursor = conn.cursor() - cursor.execute(''' - INSERT INTO non_loanable_devices - (brand, model, serial_number, device_type, location, status, notes, purchase_date, purchase_cost) - VALUES (?, ?, ?, ?, ?, 'available', ?, ?, ?) - ''', (brand, model, serial_number, device_type, location, notes, purchase_date, purchase_cost)) - conn.commit() - flash('Non-loanable device added successfully!', 'success') - except sqlite3.IntegrityError: - flash('Error: Serial number already exists!', 'error') - - return redirect(url_for('non_loanable_devices')) - - return render_template('add_non_loanable_device.html') - - -@app.route('/edit_non_loanable_device/', methods=['GET', 'POST']) -def edit_non_loanable_device(device_id): - """Edit a non-loanable device""" - with get_db() as conn: - cursor = conn.cursor() - cursor.execute("SELECT * FROM non_loanable_devices WHERE id = ?", (device_id,)) - device = cursor.fetchone() - - if not device: - flash('Error: Device not found!', 'error') - return redirect(url_for('non_loanable_devices')) - - if request.method == 'POST': - brand = request.form['brand'] - model = request.form['model'] - serial_number = request.form['serial_number'] - device_type = request.form['device_type'] - location = request.form['location'] - status = request.form['status'] - notes = request.form['notes'] - purchase_date = request.form.get('purchase_date', '') - purchase_cost = request.form.get('purchase_cost', '') - - try: - cursor.execute(''' - UPDATE non_loanable_devices SET - brand = ?, model = ?, serial_number = ?, device_type = ?, - location = ?, status = ?, notes = ?, purchase_date = ?, purchase_cost = ? - WHERE id = ? - ''', (brand, model, serial_number, device_type, location, status, notes, purchase_date, purchase_cost, device_id)) - conn.commit() - flash('Device updated successfully!', 'success') - return redirect(url_for('non_loanable_devices')) - except sqlite3.IntegrityError: - flash('Error: Serial number already exists!', 'error') - - return render_template('edit_non_loanable_device.html', device=device) - - -@app.route('/delete_non_loanable_device/') -def delete_non_loanable_device(device_id): - """Delete a non-loanable device""" - with get_db() as conn: - cursor = conn.cursor() - cursor.execute("DELETE FROM non_loanable_devices WHERE id = ?", (device_id,)) - conn.commit() - flash('Device deleted successfully!', 'success') - - return redirect(url_for('non_loanable_devices')) - - if __name__ == '__main__': # Initialize database init_db() diff --git a/docs/FRONTEND_OPTIONS.md b/docs/FRONTEND_OPTIONS.md deleted file mode 100644 index d8a141e..0000000 --- a/docs/FRONTEND_OPTIONS.md +++ /dev/null @@ -1,1191 +0,0 @@ -# Separated Frontend Architecture Options - -## Overview - -The current Tablet Management System uses **server-side rendering** with Flask templates. While this works well for tablets, the interface may be too wide for mobile devices. This document explores **separated frontend architectures** that would provide better mobile responsiveness while keeping the existing backend intact. - ---- - -## Current Architecture - -``` -┌─────────────────────────────────────┐ -│ Flask Backend │ -│ ┌─────────┐ ┌─────────┐ ┌─────┐ │ -│ │ Routes │ │ Models │ │ DB │ │ -│ └─────────┘ └─────────┘ └─────┘ │ -└──────────────┬──────────────────────┘ - │ - ▼ -┌─────────────────────────────────────┐ -│ Flask Templates (Jinja2) │ -│ ┌─────────┐ ┌─────────┐ ┌─────┐ │ -│ │ HTML │ │ CSS │ │ JS │ │ -│ └─────────┘ └─────────┘ └─────┘ │ -└─────────────────────────────────────┘ - │ - ▼ - Browser (Tablet/Desktop) -``` - -**Limitations:** -- Server-rendered HTML (not ideal for dynamic mobile UIs) -- Limited interactivity without page reloads -- CSS is basic and not responsive for mobile -- Tight coupling between backend and frontend - ---- - -## Option 1: Single Page Application (SPA) with REST API - -### Architecture - -``` -┌─────────────────────────────────────┐ -│ Flask Backend │ -│ ┌─────────────────────────────────┐ │ -│ │ REST API │ │ -│ │ /api/tablets │ │ -│ │ /api/users │ │ -│ │ /api/loans │ │ -│ │ /api/non-loanable-devices │ │ -│ └─────────────────────────────────┘ │ -└──────────────┬──────────────────────┘ - │ HTTP/JSON - ▼ -┌─────────────────────────────────────┐ -│ Frontend (React/Vue/Svelte) │ -│ ┌─────────┐ ┌─────────┐ ┌─────┐ │ -│ │ Components │ │ State │ │ Router│ │ -│ └─────────┘ └─────────┘ └─────┘ │ -└─────────────────────────────────────┘ - │ - ▼ - Browser (Mobile/Tablet/Desktop) -``` - -### Implementation Steps - -#### 1. Create REST API Layer - -Add new routes to `app.py` (without removing existing ones): - -```python -# API Routes (add to app.py) -@app.route('/api/tablets', methods=['GET']) -def api_get_tablets(): - """GET /api/tablets - List all tablets""" - with get_db() as conn: - cursor = conn.cursor() - cursor.execute("SELECT * FROM tablets") - tablets = [dict(row) for row in cursor.fetchall()] - return jsonify(tablets) - -@app.route('/api/tablets/', methods=['GET']) -def api_get_tablet(tablet_id): - """GET /api/tablets/ - Get single tablet""" - with get_db() as conn: - cursor = conn.cursor() - cursor.execute("SELECT * FROM tablets WHERE id = ?", (tablet_id,)) - tablet = cursor.fetchone() - if tablet: - return jsonify(dict(tablet)) - return jsonify({'error': 'Tablet not found'}), 404 - -@app.route('/api/tablets', methods=['POST']) -def api_create_tablet(): - """POST /api/tablets - Create new tablet""" - data = request.get_json() - # Validate and create - with get_db() as conn: - cursor = conn.cursor() - cursor.execute(''' - INSERT INTO tablets (brand, model, serial_number, status, notes) - VALUES (?, ?, ?, 'available', ?) - ''', (data['brand'], data['model'], data['serial_number'], data.get('notes'))) - conn.commit() - tablet_id = cursor.lastrowid - cursor.execute("SELECT * FROM tablets WHERE id = ?", (tablet_id,)) - return jsonify(dict(cursor.fetchone())), 201 - -# Similar endpoints for users, loans, non_loanable_devices -``` - -#### 2. Frontend Structure (React Example) - -``` -frontend/ -├── public/ -│ └── index.html -├── src/ -│ ├── components/ -│ │ ├── TabletList.jsx -│ │ ├── TabletForm.jsx -│ │ ├── UserList.jsx -│ │ ├── LoanForm.jsx -│ │ ├── LoanHistory.jsx -│ │ ├── UserLoans.jsx -│ │ └── NonLoanableDevices.jsx -│ ├── hooks/ -│ │ └── useApi.js -│ ├── services/ -│ │ └── api.js -│ ├── App.jsx -│ ├── index.js -│ └── styles/ -│ ├── main.css -│ └── responsive.css -├── package.json -└── README.md -``` - -#### 3. API Service (frontend/src/services/api.js) - -```javascript -const API_BASE = '/api'; - -export const api = { - // Tablets - getTablets: async (status = null) => { - const url = status ? `${API_BASE}/tablets?status=${status}` : `${API_BASE}/tablets`; - const response = await fetch(url); - return response.json(); - }, - - getTablet: async (id) => { - const response = await fetch(`${API_BASE}/tablets/${id}`); - return response.json(); - }, - - createTablet: async (tablet) => { - const response = await fetch(`${API_BASE}/tablets`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(tablet) - }); - return response.json(); - }, - - updateTablet: async (id, tablet) => { - const response = await fetch(`${API_BASE}/tablets/${id}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(tablet) - }); - return response.json(); - }, - - // Similar methods for users, loans, non_loanable_devices -}; -``` - -#### 4. React Components Example - -```jsx -// frontend/src/components/TabletList.jsx -import React, { useState, useEffect } from 'react'; -import { api } from '../services/api'; - -export function TabletList() { - const [tablets, setTablets] = useState([]); - const [loading, setLoading] = useState(true); - - useEffect(() => { - api.getTablets('available').then(data => { - setTablets(data); - setLoading(false); - }); - }, []); - - if (loading) return
Loading...
; - - return ( -
-

Available Tablets

-
- {tablets.map(tablet => ( -
-

{tablet.brand} {tablet.model}

-

Serial: {tablet.serial_number}

-

Status: {tablet.status}

-
- ))} -
-
- ); -} -``` - -#### 5. Responsive CSS - -```css -/* frontend/src/styles/responsive.css */ - -/* Mobile-first approach */ -* { - box-sizing: border-box; -} - -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - background-color: #f5f5f5; - min-height: 100vh; -} - -.container { - max-width: 100%; - padding: 1rem; -} - -/* Cards for mobile */ -.tablet-card, .user-card, .loan-card { - background: white; - border-radius: 8px; - padding: 1rem; - margin-bottom: 1rem; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); - border: 1px solid #e0e0e0; -} - -/* Navigation */ -.nav { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.nav a { - padding: 0.75rem 1rem; - background-color: #4CAF50; - color: white; - text-decoration: none; - border-radius: 4px; - text-align: center; -} - -/* Tables - responsive */ -.responsive-table { - overflow-x: auto; -} - -table { - width: 100%; - min-width: 600px; /* Allows horizontal scrolling on mobile */ -} - -th, td { - padding: 0.75rem; - white-space: nowrap; -} - -/* Forms */ -form { - max-width: 100%; -} - -input, select, textarea { - width: 100%; - padding: 0.75rem; - margin-bottom: 1rem; - border: 1px solid #ddd; - border-radius: 4px; -} - -/* Buttons */ -.btn { - padding: 0.75rem 1.5rem; - width: 100%; - margin-bottom: 0.5rem; -} - -/* Breakpoints */ -@media (min-width: 600px) { - .nav { - flex-direction: row; - flex-wrap: wrap; - } - - .nav a { - flex: 1 1 auto; - min-width: 120px; - } - - .tablet-card, .user-card, .loan-card { - display: flex; - justify-content: space-between; - align-items: center; - } -} - -@media (min-width: 768px) { - .container { - max-width: 720px; - margin: 0 auto; - } - - table { - min-width: auto; - } -} - -@media (min-width: 1024px) { - .container { - max-width: 960px; - } - - .nav { - flex-wrap: nowrap; - } -} - -@media (min-width: 1200px) { - .container { - max-width: 1140px; - } -} -``` - -### Pros and Cons - -| Aspect | Pros | Cons | -|--------|------|------| -| **User Experience** | Rich, dynamic UI; no page reloads | More complex to develop | -| **Performance** | Fast after initial load; client-side rendering | Larger initial bundle | -| **Mobile Support** | Excellent with responsive design | Needs careful CSS work | -| **Development** | Modern tooling (React/Vue); component-based | Separate codebase to maintain | -| **SEO** | Poor (SPA) | Needs SSR for better SEO | -| **Backend Impact** | Minimal (just add API routes) | Need to maintain both templates and API | -| **Deployment** | Can be hosted separately | More complex deployment | - -### Recommended Tech Stack - -- **Framework:** React (most popular) or Vue (simpler) or Svelte (smaller bundle) -- **State Management:** React Query or SWR for data fetching -- **Styling:** Tailwind CSS or CSS Modules -- **Routing:** React Router -- **Build Tool:** Vite (fast) or Create React App -- **TypeScript:** Optional but recommended for large projects - ---- - -## Option 2: Hybrid Approach (Progressive Enhancement) - -### Architecture - -``` -┌─────────────────────────────────────┐ -│ Flask Backend │ -│ ┌─────────────────────────────────┐ │ -│ │ Dual Mode: │ │ -│ │ - Server templates (existing) │ │ -│ │ - REST API (new) │ │ -│ └─────────────────────────────────┘ │ -└──────────────┬──────────────────────┘ - │ - ┌─────┴─────┐ - ▼ ▼ -┌─────────────┐ ┌─────────────┐ -│ Desktop │ │ Mobile │ -│ (Existing) │ │ (New SPA) │ -└─────────────┘ └─────────────┘ -``` - -### How It Works - -1. **Desktop/Tablet:** Uses existing server-rendered templates -2. **Mobile:** Detects mobile device and serves a minimal HTML page that loads the SPA -3. **Shared Backend:** Both use the same Flask backend - -### Implementation - -#### 1. Device Detection Middleware - -```python -# app.py -from flask import request, redirect, url_for -import re - -MOBILE_USER_AGENTS = re.compile( - r'android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini|mobile', - re.IGNORECASE -) - -@app.before_request -def detect_mobile(): - user_agent = request.headers.get('User-Agent', '') - if MOBILE_USER_AGENTS.search(user_agent): - request.is_mobile = True - else: - request.is_mobile = False -``` - -#### 2. Mobile-Specific Route - -```python -@app.route('/mobile') -def mobile_app(): - """Serve mobile SPA entry point""" - return render_template('mobile.html') - -@app.before_request -def redirect_mobile(): - """Redirect mobile users to SPA""" - if hasattr(request, 'is_mobile') and request.is_mobile: - if not request.path.startswith('/api') and not request.path.startswith('/mobile'): - return redirect(url_for('mobile_app')) -``` - -#### 3. Mobile Entry Point Template - -```html - - - - - - - Tablet Management - Mobile - - - -
- - - -``` - -### Pros and Cons - -| Aspect | Pros | Cons | -|--------|------|------| -| **User Experience** | Best of both worlds | Two UIs to maintain | -| **Mobile Support** | Excellent | Desktop UI unchanged (may still be wide) | -| **Development** | Gradual migration possible | More complex logic | -| **Backend Impact** | Minimal | Device detection logic | -| **SEO** | Good (server-rendered desktop) | Mobile SPA has poor SEO | -| **Deployment** | Single deployment | Larger asset bundle | - ---- - -## Option 3: Flask + HTMX (Lightweight Dynamic UI) - -### Architecture - -``` -┌─────────────────────────────────────┐ -│ Flask Backend │ -│ ┌─────────────────────────────────┐ │ -│ │ Enhanced Templates │ │ -│ │ - HTML + HTMX attributes │ │ -│ │ - Partial updates via AJAX │ │ -│ └─────────────────────────────────┘ │ -└──────────────┬──────────────────────┘ - │ - ▼ -┌─────────────────────────────────────┐ -│ Browser (Any Device) │ -│ - HTMX handles dynamic updates │ -│ - CSS handles responsiveness │ -└─────────────────────────────────────┘ -``` - -### What is HTMX? - -HTMX allows you to add interactivity to HTML without writing JavaScript. It uses attributes to: -- Make AJAX requests -- Update DOM elements -- Handle form submissions -- Show loading indicators - -### Implementation Example - -#### 1. Add HTMX to Base Template - -```html - - - - - - -``` - -#### 2. Enhance Templates with HTMX - -```html - -
-

Available Tablets

- - - - - -
- {% for tablet in available_tablets %} -
-

{{ tablet.brand }} {{ tablet.model }}

-

Serial: {{ tablet.serial_number }}

- -
- {% endfor %} -
-
- - -
-``` - -#### 3. Add HTMX Endpoints - -```python -@app.route('/api/tablets/search') -def search_tablets(): - query = request.args.get('search', '') - with get_db() as conn: - cursor = conn.cursor() - cursor.execute(""" - SELECT * FROM tablets - WHERE brand LIKE ? OR model LIKE ? OR serial_number LIKE ? - """, (f'%{query}%', f'%{query}%', f'%{query}%')) - tablets = cursor.fetchall() - return render_template('partials/tablet_list.html', tablets=tablets) - -@app.route('/api/tablets//loan', methods=['POST']) -def loan_tablet_htmx(tablet_id): - # Get user from form - user_id = request.form.get('user_id') - # Loan logic... - return render_template('partials/loan_form.html', tablet_id=tablet_id) -``` - -#### 4. Responsive CSS - -```css -/* Add to base.html or separate CSS file */ - -/* Mobile-first responsive design */ -.tablet-card, .user-card, .loan-card { - background: white; - border-radius: 8px; - padding: 1rem; - margin-bottom: 1rem; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); -} - -.nav { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.nav a { - padding: 0.75rem; - text-align: center; -} - -@media (min-width: 600px) { - .nav { - flex-direction: row; - flex-wrap: wrap; - } - - .tablet-card { - display: flex; - justify-content: space-between; - } -} - -@media (min-width: 768px) { - .container { - max-width: 720px; - margin: 0 auto; - } -} - -@media (min-width: 1024px) { - .container { - max-width: 960px; - } - - .nav { - flex-wrap: nowrap; - } -} -``` - -### Pros and Cons - -| Aspect | Pros | Cons | -|--------|------|------| -| **User Experience** | Dynamic updates without full page reloads | Less powerful than full SPA | -| **Mobile Support** | Good with responsive CSS | Still limited by server rendering | -| **Development** | Minimal changes to existing code | Need to learn HTMX | -| **Backend Impact** | Very minimal (just add endpoints) | More routes to maintain | -| **SEO** | Excellent (server-rendered) | Best of all options | -| **Deployment** | No changes needed | Simple | -| **Bundle Size** | Tiny (~14KB for HTMX) | No build step | - ---- - -## Option 4: Mobile App (Native or Cross-Platform) - -### Architecture - -``` -┌─────────────────────────────────────┐ -│ Flask Backend │ -│ ┌─────────────────────────────────┐ │ -│ │ REST API │ │ -│ │ (Same as Option 1) │ │ -│ └─────────────────────────────────┘ │ -└──────────────┬──────────────────────┘ - │ HTTP/JSON - ▼ -┌─────────────────────────────────────┐ -│ Mobile App │ -│ (React Native / Flutter / Capacitor)│ -└─────────────────────────────────────┘ - │ - ▼ - Mobile Device -``` - -### Implementation Options - -#### A. React Native (JavaScript) - -```javascript -// App.js -import React from 'react'; -import { View, Text, FlatList, TouchableOpacity, StyleSheet } from 'react-native'; - -const API_BASE = 'http://your-server:5000/api'; - -export default function App() { - const [tablets, setTablets] = React.useState([]); - - React.useEffect(() => { - fetch(`${API_BASE}/tablets`) - .then(res => res.json()) - .then(data => setTablets(data)); - }, []); - - return ( - - Tablet Management - item.id.toString()} - renderItem={({item}) => ( - - {item.brand} {item.model} - SN: {item.serial_number} - Status: {item.status} - - )} - /> - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - padding: 20, - backgroundColor: '#f5f5f5', - }, - title: { - fontSize: 24, - fontWeight: 'bold', - marginBottom: 20, - textAlign: 'center', - }, - card: { - backgroundColor: 'white', - padding: 15, - borderRadius: 8, - marginBottom: 10, - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 2, - }, - brand: { - fontSize: 18, - fontWeight: '600', - }, - serial: { - fontSize: 14, - color: '#666', - }, - status: { - fontSize: 14, - color: '#4CAF50', - }, -}); -``` - -#### B. Flutter (Dart) - -```dart -// main.dart -import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; -import 'dart:convert'; - -void main() => runApp(MyApp()); - -class MyApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Tablet Management', - home: TabletListScreen(), - ); - } -} - -class TabletListScreen extends StatefulWidget { - @override - _TabletListScreenState createState() => _TabletListScreenState(); -} - -class _TabletListScreenState extends State { - List tablets = []; - - @override - void initState() { - super.initState(); - fetchTablets(); - } - - Future fetchTablets() async { - final response = await http.get(Uri.parse('http://your-server:5000/api/tablets')); - if (response.statusCode == 200) { - setState(() { - tablets = json.decode(response.body); - }); - } - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text('Tablet Management')), - body: ListView.builder( - itemCount: tablets.length, - itemBuilder: (context, index) { - final tablet = tablets[index]; - return Card( - child: ListTile( - title: Text('${tablet['brand']} ${tablet['model']}'), - subtitle: Text('SN: ${tablet['serial_number']}'), - trailing: Text(tablet['status']), - ), - ); - }, - ), - ); - } -} -``` - -#### C. Capacitor (Web App as Mobile App) - -Use your existing web app (Option 1 SPA) and wrap it with Capacitor: - -```bash -# Install Capacitor -npm install @capacitor/core @capacitor/cli -npx cap init - -# Add platforms -npm install @capacitor/android @capacitor/ios -npx cap add android -npx cap add ios - -# Build and sync -npm run build -npx cap sync -npx cap open android # or ios -``` - -### Pros and Cons - -| Aspect | React Native | Flutter | Capacitor | -|--------|--------------|---------|-----------| -| **Language** | JavaScript | Dart | JavaScript | -| **Performance** | Native | Native | WebView | -| **Code Reuse** | ~80% with web | ~50% with web | ~100% with web | -| **Learning Curve** | Medium (if know React) | High (new language) | Low (web devs) | -| **Access to Native** | Good | Excellent | Limited | -| **Bundle Size** | Medium | Large | Small | -| **Offline Support** | Yes | Yes | Yes | - ---- - -## Comparison Matrix - -| Feature | Current | SPA (Option 1) | Hybrid (Option 2) | HTMX (Option 3) | Mobile App (Option 4) | -|---------|---------|---------------|------------------|----------------|----------------------| -| **Mobile Friendly** | ❌ No | ✅ Yes | ✅ Yes | ⚠️ Partial | ✅ Yes | -| **Desktop Friendly** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ❌ No | -| **Tablet Friendly** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | -| **Development Effort** | N/A | High | Medium | Low | High | -| **Backend Changes** | N/A | Low | Low | Very Low | Low (API only) | -| **Learning Curve** | N/A | Medium | Medium | Low | High | -| **SEO** | ✅ Good | ❌ Poor | ✅ Good | ✅ Good | ❌ Poor | -| **Offline Support** | ❌ No | ✅ Yes | ❌ No | ❌ No | ✅ Yes | -| **Performance** | ⚠️ OK | ✅ Good | ⚠️ OK | ✅ Good | ✅ Excellent | -| **Deployment** | Simple | Complex | Medium | Simple | Complex | -| **Maintenance** | Simple | Medium | Complex | Simple | Medium | - ---- - -## Recommendations - -### For Immediate Improvement (Low Effort) - -**Choose: Option 3 (HTMX)** - -- Minimal code changes -- No new build process -- Progressive enhancement -- Good mobile support with responsive CSS -- Keeps existing server rendering - -### For Best User Experience (Medium Effort) - -**Choose: Option 1 (SPA with REST API)** - -- Modern, dynamic UI -- Excellent mobile support -- Can be deployed separately -- Backend changes are minimal (just add API routes) - -### For Native Mobile Experience (High Effort) - -**Choose: Option 4 (Mobile App)** - -- Best mobile UX -- Offline capabilities -- Native device features (camera, etc.) -- Requires separate mobile development - -### For Gradual Migration - -**Choose: Option 2 (Hybrid)** - -- Start with mobile SPA -- Keep desktop as-is -- Migrate desktop later if needed -- Minimal risk - ---- - -## Implementation Roadmap - -### Phase 1: Quick Win (1-2 days) - -1. Add responsive CSS to existing templates -2. Add viewport meta tag -3. Test on mobile devices - -**Result:** Better mobile experience with minimal changes - -### Phase 2: Enhanced Interactivity (3-5 days) - -1. Add HTMX to templates -2. Create partial templates for updates -3. Add new API endpoints for HTMX -4. Test all interactions - -**Result:** Dynamic UI without full SPA complexity - -### Phase 3: Full SPA (1-2 weeks) - -1. Set up React/Vue project -2. Create API layer in Flask -3. Build frontend components -4. Add responsive design -5. Test on all devices -6. Deploy frontend separately - -**Result:** Modern, mobile-first web application - -### Phase 4: Mobile App (2-4 weeks) - -1. Choose framework (React Native/Flutter) -2. Set up mobile project -3. Connect to existing API -4. Build mobile-specific UI -5. Add offline support -6. Test on devices -7. Publish to app stores - -**Result:** Native mobile application - ---- - -## File Structure for Separated Frontend - -If you choose Option 1 (SPA), here's the recommended structure: - -``` -GestionTablets/ -├── backend/ # Existing Flask backend -│ ├── app.py # Flask app + API routes -│ ├── templates/ # Existing templates (keep for now) -│ ├── static/ # Static files -│ └── ... -│ -├── frontend/ # NEW: Separated frontend -│ ├── public/ -│ │ └── index.html -│ ├── src/ -│ │ ├── components/ -│ │ │ ├── common/ -│ │ │ │ ├── Button.jsx -│ │ │ │ ├── Card.jsx -│ │ │ │ ├── Modal.jsx -│ │ │ │ └── Table.jsx -│ │ │ ├── TabletList.jsx -│ │ │ ├── TabletForm.jsx -│ │ │ ├── UserList.jsx -│ │ │ ├── UserForm.jsx -│ │ │ ├── LoanList.jsx -│ │ │ ├── LoanForm.jsx -│ │ │ ├── LoanHistory.jsx -│ │ │ ├── UserLoans.jsx -│ │ │ └── NonLoanableDevices.jsx -│ │ ├── hooks/ -│ │ │ ├── useTablets.js -│ │ │ ├── useUsers.js -│ │ │ ├── useLoans.js -│ │ │ └── useApi.js -│ │ ├── services/ -│ │ │ └── api.js -│ │ ├── utils/ -│ │ │ ├── formatters.js -│ │ │ └── validators.js -│ │ ├── App.jsx -│ │ ├── App.css -│ │ ├── index.js -│ │ └── index.css -│ ├── package.json -│ ├── vite.config.js -│ └── README.md -│ -├── docs/ # Documentation -│ ├── MIGRATION_TO_POSTGRES.md -│ └── FRONTEND_OPTIONS.md # This document -│ -├── scripts/ # Utility scripts -│ └── migrate_to_postgres.py -│ -├── .gitignore -├── README.md -├── pyproject.toml -└── docker-compose.yml -``` - ---- - -## API Endpoints Needed - -For any separated frontend, you'll need these API endpoints: - -### Tablets -- `GET /api/tablets` - List all tablets -- `GET /api/tablets?status=available` - Filter by status -- `GET /api/tablets/` - Get single tablet -- `POST /api/tablets` - Create tablet -- `PUT /api/tablets/` - Update tablet -- `DELETE /api/tablets/` - Delete tablet -- `GET /api/tablets/search?q=query` - Search tablets - -### Users -- `GET /api/users` - List all users -- `GET /api/users/` - Get single user -- `POST /api/users` - Create user -- `PUT /api/users/` - Update user -- `DELETE /api/users/` - Delete user -- `GET /api/users/search?q=query` - Search users - -### Loans -- `GET /api/loans` - List all loans -- `GET /api/loans?status=active` - Filter by status -- `GET /api/loans/` - Get single loan -- `POST /api/loans` - Create loan -- `PUT /api/loans//return` - Return tablet -- `GET /api/loans/user/` - Get loans by user -- `GET /api/loans/tablet/` - Get loans by tablet - -### Non-Loanable Devices -- `GET /api/non-loanable-devices` - List all -- `GET /api/non-loanable-devices/` - Get single device -- `POST /api/non-loanable-devices` - Create device -- `PUT /api/non-loanable-devices/` - Update device -- `DELETE /api/non-loanable-devices/` - Delete device - -### Statistics -- `GET /api/stats` - Get dashboard statistics - ---- - -## Responsive Design Guidelines - -### Breakpoints - -```css -/* Mobile-first approach */ -:root { - --breakpoint-xs: 0px; - --breakpoint-sm: 576px; - --breakpoint-md: 768px; - --breakpoint-lg: 992px; - --breakpoint-xl: 1200px; -} - -/* Usage */ -@media (min-width: 576px) { /* Small devices (landscape phones) */ } -@media (min-width: 768px) { /* Medium devices (tablets) */ } -@media (min-width: 992px) { /* Large devices (desktops) */ } -@media (min-width: 1200px) { /* Extra large devices */ } -``` - -### Mobile-First Principles - -1. **Start with mobile** - Design for smallest screen first -2. **Progressive enhancement** - Add features for larger screens -3. **Touch targets** - Minimum 48x48px for touch elements -4. **Font sizes** - Minimum 16px for readability -5. **Spacing** - Adequate padding for touch -6. **Navigation** - Bottom navigation for mobile, top for desktop -7. **Forms** - Large, easy-to-use inputs -8. **Tables** - Consider cards instead of tables on mobile - -### Touch Target Sizes - -| Element | Minimum Size | Recommended Size | -|---------|--------------|------------------| -| Buttons | 48x48px | 56x56px | -| Form inputs | 48px height | 56px height | -| List items | 48px height | 64px height | -| Checkboxes/Radios | 24x24px | 32x32px | - ---- - -## Deployment Options - -### Option A: Separate Servers - -``` -┌─────────────────┐ ┌─────────────────┐ -│ Backend Server │────▶│ Frontend Server │ -│ (Flask) │ │ (Nginx/Apache) │ -│ :5000 │ │ :80/:443 │ -└─────────────────┘ └─────────────────┘ - │ │ - ▼ ▼ - API Requests Static Files -``` - -**Pros:** Separate scaling, independent deployment -**Cons:** More complex setup, CORS configuration - -### Option B: Same Server, Different Routes - -``` -┌─────────────────────────────────────┐ -│ Flask Server │ -│ ┌─────────────────────────────────┐ │ -│ │ /api/* → Backend routes │ │ -│ │ /* → Frontend (SPA) │ │ -│ └─────────────────────────────────┘ │ -└─────────────────────────────────────┘ - │ - ▼ - Nginx (reverse proxy) - │ - ▼ - Client -``` - -**Pros:** Simpler deployment, no CORS issues -**Cons:** Backend serves static files - -### Option C: Docker Compose - -```yaml -# docker-compose.yml -version: '3.8' - -services: - backend: - build: ./backend - ports: - - "5000:5000" - environment: - - FLASK_ENV=production - restart: unless-stopped - - frontend: - build: ./frontend - ports: - - "80:80" - - "443:443" - depends_on: - - backend - restart: unless-stopped - - nginx: - image: nginx:alpine - ports: - - "80:80" - - "443:443" - volumes: - - ./nginx.conf:/etc/nginx/nginx.conf - depends_on: - - backend - - frontend - restart: unless-stopped -``` - ---- - -## Conclusion - -For the Tablet Management System, I recommend the following approach: - -### Short Term (1-2 days) -Start with **Option 3 (HTMX)** to add dynamic updates and responsive CSS to the existing templates. This provides: -- Immediate mobile improvements -- Minimal code changes -- No new dependencies (just HTMX) -- Progressive enhancement - -### Medium Term (1-2 weeks) -Migrate to **Option 1 (SPA with REST API)** for: -- Better mobile experience -- Modern development workflow -- Separate frontend deployment -- Easier to maintain long-term - -### Long Term (Optional) -Consider **Option 4 (Mobile App)** if: -- Users need offline access -- Need native device features -- Want app store presence - -The current backend (Flask + SQLite) can remain **completely unchanged** for all these options. You only need to add API endpoints, which don't affect the existing template-based functionality. diff --git a/docs/MIGRATION_TO_POSTGRES.md b/docs/MIGRATION_TO_POSTGRES.md deleted file mode 100644 index 740f8b1..0000000 --- a/docs/MIGRATION_TO_POSTGRES.md +++ /dev/null @@ -1,987 +0,0 @@ -# PostgreSQL Migration Guide - -This document describes how to migrate the Tablet Management System from SQLite to PostgreSQL when the database grows beyond SQLite's capabilities. - -## When to Migrate - -Consider migrating to PostgreSQL when you encounter any of these scenarios: - -| Metric | SQLite Limit | PostgreSQL | Migration Trigger | -|--------|--------------|------------|-------------------| -| Database Size | ~10GB max | Unlimited | >1GB | -| Concurrent Writers | 1 | Thousands | >50 simultaneous | -| Active Users | <100 | Millions | >200 | -| Transactions/min | <100 | 100K+ | >500 | -| Servers | Single machine | Cluster | Multiple servers | -| High Availability | No | Yes | Required | -| Backup Strategy | Manual | Automated | Automated needed | - -## Architecture Overview - -The migration uses a **Repository Pattern** to abstract the database layer, allowing both SQLite and PostgreSQL to work seamlessly. - -``` -project/ -├── backend/ -│ ├── config/ -│ │ ├── __init__.py -│ │ ├── settings.py # Database configuration -│ │ └── database.py # Repository factory -│ ├── repositories/ -│ │ ├── __init__.py -│ │ ├── base_repository.py # Abstract base classes -│ │ ├── sqlite_repo.py # SQLite implementation -│ │ └── postgres_repo.py # PostgreSQL implementation -│ └── app.py # Main application (unchanged) -├── migrations/ # Alembic migrations -│ └── versions/ -│ └── initial_schema.py -├── scripts/ -│ └── migrate_to_postgres.py # Migration script -└── docker-compose.yml # Optional Docker setup -``` - -## Step 1: Install Dependencies - -```bash -# For development -pip install psycopg2-binary sqlalchemy alembic - -# For production (more efficient) -pip install psycopg2 sqlalchemy alembic -``` - -## Step 2: Set Up PostgreSQL - -### Option A: Local Installation - -```bash -# Ubuntu/Debian -sudo apt update -sudo apt install postgresql postgresql-contrib - -# Create database and user -sudo -u postgres psql -``` - -In PostgreSQL shell: -```sql -CREATE DATABASE tablet_management; -CREATE USER tablet_user WITH PASSWORD 'your_secure_password'; -GRANT ALL PRIVILEGES ON DATABASE tablet_management TO tablet_user; -ALTER USER tablet_user CREATEDB; -\q -``` - -### Option B: Docker (Recommended for Development) - -```bash -# Start PostgreSQL container -docker run --name tablet-db -e POSTGRES_PASSWORD=your_password -e POSTGRES_USER=tablet_user -e POSTGRES_DB=tablet_management -p 5432:5432 -d postgres:16-alpine - -# Or use docker-compose (see docker-compose.yml) -docker-compose up -d postgres -``` - -## Step 3: Configure Environment - -Create a `.env` file: - -```bash -# Database configuration -DB_TYPE=postgres # or 'sqlite' -DB_URL=postgresql://tablet_user:your_password@localhost:5432/tablet_management - -# For SQLite (fallback) -DB_PATH=tablets.db -``` - -Or set environment variables: - -```bash -export DB_TYPE=postgres -export DB_URL=postgresql://tablet_user:your_password@localhost:5432/tablet_management -``` - -## Step 4: Create Repository Abstraction - -### Base Repository (Abstract Interface) - -```python -# backend/repositories/base_repository.py -from abc import ABC, abstractmethod -from typing import Optional, List -from datetime import datetime - - -class BaseTabletRepository(ABC): - @abstractmethod - def get_by_id(self, tablet_id: int) -> Optional[dict]: - pass - - @abstractmethod - def get_by_serial(self, serial: str) -> Optional[dict]: - pass - - @abstractmethod - def get_all(self, status: Optional[str] = None) -> List[dict]: - pass - - @abstractmethod - def add(self, brand: str, model: str, serial_number: str, notes: Optional[str] = None) -> dict: - pass - - @abstractmethod - def update_status(self, tablet_id: int, status: str) -> bool: - pass - - @abstractmethod - def delete(self, tablet_id: int) -> bool: - pass - - -class BaseUserRepository(ABC): - @abstractmethod - def get_by_id(self, user_id: int) -> Optional[dict]: - pass - - @abstractmethod - def get_by_identification(self, identification: str) -> Optional[dict]: - pass - - @abstractmethod - def get_all(self) -> List[dict]: - pass - - @abstractmethod - def add(self, name: str, email: Optional[str], phone: Optional[str], identification: str) -> dict: - pass - - -class BaseLoanRepository(ABC): - @abstractmethod - def get_by_id(self, loan_id: int) -> Optional[dict]: - pass - - @abstractmethod - def get_active_by_tablet(self, tablet_id: int) -> Optional[dict]: - pass - - @abstractmethod - def get_by_user(self, user_id: int) -> List[dict]: - pass - - @abstractmethod - def get_all(self, status: Optional[str] = None) -> List[dict]: - pass - - @abstractmethod - def create(self, tablet_id: int, user_id: int) -> dict: - pass - - @abstractmethod - def return_loan(self, loan_id: int) -> bool: - pass - - -class BaseNonLoanableDeviceRepository(ABC): - @abstractmethod - def get_by_id(self, device_id: int) -> Optional[dict]: - pass - - @abstractmethod - def get_all(self) -> List[dict]: - pass - - @abstractmethod - def add(self, brand: str, model: str, serial_number: str, device_type: str, - location: Optional[str] = None, notes: Optional[str] = None, - purchase_date: Optional[str] = None, purchase_cost: Optional[float] = None) -> dict: - pass - - @abstractmethod - def update(self, device_id: int, **kwargs) -> bool: - pass - - @abstractmethod - def delete(self, device_id: int) -> bool: - pass -``` - -### SQLite Implementation - -```python -# backend/repositories/sqlite_repo.py -import sqlite3 -from typing import Optional, List -from .base_repository import ( - BaseTabletRepository, BaseUserRepository, - BaseLoanRepository, BaseNonLoanableDeviceRepository -) - - -class SQLiteTabletRepository(BaseTabletRepository): - def __init__(self, db_path: str = 'tablets.db'): - self.db_path = db_path - self._init_db() - - def _init_db(self): - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute(''' - CREATE TABLE IF NOT EXISTS tablets ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - brand TEXT NOT NULL, - model TEXT NOT NULL, - serial_number TEXT UNIQUE NOT NULL, - status TEXT DEFAULT 'available', - notes TEXT - ) - ''') - conn.commit() - conn.close() - - def get_by_id(self, tablet_id: int) -> Optional[dict]: - conn = sqlite3.connect(self.db_path) - conn.row_factory = sqlite3.Row - cursor = conn.cursor() - cursor.execute("SELECT * FROM tablets WHERE id = ?", (tablet_id,)) - row = cursor.fetchone() - conn.close() - return dict(row) if row else None - - def get_by_serial(self, serial: str) -> Optional[dict]: - conn = sqlite3.connect(self.db_path) - conn.row_factory = sqlite3.Row - cursor = conn.cursor() - cursor.execute("SELECT * FROM tablets WHERE serial_number = ?", (serial,)) - row = cursor.fetchone() - conn.close() - return dict(row) if row else None - - def get_all(self, status: Optional[str] = None) -> List[dict]: - conn = sqlite3.connect(self.db_path) - conn.row_factory = sqlite3.Row - cursor = conn.cursor() - query = "SELECT * FROM tablets" - params = () - if status: - query += " WHERE status = ?" - params = (status,) - cursor.execute(query, params) - results = [dict(row) for row in cursor.fetchall()] - conn.close() - return results - - def add(self, brand: str, model: str, serial_number: str, notes: Optional[str] = None) -> dict: - conn = sqlite3.connect(self.db_path) - conn.row_factory = sqlite3.Row - cursor = conn.cursor() - cursor.execute(''' - INSERT INTO tablets (brand, model, serial_number, status, notes) - VALUES (?, ?, ?, 'available', ?) - ''', (brand, model, serial_number, notes)) - conn.commit() - tablet_id = cursor.lastrowid - cursor.execute("SELECT * FROM tablets WHERE id = ?", (tablet_id,)) - row = cursor.fetchone() - conn.close() - return dict(row) - - def update_status(self, tablet_id: int, status: str) -> bool: - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute("UPDATE tablets SET status = ? WHERE id = ?", (status, tablet_id)) - conn.commit() - changed = cursor.rowcount > 0 - conn.close() - return changed - - def delete(self, tablet_id: int) -> bool: - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - cursor.execute("DELETE FROM tablets WHERE id = ?", (tablet_id,)) - conn.commit() - deleted = cursor.rowcount > 0 - conn.close() - return deleted - - -# Similar implementations for SQLiteUserRepository, SQLiteLoanRepository, etc. -``` - -### PostgreSQL Implementation - -```python -# backend/repositories/postgres_repo.py -import psycopg2 -from psycopg2 import sql -from psycopg2.extras import DictCursor -from typing import Optional, List -from .base_repository import ( - BaseTabletRepository, BaseUserRepository, - BaseLoanRepository, BaseNonLoanableDeviceRepository -) - - -class PostgreSQLTabletRepository(BaseTabletRepository): - def __init__(self, connection_string: str): - self.connection_string = connection_string - self._init_db() - - def _get_connection(self): - return psycopg2.connect(self.connection_string) - - def _init_db(self): - conn = self._get_connection() - cursor = conn.cursor() - cursor.execute(''' - CREATE TABLE IF NOT EXISTS tablets ( - id SERIAL PRIMARY KEY, - brand VARCHAR(100) NOT NULL, - model VARCHAR(100) NOT NULL, - serial_number VARCHAR(50) UNIQUE NOT NULL, - status VARCHAR(20) DEFAULT 'available', - notes TEXT, - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW() - ) - ''') - cursor.execute(''' - CREATE INDEX IF NOT EXISTS idx_tablets_serial - ON tablets(serial_number) - ''') - cursor.execute(''' - CREATE INDEX IF NOT EXISTS idx_tablets_status - ON tablets(status) - ''') - conn.commit() - cursor.close() - conn.close() - - def get_by_id(self, tablet_id: int) -> Optional[dict]: - conn = self._get_connection() - cursor = conn.cursor(cursor_factory=DictCursor) - cursor.execute("SELECT * FROM tablets WHERE id = %s", (tablet_id,)) - row = cursor.fetchone() - conn.close() - return dict(row) if row else None - - def get_by_serial(self, serial: str) -> Optional[dict]: - conn = self._get_connection() - cursor = conn.cursor(cursor_factory=DictCursor) - cursor.execute("SELECT * FROM tablets WHERE serial_number = %s", (serial,)) - row = cursor.fetchone() - conn.close() - return dict(row) if row else None - - def get_all(self, status: Optional[str] = None) -> List[dict]: - conn = self._get_connection() - cursor = conn.cursor(cursor_factory=DictCursor) - query = "SELECT * FROM tablets" - params = () - if status: - query += " WHERE status = %s" - params = (status,) - cursor.execute(query, params) - results = [dict(row) for row in cursor.fetchall()] - conn.close() - return results - - def add(self, brand: str, model: str, serial_number: str, notes: Optional[str] = None) -> dict: - conn = self._get_connection() - cursor = conn.cursor(cursor_factory=DictCursor) - cursor.execute(''' - INSERT INTO tablets (brand, model, serial_number, status, notes) - VALUES (%s, %s, %s, 'available', %s) - RETURNING * - ''', (brand, model, serial_number, notes)) - row = cursor.fetchone() - conn.commit() - conn.close() - return dict(row) - - def update_status(self, tablet_id: int, status: str) -> bool: - conn = self._get_connection() - cursor = conn.cursor() - cursor.execute( - "UPDATE tablets SET status = %s, updated_at = NOW() WHERE id = %s", - (status, tablet_id) - ) - conn.commit() - changed = cursor.rowcount > 0 - conn.close() - return changed - - def delete(self, tablet_id: int) -> bool: - conn = self._get_connection() - cursor = conn.cursor() - cursor.execute("DELETE FROM tablets WHERE id = %s", (tablet_id,)) - conn.commit() - deleted = cursor.rowcount > 0 - conn.close() - return deleted - - -# Similar implementations for PostgreSQLUserRepository, PostgreSQLLoanRepository, etc. -``` - -## Step 5: Create Repository Factory - -```python -# backend/config/database.py -import os -from backend.repositories.sqlite_repo import ( - SQLiteTabletRepository, SQLiteUserRepository, - SQLiteLoanRepository, SQLiteNonLoanableDeviceRepository -) -from backend.repositories.postgres_repo import ( - PostgreSQLTabletRepository, PostgreSQLUserRepository, - PostgreSQLLoanRepository, PostgreSQLNonLoanableDeviceRepository -) -from backend.repositories.base_repository import ( - BaseTabletRepository, BaseUserRepository, - BaseLoanRepository, BaseNonLoanableDeviceRepository -) - - -class DatabaseConfig: - def __init__(self): - self.db_type = os.getenv('DB_TYPE', 'sqlite') - self.db_url = os.getenv('DB_URL', '') - self.db_path = os.getenv('DB_PATH', 'tablets.db') - - @property - def is_postgres(self) -> bool: - return self.db_type == 'postgres' - - -def get_tablet_repository() -> BaseTabletRepository: - config = DatabaseConfig() - if config.is_postgres: - return PostgreSQLTabletRepository(config.db_url) - else: - return SQLiteTabletRepository(config.db_path) - - -def get_user_repository() -> BaseUserRepository: - config = DatabaseConfig() - if config.is_postgres: - return PostgreSQLUserRepository(config.db_url) - else: - return SQLiteUserRepository(config.db_path) - - -def get_loan_repository() -> BaseLoanRepository: - config = DatabaseConfig() - if config.is_postgres: - return PostgreSQLLoanRepository(config.db_url) - else: - return SQLiteLoanRepository(config.db_path) - - -def get_non_loanable_device_repository() -> BaseNonLoanableDeviceRepository: - config = DatabaseConfig() - if config.is_postgres: - return PostgreSQLNonLoanableDeviceRepository(config.db_url) - else: - return SQLiteNonLoanableDeviceRepository(config.db_path) -``` - -## Step 6: Update Application to Use Repositories - -Modify your application to use the repository pattern: - -```python -# In your app.py or service layer -from backend.config.database import ( - get_tablet_repository, get_user_repository, - get_loan_repository, get_non_loanable_device_repository -) - -# Instead of direct SQLite calls: -tablet_repo = get_tablet_repository() -user_repo = get_user_repository() -loan_repo = get_loan_repository() - -# Example: Loan a tablet -def loan_tablet(tablet_id: int, user_id: int): - # Get repositories - tablet_repo = get_tablet_repository() - user_repo = get_user_repository() - loan_repo = get_loan_repository() - - # Validate - tablet = tablet_repo.get_by_id(tablet_id) - if not tablet: - raise ValueError("Tablet not found") - - if tablet['status'] != 'available': - raise ValueError("Tablet not available") - - user = user_repo.get_by_id(user_id) - if not user: - raise ValueError("User not found") - - # Check for active loan - active_loan = loan_repo.get_active_by_tablet(tablet_id) - if active_loan: - raise ValueError("Tablet already loaned") - - # Create loan - loan = loan_repo.create(tablet_id, user_id) - - # Update tablet status - tablet_repo.update_status(tablet_id, 'loaned') - - return loan -``` - -## Step 7: Create Migration Script - -```python -# scripts/migrate_to_postgres.py -#!/usr/bin/env python3 -""" -Migration script from SQLite to PostgreSQL -""" -import sqlite3 -import psycopg2 -from psycopg2.extras import DictCursor -import argparse -from tqdm import tqdm -import os - - -def create_postgres_tables(conn): - """Create all tables in PostgreSQL""" - cursor = conn.cursor() - - # Tablets - cursor.execute(''' - CREATE TABLE IF NOT EXISTS tablets ( - id SERIAL PRIMARY KEY, - brand VARCHAR(100) NOT NULL, - model VARCHAR(100) NOT NULL, - serial_number VARCHAR(50) UNIQUE NOT NULL, - status VARCHAR(20) DEFAULT 'available', - notes TEXT, - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW() - ) - ''') - - # Users - cursor.execute(''' - CREATE TABLE IF NOT EXISTS users ( - id SERIAL PRIMARY KEY, - name VARCHAR(100) NOT NULL, - email VARCHAR(255), - phone VARCHAR(20), - identification VARCHAR(50) UNIQUE NOT NULL, - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW() - ) - ''') - - # Loans - cursor.execute(''' - CREATE TABLE IF NOT EXISTS loans ( - id SERIAL PRIMARY KEY, - tablet_id INTEGER NOT NULL REFERENCES tablets(id) ON DELETE RESTRICT, - user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE RESTRICT, - loan_date TIMESTAMP NOT NULL DEFAULT NOW(), - return_date TIMESTAMP, - status VARCHAR(20) DEFAULT 'active', - created_at TIMESTAMP DEFAULT NOW() - ) - ''') - - # Non-loanable devices - cursor.execute(''' - CREATE TABLE IF NOT EXISTS non_loanable_devices ( - id SERIAL PRIMARY KEY, - brand VARCHAR(100) NOT NULL, - model VARCHAR(100) NOT NULL, - serial_number VARCHAR(50) UNIQUE NOT NULL, - device_type VARCHAR(50) NOT NULL, - location VARCHAR(100), - status VARCHAR(20) DEFAULT 'available', - notes TEXT, - purchase_date DATE, - purchase_cost DECIMAL(10,2), - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW() - ) - ''') - - # Indexes for performance - cursor.execute('CREATE INDEX IF NOT EXISTS idx_tablets_serial ON tablets(serial_number)') - cursor.execute('CREATE INDEX IF NOT EXISTS idx_tablets_status ON tablets(status)') - cursor.execute('CREATE INDEX IF NOT EXISTS idx_tablets_brand ON tablets(brand)') - cursor.execute('CREATE INDEX IF NOT EXISTS idx_users_identification ON users(identification)') - cursor.execute('CREATE INDEX IF NOT EXISTS idx_loans_tablet ON loans(tablet_id)') - cursor.execute('CREATE INDEX IF NOT EXISTS idx_loans_user ON loans(user_id)') - cursor.execute('CREATE INDEX IF NOT EXISTS idx_loans_status ON loans(status)') - - conn.commit() - - -def migrate_table(conn_sqlite, conn_pg, table_name: str, pg_create_table: str): - """Generic migration for a table""" - cursor_sqlite = conn_sqlite.cursor() - cursor_pg = conn_pg.cursor() - - # Get all data from SQLite - cursor_sqlite.execute(f"SELECT * FROM {table_name}") - rows = cursor_sqlite.fetchall() - - if not rows: - print(f"No data to migrate for {table_name}") - return - - # Get column names - column_names = [desc[0] for desc in cursor_sqlite.description] - - # Prepare INSERT statement - columns = ', '.join(column_names) - placeholders = ', '.join(['%s'] * len(column_names)) - insert_sql = f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders}) ON CONFLICT DO NOTHING" - - # Migrate data - for row in tqdm(rows, desc=f"Migrating {table_name}"): - cursor_pg.execute(insert_sql, row) - - conn_pg.commit() - print(f"✓ Migrated {len(rows)} rows from {table_name}") - - -def migrate_all(sqlite_path: str, pg_url: str): - """Migrate all data from SQLite to PostgreSQL""" - print("Starting migration from SQLite to PostgreSQL...") - - # Connect to SQLite - conn_sqlite = sqlite3.connect(sqlite_path) - - # Connect to PostgreSQL - conn_pg = psycopg2.connect(pg_url) - - try: - # Create tables - print("Creating PostgreSQL tables...") - create_postgres_tables(conn_pg) - - # Migrate each table - migrate_table(conn_sqlite, conn_pg, 'tablets', '') - migrate_table(conn_sqlite, conn_pg, 'users', '') - migrate_table(conn_sqlite, conn_pg, 'loans', '') - migrate_table(conn_sqlite, conn_pg, 'non_loanable_devices', '') - - print("\n✓ Migration completed successfully!") - print(f" SQLite: {sqlite_path}") - print(f" PostgreSQL: {pg_url}") - - except Exception as e: - conn_pg.rollback() - print(f"\n✗ Migration failed: {e}") - raise - finally: - conn_sqlite.close() - conn_pg.close() - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description='Migrate from SQLite to PostgreSQL') - parser.add_argument('--sqlite', default='tablets.db', help='SQLite database path') - parser.add_argument('--postgres', required=True, help='PostgreSQL connection URL') - args = parser.parse_args() - - migrate_all(args.sqlite, args.postgres) -``` - -## Step 8: Run Migration - -```bash -# Test the migration first (dry run) -python scripts/migrate_to_postgres.py --sqlite tablets.db --postgres postgresql://tablet_user:password@localhost:5432/tablet_management_test - -# Verify data in test database -psql -U tablet_user -d tablet_management_test -c "SELECT COUNT(*) FROM tablets;" - -# When ready, migrate to production -python scripts/migrate_to_postgres.py --sqlite tablets.db --postgres postgresql://tablet_user:password@localhost:5432/tablet_management -``` - -## Step 9: Switch to PostgreSQL - -```bash -# Update environment variables -export DB_TYPE=postgres -export DB_URL=postgresql://tablet_user:password@localhost:5432/tablet_management - -# Restart application -python app.py -``` - -## Step 10: Verify and Monitor - -```bash -# Check application logs for errors -# Monitor database connections -psql -U tablet_user -d tablet_management -c "SELECT COUNT(*) FROM tablets;" - -# Check active connections -psql -U postgres -c "SELECT * FROM pg_stat_activity WHERE datname = 'tablet_management';" -``` - -## Rollback Plan - -If something goes wrong: - -1. **Immediate rollback:** - ```bash - # Switch back to SQLite -export DB_TYPE=sqlite -export DB_PATH=tablets.db - python app.py - ``` - -2. **Data verification:** - ```bash - # Compare counts - sqlite3 tablets.db "SELECT COUNT(*) FROM tablets;" - psql -U tablet_user -d tablet_management -c "SELECT COUNT(*) FROM tablets;" - ``` - -3. **Backup PostgreSQL data:** - ```bash - pg_dump -U tablet_user -d tablet_management > postgres_backup_$(date +%Y%m%d).sql - ``` - -## Docker Compose (Optional) - -For easy deployment with Docker: - -```yaml -# docker-compose.yml -version: '3.8' - -services: - postgres: - image: postgres:16-alpine - container_name: tablet_db - environment: - POSTGRES_DB: tablet_management - POSTGRES_USER: tablet_user - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme} - ports: - - "5432:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U tablet_user -d tablet_management"] - interval: 5s - timeout: 5s - retries: 5 - restart: unless-stopped - - app: - build: . - container_name: tablet_app - environment: - DB_TYPE: postgres - DB_URL: postgresql://tablet_user:${POSTGRES_PASSWORD:-changeme}@postgres:5432/tablet_management - ports: - - "5000:5000" - depends_on: - postgres: - condition: service_healthy - restart: unless-stopped - -volumes: - postgres_data: -``` - -Start with Docker: -```bash -docker-compose up -d -``` - -## Benefits of PostgreSQL - -### Performance -- **Concurrency:** Multiple writers simultaneously (no lock contention) -- **Indexing:** Advanced index types (B-tree, Hash, GiST, GIN, BRIN) -- **Query Optimization:** Advanced query planner -- **Connection Pooling:** Built-in support - -### Scalability -- **Vertical:** Handles large datasets efficiently -- **Horizontal:** Read replicas, partitioning, sharding -- **Connections:** Supports thousands of concurrent connections - -### Reliability -- **ACID Compliance:** Full transaction support -- **Point-in-Time Recovery:** Restore to any moment -- **Replication:** Master-slave, synchronous, asynchronous -- **Backups:** `pg_dump`, `pg_basebackup`, continuous archiving - -### Security -- **Authentication:** Multiple methods (password, MD5, SCRAM, LDAP, Kerberos) -- **Authorization:** Role-based access control (RBAC) -- **Row-Level Security:** Policies for fine-grained access -- **Encryption:** SSL, at-rest encryption - -### Features -- **JSON Support:** Native JSON/JSONB data type -- **Full-Text Search:** Advanced text search capabilities -- **Arrays:** Store arrays of values -- **Custom Types:** Create your own data types -- **Triggers:** Automatic actions on events -- **Stored Procedures:** Server-side functions - -## Monitoring PostgreSQL - -### Basic Queries - -```sql --- Active connections -SELECT * FROM pg_stat_activity WHERE datname = 'tablet_management'; - --- Table sizes -SELECT table_name, pg_size_pretty(pg_total_relation_size(table_name)) -FROM information_schema.tables WHERE table_schema = 'public'; - --- Index usage -SELECT indexrelname, idx_scan, idx_tup_read, idx_tup_fetch -FROM pg_stat_user_indexes; - --- Slow queries (requires pg_stat_statements extension) -SELECT query, total_time, calls, mean_time -FROM pg_stat_statements ORDER BY mean_time DESC LIMIT 10; -``` - -### Enable pg_stat_statements - -```sql --- In PostgreSQL -CREATE EXTENSION pg_stat_statements; - --- Then in postgresql.conf -shared_preload_libraries = 'pg_stat_statements' -pg_stat_statements.track = all -``` - -## Maintenance Tasks - -### Regular Maintenance - -```bash -# Vacuum (reclaim space, update statistics) -vacuumdb -U tablet_user -d tablet_management --analyze - -# Reindex (rebuild indexes) -reindexdb -U tablet_user -d tablet_management -``` - -### Backup Strategy - -```bash -# Daily backup -pg_dump -U tablet_user -d tablet_management > /backups/tablet_management_$(date +%Y%m%d).sql - -# Compressed backup -pg_dump -U tablet_user -d tablet_management | gzip > /backups/tablet_management_$(date +%Y%m%d).sql.gz - -# Continuous archiving (WAL) -# In postgresql.conf: -wal_level = replica -archive_mode = on -archive_command = 'test ! -f /backups/wal/%f && cp %p /backups/wal/%f' -``` - -## Performance Optimization - -### Configuration Tuning - -```conf -# postgresql.conf recommendations -shared_buffers = 4GB # 25% of total RAM -work_mem = 16MB # For complex sorts -maintenance_work_mem = 512MB # For VACUUM, index creation -effective_cache_size = 12GB # 75% of total RAM -random_page_cost = 1.1 # SSD: 1.1, HDD: 4.0 -max_worker_processes = 8 # Number of CPU cores -max_parallel_workers_per_gather = 4 # Parallel query workers -max_connections = 200 # Expected max connections -``` - -### Index Optimization - -```sql --- Add indexes for common queries -CREATE INDEX idx_loans_user_status ON loans(user_id, status); -CREATE INDEX idx_loans_date_range ON loans(loan_date, return_date); - --- Partial index for active loans -CREATE INDEX idx_loans_active ON loans(tablet_id) WHERE status = 'active'; - --- Composite index for user loans -CREATE INDEX idx_loans_user_tablet ON loans(user_id, tablet_id); -``` - -## Troubleshooting - -### Common Issues - -**Connection refused:** -```bash -# Check if PostgreSQL is running -sudo systemctl status postgresql - -# Check port -netstat -tuln | grep 5432 -``` - -**Authentication failed:** -```bash -# Verify user and password -psql -U tablet_user -d tablet_management -h localhost - -# Check pg_hba.conf -sudo nano /etc/postgresql/16/main/pg_hba.conf -``` - -**Database does not exist:** -```bash -# Create database -createdb -U postgres tablet_management -``` - -**Permission denied:** -```sql --- Grant permissions -GRANT ALL PRIVILEGES ON DATABASE tablet_management TO tablet_user; -GRANT ALL ON SCHEMA public TO tablet_user; -``` - -### Logs - -```bash -# PostgreSQL logs -sudo tail -f /var/log/postgresql/postgresql-16-main.log - -# Application logs -journalctl -u tablet_management -f -``` - -## Conclusion - -Migrating from SQLite to PostgreSQL provides: -- Better performance at scale -- True concurrency -- Enhanced reliability -- Advanced features -- Production-ready infrastructure - -The repository pattern ensures a smooth transition with minimal code changes, and the migration script automates the data transfer process. diff --git a/gestiontablets.sh b/gestiontablets.sh deleted file mode 100644 index 1d4b29d..0000000 --- a/gestiontablets.sh +++ /dev/null @@ -1 +0,0 @@ -vibe --resume fa1bb230 diff --git a/minimal_app.py b/minimal_app.py index 381a26d..ff0295a 100644 --- a/minimal_app.py +++ b/minimal_app.py @@ -44,22 +44,6 @@ def init_db(): ) ''') - # Create non_loanable_devices table - cursor.execute(''' - CREATE TABLE IF NOT EXISTS non_loanable_devices ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - brand TEXT NOT NULL, - model TEXT NOT NULL, - serial_number TEXT UNIQUE NOT NULL, - device_type TEXT NOT NULL, - location TEXT, - status TEXT DEFAULT 'available', - notes TEXT, - purchase_date TEXT, - purchase_cost REAL - ) - ''') - conn.commit() conn.close() @@ -218,59 +202,6 @@ def show_loan_history(): conn.close() - -def add_non_loanable_device(brand, model, serial_number, device_type, location='', notes='', purchase_date='', purchase_cost=None): - """Add a new non-loanable device to inventory""" - conn = sqlite3.connect('tablets.db') - cursor = conn.cursor() - - try: - cursor.execute(''' - INSERT INTO non_loanable_devices - (brand, model, serial_number, device_type, location, status, notes, purchase_date, purchase_cost) - VALUES (?, ?, ?, ?, ?, 'available', ?, ?, ?) - ''', (brand, model, serial_number, device_type, location, notes, purchase_date, purchase_cost)) - conn.commit() - print(f"✓ Added non-loanable device: {brand} {model} ({serial_number}) - Type: {device_type}") - except sqlite3.IntegrityError: - print(f"✗ Error: Serial number {serial_number} already exists") - finally: - conn.close() - - -def show_non_loanable_devices(): - """Show all non-loanable devices""" - conn = sqlite3.connect('tablets.db') - cursor = conn.cursor() - - cursor.execute("SELECT id, brand, model, serial_number, device_type, location, status FROM non_loanable_devices") - devices = cursor.fetchall() - - print("\n=== Non-Loanable Devices ===") - if devices: - for device in devices: - print(f"ID: {device[0]}, Type: {device[4]}, {device[1]} {device[2]} ({device[3]}), Location: {device[5] or 'N/A'}, Status: {device[6]}") - else: - print("No non-loanable devices") - - conn.close() - - -def delete_non_loanable_device(device_id): - """Delete a non-loanable device""" - conn = sqlite3.connect('tablets.db') - cursor = conn.cursor() - - cursor.execute("DELETE FROM non_loanable_devices WHERE id = ?", (device_id,)) - conn.commit() - - if cursor.rowcount > 0: - print(f"✓ Non-loanable device {device_id} deleted") - else: - print(f"✗ Error: Device {device_id} not found") - - conn.close() - def main(): """Main menu""" init_db() @@ -287,12 +218,9 @@ def main(): print("5. Show Available Tablets") print("6. Show Active Loans") print("7. Show Loan History") - print("8. Add Non-Loanable Device") - print("9. Show Non-Loanable Devices") - print("10. Delete Non-Loanable Device") - print("11. Exit") + print("8. Exit") - choice = input("Enter your choice (1-11): ") + choice = input("Enter your choice (1-8): ") if choice == '1': print("\n=== Add Tablet ===") @@ -341,28 +269,6 @@ def main(): show_loan_history() elif choice == '8': - print("\n=== Add Non-Loanable Device ===") - brand = input("Brand: ") - model = input("Model: ") - serial_number = input("Serial Number: ") - device_type = input("Device Type (e.g., projector, monitor): ") - location = input("Location (optional): ") - notes = input("Notes (optional): ") - add_non_loanable_device(brand, model, serial_number, device_type, location, notes) - - elif choice == '9': - show_non_loanable_devices() - - elif choice == '10': - print("\n=== Delete Non-Loanable Device ===") - show_non_loanable_devices() - device_id = input("Enter Device ID to delete: ") - try: - delete_non_loanable_device(int(device_id)) - except ValueError: - print("✗ Error: Invalid ID format") - - elif choice == '11': print("Goodbye!") break diff --git a/pyproject.toml b/pyproject.toml index 08df5da..e409926 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,33 +5,3 @@ description = "Add your description here" readme = "README.md" requires-python = ">=3.14" dependencies = [] - -[tool.pytest.ini_options] -testpaths = ["tests"] -python_files = "test_*.py" -python_classes = "Test*" -python_functions = "test_*" -verbose = 1 -addopts = "-v" - -[tool.coverage.run] -source = ["."] -omit = [ - "*/tests/*", - "*/.venv/*", - "*/__pycache__/*", - "*/.git/*", - "*/templates/*", -] - -[tool.coverage.report] -exclude_lines = [ - "pragma: no cover", - "def __repr__", - "raise NotImplementedError", - "if __name__ == .__main__.:", - "if TYPE_CHECKING:", -] - -[tool.coverage.html] -directory = "htmlcov" diff --git a/simple_app.py b/simple_app.py index 0674035..f50478d 100644 --- a/simple_app.py +++ b/simple_app.py @@ -62,22 +62,6 @@ def init_db(): ) ''') - # Create non_loanable_devices table - cursor.execute(''' - CREATE TABLE IF NOT EXISTS non_loanable_devices ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - brand TEXT NOT NULL, - model TEXT NOT NULL, - serial_number TEXT UNIQUE NOT NULL, - device_type TEXT NOT NULL, - location TEXT, - status TEXT DEFAULT 'available', - notes TEXT, - purchase_date TEXT, - purchase_cost REAL - ) - ''') - conn.commit() class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): diff --git a/templates/add_non_loanable_device.html b/templates/add_non_loanable_device.html deleted file mode 100644 index ae0489e..0000000 --- a/templates/add_non_loanable_device.html +++ /dev/null @@ -1,63 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -

Add Non-Loanable Device

-

Add a device that will be tracked in inventory but cannot be loaned to users (e.g., projectors, monitors, etc.)

- -
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - Cancel -
-
-{% endblock %} diff --git a/templates/base.html b/templates/base.html index e93f5f7..629882b 100644 --- a/templates/base.html +++ b/templates/base.html @@ -144,7 +144,6 @@ Loan Tablet Loan History User Loans - Non-Loanable Devices Project Management diff --git a/templates/edit_non_loanable_device.html b/templates/edit_non_loanable_device.html deleted file mode 100644 index 80c90b4..0000000 --- a/templates/edit_non_loanable_device.html +++ /dev/null @@ -1,71 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -

Edit Non-Loanable Device

- -
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - Cancel -
-
-{% endblock %} diff --git a/templates/non_loanable_devices.html b/templates/non_loanable_devices.html deleted file mode 100644 index a27e6ac..0000000 --- a/templates/non_loanable_devices.html +++ /dev/null @@ -1,43 +0,0 @@ -{% extends "base.html" %} - -{% block content %} -
-

Non-Loanable Devices Inventory

-

These devices are tracked in inventory but cannot be loaned to users.

- Add Non-Loanable Device - - {% if devices %} - - - - - - - - - - - - - - {% for device in devices %} - - - - - - - - - - {% endfor %} - -
TypeBrandModelSerial NumberLocationStatusActions
{{ device.device_type }}{{ device.brand }}{{ device.model }}{{ device.serial_number }}{{ device.location or '-' }}{{ device.status }} - Edit - Delete -
- {% else %} -

No non-loanable devices registered.

- {% endif %} -
-{% endblock %} diff --git a/templates/user_loans.html b/templates/user_loans.html index 07fac86..8c10045 100644 --- a/templates/user_loans.html +++ b/templates/user_loans.html @@ -7,98 +7,45 @@ This page demonstrates the many-to-many relationship between users and tablets via the loans table.

- -
- - -
- -
+
{% for user_data in users_with_loans %} {% set user = user_data.user %} {% set loans = user_data.loans %} - {% set active_loans = loans|selectattr('status', 'equalto', 'active')|list %} - {% set returned_loans = loans|selectattr('status', 'equalto', 'returned')|list %} -
+

{{ user.name }} ({{ user.identification }})

- - {{ active_loans|length }} active, {{ returned_loans|length }} past - + {{ loans|length }} tablet{{ 's' if loans|length != 1 else '' }} loaned
- {% if active_loans %} -
-

Current Loans

- - - - - - - + {% if loans %} +
TabletSerial NumberLoan DateStatus
+ + + + + + + + + + + {% for loan in loans %} + + + + + + - - - {% for loan in active_loans %} - - - - - - - {% endfor %} - -
TabletSerial NumberLoan DateReturn DateStatus
{{ loan.brand }} {{ loan.model }}{{ loan.serial_number }}{{ loan.loan_date }}{{ loan.return_date or '-' }} + + {{ loan.status }} + +
{{ loan.brand }} {{ loan.model }}{{ loan.serial_number }}{{ loan.loan_date }} - - {{ loan.status }} - -
-
- {% endif %} - - {% if returned_loans %} -
-

- -

- -
- {% endif %} - - {% if not active_loans and not returned_loans %} + {% endfor %} + + + {% else %}

No loans recorded for this user.

{% endif %}
@@ -109,32 +56,6 @@ {% endif %}
- -