Skip to content

Commit 2730089

Browse files
committed
Fix architecture: Store metadata in our PostgreSQL DB
PROBLÈME ROOT: - CompreFace ne supporte PAS GET /api/v1/recognition/subjects/{name} - Erreur API: "Request method 'GET' not supported" (404) - Impossible de récupérer metadata (department, rank) depuis CompreFace - Logs: "Failed to get details for subject 'BADR MELLAL': 404" ARCHITECTURE ORIGINALE (CASSÉE): 1. Add: Save subject + metadata to CompreFace 2. List: GET metadata from CompreFace ❌ Endpoint n'existe pas! 3. Résultat: 0 personnel records même si sujets existent NOUVELLE ARCHITECTURE (FIX): 1. CompreFace: Stocke faces pour reconnaissance UNIQUEMENT 2. PostgreSQL: Stocke metadata militaire (department, sub_department, rank) 3. Séparation des responsabilités CHANGEMENTS: ✅ Table: personnel_metadata (subject_name PK, department, sub_department, rank) ✅ Migration SQL: Auto-exécutée au démarrage ✅ add_personnel(): Save to CompreFace + OUR DB ✅ get_personnel_list(): Fetch subjects from CompreFace, metadata from OUR DB ✅ delete_personnel(): Delete from CompreFace + OUR DB RÉSULTAT: - ✅ Metadata department/rank visible dans liste personnel - ✅ Plus d'erreur 404 CompreFace - ✅ Fonctionne pour tous les noms (espaces, accents) - ✅ Peut ajouter sujets via port 8000 (sans metadata) FICHIERS: - dashboard-service/migrations/001_create_personnel_metadata.sql - dashboard-service/Dockerfile (copy migrations/) - dashboard-service/src/app.py (3 endpoints modifiés) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 4ea90b2 commit 2730089

3 files changed

Lines changed: 133 additions & 34 deletions

File tree

dashboard-service/Dockerfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ RUN pip install --no-cache-dir -r requirements.txt
2020
# Copy application source code
2121
COPY src/ /app/
2222

23+
# Copy database migrations
24+
COPY migrations/ /app/migrations/
25+
2326
# Create static directories if they don't exist
2427
RUN mkdir -p /app/static/css /app/static/js /app/static/img
2528

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
-- Migration: Create personnel_metadata table
2+
-- Purpose: Store military personnel metadata (department, sub_department, rank)
3+
-- Why: CompreFace doesn't provide API to retrieve metadata, so we store it separately
4+
5+
CREATE TABLE IF NOT EXISTS personnel_metadata (
6+
subject_name VARCHAR(255) PRIMARY KEY,
7+
department VARCHAR(100) NOT NULL,
8+
sub_department VARCHAR(100),
9+
rank VARCHAR(100),
10+
created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
11+
updated_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
12+
);
13+
14+
-- Index for faster lookups
15+
CREATE INDEX IF NOT EXISTS idx_personnel_department ON personnel_metadata(department);
16+
CREATE INDEX IF NOT EXISTS idx_personnel_sub_department ON personnel_metadata(sub_department);
17+
18+
-- Comments for documentation
19+
COMMENT ON TABLE personnel_metadata IS '1BIP Personnel metadata - stores department, rank, etc. Synced with CompreFace subjects';
20+
COMMENT ON COLUMN personnel_metadata.subject_name IS 'Subject name from CompreFace (PRIMARY KEY)';
21+
COMMENT ON COLUMN personnel_metadata.department IS 'Bataillon / Unité (e.g., 10BPAG, 1BCAS)';
22+
COMMENT ON COLUMN personnel_metadata.sub_department IS 'Compagnie / Section (manually entered)';
23+
COMMENT ON COLUMN personnel_metadata.rank IS 'Grade militaire (e.g., Lieutenant, Sergent)';

dashboard-service/src/app.py

Lines changed: 107 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -818,9 +818,9 @@ def get_unauthorized_paginated():
818818

