diff --git a/.perm_test_apache b/.perm_test_apache new file mode 100644 index 0000000..e69de29 diff --git a/.perm_test_exec b/.perm_test_exec new file mode 100644 index 0000000..e69de29 diff --git a/ai/__init__.py b/ai/__init__.py new file mode 100644 index 0000000..37a7b09 --- /dev/null +++ b/ai/__init__.py @@ -0,0 +1,3 @@ +"""Helpers for interacting with the Flatlogic AI proxy from Django code.""" + +from .local_ai_api import LocalAIApi, create_response, request, decode_json_from_response # noqa: F401 diff --git a/ai/local_ai_api.py b/ai/local_ai_api.py new file mode 100644 index 0000000..bcff732 --- /dev/null +++ b/ai/local_ai_api.py @@ -0,0 +1,420 @@ +""" +LocalAIApi — lightweight Python client for the Flatlogic AI proxy. + +Usage (inside the Django workspace): + + from ai.local_ai_api import LocalAIApi + + response = LocalAIApi.create_response({ + "input": [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Summarise this text in two sentences."}, + ], + "text": {"format": {"type": "json_object"}}, + }) + + if response.get("success"): + data = LocalAIApi.decode_json_from_response(response) + # ... + +# Typical successful payload (truncated): +# { +# "id": "resp_xxx", +# "status": "completed", +# "output": [ +# {"type": "reasoning", "summary": []}, +# {"type": "message", "content": [{"type": "output_text", "text": "Your final answer here."}]} +# ], +# "usage": { "input_tokens": 123, "output_tokens": 456 } +# } + +The helper automatically injects the project UUID header and falls back to +reading executor/.env if environment variables are missing. +""" + +from __future__ import annotations + +import json +import os +import time +import ssl +from typing import Any, Dict, Iterable, Optional +from urllib import error as urlerror +from urllib import request as urlrequest + +__all__ = [ + "LocalAIApi", + "create_response", + "request", + "fetch_status", + "await_response", + "extract_text", + "decode_json_from_response", +] + + +_CONFIG_CACHE: Optional[Dict[str, Any]] = None + + +class LocalAIApi: + """Static helpers mirroring the PHP implementation.""" + + @staticmethod + def create_response(params: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + return create_response(params, options or {}) + + @staticmethod + def request(path: Optional[str] = None, payload: Optional[Dict[str, Any]] = None, + options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + return request(path, payload or {}, options or {}) + + @staticmethod + def extract_text(response: Dict[str, Any]) -> str: + return extract_text(response) + + @staticmethod + def decode_json_from_response(response: Dict[str, Any]) -> Optional[Dict[str, Any]]: + return decode_json_from_response(response) + + +def create_response(params: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Signature compatible with the OpenAI Responses API.""" + options = options or {} + payload = dict(params) + + if not isinstance(payload.get("input"), list) or not payload["input"]: + return { + "success": False, + "error": "input_missing", + "message": 'Parameter "input" is required and must be a non-empty list.', + } + + cfg = _config() + if not payload.get("model"): + payload["model"] = cfg["default_model"] + + initial = request(options.get("path"), payload, options) + if not initial.get("success"): + return initial + + data = initial.get("data") + if isinstance(data, dict) and "ai_request_id" in data: + ai_request_id = data["ai_request_id"] + poll_timeout = int(options.get("poll_timeout", 300)) + poll_interval = int(options.get("poll_interval", 5)) + return await_response(ai_request_id, { + "interval": poll_interval, + "timeout": poll_timeout, + "headers": options.get("headers"), + "timeout_per_call": options.get("timeout"), + }) + + return initial + + +def request(path: Optional[str], payload: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Perform a raw request to the AI proxy.""" + cfg = _config() + options = options or {} + + resolved_path = path or options.get("path") or cfg["responses_path"] + if not resolved_path: + return { + "success": False, + "error": "project_id_missing", + "message": "PROJECT_ID is not defined; cannot resolve AI proxy endpoint.", + } + + project_uuid = cfg["project_uuid"] + if not project_uuid: + return { + "success": False, + "error": "project_uuid_missing", + "message": "PROJECT_UUID is not defined; aborting AI request.", + } + + if "project_uuid" not in payload and project_uuid: + payload["project_uuid"] = project_uuid + + url = _build_url(resolved_path, cfg["base_url"]) + opt_timeout = options.get("timeout") + timeout = int(cfg["timeout"] if opt_timeout is None else opt_timeout) + verify_tls = options.get("verify_tls", cfg["verify_tls"]) + + headers: Dict[str, str] = { + "Content-Type": "application/json", + "Accept": "application/json", + cfg["project_header"]: project_uuid, + } + extra_headers = options.get("headers") + if isinstance(extra_headers, Iterable): + for header in extra_headers: + if isinstance(header, str) and ":" in header: + name, value = header.split(":", 1) + headers[name.strip()] = value.strip() + + body = json.dumps(payload, ensure_ascii=False).encode("utf-8") + return _http_request(url, "POST", body, headers, timeout, verify_tls) + + +def fetch_status(ai_request_id: Any, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Fetch status for a queued AI request.""" + cfg = _config() + options = options or {} + + project_uuid = cfg["project_uuid"] + if not project_uuid: + return { + "success": False, + "error": "project_uuid_missing", + "message": "PROJECT_UUID is not defined; aborting status check.", + } + + status_path = _resolve_status_path(ai_request_id, cfg) + url = _build_url(status_path, cfg["base_url"]) + + opt_timeout = options.get("timeout") + timeout = int(cfg["timeout"] if opt_timeout is None else opt_timeout) + verify_tls = options.get("verify_tls", cfg["verify_tls"]) + + headers: Dict[str, str] = { + "Accept": "application/json", + cfg["project_header"]: project_uuid, + } + extra_headers = options.get("headers") + if isinstance(extra_headers, Iterable): + for header in extra_headers: + if isinstance(header, str) and ":" in header: + name, value = header.split(":", 1) + headers[name.strip()] = value.strip() + + return _http_request(url, "GET", None, headers, timeout, verify_tls) + + +def await_response(ai_request_id: Any, options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """Poll status endpoint until the request is complete or timed out.""" + options = options or {} + timeout = int(options.get("timeout", 300)) + interval = int(options.get("interval", 5)) + if interval <= 0: + interval = 5 + per_call_timeout = options.get("timeout_per_call") + + deadline = time.time() + max(timeout, interval) + + while True: + status_resp = fetch_status(ai_request_id, { + "headers": options.get("headers"), + "timeout": per_call_timeout, + "verify_tls": options.get("verify_tls"), + }) + if status_resp.get("success"): + data = status_resp.get("data") or {} + if isinstance(data, dict): + status_value = data.get("status") + if status_value == "success": + return { + "success": True, + "status": 200, + "data": data.get("response", data), + } + if status_value == "failed": + return { + "success": False, + "status": 500, + "error": str(data.get("error") or "AI request failed"), + "data": data, + } + else: + return status_resp + + if time.time() >= deadline: + return { + "success": False, + "error": "timeout", + "message": "Timed out waiting for AI response.", + } + time.sleep(interval) + + +def extract_text(response: Dict[str, Any]) -> str: + """Public helper to extract plain text from a Responses payload.""" + return _extract_text(response) + + +def decode_json_from_response(response: Dict[str, Any]) -> Optional[Dict[str, Any]]: + """Attempt to decode JSON emitted by the model (handles markdown fences).""" + text = _extract_text(response) + if text == "": + return None + + try: + decoded = json.loads(text) + if isinstance(decoded, dict): + return decoded + except json.JSONDecodeError: + pass + + stripped = text.strip() + if stripped.startswith("```json"): + stripped = stripped[7:] + if stripped.endswith("```"): + stripped = stripped[:-3] + stripped = stripped.strip() + if stripped and stripped != text: + try: + decoded = json.loads(stripped) + if isinstance(decoded, dict): + return decoded + except json.JSONDecodeError: + return None + return None + + +def _extract_text(response: Dict[str, Any]) -> str: + payload = response.get("data") if response.get("success") else response.get("response") + if isinstance(payload, dict): + output = payload.get("output") + if isinstance(output, list): + combined = "" + for item in output: + content = item.get("content") if isinstance(item, dict) else None + if isinstance(content, list): + for block in content: + if isinstance(block, dict) and block.get("type") == "output_text" and block.get("text"): + combined += str(block["text"]) + if combined: + return combined + choices = payload.get("choices") + if isinstance(choices, list) and choices: + message = choices[0].get("message") + if isinstance(message, dict) and message.get("content"): + return str(message["content"]) + if isinstance(payload, str): + return payload + return "" + + +def _config() -> Dict[str, Any]: + global _CONFIG_CACHE # noqa: PLW0603 + if _CONFIG_CACHE is not None: + return _CONFIG_CACHE + + _ensure_env_loaded() + + base_url = os.getenv("AI_PROXY_BASE_URL", "https://flatlogic.com") + project_id = os.getenv("PROJECT_ID") or None + responses_path = os.getenv("AI_RESPONSES_PATH") + if not responses_path and project_id: + responses_path = f"/projects/{project_id}/ai-request" + + _CONFIG_CACHE = { + "base_url": base_url, + "responses_path": responses_path, + "project_id": project_id, + "project_uuid": os.getenv("PROJECT_UUID"), + "project_header": os.getenv("AI_PROJECT_HEADER", "project-uuid"), + "default_model": os.getenv("AI_DEFAULT_MODEL", "gpt-5-mini"), + "timeout": int(os.getenv("AI_TIMEOUT", "30")), + "verify_tls": os.getenv("AI_VERIFY_TLS", "true").lower() not in {"0", "false", "no"}, + } + return _CONFIG_CACHE + + +def _build_url(path: str, base_url: str) -> str: + trimmed = path.strip() + if trimmed.startswith("http://") or trimmed.startswith("https://"): + return trimmed + if trimmed.startswith("/"): + return f"{base_url}{trimmed}" + return f"{base_url}/{trimmed}" + + +def _resolve_status_path(ai_request_id: Any, cfg: Dict[str, Any]) -> str: + base_path = (cfg.get("responses_path") or "").rstrip("/") + if not base_path: + return f"/ai-request/{ai_request_id}/status" + if not base_path.endswith("/ai-request"): + base_path = f"{base_path}/ai-request" + return f"{base_path}/{ai_request_id}/status" + + +def _http_request(url: str, method: str, body: Optional[bytes], headers: Dict[str, str], + timeout: int, verify_tls: bool) -> Dict[str, Any]: + """ + Shared HTTP helper for GET/POST requests. + """ + req = urlrequest.Request(url, data=body, method=method.upper()) + for name, value in headers.items(): + req.add_header(name, value) + + context = None + if not verify_tls: + context = ssl.create_default_context() + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + + try: + with urlrequest.urlopen(req, timeout=timeout, context=context) as resp: + status = resp.getcode() + response_body = resp.read().decode("utf-8", errors="replace") + except urlerror.HTTPError as exc: + status = exc.getcode() + response_body = exc.read().decode("utf-8", errors="replace") + except Exception as exc: # pylint: disable=broad-except + return { + "success": False, + "error": "request_failed", + "message": str(exc), + } + + decoded = None + if response_body: + try: + decoded = json.loads(response_body) + except json.JSONDecodeError: + decoded = None + + if 200 <= status < 300: + return { + "success": True, + "status": status, + "data": decoded if decoded is not None else response_body, + } + + error_message = "AI proxy request failed" + if isinstance(decoded, dict): + error_message = decoded.get("error") or decoded.get("message") or error_message + elif response_body: + error_message = response_body + + return { + "success": False, + "status": status, + "error": error_message, + "response": decoded if decoded is not None else response_body, + } + + +def _ensure_env_loaded() -> None: + """Populate os.environ from executor/.env if variables are missing.""" + if os.getenv("PROJECT_UUID") and os.getenv("PROJECT_ID"): + return + + env_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".env")) + if not os.path.exists(env_path): + return + + try: + with open(env_path, "r", encoding="utf-8") as handle: + for line in handle: + stripped = line.strip() + if not stripped or stripped.startswith("#") or "=" not in stripped: + continue + key, value = stripped.split("=", 1) + key = key.strip() + value = value.strip().strip('\'"') + if key and not os.getenv(key): + os.environ[key] = value + except OSError: + pass diff --git a/config/__pycache__/__init__.cpython-311.pyc b/config/__pycache__/__init__.cpython-311.pyc index 3d6501c..397a328 100644 Binary files a/config/__pycache__/__init__.cpython-311.pyc and b/config/__pycache__/__init__.cpython-311.pyc differ diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index dadfaa7..0255491 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 139db10..5545dff 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/__pycache__/wsgi.cpython-311.pyc b/config/__pycache__/wsgi.cpython-311.pyc index 79ce690..33a3e60 100644 Binary files a/config/__pycache__/wsgi.cpython-311.pyc and b/config/__pycache__/wsgi.cpython-311.pyc differ diff --git a/config/urls.py b/config/urls.py index 5093d47..3f3c4b6 100644 --- a/config/urls.py +++ b/config/urls.py @@ -15,9 +15,12 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import include, path +from django.urls import path, include +from core import views as core_views urlpatterns = [ - path("admin/", admin.site.urls), - path("", include("core.urls")), + path('admin/', admin.site.urls), + path('', core_views.dashboard, name='dashboard'), + path('', include('core.urls')), ] + diff --git a/core/__pycache__/__init__.cpython-311.pyc b/core/__pycache__/__init__.cpython-311.pyc index 3b7774e..8e96518 100644 Binary files a/core/__pycache__/__init__.cpython-311.pyc and b/core/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 5e41572..e4a8869 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/apps.cpython-311.pyc b/core/__pycache__/apps.cpython-311.pyc index 6435d92..649e508 100644 Binary files a/core/__pycache__/apps.cpython-311.pyc and b/core/__pycache__/apps.cpython-311.pyc differ diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index f6e5c4e..6028176 100644 Binary files a/core/__pycache__/forms.cpython-311.pyc and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 5b41fe1..262f65d 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 4e4f113..23f4ce8 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 9d0ddd8..06762aa 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 639ff3a..4f6163a 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,8 +1,8 @@ from django.contrib import admin -from .models import Ticket +from .models import Unit -@admin.register(Ticket) -class TicketAdmin(admin.ModelAdmin): - list_display = ('subject', 'status', 'priority', 'requester_email', 'created_at') - list_filter = ('status', 'priority') - search_fields = ('subject', 'requester_email', 'description') +@admin.register(Unit) +class UnitAdmin(admin.ModelAdmin): + list_display = ('unit_id', 'unit_type', 'status', 'location_lat', 'location_lon') + list_filter = ('unit_type', 'status') + search_fields = ('unit_id',) diff --git a/core/context_processors.py b/core/context_processors.py new file mode 100644 index 0000000..5de8c8d --- /dev/null +++ b/core/context_processors.py @@ -0,0 +1,10 @@ +import os + +def project_context(request): + """ + Adds project-specific environment variables to the template context globally. + """ + return { + "project_description": os.getenv("PROJECT_DESCRIPTION", ""), + "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), + } diff --git a/core/forms.py b/core/forms.py index 7a6b83b..b65f745 100644 --- a/core/forms.py +++ b/core/forms.py @@ -1,7 +1,13 @@ from django import forms -from .models import Ticket +from .models import Unit -class TicketForm(forms.ModelForm): +class UnitForm(forms.ModelForm): class Meta: - model = Ticket - fields = ['subject', 'requester_email', 'priority', 'description'] + model = Unit + fields = ('location_lat', 'location_lon', 'status') + widgets = { + 'location_lat': forms.NumberInput(attrs={'class': 'form-control'}), + 'location_lon': forms.NumberInput(attrs={'class': 'form-control'}), + 'status': forms.Select(attrs={'class': 'form-select'}), + } + diff --git a/core/management/__init__.py b/core/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/management/commands/__init__.py b/core/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/management/commands/run_simulation.py b/core/management/commands/run_simulation.py new file mode 100644 index 0000000..aa7cbba --- /dev/null +++ b/core/management/commands/run_simulation.py @@ -0,0 +1,35 @@ + +import random +import time +from django.core.management.base import BaseCommand +from core.models import Unit + +class Command(BaseCommand): + help = 'Starts the data simulation for emergency units.' + + def handle(self, *args, **options): + self.stdout.write(self.style.SUCCESS('Starting simulation...')) + statuses = ['Available', 'Enroute', 'On Scene', 'Returning'] + + while True: + try: + units = Unit.objects.all() + for unit in units: + # Simulate location change + unit.location_lat += random.uniform(-0.01, 0.01) + unit.location_lon += random.uniform(-0.01, 0.01) + + # Simulate status change + if random.random() < 0.1: # 10% chance to change status + unit.status = random.choice(statuses) + + unit.save() + + self.stdout.write(self.style.SUCCESS(f'Updated {len(units)} units.')) + time.sleep(5) + except KeyboardInterrupt: + self.stdout.write(self.style.WARNING('Simulation stopped by user.')) + break + except Exception as e: + self.stderr.write(self.style.ERROR(f'An error occurred: {e}')) + time.sleep(10) # Wait a bit before retrying diff --git a/core/migrations/0002_unit.py b/core/migrations/0002_unit.py new file mode 100644 index 0000000..732922a --- /dev/null +++ b/core/migrations/0002_unit.py @@ -0,0 +1,30 @@ +# Generated by Django 5.0.1 on 2024-01-26 12:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Unit', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('unit_id', models.CharField(max_length=100, unique=True)), + ('unit_type', models.CharField(choices=[('Ambulance', 'Ambulance'), ('Police Car', 'Police Car')], max_length=50)), + ('location_lat', models.FloatField(default=0.0)), + ('location_lon', models.FloatField(default=0.0)), + ('status', models.CharField(choices=[('Available', 'Available'), ('Dispatched', 'Dispatched'), ('Refueling', 'Refueling'), ('Out of Service', 'Out of Service')], default='Available', max_length=50)), + ], + options={ + 'ordering': ['unit_id'], + }, + ), + migrations.DeleteModel( + name='Ticket', + ), + ] diff --git a/core/migrations/0003_seed_units.py b/core/migrations/0003_seed_units.py new file mode 100644 index 0000000..2c576f6 --- /dev/null +++ b/core/migrations/0003_seed_units.py @@ -0,0 +1,37 @@ +# Generated by Django 5.0.1 on 2024-01-26 12:01 + +from django.db import migrations +import random + +def create_initial_units(apps, schema_editor): + Unit = apps.get_model('core', 'Unit') + + # 10 Ambulances + for i in range(1, 11): + Unit.objects.create( + unit_id=f'AMB-{i:03}', + unit_type='Ambulance', + location_lat=round(random.uniform(34.0, 35.0), 6), + location_lon=round(random.uniform(-118.0, -119.0), 6), + status='Available' + ) + + # 5 Police Cars + for i in range(1, 6): + Unit.objects.create( + unit_id=f'POL-{i:03}', + unit_type='Police Car', + location_lat=round(random.uniform(34.0, 35.0), 6), + location_lon=round(random.uniform(-118.0, -119.0), 6), + status='Available' + ) + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_unit'), + ] + + operations = [ + migrations.RunPython(create_initial_units), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc index 64d8a55..81369bc 100644 Binary files a/core/migrations/__pycache__/0001_initial.cpython-311.pyc and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0002_unit.cpython-311.pyc b/core/migrations/__pycache__/0002_unit.cpython-311.pyc new file mode 100644 index 0000000..03289dd Binary files /dev/null and b/core/migrations/__pycache__/0002_unit.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0003_seed_units.cpython-311.pyc b/core/migrations/__pycache__/0003_seed_units.cpython-311.pyc new file mode 100644 index 0000000..98c1c49 Binary files /dev/null and b/core/migrations/__pycache__/0003_seed_units.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/__init__.cpython-311.pyc b/core/migrations/__pycache__/__init__.cpython-311.pyc index 58b1c14..b3eb13c 100644 Binary files a/core/migrations/__pycache__/__init__.cpython-311.pyc and b/core/migrations/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 78b60d1..30e03ba 100644 --- a/core/models.py +++ b/core/models.py @@ -1,25 +1,25 @@ from django.db import models -class Ticket(models.Model): +class Unit(models.Model): + UNIT_TYPE_CHOICES = [ + ('Ambulance', 'Ambulance'), + ('Police Car', 'Police Car'), + ] STATUS_CHOICES = [ - ('open', 'Open'), - ('in_progress', 'In Progress'), - ('closed', 'Closed'), + ('Available', 'Available'), + ('Dispatched', 'Dispatched'), + ('Refueling', 'Refueling'), + ('Out of Service', 'Out of Service'), ] - PRIORITY_CHOICES = [ - ('low', 'Low'), - ('medium', 'Medium'), - ('high', 'High'), - ] - - subject = models.CharField(max_length=255) - status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='open') - priority = models.CharField(max_length=20, choices=PRIORITY_CHOICES, default='medium') - requester_email = models.EmailField() - description = models.TextField() - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) + unit_id = models.CharField(max_length=100, unique=True) + unit_type = models.CharField(max_length=50, choices=UNIT_TYPE_CHOICES) + location_lat = models.FloatField(default=0.0) + location_lon = models.FloatField(default=0.0) + status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='Available') def __str__(self): - return self.subject \ No newline at end of file + return self.unit_id + + class Meta: + ordering = ['unit_id'] diff --git a/core/templates/base.html b/core/templates/base.html new file mode 100644 index 0000000..c1dcb06 --- /dev/null +++ b/core/templates/base.html @@ -0,0 +1,50 @@ +{% load static %} + + +
+ + +Published on {{ article.created_at|date:"F d, Y" }}
+Real-time operational overview of emergency response units.
+Map visualization will be implemented here.
+-- min
+--
+-- / 15
+--
+Use the controls below to start or stop the real-time data simulation. When running, unit locations and statuses will be updated automatically.
+ +Update the status and location for this unit.
+Real-time status and location of all emergency response units.
+| Unit ID | +Type | +Status | +Latitude | +Longitude | ++ |
|---|---|---|---|---|---|
| {{ unit.unit_id }} | +{{ unit.unit_type }} | +{{ unit.status }} | +{{ unit.location_lat }} | +{{ unit.location_lon }} | ++ Edit + | +
| No units found. | +|||||