o
    ƒCVj`\  ã                   @   sx  d dl Z d dlZej d d¡ ej d d¡ d dlmZmZ d dlmZm	Z	m
Z
 e  ¡ ZedƒZe e¡\ZZZerXedƒ erDeeƒ edƒ erOed	ƒ ned
ƒ e d ¡ ermedƒ edƒ eedƒƒ e d ¡ e ¡ Z	 d dlZd dlZd dlZd dlZd dlmZ d dlmZmZ dZdZdZ dZ!dZ"dd„ Z#dd„ Z$dd„ Z%dd„ Z&dd„ Z'dd„ Z(d d!„ Z)d"d#„ Z*d$Z+e ,d%d&¡Z-ze.d'e/e+e0e-ƒƒƒZ1W n e2yÙ   e+Z1Y nw e$ƒ Z3e&e1ƒZ4e'e4e3ƒ\Z5Z6e(e4e5e6e3ƒZ7e8e4ƒZ9e8e5ƒZ:e;d(d)„ e5D ƒƒZ<e:e< Z=e>d*d+„ e7 ?¡ D ƒd,d-„ d.�dd/… Z@ed0d-„ ƒZAe7 ?¡ D ]ZBeAeBd1  eBd2   CeB¡ �q!eA ?¡ D ]ZDeD ?¡ D ]ZEeEjFd3d-„ d.� �q<�q6e>e5d4d-„ d5d6�dd7… ZGedƒ edƒ g ZHe@D ].ZBeH Cd8eBd9 › d:eBd1 › d:eBd2 › d;eBd< › d=eBd> › d?eBd@ › d:e*eBdA ƒ› dB�¡ �qcdC IeH¡�p™dDZJg ZKe>eA L¡ ƒD ]®ZMg ZNe>eAeM  L¡ ƒD ]‘ZOg ZPeAeM eO D ]LZBeBd< d k�rÃdEndFZQeBd> d k�rÎdGndFZReP Cd8eBd9 › dHeQ› dIeBd< › dHeR› dIeBd> › d?eBdJ › dKeBd@ › d:e)eBdA ƒ› dLe*eBdA ƒ› dB�¡ �q¸e;dMd)„ eAeM eO D ƒƒZSe;dNd)„ eAeM eO D ƒƒZTeN CdOeO› dPeS› dQeT› dReTd'k�r2dSnd› dTd IeP¡› dU�¡ �q®eK CdVeM› dWd IeN¡› dX�¡ �q¢d IeK¡�pXdYZUg ZVeGD ]CZWeWdZ �rfd[nd\ZXeV Cd]eWdZ �rrd^nd› d_e)eWd` ƒ› d:eX› d:eWd1 › d:eWd2 › daeWdb › dKeWdc dd›dedf IeWdg ¡› dB�¡ �q]dC IeV¡�p¨dhZYed Ig di‘e	ƒ › ‘dj‘eeƒ› ‘dk‘e1d'k�rÃdlnd› ‘dm‘e1dnk�rÏdlnd› ‘do‘e1dpk�rÛdlnd› ‘dq‘e1drk�rçdlnd› ‘ds‘e1d$k�ródlnd› ‘dt‘e+› ‘du‘e1› ‘dv‘e9› ‘dw‘e=› ‘dx‘e<› ‘dy‘e!› ‘dz‘e › ‘d{‘e"d| › ‘d}‘eJ› ‘d~‘eU› ‘d‘eY› ‘d€‘e
ƒ › ‘d�‘¡ƒ dS )‚é    Nz/opt/ngon/appsz/var/www/html/ngon)ÚAuthManagerÚgenerate_login_page_html)Úgenerate_dropdown_htmlÚgenerate_dropdown_cssÚgenerate_dropdown_jsÚgen_outageszContent-Type: application/jsonÚ z6{"success": false, "error": "Authentication required"}z{"success": true}zContent-Type: text/htmlzGen Outage Report)Údefaultdict)ÚdatetimeÚ	timedeltaz#/opt/ngon/logs/generator_alerts.logz#/opt/ngon/config/master_config.jsonéx   é   iX  c                 C   sN   |   ¡ } zt | ¡W S  ty&   z	t | d¡W  Y S  ty%   Y Y dS w w )zUHandle both ISO (2025-10-23T17:19:15.816333) and space (2026-05-03 01:48:52) formats.z%Y-%m-%d %H:%M:%SN)Ústripr
   ÚfromisoformatÚ