819819
@app.route('/api/personnel', methods=['GET'])
820820
def get_personnel_list():
821-
"""Get list of all personnel from CompreFace"""
821+
"""Get list of all personnel from CompreFace + our metadata DB"""
822822
try:
823-
# Call CompreFace API to get all subjects
823+
# Step 1: Get all subjects from CompreFace
824824
url = f"{COMPREFACE_API_URL}/api/v1/recognition/subjects"
825825
headers = {'x-api-key': COMPREFACE_API_KEY}
826826

@@ -830,37 +830,42 @@ def get_personnel_list():
830830
subjects = response.json().get('subjects', [])
831831
logger.info(f"Fetched {len(subjects)} subjects from CompreFace: {subjects}")
832832

833-
# For each subject, get metadata
833+
# Step 2: Get metadata from OUR database
834834
personnel_list = []
835-
for subject in subjects:
836-
# Get subject details (URL encode the subject name for spaces, etc.)
837-
encoded_subject = quote(subject, safe='')
838-
detail_url = f"{COMPREFACE_API_URL}/api/v1/recognition/subjects/{encoded_subject}"
839-
detail_response = requests.get(detail_url, headers=headers)
840-
841-
if detail_response.status_code == 200:
842-
detail_data = detail_response.json()
843-
logger.debug(f"Subject '{subject}' details: {detail_data}")
844-
845-
# Parse metadata (department, sub_department, rank stored as JSON)
846-
metadata = {}
847-
if 'metadata' in detail_data:
848-
try:
849-
metadata = json.loads(detail_data['metadata']) if isinstance(detail_data['metadata'], str) else detail_data['metadata']
850-
except Exception as parse_error:
851-
logger.error(f"Failed to parse metadata for '{subject}': {parse_error}")
852-
metadata = {}
853-
854-
personnel_list.append({
855-
'subject': subject,
856-
'name': subject, # CompreFace uses subject name as identifier
857-
'department': metadata.get('department', ''),
858-
'sub_department': metadata.get('sub_department', ''),
859-
'rank': metadata.get('rank', ''),
860-
'created_date': metadata.get('created_date', '')
861-
})
862-
else:
863-
logger.warning(f"Failed to get details for subject '{subject}': {detail_response.status_code}")
835+
836+
with DatabaseConnection() as conn:
837+
with conn.cursor(cursor_factory=RealDictCursor) as cursor:
838+
for subject in subjects:
839+
# Fetch metadata from our personnel_metadata table
840+
cursor.execute("""
841+
SELECT subject_name, department, sub_department, rank, created_date
842+
FROM personnel_metadata
843+
WHERE subject_name = %s
844+
""", (subject,))
845+
846+
metadata_row = cursor.fetchone()
847+
848+
if metadata_row:
849+
# Personnel with metadata
850+
personnel_list.append({
851+
'subject': subject,
852+
'name': subject,
853+
'department': metadata_row['department'] or '',
854+
'sub_department': metadata_row['sub_department'] or '',
855+
'rank': metadata_row['rank'] or '',
856+
'created_date': metadata_row['created_date'].isoformat() if metadata_row['created_date'] else ''
857+
})
858+
else:
859+
# Personnel without metadata (added via port 8000 directly)
860+
logger.warning(f"Subject '{subject}' has no metadata in DB (added externally?)")
861+
personnel_list.append({
862+
'subject': subject,
863+
'name': subject,
864+
'department': '',
865+
'sub_department': '',
866+
'rank': '',
867+
'created_date': ''
868+
})
864869

865870
logger.info(f"Returning {len(personnel_list)} personnel records")
866871
return jsonify({'personnel': personnel_list})
@@ -990,6 +995,26 @@ def add_personnel():
990995
'details': errors
991996
}), 500
992997

