@@ -113,8 +113,86 @@ def safe_filename(filename):
113113socketio = SocketIO (app , cors_allowed_origins = "*" , async_mode = 'eventlet' , allow_upgrades = True )
114114
115115@app .context_processor
116- def inject_version ():
117- return {'version' : VERSION }
116+ def inject_globals ():
117+ """Inject global variables available in all templates."""
118+ result = {'version' : VERSION , 'problem_tickets' : 0 }
119+
120+ # Get problem tickets count (stuck + failed)
121+ try :
122+ conn = get_db ()
123+ cursor = conn .cursor (dictionary = True )
124+ cursor .execute ("SELECT COUNT(*) as cnt FROM tickets WHERE status IN ('stuck', 'failed')" )
125+ result ['problem_tickets' ] = cursor .fetchone ()['cnt' ]
126+ cursor .close ()
127+ conn .close ()
128+ except :
129+ pass
130+
131+ return result
132+
133+
134+ @app .after_request
135+ def inject_problem_badge (response ):
136+ """Inject problem tickets badge script into all HTML responses."""
137+ # Only for HTML responses
138+ if not response .content_type or not response .content_type .startswith ('text/html' ):
139+ return response
140+
141+ # Check if user is logged in
142+ if 'user' not in session :
143+ return response
144+
145+ try :
146+ data = response .get_data (as_text = True )
147+ if '</body>' not in data :
148+ return response
149+
150+ script = '''<script>
151+ (function(){
152+ function updateProblemBadge(){
153+ fetch('/api/problem-tickets-count')
154+ .then(r=>r.json())
155+ .then(d=>{
156+ let b=document.getElementById('problemBadge');
157+ if(d.count>0){
158+ if(!b){
159+ b=document.createElement('a');
160+ b.id='problemBadge';
161+ b.href='/tickets?status=problems';
162+ b.style.cssText='display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;font-size:14px;line-height:1;padding-bottom:2px;background:linear-gradient(135deg,#ef4444,#dc2626);border-radius:6px;text-decoration:none;animation:pulse-problem 2s infinite;cursor:pointer';
163+ b.title='Problem tickets (stuck/failed)';
164+ b.textContent='\\ u26A0\\ uFE0F';
165+ let header=document.querySelector('.header-left');
166+ if(header) header.appendChild(b);
167+ else{
168+ let h1=document.querySelector('.header h1');
169+ if(h1&&h1.parentElement) h1.parentElement.appendChild(b);
170+ }
171+ }
172+ b.style.display='inline-flex';
173+ } else if(b) b.style.display='none';
174+ }).catch(()=>{});
175+ }
176+ if(!document.getElementById('problemBadgeStyle')){
177+ let s=document.createElement('style');
178+ s.id='problemBadgeStyle';
179+ s.textContent='@keyframes pulse-problem{0%,100%{opacity:1;box-shadow:0 0 0 0 rgba(239,68,68,0.4)}50%{opacity:0.9;box-shadow:0 0 10px 2px rgba(239,68,68,0.3)}}';
180+ document.head.appendChild(s);
181+ }
182+ if(document.readyState==='loading'){
183+ document.addEventListener('DOMContentLoaded',updateProblemBadge);
184+ }else{
185+ updateProblemBadge();
186+ }
187+ setInterval(updateProblemBadge,30000);
188+ })();
189+ </script>'''
190+ data = data .replace ('</body>' , script + '</body>' )
191+ response .set_data (data )
192+ except Exception as e :
193+ pass # Don't break page if injection fails
194+
195+ return response
118196
119197
120198# Configure logging for error tracking
@@ -784,7 +862,7 @@ def validate_project_key():
784862 max_age = 86400 * 7 ,
785863 path = '/' + project_folder ,
786864 httponly = True ,
787- secure = True ,
865+ secure = request . is_secure , # True only for HTTPS, False for HTTP
788866 samesite = 'Lax'
789867 )
790868 return response
@@ -837,6 +915,10 @@ def dashboard():
837915 cursor .execute ("SELECT COUNT(*) as cnt FROM tickets WHERE status = 'done' AND DATE(updated_at) = CURDATE()" )
838916 stats ['completed_today' ] = cursor .fetchone ()['cnt' ]
839917
918+ # Problem tickets (stuck + failed) for notification
919+ cursor .execute ("SELECT COUNT(*) as cnt FROM tickets WHERE status IN ('stuck', 'failed')" )
920+ stats ['problem_tickets' ] = cursor .fetchone ()['cnt' ]
921+
840922 # Daemon status
841923 if os .path .exists (PID_FILE ):
842924 try :
@@ -900,6 +982,8 @@ def tickets_list():
900982 if status_filter :
901983 if status_filter == 'open' :
902984 query += " AND t.status IN ('new', 'open', 'pending')"
985+ elif status_filter == 'problems' :
986+ query += " AND t.status IN ('stuck', 'failed')"
903987 else :
904988 query += " AND t.status = %s"
905989 params .append (status_filter )
@@ -926,6 +1010,9 @@ def tickets_list():
9261010 'in_progress' : 'In Progress' ,
9271011 'awaiting_input' : 'Awaiting Input' ,
9281012 'done' : 'Completed' + (' Today' if today_only == '1' else '' ),
1013+ 'stuck' : 'Stuck Tickets' ,
1014+ 'failed' : 'Failed Tickets' ,
1015+ 'problems' : 'Problem Tickets' ,
9291016 '' : 'All Tickets'
9301017 }
9311018 title = title_map .get (status_filter , f'{ status_filter .title ()} Tickets' )
@@ -1045,6 +1132,22 @@ def project_progress_view(project_id):
10451132 project = project )
10461133
10471134
1135+ @app .route ('/api/problem-tickets-count' )
1136+ @login_required
1137+ def api_problem_tickets_count ():
1138+ """Get count of stuck + failed tickets for header notification badge."""
1139+ try :
1140+ conn = get_db ()
1141+ cursor = conn .cursor (dictionary = True )
1142+ cursor .execute ("SELECT COUNT(*) as cnt FROM tickets WHERE status IN ('stuck', 'failed')" )
1143+ count = cursor .fetchone ()['cnt' ]
1144+ cursor .close ()
1145+ conn .close ()
1146+ return jsonify ({'count' : count })
1147+ except :
1148+ return jsonify ({'count' : 0 })
1149+
1150+
10481151@app .route ('/api/project/<int:project_id>/archive' , methods = ['POST' ])
10491152@login_required
10501153def archive_project (project_id ):
0 commit comments