SHELLAPANDIANGANHUNGING commited on
Commit
0f37185
·
verified ·
1 Parent(s): 3133ce7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +51 -36
app.py CHANGED
@@ -519,7 +519,7 @@ if 'temuan_kode_distrik' in df_local.columns:
519
  col1, col2 = st.columns(2)
520
 
521
  with col1:
522
- st.markdown("<h5>Unit Pembangkit: Avg Monthly Finding by Company</h5>", unsafe_allow_html=True)
523
  fig_pg = create_polar_bar_chart(avg_ratio_pg, 'PG')
524
  if fig_pg:
525
  st.plotly_chart(fig_pg, use_container_width=True)
@@ -539,7 +539,7 @@ if 'temuan_kode_distrik' in df_local.columns:
539
  st.warning("No data for PG area.")
540
 
541
  with col2:
542
- st.markdown("<h5>Unit Maintenance: Avg Monthly Finding by Company</h5>", unsafe_allow_html=True)
543
  fig_um = create_polar_bar_chart(avg_ratio_um, 'UM')
544
  if fig_um:
545
  st.plotly_chart(fig_um, use_container_width=True)
@@ -549,7 +549,7 @@ if 'temuan_kode_distrik' in df_local.columns:
549
  st.markdown("### Insight")
550
  st.markdown(
551
  f"<div class='ai-insight'>"
552
- f"Across all companies, the finding-per-person ratio is similar. In the UM Area "
553
 
554
  f"</div>",
555
  unsafe_allow_html=True
@@ -614,20 +614,35 @@ avg_ratio_per_location = avg_ratio_per_location.dropna(subset=['avg_monthly_rati
614
 
615
  # Plot Treemap dengan gradasi warna
616
  if not avg_ratio_per_location.empty:
617
- # Gunakan color_continuous_scale untuk gradasi warna: merah → kuning → hijau
618
  fig_treemap = px.treemap(
619
  avg_ratio_per_location,
620
- path=['nama_lokasi_full'], # Path untuk hierarki (hanya satu level di sini)
621
- values='avg_monthly_ratio', # Nilai yang menentukan ukuran area
622
- title='Avg Monthly Finding by Location',
623
- labels={'avg_monthly_ratio': 'Avg Monthly Finding/Person Ratio', 'nama_lokasi_full': 'Location'},
624
- color='avg_monthly_ratio', # Warna berdasarkan nilai rasio (bukan kategori)
 
 
 
625
  color_continuous_scale=[
626
- [0.0, '#D32F2F'], # Merah untuk rendah
627
- [0.5, '#FFB300'], # Kuning untuk sedang
628
- [1.0, '#4CAF50'] # Hijau untuk tinggi
629
  ]
630
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
631
  # Format hover
632
  fig_treemap.update_traces(
633
  hovertemplate="<b>%{label}</b><br>Avg Ratio: %{value:.2f}<extra></extra>"
@@ -650,7 +665,7 @@ if not avg_ratio_per_location.empty:
650
  # f"Locations with <span style='color:#D32F2F; font-weight:bold;'>red</span> color have a low ratio, indicating lower activity levels or potentially under-reporting. "
651
  f"<strong>{top_location['nama_lokasi_full']}</strong> shows the highest activity level "
652
  f"(<strong>{top_location['avg_monthly_ratio']:.2f}</strong>). "
653
- f"<strong>{low_location['nama_lokasi_full']}</strong> shows the lowest activity level "
654
  f"(<strong>{low_location['avg_monthly_ratio']:.2f}</strong>). "
655
  f"Areas with high activity (green) warrant inspection into the underlying causes of frequent findings. "
656
  f"Areas with low activity (red) should be reviewed to ensure reporting completeness and identify any hidden risks."
@@ -805,7 +820,7 @@ col_3a, col_3c = st.columns(2)
805
 
806
  # ─── 3a: Reporter by Division (Rasio Temuan/Orang) ───────────────────────────
807
  with col_3a:
808
- st.markdown("<h5 style='text-align:center;'>3a. Avg Finding/Person Ratio by Division (Reporter)</h5>", unsafe_allow_html=True)
809
  if avg_ratio_per_nama.empty:
810
  st.warning("No data for division-level reporter analysis.")
811
  else:
@@ -825,7 +840,7 @@ with col_3a:
825
  fig = px.bar(
826
  subset, x='avg_monthly_ratio', y='nama', orientation='h',
827
  title=f'{sort_opt} Divisions',
828
- labels={'avg_monthly_ratio': 'Avg Monthly Ratio', 'nama': 'Division'},
829
  color='color', color_discrete_map={c: c for c in subset['color'].unique()},
830
  text=subset['avg_monthly_ratio'].apply(lambda x: f'{x:.2f}')
831
  )
@@ -839,8 +854,9 @@ with col_3a:
839
  best, worst = full_sorted.iloc[0]['nama'], full_sorted.iloc[-1]['nama']
840
  st.markdown(
841
  f"<div class='ai-insight'>"
842
- f"<strong>Insight:</strong> Division reporting efficiency ranges from {min_r:.2f} to {max_r:.2f} (avg: {mean_r:.2f}). "
843
- f"<strong>{best}</strong> leads; <strong>{worst}</strong> lags. "
 
844
  f"<strong>Recommendation:</strong> Benchmark processes from {best}; assess capacity/tooling gaps in {worst}."
845
  f"</div>",
846
  unsafe_allow_html=True
@@ -849,7 +865,7 @@ with col_3a:
849
 
850
  # ─── 3c: Reporter by Individual ──────────────────────────────────────────────
851
  with col_3c:
852
- st.markdown("<h5 style='text-align:center;'>3c. Avg Monthly Findings per Reporter (Individual)</h5>", unsafe_allow_html=True)
853
  if avg_rate_per_creator.empty:
854
  st.warning("No data for individual reporter analysis.")
855
  else:
@@ -867,7 +883,7 @@ with col_3c:
867
  fig = px.bar(
868
  subset, x='avg_monthly_rate', y='creator_name', orientation='h',
869
  title=f'{sort_opt} Reporters',
870
- labels={'avg_monthly_rate': 'Avg Monthly Findings', 'creator_name': 'Reporter'},
871
  color='color', color_discrete_map={c: c for c in subset['color'].unique()},
872
  text=subset['avg_monthly_rate'].apply(lambda x: f'{x:.2f}')
873
  )
@@ -893,7 +909,7 @@ col_3b, col_3d = st.columns(2)
893
 
894
  # ─── 3b: Executor by Division (Lead Time) ────────────────────────────────────
895
  with col_3b:
896
- st.markdown("<h5 style='text-align:center;'>3b. Avg Lead Time by Division (Executor)</h5>", unsafe_allow_html=True)
897
  if avg_leadtime_nama.empty:
898
  st.warning("No data for division-level executor analysis.")
899
  else:
@@ -929,7 +945,7 @@ with col_3b:
929
  y='nama',
930
  orientation='h',
931
  title=sort_opt,
932
- labels={'avg_monthly_leadtime': 'Avg Lead Time (Days)', 'nama': 'Division'},
933
  color='color',
934
  color_discrete_map={c: c for c in subset['color'].unique()},
935
  text=subset['avg_monthly_leadtime'].apply(lambda x: f'{x:.1f}')
@@ -961,7 +977,7 @@ with col_3b:
961
  # ─── 3d: Executor by Individual ──────────────────────────────────────────────
962
  with col_3d:
963
  st.markdown(
964
- f"<h5 style='text-align:center;'>3d. Avg Lead Time per Executor ({EXECUTOR_INDIV_COL})</h5>",
965
  unsafe_allow_html=True
966
  )
967
  if avg_leadtime_per_indiv.empty:
@@ -996,7 +1012,7 @@ with col_3d:
996
  y=id_col,
997
  orientation='h',
998
  title=sort_opt,
999
- labels={'avg_monthly_leadtime': 'Avg Lead Time (Days)', id_col: 'Executor'},
1000
  color='color',
1001
  color_discrete_map={c: c for c in subset['color'].unique()},
1002
  text=subset['avg_monthly_leadtime'].apply(lambda x: f'{x:.1f}')
@@ -1032,7 +1048,7 @@ try:
1032
  except ImportError:
1033
  WORDCLOUD_AVAILABLE = False
1034
 
1035
- st.markdown("<h3 class='section-title'>OBJECTIVE 4 - What Unsafe Issues Appear Most Often?</h3>",
1036
  unsafe_allow_html=True)
1037
 
1038
  # 🔹 Fungsi untuk membuat judul seragam
@@ -1220,7 +1236,7 @@ try:
1220
  operator_lead = (
1221
  df_local_matrix.groupby('nama')['lead_time_days']
1222
  .mean()
1223
- .reset_index(name='Average Lead Time')
1224
  )
1225
  # ============================
1226
  # 6. Merge Risk Matrix
@@ -1307,7 +1323,7 @@ import numpy as np
1307
  import pandas as pd
1308
 
1309
  # =================== OBJECTIVE 6 - Predictive Dashboard & Early Warning Signals ===================
1310
- st.markdown("<h3 class='section-title'>OBJECTIVE 6 — Predictive Dashboard & Early Warning Signals</h3>", unsafe_allow_html=True)
1311
 
1312
  # ✅ Enhanced CSS + Minimal Sortable JS
1313
  st.markdown("""
@@ -1915,7 +1931,7 @@ if not df_category.empty:
1915
  # title=dict(text="<b>Issue Category Trend vs Frequency (Non-Positive)</b>", x=0.5, y=0.95),
1916
  xaxis=dict(
1917
  title="Category",
1918
- tickangle=45,
1919
  showgrid=True,
1920
  gridcolor="#e0e0e0",
1921
  gridwidth=1,
@@ -1940,7 +1956,6 @@ if not df_category.empty:
1940
  fig.add_annotation(
1941
  xref="paper", yref="paper",
1942
  x=0.05, y=0.95,
1943
- text="Trend<br>(Slope)",
1944
  showarrow=False,
1945
  font=dict(size=12, color="#003DA5"),
1946
  textangle=-90,
@@ -1949,7 +1964,7 @@ if not df_category.empty:
1949
  fig.add_annotation(
1950
  xref="paper", yref="paper",
1951
  x=0.5, y=0.05,
1952
- text="Category",
1953
  showarrow=False,
1954
  font=dict(size=12, color="#003DA5"),
1955
  align="center"
@@ -1957,7 +1972,7 @@ if not df_category.empty:
1957
  fig.add_annotation(
1958
  xref="paper", yref="paper",
1959
  x=0.95, y=0.95,
1960
- text="<b>Legend of Frequency</b><br>• 10<br>•• 20<br>••• 30<br>•••• 40",
1961
  showarrow=False,
1962
  font=dict(size=12, color="#003DA5"),
1963
  align="left"
@@ -1965,7 +1980,7 @@ if not df_category.empty:
1965
  fig.add_annotation(
1966
  xref="paper", yref="paper",
1967
  x=0.05, y=0.1,
1968
- text="<b>Semakin tinggi = semakin sering ditemukan deviasi</b><br><b>Semakin besar = semakin banyak ditemukan deviasi</b><br><b>Non-positive</b>",
1969
  showarrow=False,
1970
  font=dict(size=12, color="#003DA5"),
1971
  align="left"
@@ -2146,7 +2161,7 @@ import math
2146
  # OBJECTIVE 7 — INSIGHT & RECOMMENDATION (LLM FIRST, RULE-BASED IF FAIL)
2147
  # =====================================================================
2148
 
2149
- st.markdown("<h3 class='section-title'>OBJECTIVE 7 — Insight and Recommendation</h3>", unsafe_allow_html=True)
2150
 
2151
 
2152
  # ============================================================
@@ -2354,7 +2369,7 @@ if dev["obj3b_slowest_executor"]:
2354
  parts.append(f"executor <strong>{dev['obj3b_slowest_executor'][0]}</strong> ({dev['obj3b_slowest_executor'][1]} days)")
2355
 
2356
  if parts:
2357
- insight_lines.append("2. Agentic AI detection flags uneven operational capacity: " + "; ".join(parts))
2358
 
2359
  uc, ua, nm = dev["obj4_unsafe_condition_pct"], dev["obj4_unsafe_action_pct"], dev["obj4_near_miss_pct"]
2360
  if uc + ua + nm > 0:
@@ -2369,7 +2384,7 @@ if dev["obj6_top2_categories"]:
2369
  c1, c2 = dev["obj6_top2_categories"]
2370
  insight_lines.append(f"5. Top recurring non-Positive categories: <strong>{c1[0]}</strong> ({c1[1]}/mo) & <strong>{c2[0]}</strong> ({c2[1]}/mo).")
2371
 
2372
- insight_text = "<br>".join(insight_lines)
2373
 
2374
 
2375
  # ============================================================
@@ -2387,7 +2402,7 @@ else:
2387
  recs.append({"point":"1","rec":"Launch spot-inspection sprint at across the 9 lowest-ratio locations.","mit":"Enable 3-min QR checklist + automated WhatsApp reminders."})
2388
 
2389
  if parts:
2390
- recs.append({"point":"2","rec":"real-time monitoring of finding/reporter ratios and resolution lead times per division/individual.","mit":"Trigger coaching alerts to Area PICs & Division"})
2391
 
2392
  if uc + ua + nm > 0:
2393
  recs.append({"point":"3","rec":"Enforce photo-based validation for Unsafe Condition/Action/Near Miss submissions to ensure accurate categorization.","mit":"System blocks submission if photo evidence or category justification is missing."})
@@ -2408,7 +2423,7 @@ else:
2408
  st.markdown(
2409
  f"""
2410
  <div class="card" style="background-color:#f8f9fa;border-left:4px solid #003DA5;padding:16px;margin-bottom:20px;border-radius:4px;box-shadow:0 2px 4px rgba(0,0,0,0.05);">
2411
- <h4 style="margin-top:0;color:#FF6B6B;">Insight Summary</h4>
2412
  <p style="margin-bottom:0;line-height:1.6;font-size:0.98em;">{insight_text}</p>
2413
  </div>
2414
  """,
 
519
  col1, col2 = st.columns(2)
520
 
521
  with col1:
522
+ st.markdown("<h5>Unit Pembangkit: Monthly Finding by Company</h5>", unsafe_allow_html=True)
523
  fig_pg = create_polar_bar_chart(avg_ratio_pg, 'PG')
524
  if fig_pg:
525
  st.plotly_chart(fig_pg, use_container_width=True)
 
539
  st.warning("No data for PG area.")
540
 
541
  with col2:
542
+ st.markdown("<h5>Unit Maintenance: Monthly Finding by Company</h5>", unsafe_allow_html=True)
543
  fig_um = create_polar_bar_chart(avg_ratio_um, 'UM')
544
  if fig_um:
545
  st.plotly_chart(fig_um, use_container_width=True)
 
549
  st.markdown("### Insight")
550
  st.markdown(
551
  f"<div class='ai-insight'>"
552
+ f"Across all companies, the finding-per-person ratio is similar in the UM Area "
553
 
554
  f"</div>",
555
  unsafe_allow_html=True
 
614
 
615
  # Plot Treemap dengan gradasi warna
616
  if not avg_ratio_per_location.empty:
 
617
  fig_treemap = px.treemap(
618
  avg_ratio_per_location,
619
+ path=['nama_lokasi_full'],
620
+ values='avg_monthly_ratio',
621
+ title='Monthly Finding by Location',
622
+ labels={
623
+ 'avg_monthly_ratio': 'Monthly Finding/Person Ratio',
624
+ 'nama_lokasi_full': 'Location'
625
+ },
626
+ color='avg_monthly_ratio',
627
  color_continuous_scale=[
628
+ [0.0, '#D32F2F'],
629
+ [0.5, '#FFB300'],
630
+ [1.0, '#4CAF50']
631
  ]
632
  )
633
+
634
+ # --- Set global font size 12 ---
635
+ fig_treemap.update_layout(
636
+ font=dict(size=12)
637
+ )
638
+
639
+ # --- Set label font size inside the treemap ---
640
+ fig_treemap.update_traces(
641
+ textfont=dict(size=12)
642
+ )
643
+
644
+ st.plotly_chart(fig_treemap)
645
+
646
  # Format hover
647
  fig_treemap.update_traces(
648
  hovertemplate="<b>%{label}</b><br>Avg Ratio: %{value:.2f}<extra></extra>"
 
665
  # f"Locations with <span style='color:#D32F2F; font-weight:bold;'>red</span> color have a low ratio, indicating lower activity levels or potentially under-reporting. "
666
  f"<strong>{top_location['nama_lokasi_full']}</strong> shows the highest activity level "
667
  f"(<strong>{top_location['avg_monthly_ratio']:.2f}</strong>). "
668
+ f"<strong>{low_location['nama_lokasi_full']}</strong> is one of areas with the lowest activity level "
669
  f"(<strong>{low_location['avg_monthly_ratio']:.2f}</strong>). "
670
  f"Areas with high activity (green) warrant inspection into the underlying causes of frequent findings. "
671
  f"Areas with low activity (red) should be reviewed to ensure reporting completeness and identify any hidden risks."
 
820
 
821
  # ─── 3a: Reporter by Division (Rasio Temuan/Orang) ───────────────────────────
822
  with col_3a:
823
+ st.markdown("<h5 style='text-align:center;'>3a. Finding/Person Ratio by Division (Reporter)</h5>", unsafe_allow_html=True)
824
  if avg_ratio_per_nama.empty:
825
  st.warning("No data for division-level reporter analysis.")
826
  else:
 
840
  fig = px.bar(
841
  subset, x='avg_monthly_ratio', y='nama', orientation='h',
842
  title=f'{sort_opt} Divisions',
843
+ labels={'avg_monthly_ratio': 'Monthly Ratio', 'nama': 'Division'},
844
  color='color', color_discrete_map={c: c for c in subset['color'].unique()},
845
  text=subset['avg_monthly_ratio'].apply(lambda x: f'{x:.2f}')
846
  )
 
854
  best, worst = full_sorted.iloc[0]['nama'], full_sorted.iloc[-1]['nama']
855
  st.markdown(
856
  f"<div class='ai-insight'>"
857
+ f"<strong>Insight:</strong> Division reporting ratio ranges from {min_r:.2f} to {max_r:.2f} (avg: {mean_r:.2f}). "
858
+ f"<strong>{best}</strong> leads."
859
+ # <strong>{worst}</strong> lags.
860
  f"<strong>Recommendation:</strong> Benchmark processes from {best}; assess capacity/tooling gaps in {worst}."
861
  f"</div>",
862
  unsafe_allow_html=True
 
865
 
866
  # ─── 3c: Reporter by Individual ──────────────────────────────────────────────
867
  with col_3c:
868
+ st.markdown("<h5 style='text-align:center;'>3c. Monthly Findings per Reporter (Individual)</h5>", unsafe_allow_html=True)
869
  if avg_rate_per_creator.empty:
870
  st.warning("No data for individual reporter analysis.")
871
  else:
 
883
  fig = px.bar(
884
  subset, x='avg_monthly_rate', y='creator_name', orientation='h',
885
  title=f'{sort_opt} Reporters',
886
+ labels={'avg_monthly_rate': 'Monthly Findings', 'creator_name': 'Reporter'},
887
  color='color', color_discrete_map={c: c for c in subset['color'].unique()},
888
  text=subset['avg_monthly_rate'].apply(lambda x: f'{x:.2f}')
889
  )
 
909
 
910
  # ─── 3b: Executor by Division (Lead Time) ────────────────────────────────────
911
  with col_3b:
912
+ st.markdown("<h5 style='text-align:center;'>3b. Monthly Lead Time by Division (Executor)</h5>", unsafe_allow_html=True)
913
  if avg_leadtime_nama.empty:
914
  st.warning("No data for division-level executor analysis.")
915
  else:
 
945
  y='nama',
946
  orientation='h',
947
  title=sort_opt,
948
+ labels={'avg_monthly_leadtime': 'Monthly Lead Time (Days)', 'nama': 'Division'},
949
  color='color',
950
  color_discrete_map={c: c for c in subset['color'].unique()},
951
  text=subset['avg_monthly_leadtime'].apply(lambda x: f'{x:.1f}')
 
977
  # ─── 3d: Executor by Individual ──────────────────────────────────────────────
978
  with col_3d:
979
  st.markdown(
980
+ f"<h5 style='text-align:center;'>3d. Monthly Lead Time per Executor (Individual)</h5>",
981
  unsafe_allow_html=True
982
  )
983
  if avg_leadtime_per_indiv.empty:
 
1012
  y=id_col,
1013
  orientation='h',
1014
  title=sort_opt,
1015
+ labels={'avg_monthly_leadtime': 'Monthly Lead Time (Days)', id_col: 'Executor'},
1016
  color='color',
1017
  color_discrete_map={c: c for c in subset['color'].unique()},
1018
  text=subset['avg_monthly_leadtime'].apply(lambda x: f'{x:.1f}')
 
1048
  except ImportError:
1049
  WORDCLOUD_AVAILABLE = False
1050
 
1051
+ st.markdown("<h3 class='section-title'>OBJECTIVE 4 - Unsafe Issues: Which One is the Most Often?</h3>",
1052
  unsafe_allow_html=True)
1053
 
1054
  # 🔹 Fungsi untuk membuat judul seragam
 
1236
  operator_lead = (
1237
  df_local_matrix.groupby('nama')['lead_time_days']
1238
  .mean()
1239
+ .reset_index(name='Lead Time')
1240
  )
1241
  # ============================
1242
  # 6. Merge Risk Matrix
 
1323
  import pandas as pd
1324
 
1325
  # =================== OBJECTIVE 6 - Predictive Dashboard & Early Warning Signals ===================
1326
+ st.markdown("<h3 class='section-title'>OBJECTIVE 6 — Prediction for Early Warning Signals: Which Ones Have Less Future Inspections?</h3>", unsafe_allow_html=True)
1327
 
1328
  # ✅ Enhanced CSS + Minimal Sortable JS
1329
  st.markdown("""
 
1931
  # title=dict(text="<b>Issue Category Trend vs Frequency (Non-Positive)</b>", x=0.5, y=0.95),
1932
  xaxis=dict(
1933
  title="Category",
1934
+ tickangle=135,
1935
  showgrid=True,
1936
  gridcolor="#e0e0e0",
1937
  gridwidth=1,
 
1956
  fig.add_annotation(
1957
  xref="paper", yref="paper",
1958
  x=0.05, y=0.95,
 
1959
  showarrow=False,
1960
  font=dict(size=12, color="#003DA5"),
1961
  textangle=-90,
 
1964
  fig.add_annotation(
1965
  xref="paper", yref="paper",
1966
  x=0.5, y=0.05,
1967
+ text="",
1968
  showarrow=False,
1969
  font=dict(size=12, color="#003DA5"),
1970
  align="center"
 
1972
  fig.add_annotation(
1973
  xref="paper", yref="paper",
1974
  x=0.95, y=0.95,
1975
+ text="<b>Number of Findings</b><br>• 10<br>•• 20<br>••• 30<br>•••• 40",
1976
  showarrow=False,
1977
  font=dict(size=12, color="#003DA5"),
1978
  align="left"
 
1980
  fig.add_annotation(
1981
  xref="paper", yref="paper",
1982
  x=0.05, y=0.1,
1983
+ text="<b>Semakin tinggi = semakin sering ditemukan deviasi</b><br><b>Semakin besar = semakin banyak ditemukan deviasi</b><br><b></b>",
1984
  showarrow=False,
1985
  font=dict(size=12, color="#003DA5"),
1986
  align="left"
 
2161
  # OBJECTIVE 7 — INSIGHT & RECOMMENDATION (LLM FIRST, RULE-BASED IF FAIL)
2162
  # =====================================================================
2163
 
2164
+ st.markdown("<h3 class='section-title'>OBJECTIVE 7 — Risk Mitigation: What are the Insights and Recommendations?</h3>", unsafe_allow_html=True)
2165
 
2166
 
2167
  # ============================================================
 
2369
  parts.append(f"executor <strong>{dev['obj3b_slowest_executor'][0]}</strong> ({dev['obj3b_slowest_executor'][1]} days)")
2370
 
2371
  if parts:
2372
+ insight_lines.append("2. Uneven operational capacity: " + "; ".join(parts))
2373
 
2374
  uc, ua, nm = dev["obj4_unsafe_condition_pct"], dev["obj4_unsafe_action_pct"], dev["obj4_near_miss_pct"]
2375
  if uc + ua + nm > 0:
 
2384
  c1, c2 = dev["obj6_top2_categories"]
2385
  insight_lines.append(f"5. Top recurring non-Positive categories: <strong>{c1[0]}</strong> ({c1[1]}/mo) & <strong>{c2[0]}</strong> ({c2[1]}/mo).")
2386
 
2387
+ insight_text = "<br><br>".join(insight_lines)
2388
 
2389
 
2390
  # ============================================================
 
2402
  recs.append({"point":"1","rec":"Launch spot-inspection sprint at across the 9 lowest-ratio locations.","mit":"Enable 3-min QR checklist + automated WhatsApp reminders."})
2403
 
2404
  if parts:
2405
+ recs.append({"point":"2","rec":"Real-time monitoring of finding/reporter ratios and resolution lead times per division/individual.","mit":"Trigger coaching alerts to Area PICs & Division"})
2406
 
2407
  if uc + ua + nm > 0:
2408
  recs.append({"point":"3","rec":"Enforce photo-based validation for Unsafe Condition/Action/Near Miss submissions to ensure accurate categorization.","mit":"System blocks submission if photo evidence or category justification is missing."})
 
2423
  st.markdown(
2424
  f"""
2425
  <div class="card" style="background-color:#f8f9fa;border-left:4px solid #003DA5;padding:16px;margin-bottom:20px;border-radius:4px;box-shadow:0 2px 4px rgba(0,0,0,0.05);">
2426
+ <h4 style="margin-top:0;color:#FF6B6B;">Summary</h4>
2427
  <p style="margin-bottom:0;line-height:1.6;font-size:0.98em;">{insight_text}</p>
2428
  </div>
2429
  """,