998+
# Step 3: Save metadata to OUR database
999+
try:
1000+
with DatabaseConnection() as conn:
1001+
with conn.cursor() as cursor:
1002+
cursor.execute("""
1003+
INSERT INTO personnel_metadata (subject_name, department, sub_department, rank, created_date)
1004+
VALUES (%s, %s, %s, %s, CURRENT_TIMESTAMP)
1005+
ON CONFLICT (subject_name)
1006+
DO UPDATE SET
1007+
department = EXCLUDED.department,
1008+
sub_department = EXCLUDED.sub_department,
1009+
rank = EXCLUDED.rank,
1010+
updated_date = CURRENT_TIMESTAMP
1011+
""", (name, department, sub_department, rank))
1012+
conn.commit()
1013+
logger.info(f"Saved metadata for {name} to database")
1014+
except Exception as db_error:
1015+
logger.error(f"Failed to save metadata to database: {db_error}")
1016+
# Don't fail the entire operation, metadata can be added later
1017+
9931018
return jsonify({
9941019
'success': True,
9951020
'message': f'Personnel "{name}" added successfully',
@@ -1005,15 +1030,31 @@ def add_personnel():
10051030

10061031
@app.route('/api/personnel/<subject>', methods=['DELETE'])
10071032
def delete_personnel(subject):
1008-
"""Delete personnel from CompreFace"""
1033+
"""Delete personnel from CompreFace and our metadata DB"""
10091034
try:
1035+
# Step 1: Delete from CompreFace
10101036
headers = {'x-api-key': COMPREFACE_API_KEY}
10111037
delete_url = f"{COMPREFACE_API_URL}/api/v1/recognition/subjects/{subject}"
10121038

10131039
response = requests.delete(delete_url, headers=headers)
10141040

10151041
if response.status_code in [200, 204]:
1016-
logger.info(f"Deleted personnel: {subject}")
1042+
logger.info(f"Deleted personnel from CompreFace: {subject}")
1043+
1044+
# Step 2: Delete metadata from our database
1045+
try:
1046+
with DatabaseConnection() as conn:
1047+
with conn.cursor() as cursor:
1048+
cursor.execute("""
1049+
DELETE FROM personnel_metadata
1050+
WHERE subject_name = %s
1051+
""", (subject,))
1052+
conn.commit()
1053+
logger.info(f"Deleted metadata from database: {subject}")
1054+
except Exception as db_error:
1055+
logger.error(f"Failed to delete metadata from database: {db_error}")
1056+
# Continue anyway, CompreFace deletion succeeded
1057+
10171058
return jsonify({
10181059
'success': True,
10191060
'message': f'Personnel "{subject}" deleted successfully'
@@ -1070,6 +1111,33 @@ def internal_error(error):
10701111
return jsonify({'error': 'Internal server error'}), 500
10711112

10721113

1114+
# ============================================
1115+
# DATABASE MIGRATIONS
1116+
# ============================================
1117+
1118+
def run_migrations():
1119+
"""Run database migrations on startup"""
1120+
try:
1121+
migration_file = '/app/migrations/001_create_personnel_metadata.sql'
1122+
1123+
# Check if file exists
1124+
if not os.path.exists(migration_file):
1125+
logger.warning(f"Migration file not found: {migration_file}")
1126+
return
1127+
1128+
with DatabaseConnection() as conn:
1129+
with conn.cursor() as cursor:
1130+
# Read and execute migration
1131+
with open(migration_file, 'r') as f:
1132+
migration_sql = f.read()
1133+
cursor.execute(migration_sql)
1134+
conn.commit()
1135+
logger.info("Database migrations completed successfully")
1136+
except Exception as e:
1137+
logger.error(f"Migration failed: {e}")
1138+
# Don't crash the app, just log the error
1139+
1140+
10731141
# ============================================
10741142
# APPLICATION STARTUP
10751143
# ============================================
@@ -1080,6 +1148,11 @@ def internal_error(error):
10801148

10811149
logger.info(f"Starting 1BIP Dashboard Service on port {port}")
10821150
logger.info(f"Database: {DB_CONFIG['host']}:{DB_CONFIG['port']}/{DB_CONFIG['database']}")
1151+
1152+
# Run migrations
1153+
logger.info("Running database migrations...")
1154+
run_migrations()
1155+
10831156
logger.info("Dashboard will be available at http://localhost:5000")
10841157

10851158
app.run(

0 commit comments

Comments
 (0)