@@ -818,9 +818,9 @@ def get_unauthorized_paginated():
818818
819819@app .route ('/api/personnel' , methods = ['GET' ])
820820def 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' ])
10071032def 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