ValueErrorÚstrptime©Ús© r   ú!/var/www/html/ngon/gen_outages.pyÚparse_ts?   s   ÿýr   c                  C   s¨   i } zGt tƒ�}t |¡}W d  ƒ n1 sw   Y  | di ¡ ¡ D ]"\}}| di ¡ ¡ D ]\}}| di ¡ ¡ D ]}||f| |< q<q0q$W | S  tyS   Y | S w )u@   Build gen_id â†’ (site_name, group_name) map from master_config.NÚsitesÚgenerator_groupsÚ
generators)ÚopenÚMASTER_CONFIGÚjsonÚloadÚgetÚitemsÚkeysÚ	Exception)ÚlookupÚfÚcfgÚ	site_nameÚ	site_dataÚ
group_nameÚ
group_dataÚgen_idr   r   r   Úload_gen_lookupK   s"   
ÿÿÿÿþþr*   c                  C   s6   g } t j t¡r|  t¡ |  tt td ¡ƒ¡ | S )u´  All available alert-log files: live + rotated + gzipped.

    logrotate (/etc/logrotate.d/ngon) rotates generator_alerts.log daily with
    `rotate 14` + delaycompress, so the live file only ever holds today. The
    history lives in generator_alerts.log.1 (uncompressed) and .2..14.gz â€” read
    them all or the page only ever sees the current day. Retention caps real
    coverage at ~14 days regardless of the selected window.
    z.*)ÚosÚpathÚexistsÚ
ALERTS_LOGÚappendÚextendÚsortedÚglob)Úfilesr   r   r   Ú_alert_log_filesZ   s
   	
r4   c                 C   sP  t  ¡ t| d� }g }tƒ D ]�}z‚| d¡rtjnt}||ddd��g}|D ]\}| ¡  d¡}t	|ƒdk r5q%t
|d ƒ}|d	u sC||k rDq%|d
  ¡ }	|d  ¡ }
|d  ¡ }|d  ¡ }|
dksd|dkreq%zt|ƒ}W n ttfyx   d}Y nw | ||	|f¡ q%W d	  ƒ n1 sŒw   Y  W q ttfy�   Y qw |jdd„ d� |S )z’Return list of fresh Down transitions: [(ts, gen_id, power_at_trip), ...]
    sorted by timestamp ascending, across the live + rotated alert logs.)Údaysz.gzÚrtÚreplace)Úerrorsú,é   r   Né   r   é   é   ÚDownç        c                 S   ó   | d S )Nr   r   )Úer   r   r   Ú<lambda>ˆ   ó    z"load_down_events.<locals>.<lambda>©Úkey)r
   Únowr   r4   ÚendswithÚgzipr   r   ÚsplitÚlenr   Úfloatr   Ú	TypeErrorr/   ÚFileNotFoundErrorÚOSErrorÚsort)r5   ÚcutoffÚeventsr,   Úopenerr#   ÚlineÚpartsÚtsr)   Úalert_levelÚ	power_strÚ
prev_alertÚpowerr   r   r   Úload_down_eventsj   sB   
ÿîÿ€ÿrZ   c                    sì  g }g }t ƒ }tˆ ƒD ]w\}\}}}||v rq| |¡}	|	sq|	\}
}g }t|d tˆ ƒƒD ](}ˆ | \}}}||  ¡ tkrA n||krFq.| |¡}|rV||	krV| |¡ q.t|ƒtkrx| |||
|‡ fdd„|D ƒ||ddœ¡ | 	|¡ q| ||||
|f¡ qt
tƒ}|D ]}||d   |¡ q‰| ¡ D ]X\}
}|jdd„ d	� t|ƒD ]G\}}|d
 h}|g}t|d t|ƒƒD ] }|| }|d |d   ¡ tkrÒ n| |d
 ¡ | |¡ q¾t|ƒdkrð|D ]}d|| d< qçq©q™||fS )aT  For each fresh Down event, decide initiator / follower / lonely.
    Also detect site-wide cascades (multi-group within SITE_WIDE_WINDOW).

    Returns:
        cascades: [{initiator, initiator_ts, group, site, followers,
                    is_site_wide, power_at_trip}, ...]
        lonely:   list of (ts, gen_id, group, site, power)
    r;   c                    s   g | ]}ˆ | d  ‘qS )r;   r   )Ú.0Új©rQ   r   r   Ú
<listcomp>²   s    z#classify_events.<locals>.<listcomp>F)Ú	initiatorÚinitiator_tsÚsiteÚgroupÚ	followersÚfollower_idxsÚpower_at_tripÚis_site_widera   c                 S   r@   ©Nr`   r   ©Úcr   r   r   rB   Ã   rC   z!classify_events.<locals>.<lambda>rD   rb   r`   r   Trf   )ÚsetÚ	enumerater   ÚrangerJ   Útotal_secondsÚWINDOW_SECONDSr/   ÚCASCADE_THRESHOLDÚupdater	   Úlistr   rO   ÚSITE_WIDE_WINDOW_SECONDSÚadd)rQ   Ú
gen_lookupÚcascadesÚlonelyÚused_as_followerÚirU   r)   rY   Úsgra   rb   rc   r\   Úf_tsÚf_genÚ_Úf_sgÚby_siteri   Úsite_cascadesÚwindow_groupsÚwindow_indicesÚdÚkr   r]   r   Úclassify_eventsŒ   sl   	


€ø

€õr„   c                 C   s,  i }| D ]>\}}}|  |¡}|sq|\}	}
| |||	|
ddddddddœ
¡}|d  d7  < |d du s:||d krB||d< ||d< q|D ]5}|  |d	 ¡}|rd|d
  d7  < |d sd|d  d7  < |d D ]}|  |¡}|ry|d  d7  < qhqE|D ]\}}}
}	}|  |¡}|r“|d  d7  < q}|S )zLPer-gen rollup: total outages, initiator count, follower count, last outage.r   Nr?   )
r)   ra   rb   ÚtotalÚ	initiatedÚfollowedrv   Úinitiated_excl_sitewideÚlast_outageÚ
last_powerr…   r;   r‰   rŠ   r_   r†   rf   rˆ   rc   r‡   rv   )r   Ú
setdefault)rQ   ru   rv   rt   ÚstatsrU   r)   rY   ry   ra   rb   r   ri   r{   Úfsr   r   r   Úbuild_gen_statsÔ   sF   
ü€
€ý
€rŽ   c                 C   s   | d u rdS |   d¡S )Nõ   â€”z%Y-%m-%d %H:%M)Ústrftime)Údtr   r   r   Úfmt_dtü   s   
r’   c                 C   s`   | d u rdS t  ¡ |  }|j}|dkr|› d�S |jd }|dkr&|› d�S |jd }|› d�S )Nr�   r;   zd agoi  zh agoé<   zm ago)r
   rF   r5   Úseconds)r‘   Údeltar5   ÚhoursÚminsr   r   r   Úfmt_ago  s   




r˜   é   r5   Ú14r;   c                 c   s   � | ]	}|d  rdV  qdS )rf   r;   Nr   )r[   ri   r   r   r   Ú	<genexpr>%  s   € r›   c                 C   s   g | ]
}|d  dkr|‘qS )rˆ   r   r   ©r[   r   r   r   r   r^   *  s    r^   c                 C   ó   | d  | d  fS ©Nrˆ   r…   r   r   r   r   r   rB   +  ó    rB   rD   é
   c                   C   s   t tƒS )N)r	   rq   r   r   r   r   rB   /  rC   ra   rb   c                 C   r�   rž   r   r   r   r   r   rB   6  rŸ   c                 C   r@   rg   r   rh   r   r   r   rB   9  rC   T)rE   Úreverseé2   z<tr><td class='mono'>r)   z	</td><td>z</td><td class='num red'>rˆ   z</td><td class='num orange'>r‡   z</td><td class='num grey'>r…   r‰   z
</td></tr>Ú
zT<tr><td colspan='7' class='no-data'>No cascade initiators found in window.</td></tr>ÚredÚgreyÚorangez</td><td class='num z'>rv   z</td><td class='num'>z</td><td class='dim'>c                 c   ó   � | ]}|d  V  qdS )r…   Nr   rœ   r   r   r   r›   a  ó   € c                 c   r§   )rˆ   Nr   rœ   r   r   r   r›   b  r¨   z 
        <h3 class="group-name">z <span class="group-stats">(z
 outages, z cascader   a‡   initiated)</span></h3>
        <table>
            <thead><tr>
                <th>Gen ID</th>
                <th class="num">Initiated</th>
                <th class="num">Followed Others</th>
                <th class="num">Lonely</th>
                <th class="num">Total</th>
                <th>Last Outage</th>
                <th></th>
            </tr></thead>
            <tbody>z"</tbody>
        </table>
        z>
    <div class="site-section">
        <h2 class="site-name">z</h2>
        z
    </div>
    z9<div class='no-data'>No outages in selected window.</div>rf   z-<span class='badge sitewide'>SITE-WIDE</span>z&<span class='badge group'>GROUP</span>z<tr class='zsitewide-rowz'><td>r`   z</td><td class='mono'>r_   re   z.0fz kW</td><td class='followers'>z, rc   zD<tr><td colspan='7' class='no-data'>No cascades in window.</td></tr>a  <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gen Outage Report</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<style>
a0  

* { box-sizing:border-box; }
body {
    font-family:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    background:#1a1a1a; color:#e0e0e0; min-height:100vh; padding-bottom:40px;
    margin:0; padding:0; line-height:1.4;
}

/* Floating-box header (matches status/gen_manager) */
.header {
    background-color:rgba(255,255,255,0.05) !important;
    border:1px solid rgba(255,255,255,0.1) !important;
    border-radius:8px !important;
    padding:30px !important;
    margin:20px !important;
    display:flex !important;
    justify-content:space-between !important;
    align-items:flex-start !important;
    border-bottom:none !important;
}
.header h1.dropdown-title {
    font-size:2.2em !important;
    font-weight:600 !important;
    margin:3px 0 0 0 !important;
    line-height:1.2 !important;
    color:#00ff00 !important;
}
.header .subtitle { color:#888; margin-top:6px; font-size:14px; }

.controls { margin:0 20px 20px; display:flex; flex-wrap:wrap; gap:8px; align-items:center; }
.controls label { color:#888; font-size:0.9rem; }
.controls select {
    padding:8px 12px; border-radius:4px; font-size:0.9rem; background:#2a2a2a;
    border:1px solid #3a3a3a; color:#ccc; cursor:pointer;
}
.controls select:focus { outline:none; border-color:rgba(0,255,0,0.4); }
.controls .retention-note { color:#666; font-size:0.8rem; font-style:italic; }

.summary-row { margin:0 20px 20px; display:flex; gap:12px; flex-wrap:wrap; }
.summary-badge {
    padding:10px 20px; border-radius:6px; font-size:0.9rem; font-weight:600;
    background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.1);
}
.summary-badge .label { color:#888; font-weight:400; margin-right:6px; }
.summary-badge.red { background:rgba(255,68,68,0.15); border-color:rgba(255,68,68,0.3); color:#ff4444; }
.summary-badge.orange { background:rgba(255,102,0,0.15); border-color:rgba(255,102,0,0.3); color:#ff6600; }
.summary-badge.grey { background:rgba(136,136,136,0.15); border-color:rgba(136,136,136,0.3); color:#aaa; }

.section { margin:0 20px 30px; }
.section h2 {
    color:#00ff00; font-size:1.4em; margin:0 0 10px; padding-bottom:8px;
    border-bottom:1px solid #2a2a2a;
}
.section .section-blurb { color:#888; font-size:0.9rem; margin-bottom:14px; }

.site-section { margin-bottom:24px; }
.site-name {
    color:#00ff00; font-size:1.4em; margin:0 0 10px; padding-bottom:8px;
    border-bottom:1px solid #2a2a2a;
}
.group-name {
    color:#4a9eff; font-size:1.05em; margin:18px 0 6px;
}
.group-name .group-stats { color:#888; font-size:0.85em; font-weight:400; margin-left:8px; }

.table-wrap {
    border:1px solid #2a2a2a; border-radius:6px; overflow-y:auto;
    scrollbar-width:thin; scrollbar-color:#666 transparent;
}
table { width:100%; border-collapse:collapse; font-size:0.9rem; }
thead th {
    position:sticky; top:0; background:#1a1a1a; padding:10px 14px; text-align:left;
    font-size:0.78rem; text-transform:uppercase; color:#888; letter-spacing:0.5px;
    border-bottom:1px solid #2a2a2a; user-select:none; white-space:nowrap;
}
thead th.num { text-align:right; }
tbody tr { border-bottom:1px solid #1e1e1e; transition:background 0.1s; }
tbody tr:hover { background:rgba(255,255,255,0.03); }
td { padding:8px 14px; vertical-align:middle; }
td.mono { font-family:'Courier New',monospace; font-size:0.85rem; }
td.num { font-weight:600; text-align:right; }
td.num.red { color:#ff4444; }
td.num.orange { color:#ff6600; }
td.num.grey { color:#888; }
td.dim { color:#666; font-size:0.85rem; }
td.followers { color:#888; font-family:'Courier New',monospace; font-size:0.82rem; }

.no-data { padding:30px; text-align:center; color:#555; }

.badge {
    display:inline-block; padding:2px 8px; border-radius:3px; font-size:0.7rem;
    font-weight:600; letter-spacing:0.5px; text-transform:uppercase;
}
.badge.group { background:rgba(255,68,68,0.15); border:1px solid rgba(255,68,68,0.3); color:#ff4444; }
.badge.sitewide { background:rgba(136,136,136,0.15); border:1px solid rgba(136,136,136,0.3); color:#aaa; }

tr.sitewide-row { opacity:0.55; }
tr.sitewide-row:hover { opacity:1.0; }

.legend {
    margin:0 20px 20px; padding:12px 16px; background:rgba(255,255,255,0.03);
    border:1px solid #2a2a2a; border-radius:6px; color:#888; font-size:0.85rem;
    line-height:1.6;
}
.legend strong { color:#ccc; }
.legend code {
    background:#1a1a1a; padding:2px 6px; border-radius:3px;
    font-family:'Courier New',monospace; color:#4a9eff;
}
</style>
</head>
<body>

<div class="header">
    <div>
        <div class="dropdown">
            <h1 class="dropdown-title">NGON Mining - Gen Outage Report</h1>
            <div class="dropdown-content">ar  </div>
        </div>
        <div class="subtitle">Generator outages and cascade-trigger analysis &middot; identifies repeat-offender gens whose trips knock out their group</div>
    </div>
</div>

<div class="controls">
    <label for="days">Window:</label>
    <select id="days" onchange="window.location='gen_outages.py?days='+this.value">
        <option value="1" Úselectedz*>1 day</option>
        <option value="3" r<   z+>3 days</option>
        <option value="7" é   z,>7 days</option>
        <option value="14" é   z->14 days</option>
        <option value="30" zX>30 days</option>
    </select>
    <span class="retention-note">alert-log retention is zp days</span>
</div>

<div class="summary-row">
    <div class="summary-badge"><span class="label">Window:</span>zY days</div>
    <div class="summary-badge grey"><span class="label">Total outages:</span>zT</div>
    <div class="summary-badge red"><span class="label">Group cascades:</span>zY</div>
    <div class="summary-badge orange"><span class="label">Site-wide events:</span>uˆ   </div>
</div>

<div class="legend">
    <strong>How to read:</strong>
    A <strong>group cascade</strong> is when one gen trips and â‰¥z/ other gens in the same group also trip within uÖ   s.
    The first gen down is classified as the <code>Initiator</code>; the rest are <code>Followers</code>.
    A <strong>site-wide event</strong> is when cascades hit â‰¥2 different groups at the same site within r“   uA   min â€” usually a shared external cause (gas, weather, transformer)
    and excluded from the per-gen Initiator counts on this page so the problem-child stats stay focused on actionable failures.
    <code>Lonely</code> = a Down with no sibling trips in window (single-gen failure, group survived).
    <br><br>
    <strong>Initiated vs Followed Others:</strong>
    <code>Initiated</code> counts cascades this gen <em>started</em> â€” its trip dragged the group down. High = root-cause troublemaker.
    <code>Followed Others</code> counts cascades where a sibling tripped first and this gen fell along with the rest. High = structural weakness; gen survives standalone but loses load-redistribution races.
    A gen high in <strong>both</strong> is a top swap candidate; high in only one tells you whether the failure mode is "this gen is the trigger" or "this gen can't take the hit when others fail."
</div>

<div class="section">
    <h2>Top Cascade Initiators</h2>
    <div class="section-blurb">Gens whose trips have triggered group cascades. Site-wide events excluded. Repeat offenders here are candidates for swap or focused diagnostics.</div>
    <div class="table-wrap" style="max-height:400px;">
        <table>
            <thead><tr>
                <th>Gen ID</th>
                <th>Site</th>
                <th>Group</th>
                <th class="num">Cascades Initiated</th>
                <th class="num">Followed Others</th>
                <th class="num">Total Outages</th>
                <th>Last Outage</th>
            </tr></thead>
            <tbody>
                zö
            </tbody>
        </table>
    </div>
</div>

<div class="section">
    <h2>All Outages by Site & Group</h2>
    <div class="section-blurb">Per-gen breakdown. Sorted within each group by initiator count, then total outages.</div>
    ac  
</div>

<div class="section">
    <h2>Recent Cascade Timeline</h2>
    <div class="section-blurb">Last 50 detected cascades, most recent first. Site-wide events shown dimmed (not actionable at gen level).</div>
    <div class="table-wrap" style="max-height:500px;">
        <table>
            <thead><tr>
                <th>Time</th>
                <th>Type</th>
                <th>Site</th>
                <th>Group</th>
                <th>Initiator</th>
                <th class="num">Power at Trip</th>
                <th>Followers</th>
            </tr></thead>
            <tbody>
                zB
            </tbody>
        </table>
    </div>
</div>

<script>z</script>
</body>
</html>)ZÚcgiÚsysr,   ÚinsertÚmanagers.auth_managerr   r   Úlinksr   r   r   ÚFieldStorageÚ_formÚ_authÚrequire_authÚ_auth_requiredÚ_should_exitÚ_headersÚprintÚexitÚget_user_accessÚ_user_accessr   r+   r2   rH   Úcollectionsr	   r
   r   r.   r   rn   ro   rr   r   r*   r4   rZ   r„   rŽ   r’   r˜   ÚRETENTION_DAYSÚgetvalueÚ
days_paramÚmaxÚminÚintr5   r   rt   rQ   ru   rv   rŒ   rJ   Útotal_eventsÚtotal_cascadesÚsumÚtotal_site_wideÚtotal_group_cascadesr1   ÚvaluesÚtop_initiatorsr   r   r/   Úsite_groupsÚ
group_listrO   Úrecent_cascadesÚtop_rows_htmlÚjoinÚtop_rowsÚsite_sections_htmlr    r%   Úgroup_blocksr'   ÚrowsÚ
init_classÚfollow_classÚgroup_totalÚ
group_initÚsite_sectionsÚcascade_rows_htmlri   ÚbadgeÚcascade_rowsr   r   r   r   Ú<module>   sØ  


"H(ÿþýÿÿþýüûú
ùÿÿþþýýüû
ú
ùÿÿÿÿÿôþý
ÿþýüûúùÿø~‚  �ø 	 �÷ 
 �ö  �õ  �ô  �ò  �î  �í  �ì  �ë  �æ  �æ  �ä 5 �Ë > �Â P �° V �ª