
    ܒ$jk                    
   d dl Z d dlmZ d dlmZ d dlmZ d dlZdZdZ	dZ
dZd	Zd
ZdZdZg dZ e            Z eee          re                    dg           Zn eee          reZng Zd Zd Zd Z e ed eD                                 Zi ZeD ]DZd eD             Z e d eD                       Z! e"d eD                       Z#e!e#rdnd z   ee<   E e$eej                  Z%d eD             Z&er e$d eD                       nd Z' e$d e&D             d           Z(e'e(k    r< ed eD                       Z)dZ*e)pe*gD ]Z+e&,                    e%e+e(dz   e'd            d  eD             Z-i Z.e-D ]$Z/e.0                    e/d!         e/d"                    %i Z1 eee          r,e                    d#i                               d$i           pi Z1e12                                D ]\  Z3Z4e3e.v re4re4e.e3<    e            Z5e52                                D ]\  Z3Z4e4pe3e.e3<    e ed% e-D                        ee56                                          z  d&           Z7 e$d' eD             d          Z8ee8e
z  z   d(z   Z9e e:e7          ez  z   d(z   Z;g Z< e=e7          D ]R\  Z>Z?ee> e:e          z           Z@ e ee.e?                             ZAe<,                    d)e@ d*e? d+eA d,           Sg ZBe-D ]Z/e7C                    e/d!                   Z>ee> e:e          z           Z@e/                    d-          p@e/                    d.          p+e/                    d/          pe/                    d0          pd1ZDe/                    d2          pe/                    d3          pd1ZEeB,                    i d4 ee/                    d4                    d5e/                    d5d1          d! eFe/d!                   d6 eFe/                    d6          pe/d!                   d" ee/d"                   d7 ee/                    d7          pe/d"                   d8e/d9         d:e/d;         d9e/d9         d;e/d;         d<e@d= ee/                    d=          pe/                    d>                    d> ee/                    d>          pe/                    d=                    d?e/                    d?          pe/                    d@          pd1dAe/                    dA          pe/                    dB          pd1dCe/                    dC          pe/                    dD          pd1dE ee/                    dE          pe/                    dF                     ee/                    dE          pe/                    dF                     ee/                    dG          pe/                    dH                     ee/                    dG          pe/                    dH                     eeD           eeD           eeD           eeD           ee/                    dI                     ee/                    dI          pe/                    dJ                     eeE           eeE          dK           dL e&D             ZGdM e7D             ZHd1I                    g dNe9 dOe; dPe9 dQe; dRe dSe8e
z   dTe dUe dVd1I                    e<           dW e jJ        eGdXY           dZ e jJ        eBdXY           d[ e jJ        eHdXY           d\e9 d]e; d^e d_e d`e dae
 dbe dce
 dde dee
 dfe dge dhe
 die dje
 dk          Z edl          K                    dmn            eLe	dodpq          5 ZMeMN                    e           ddd           n# 1 swxY w Y    eOdre	           dsZP ee	          ZQeQR                    dpq          ZSdteSvr=eST                    duePdvz             ZSeQU                    eSdpq            eOdwe	           n eOdxe	           dyZV ee	          ZQeQR                    dpq          ZSdzeSvr=eST                    dueVdvz             ZSeQU                    eSdpq            eOd{e	           n eOd|e	           d}ZW ee	          ZQeQR                    dpq          ZSd~eSvr=eST                    dueWdvz             ZSeQU                    eSdpq            eOde	           n eOde	           d dlmZX  eXd          ZYeYZ                                rgeYR                    dpq          Z[de[vrMdZ\dZ]e[T                    de\dz             Z[e[T                    due]dvz             Z[eYU                    e[dpq           d dlmZ^  e^d          Z_e_Z                                rWe_R                    dpq          Z`de`vr=dZae`T                    dueadvz             Z`e_U                    e`dpq            eOd           d dlmZb  ebd          ZcecZ                                recR                    dpq          ZddedvrdZeedT                    deedz             ZddedvrdZfedT                    ddefz   dz   d          ZddedvrdZgedT                    duegdvz             ZdecU                    eddpq            eOd           d dlmZh d Zi ei             d dlmZj d Zk ek             d dlmZl d Zm em             d dlmZn d dloZpd Zq eq             d dlmZr d dloZsd Zt et             d dlmZu d dloZvd Zw ew             d dlmZx d dloZyd Zz ez             d dlmZ{ d Z| e|             d dlmZ} d dloZ~d Z e             d dlmZ d dloZd Z e             d dlmZ d dloZd Z e             d dlmZ d dloZd Z e             d dlmZ d dloZd Z e             d dlmZ d dloZd Z e             d dlmZ d dloZd Z e             d dlmZ d dloZd Z e             d dlmZ d dloZd Z e             d dlmZ d dloZd Z e             d dlmZ d dloZd Z e             dS )    N)Path)escape)load_database DATABASE/GRAFO_D_DATABASE.sqlite outputs/timeline_excel_like.htmlx   .   i  &      8   )
z#1d7afcz#43a047z#f4511ez#8e24aaz#e53935z#00a6a6z#5145cdz#d29600z#d81b8az#795548eventsc                 L    | dS t          |                                           S )N )strstrip)vs    render_excel_like.pycleanr      s     yrq66<<>>    c                     	 t          t          t          |                               dd                              }d|cxk    odk    nc S # t          $ r Y dS w xY w)N~r   iPLi'  F)intfloatr   replace	Exception)r   xs     r   is_year_valuer   $   sv    eAhh&&sB//0011""""d"""""   uus   AA 
AAc                  &   t          d          } i }|                                 s|S t          j        t	          |                     }t          j        |_        	 |                    d                                          }|D ]]}t	          |d         pd          	                                }t	          |d         p|          	                                }|r|p|||<   ^	 |
                                 n# |
                                 w xY w|S )z
    Load graph rows directly from SQLite graphs.

    A graph with zero events must still be visible as a graph/container.
    It must not fabricate a timeline component.
    r   z4SELECT row_key, visible_name FROM graphs ORDER BY idrow_keyr   visible_name)r   existssqlite3connectr   Rowrow_factoryexecutefetchallr   close)db_pathlabelsconrowsrowr   r    s          r   'load_sqlite_graph_labels_for_empty_rowsr.   ,   s    566GF>> 
/#g,,
'
'CkCO{{B
 

(** 	  	: 	:C#i..B//5577Gs>2=g>>DDFFL :"."9'w		: 					Ms   BC8 8Dc              #   &   K   | ]}|d          V  dS r-   N .0es     r   	<genexpr>r5   L   s&      ++q!E(++++++r   c                 @    g | ]}|d          t           k    |d         S r-   namerr2   s     r   
<listcomp>r;   P   s%    777!5QAfIr   c              #   8   K   | ]}t          |          d V  dS )   N)r   r3   r   s     r   r5   r5   Q   s/      <<a=+;+;<<<<<<<r   c              #   `   K   | ])}t          |                              d d          dv V  *dS )r   r   )2025z2025.0N)r   r   r>   s     r   r5   r5   R   s>      QQq588##C,,0BBQQQQQQr   i  )keyc                 ^    g | ]*}|d          t           k    t          |d                   (|+S r7   )axis_rowr   r2   s     r   r;   r;   W   sB       
x8ai 8 8 r   c              #   B   K   | ]}|                     d d          V  dS end_colr   Ngetr2   s     r   r5   r5   ^   s0      88AAEE)Q''888888r   c              #   B   K   | ]}|                     d d          V  dS rE   rG   r2   s     r   r5   r5   _   s0      ==AAEE)Q''======r   )defaultc                    h | ]}|                     d d          t          k    !t          |                     dd                    dd                                         ^t          |                     dd                    dd         S )rF   r   dater   N   )rH   max_axis_colr   isdigitr2   s     r   	<setcomp>rP   b   s       55A--#aeeFB6G6G2H2H!2L2T2T2V2V- 	AEE&"rr"---r   2026r=   )r-   r8   	start_colrF   c                 4    g | ]}|d          t           k    |S )r-   )rC   r2   s     r   r;   r;   r   s1       
x8 r   r-   r8   ui_stategraph_labelsc              #   &   K   | ]}|d          V  dS r0   r1   r2   s     r   r5   r5      s&      ''Q%''''''r   c                 v    t          t                              | |                                                     S )N)r   
row_labelsrH   lowerr9   s    r   <lambda>rZ      s(    %
q!,,--3355 r   c              #   &   K   | ]}|d          V  dS )rF   Nr1   r2   s     r   r5   r5      s&      ,,q|,,,,,,r   i   z+
    <label class="graphOption" style="--c:z=;">
        <input type="checkbox" class="graphCheck" value="zE">
        <span class="dot"></span>
        <span class="graphName">u\   </span>
        <span class="toggleItems" title="Ver elementos">▸</span>
    </label>
    brief_descriptionbriefBRIEFdescriptionr   api_textapiiddb_idr   
event_namestartrR   endrF   colorrL   	date_textyear	date_yearmonth
date_monthdaydate_dayplacePLACEwhoWHOsource_linksource)rp   rq   rr   r_   r]   r^   r\   rs   rt   ra   r`   c           	          g | ]a}t          |d                                        dd          |d         |d         t          |d         |d         z   dz  dz
  t          z  z   dbS )r8   z.0r   rR   rF      r=   )r8   re   rf   r   )r   r   LEFT_PADCELL_Wr2   s     r   r;   r;      s        	
 ai  ((r22;|!K.1Y<71<q@FJJ	   r   c                 ^    i | ]*}t          |          t          t          |                   +S r1   )r   r   rX   )r3   r:   s     r   
<dictcomp>rz      s<       	 FFE*Q-    r   u  
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>GRAFO D - Línea Temporal Inteligente</title>
<style>
html, body {
    margin: 0;
    width: 100%;
    height: 100%;
    overflow: hidden;
    font-family: Arial, sans-serif;
    background: #0b0b0b;
}

#toolbar {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    height: 58px;
    z-index: 400;
    background: linear-gradient(#111, #050505);
    color: white;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 0 14px;
    box-shadow: 0 2px 12px #0008;
}

button {
    height: 34px;
    padding: 0 14px;
    background: #1d1d1d;
    color: white;
    border: 1px solid #333;
    border-radius: 6px;
    cursor: pointer;
}

button:hover {
    background: #1565c0;
}

#zoomLabel {
    margin-left: auto;
    margin-right: 20px;
}

#sidebar {
    font-size: 11px;
    position: fixed;
    top: 58px;
    left: 0;
    bottom: 0;
    width: 250px;
    z-index: 350;
    background: #111;
    color: white;
    overflow: auto;
    border-right: 1px solid #333;
}

#searchBox {
    position: sticky;
    top: 0;
    z-index: 50;
    background: #111;
    padding: 10px 12px;
    border-bottom: 1px solid #222;
    display: flex;
    align-items: center;
    gap: 8px;
}

.searchInputWrap {
    position: relative;
    flex: 1 1 auto;
    min-width: 0;
}

#graphSearch {
    width: 100%;
    height: 30px;
    box-sizing: border-box;
    border: 1px solid #333;
    border-radius: 5px;
    background: #0b0b0b;
    color: white;
    padding: 0 28px 0 8px;
    outline: none;
}

#graphSearch:focus {
    border-color: #6b7280;
}

#clearSearch {
    position: absolute;
    right: 6px;
    top: 5px;
    width: 20px;
    height: 20px;
    border: none;
    background: transparent;
    color: #9ca3af;
    cursor: pointer;
    font-size: 16px;
    line-height: 18px;
    padding: 0;
    z-index: 2;
}

#clearSearch:hover {
    color: white;
}

#folderEdit {
    flex: 0 0 auto;
    width: 30px;
    height: 30px;
    border: 1px solid #333;
    border-radius: 5px;
    background: #0b0b0b;
    color: #9ca3af;
    cursor: pointer;
    font-size: 14px;
    line-height: 18px;
    padding: 0;
}

#folderEdit:hover {
    color: white;
    border-color: #555;
}

#rootDropZone {
    display: none;
    margin: 10px;
    height: 44px;
    border: 1px dashed #3f3f46;
    border-radius: 6px;
    color: #71717a;
    align-items: center;
    justify-content: center;
    font-size: 11px;
}

#folderTools.active ~ #rootDropZone {
    display: flex;
}

#rootDropZone.dragTarget {
    border-color: #e5e7eb;
    color: #e5e7eb;
    background: #1f1f1f;
}

.folderBlock {
    border-bottom: 1px solid #222;
}

.folderHeader {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 7px 10px;
    cursor: pointer;
    color: #e5e7eb;
    font-size: 12px;
    background: #151515;
}

.folderHeader:hover {
    background: #1f1f1f;
}

.folderName {
    flex: 1;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.folderCount {
    color: #9ca3af;
    font-size: 11px;
}

.folderContent {
    display: block;
}

.folderContent.closed {
    display: none;
}

.folderTools {
    display: none;
    padding: 6px 10px;
    background: #101010;
    border-bottom: 1px solid #222;
}

.folderTools.active {
    display: flex;
    gap: 6px;
}

.folderTools button {
    height: 26px;
    font-size: 11px;
    padding: 0 8px;
}

.graphOption {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 10px;
    cursor: pointer;
    font-size: 11px;
    border-bottom: 1px solid #222;
}

.graphCheck,
.folderCheck {
    width: 11px;
    height: 11px;
    accent-color: #3b82f6;
}

body:not(.editMode) .graphCheck,
body:not(.editMode) .folderCheck {
    appearance: none;
    border-radius: 50%;
    border: 1px solid #6b7280;
    background: #111;
}

body:not(.editMode) .graphCheck:checked,
body:not(.editMode) .folderCheck:checked {
    background: #3b82f6;
    border-color: #3b82f6;
    box-shadow: inset 0 0 0 2px #111;
}

body.editMode .graphCheck,
body.editMode .folderCheck {
    appearance: auto;
    border-radius: 2px;
}

.graphOption.dragging {
    opacity: 0.45;
}

.folderHeader.dragTarget {
    outline: 1px dashed #9ca3af;
    background: #2a2a2a;
}

.graphOption:hover {
    background: #1e1e1e;
}

.dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--c);
    flex: 0 0 auto;
}

.graphName {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    flex: 1;
}

.toggleItems {
    margin-left: auto;
    color: #9ca3af;
    font-size: 13px;
    cursor: pointer;
    padding: 2px 6px;
    border-radius: 0;
}

.toggleItems:hover {
    background: #333;
    color: #fff;
}

.graphDetails {
    display: none;
    margin: 0 0 0 22px;
    padding: 4px 8px 8px 8px;
    border-bottom: 1px solid #222;
    color: #d1d5db;
    font-size: 11px;
}

.graphDetails summary {
    cursor: pointer;
    color: #9ca3af;
    padding: 3px 0;
}

.graphDetails ul {
    margin: 6px 0 0 0;
    padding-left: 14px;
    max-height: 220px;
    overflow-y: auto;
}

.graphDetails li {
    margin: 3px 0;
    line-height: 1.25;
}

#viewport {
    position: absolute;
    top: 58px;
    left: 250px;
    right: 0;
    bottom: 0;
    overflow: scroll;
    background: white;
}

#frozenLabels {
    position: fixed;
    top: 140px;
    left: 250px;
    width: 250px;
    bottom: 0;
    z-index: 280;
    pointer-events: none;
}

#spacer {
    position: relative;
    width: zpx;
    height: zMpx;
}

#canvas {
    position: absolute;
    left: 0;
    top: 0;
    width: zpx;
    min-height: a  px;
    transform-origin: 0 0;
    background: white;
}

#axis {
    position: sticky;
    top: 0;
    left: 0;
    z-index: 300;
    height: 82px;
    background: #f2f4f7;
    border-bottom: 1px solid #c8ccd2;
    box-shadow: 0 2px 6px #0001;
}

#axisTitle {
    position: absolute;
    left: 22px;
    top: 28px;
    color: #1f2937;
    font-weight: bold;
    font-size: 15px;
}

#axisLine {
    position: absolute;
    left: zpx;
    top: 41px;
    width: a
  px;
    height: 1px;
    background: #9aa3af;
}

.yearTick {
    position: absolute;
    top: 20px;
    height: 42px;
    border-left: 1px solid #6b7280;
}

.yearLabel {
    position: absolute;
    top: 13px;
    left: 4px;
    min-width: 58px;
    text-align: left;
    color: #111827;
    font-weight: 700;
    font-size: 13px;
    background: #ffffff;
    border: 1px solid #c8ccd2;
    border-radius: 3px;
    padding: 2px 5px;
    box-shadow: 0 1px 2px #0001;
}

#selectedGraphLayer {
    position: relative;
    top: 0;
    left: 0;
}

.graphLaneLabel {
    position: absolute;
    left: 0;
    width: max-content;
    min-width: 90px;
    max-width: 215px;
    height: 30px;
    line-height: 30px;
    padding: 0 14px 0 10px;
    box-sizing: border-box;
    border-left: 8px solid var(--c);
    border-radius: 5px;
    background: #f7f7f7;
    color: #102a43;
    font-size: 12px;
    font-weight: 700;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.event {
    position: absolute;
    height: zpx;
    line-height: u+  px;
    padding: 0 7px;
    box-sizing: border-box;
    border: 1px solid var(--c);
    border-radius: 0 !important;
    background: color-mix(in srgb, var(--c) 18%, white);
    color: #102a43;
    font-size: 13px;
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}


.event {
    overflow: visible !important;
}

.eventMainText {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.eventPlaceTag {
    position: absolute;
    left: 0;
    top: -15px;
    z-index: 85;
    height: 12px;
    line-height: 12px;
    max-width: 21ch;
    overflow: hidden;
    text-overflow: ellipsis;
    padding: 1px 6px;
    background: #05070b;
    color: #f8fafc;
    border: 1px solid rgba(148, 163, 184, 0.65);
    border-radius: 0;
    font-size: 9px;
    font-weight: 700;
    white-space: nowrap;
    pointer-events: none;
}

.eventWhoTag {
    position: absolute;
    left: 0;
    bottom: -13px;
    z-index: 80;
    height: 12px;
    line-height: 12px;
    max-width: 21ch;
    overflow: hidden;
    text-overflow: ellipsis;
    padding: 1px 6px;
    background: #05070b;
    color: #f8fafc;
    border: 1px solid rgba(148, 163, 184, 0.65);
    border-radius: 0;
    font-size: 9px;
    font-weight: 700;
    white-space: nowrap;
    pointer-events: none;
}

.event:hover {
    background: #fff3b0;
    z-index: 250;
    width: auto !important;
    min-width: 260px;
    overflow: visible;
}

#deleteSelectedGraphs {
    height: 26px;
    border: 1px solid #444;
    border-radius: 5px;
    background: #171717;
    color: #e5e7eb;
    cursor: pointer;
    font-size: 11px;
    padding: 0 8px;
}

#deleteSelectedGraphs:hover {
    background: #262626;
    border-color: #666;
}


body.editMode .graphOption {
    cursor: pointer;
}

body.editMode .graphOption.graphSelected {
    background: rgba(37, 99, 235, 0.35) !important;
    outline: 1px solid #60a5fa !important;
}

body.editMode .graphCheck,
body.editMode .folderCheck {
    width: 14px !important;
    height: 14px !important;
    accent-color: #3b82f6 !important;
    cursor: pointer !important;
}

#deleteSelectedGraphs {
    height: 26px;
    border: 1px solid #444;
    border-radius: 5px;
    background: #171717;
    color: #e5e7eb;
    cursor: pointer;
    font-size: 11px;
    padding: 0 8px;
}

#deleteSelectedGraphs:hover {
    background: #262626;
    border-color: #666;
}



</style>
</head>
<body>

<div id="toolbar">
    <strong>GRAFO D — Línea Temporal Inteligente</strong>
    <button id="v49SaveButton" type="button" title="Autosave status">Save <span id="v49SaveDotFinal" aria-hidden="true"></span></button>
</div>

<div id="sidebar">
    <div id="searchBox">
        <div class="searchInputWrap">
            <input id="graphSearch" type="text" placeholder="Search">
            <button id="clearSearch" type="button" title="Clear search">×</button>
        </div>
        <button id="folderEdit" type="button" title="Edit folders">▦</button>
    </div>
    <div id="folderTools" class="folderTools">
        <button id="newFolder" type="button">New folder</button>
        <button id="newGraph" type="button">New graph</button>
        <button id="moveSelected" type="button">Move selected</button>
        <button id="deleteSelectedGraphs" type="button">Delete selected</button>
    </div>
    a  
</div>

<div id="frozenLabels"></div>

<div id="viewport">
    <div id="spacer">
        <div id="canvas">
            <div id="axis">
                <div id="axisTitle">TIMELINE</div>
                <div id="axisLine"></div>
                <div id="ticks"></div>
            </div>
            <div id="selectedGraphLayer"></div>
        </div>
    </div>
</div>

<script>
let zoom = 1.0;
let selectionOrder = [];
let selectedEmptyFolders = [];
let editMode = false;

const axisData = F)ensure_asciiz;
const eventData = z;
const rowLabels = aS  ;

const canvas = document.getElementById("canvas");
const spacer = document.getElementById("spacer");
const label = document.getElementById("zoomLabel");
const ticks = document.getElementById("ticks");
const layer = document.getElementById("selectedGraphLayer");
const frozenLabels = document.getElementById("frozenLabels");

let baseW = z;
let baseH = u]  ;

function selectedRows() {
    return Array.from(document.querySelectorAll(".graphCheck:checked"))
        .map(x => x.value);
}


function selectedFolderRows() {
    return [...selectedEmptyFolders];
}

function setEmptyFolderSelected(path, checked) {
    if (!path || path === "Uncategorized") return;

    if (checked && !selectedEmptyFolders.includes(path)) {
        selectedEmptyFolders.push(path);
    }

    if (!checked) {
        selectedEmptyFolders = selectedEmptyFolders.filter(x => x !== path);
    }
}

function clearEmptyFolderSelection() {
    selectedEmptyFolders = [];
}

function deleteEmptyFolderSql(path) {
    return fetch("/api/folders/" + encodeURIComponent(path), {
        method: "DELETE"
    }).then(r => r.json().then(data => {
        if (!r.ok || data.ok === false) {
            throw new Error(data.error || ("Folder delete failed: " + path));
        }

        if (!data.deleted || !data.deleted.includes(path)) {
            const reason = data.skipped && data.skipped.length
                ? data.skipped.map(x => x.folder + ": " + x.reason).join("; ")
                : "folder was not deleted";
            throw new Error(reason);
        }

        return data;
    }));
}


function syncSelectionOrder() {
    const active = selectedRows();

    selectionOrder = selectionOrder.filter(r => active.includes(r));

    active.forEach(r => {
        if (!selectionOrder.includes(r)) {
            selectionOrder.push(r);
        }
    });

    reorderSidebar();
}

function reorderSidebar() {
    renderFolderTree();
}

let grafoDFolderState = null;

function normalizeFolderState(state) {
    if (!state || typeof state !== "object") state = {};
    if (!Array.isArray(state.folders)) state.folders = ["Uncategorized"];
    if (!state.graphFolder || typeof state.graphFolder !== "object") state.graphFolder = {};
    if (!state.open || typeof state.open !== "object") state.open = {"Uncategorized": true};
    if (!Array.isArray(state.deletedRows)) state.deletedRows = [];
    if (!state.folders.includes("Uncategorized")) state.folders.unshift("Uncategorized");
    return state;
}

function getFolderState() {
    if (!grafoDFolderState) {
        grafoDFolderState = normalizeFolderState({
            folders: ["Uncategorized"],
            graphFolder: {},
            open: {"Uncategorized": true},
            deletedRows: []
        });
    }
    return grafoDFolderState;
}

function loadFolderStateFromDatabase() {
    fetch("/api/db")
        .then(r => r.json())
        .then(db => {
            grafoDFolderState = normalizeFolderState(
                db && db.ui_state && db.ui_state.folders ? db.ui_state.folders : null
            );
            renderFolderTree();
            syncFolderChecks();
        })
        .catch(() => {});
}

function saveFolderState(state) {
    grafoDFolderState = normalizeFolderState(state);
    return fetch("/api/ui-state", {
        method: "POST",
        headers: {"Content-Type": "application/json"},
        body: JSON.stringify({key: "folders", value: grafoDFolderState})
    }).catch(() => {});
}

try {
} catch(e) {}

loadFolderStateFromDatabase();

let grafoDMasterBlocks = null;

function collectGraphBlocks() {
    if (!grafoDMasterBlocks) {
        grafoDMasterBlocks = Array.from(document.querySelectorAll(".graphOption")).map(label => {
            const check = label.querySelector(".graphCheck");
            const details = label.nextElementSibling && label.nextElementSibling.classList.contains("graphDetails")
                ? label.nextElementSibling
                : null;

            if (check) {
                label.draggable = true;
                label.dataset.row = check.value;
            }

            return {
                row: check ? check.value : "",
                selected: check ? check.checked : false,
                label: label,
                details: details
            };
        });
    }

    grafoDMasterBlocks.forEach(b => {
        const check = b.label.querySelector(".graphCheck");
        b.selected = check ? check.checked : false;
    });

    const state = getFolderState();
    const deleted = new Set(state.deletedRows || []);
    return grafoDMasterBlocks.filter(b => !deleted.has(b.row));
}

function folderDepth(path) {
    if (path === "Uncategorized") return 0;
    return path.split("/").length - 1;
}

function folderName(path) {
    const parts = path.split("/");
    return parts[parts.length - 1];
}

function directChildren(path, folders) {
    if (path === "Uncategorized") {
        return [];
    }

    const prefix = path + "/";

    return folders
        .filter(f => f !== path)
        .filter(f => f.startsWith(prefix) && !f.slice(prefix.length).includes("/"))
        .sort((a, b) => folderName(a).localeCompare(folderName(b)));
}

function rowsUnderFolder(path, blocks, state) {
    return blocks
        .filter(b => {
            const f = state.graphFolder[b.row] || "Uncategorized";
            return f === path || f.startsWith(path + "/");
        })
        .map(b => b.row);
}

function renderFolderTree() {
    const sidebar = document.getElementById("sidebar");
    const searchBox = document.getElementById("searchBox");
    const tools = document.getElementById("folderTools");
    const q = document.getElementById("graphSearch").value.trim().toLowerCase();

    const state = getFolderState();
    const blocks = collectGraphBlocks();

    const selected = blocks.filter(b => b.selected);
    const unselected = blocks.filter(b => !b.selected);
    const ordered = selected.concat(unselected);

    sidebar.innerHTML = "";
    sidebar.appendChild(searchBox);
    sidebar.appendChild(tools);

    const rootDrop = document.createElement("div");
    rootDrop.id = "rootDropZone";
    rootDrop.textContent = "Drop folder here to move to root";

    if (document.getElementById("folderTools").classList.contains("active")) {
        rootDrop.style.display = "flex";
    } else {
        rootDrop.style.display = "none";
    }

    rootDrop.addEventListener("dragover", function(ev) {
        ev.preventDefault();
        rootDrop.classList.add("dragTarget");
    });

    rootDrop.addEventListener("dragleave", function() {
        rootDrop.classList.remove("dragTarget");
    });

    rootDrop.addEventListener("drop", function(ev) {
        ev.preventDefault();
        rootDrop.classList.remove("dragTarget");

        const movingFolder = ev.dataTransfer.getData("application/grafo-folder");
        if (!movingFolder) return;

        const state = getFolderState();
        moveFolderToFolder(movingFolder, "Uncategorized", state);
        saveFolderState(state);
        renderFolderTree();
    });

    sidebar.appendChild(rootDrop);

    function renderNode(path, parent) {
        const depth = folderDepth(path);
        const children = directChildren(path, state.folders);

        const exactMembers = ordered.filter(b => (state.graphFolder[b.row] || "Uncategorized") === path);

        const visibleMembers = exactMembers.filter(b => {
            const text = b.label.innerText.toLowerCase();
            return !q || text.includes(q) || path.toLowerCase().includes(q);
        });

        const visibleChildren = children.filter(child => {
            if (!q) return true;

            const childRows = rowsUnderFolder(child, blocks, state);
            const childHasVisibleGraph = blocks.some(b =>
                childRows.includes(b.row) && b.label.innerText.toLowerCase().includes(q)
            );

            return child.toLowerCase().includes(q) || childHasVisibleGraph;
        });

        if (q && visibleMembers.length === 0 && visibleChildren.length === 0) return;

        const block = document.createElement("div");
        block.className = "folderBlock";

        const header = document.createElement("div");
        header.className = "folderHeader";
        header.style.paddingLeft = (10 + depth * 18) + "px";
        header.dataset.folder = path;
        header.draggable = path !== "Uncategorized";

        header.addEventListener("dragstart", function(ev) {
            if (path === "Uncategorized") return;
            ev.dataTransfer.setData("application/grafo-folder", path);
            ev.dataTransfer.setData("text/plain", "");
        });

        const folderRows = rowsUnderFolder(path, blocks, state);
        const selectedRowsInFolder = folderRows.filter(row => selectionOrder.includes(row)).length;

        const folderCheck = document.createElement("input");
        folderCheck.type = "checkbox";
        folderCheck.className = "folderCheck";
        folderCheck.checked = folderRows.length > 0
            ? selectedRowsInFolder === folderRows.length
            : selectedEmptyFolders.includes(path);
        folderCheck.indeterminate = folderRows.length > 0 &&
            selectedRowsInFolder > 0 &&
            selectedRowsInFolder < folderRows.length;

        folderCheck.addEventListener("click", function(ev) {
            ev.stopPropagation();

            const checked = folderCheck.checked;

            if (folderRows.length === 0) {
                setEmptyFolderSelected(path, checked);
                renderFolderTree();
                return;
            }

            folderRows.forEach(row => {
                const input = document.querySelector('.graphCheck[value="' + row + '"]');
                if (input) input.checked = checked;

                if (checked && !selectionOrder.includes(row)) {
                    selectionOrder.push(row);
                }

                if (!checked) {
                    selectionOrder = selectionOrder.filter(x => x !== row);
                }
            });

            if (editMode) {
                renderFolderTree();
                return;
            }

            applyFilter();
            applyZoom();
        });

        const arrow = document.createElement("span");
        arrow.textContent = state.open[path] === false ? "▸" : "▾";

        const name = document.createElement("span");
        name.className = "folderName";
        name.textContent = folderName(path);

        const count = document.createElement("span");
        count.className = "folderCount";
        count.textContent = folderRows.length;

        header.appendChild(folderCheck);
        header.appendChild(arrow);
        header.appendChild(name);
        header.appendChild(count);

        const content = document.createElement("div");
        content.className = "folderContent";
        if (state.open[path] === false) content.classList.add("closed");

        header.addEventListener("click", function() {
            state.open[path] = !(state.open[path] !== false);
            saveFolderState(state);
            renderFolderTree();
            syncFolderChecks();
        });

        header.addEventListener("dragover", function(ev) {
            ev.preventDefault();
            header.classList.add("dragTarget");
        });

        header.addEventListener("dragleave", function() {
            header.classList.remove("dragTarget");
        });

        header.addEventListener("drop", function(ev) {
            ev.preventDefault();
            header.classList.remove("dragTarget");

            const movingFolder = ev.dataTransfer.getData("application/grafo-folder");
            const row = ev.dataTransfer.getData("text/plain");

            const state = getFolderState();

            if (movingFolder) {
                moveFolderToFolder(movingFolder, path, state);
                saveFolderState(state);
                renderFolderTree();
                return;
            }

            if (!row) return;

            state.graphFolder[row] = path;
            state.open[path] = true;
            saveFolderState(state);
            renderFolderTree();
            syncFolderChecks();
        });

        block.appendChild(header);

        visibleChildren.forEach(child => renderNode(child, content));

        visibleMembers.forEach(b => {
            b.label.style.marginLeft = (depth * 18 + 12) + "px";

            b.label.addEventListener("dragstart", function(ev) {
                ev.dataTransfer.setData("text/plain", b.row);
                b.label.classList.add("dragging");
            });

            b.label.addEventListener("dragend", function() {
                b.label.classList.remove("dragging");
            });

            const graphInput = b.label.querySelector(".graphCheck");
            if (graphInput) {
                const isSelected = selectionOrder.includes(b.row);
                graphInput.checked = isSelected;
                b.label.classList.toggle("graphSelected", isSelected);

                graphInput.onclick = function(ev) {
                    ev.stopPropagation();
                    setGraphSelected(b.row, graphInput.checked);
                    if (!editMode) {
                        applyFilter();
                        applyZoom();
                    }
                };

                b.label.onclick = function(ev) {
                    if (!editMode) return;
                    if (ev.target.closest(".toggleItems")) return;
                    if (ev.target.classList.contains("graphCheck")) return;

                    graphInput.checked = !graphInput.checked;
                    setGraphSelected(b.row, graphInput.checked);
                };
            }

            content.appendChild(b.label);
            if (b.details) content.appendChild(b.details);
        });

        block.appendChild(content);
        parent.appendChild(block);
    }

    renderNode("Uncategorized", sidebar);

    state.folders
        .filter(f => f !== "Uncategorized" && !f.includes("/"))
        .sort((a, b) => folderName(a).localeCompare(folderName(b)))
        .forEach(f => renderNode(f, sidebar));
}

function moveFolderToFolder(source, target, state) {
    if (!source || source === "Uncategorized") return;
    if (source === target) return;
    if (target.startsWith(source + "/")) return;

    const shortName = folderName(source);
    const newBase = target === "Uncategorized" ? shortName : target + "/" + shortName;

    if (state.folders.includes(newBase)) {
        alert("A folder with that name already exists in the destination.");
        return;
    }

    const oldFolders = [...state.folders];

    oldFolders.forEach(f => {
        if (f === source || f.startsWith(source + "/")) {
            const suffix = f.slice(source.length);
            const newPath = newBase + suffix;

            state.folders = state.folders.filter(x => x !== f);
            if (!state.folders.includes(newPath)) state.folders.push(newPath);

            state.open[newPath] = state.open[f] !== false;
            delete state.open[f];

            Object.keys(state.graphFolder).forEach(row => {
                if (state.graphFolder[row] === f) {
                    state.graphFolder[row] = newPath;
                }
            });
        }
    });

    state.open[target] = true;
    state.open[newBase] = true;
}

function createGraph() {
    const state = getFolderState();

    const name = prompt("New graph name:");
    if (!name) return;

    const cleanName = name.trim();
    if (!cleanName) return;

    const folder = prompt(
        "Folder path for new graph:",
        state.folders[0] || "Uncategorized"
    );

    const cleanFolder = (folder || "Uncategorized").trim() || "Uncategorized";

    fetch("/api/graphs", {
        method: "POST",
        headers: {"Content-Type": "application/json"},
        body: JSON.stringify({
            name: cleanName,
            folder: cleanFolder
        })
    })
    .then(r => r.json())
    .then(data => {
        if (!data.ok) {
            throw new Error(data.error || "Graph creation failed");
        }
        window.location.reload();
    })
    .catch(err => {
        alert("Error creating graph: " + err.message);
    });
}


function createFolder() {
    const name = prompt("Folder name:");

    if (!name) return;

    const cleanName = name.trim();
    if (!cleanName) return;

    fetch("/api/folders", {
        method: "POST",
        headers: {"Content-Type": "application/json"},
        body: JSON.stringify({name: cleanName})
    })
    .then(r => r.json())
    .then(data => {
        if (!data.ok) {
            throw new Error(data.error || "Folder creation failed");
        }

        window.location.reload();
    })
    .catch(err => {
        alert("Error creating folder: " + err.message);
    });
}

function createSubfolder() {
    const state = getFolderState();

    const parent = prompt(
        "Parent folder path:",
        state.folders[state.folders.length - 1] || "Uncategorized"
    );

    if (!parent) return;

    const cleanParent = parent.trim();

    if (!state.folders.includes(cleanParent)) {
        alert("Parent folder does not exist.");
        return;
    }

    const name = prompt("Subfolder name:");
    if (!name) return;

    const cleanName = name.trim();
    if (!cleanName) return;

    const path = cleanParent === "Uncategorized"
        ? cleanName
        : cleanParent + "/" + cleanName;

    if (!state.folders.includes(path)) {
        state.folders.push(path);
        state.open[path] = true;
        state.open[cleanParent] = true;
        saveFolderState(state);
    }

    renderFolderTree();
}


function setGraphSelected(row, checked) {
    const input = document.querySelector('.graphCheck[value="' + row + '"]');
    if (input) input.checked = checked;

    if (checked && !selectionOrder.includes(row)) {
        selectionOrder.push(row);
    }

    if (!checked) {
        selectionOrder = selectionOrder.filter(x => x !== row);
    }

    document.querySelectorAll('.graphCheck[value="' + row + '"]').forEach(chk => {
        const label = chk.closest(".graphOption");
        if (label) label.classList.toggle("graphSelected", checked);
    });

    syncFolderChecks();
}

async function deleteSelectedGraphs() {
    const selectedGraphs = Array.isArray(selectionOrder) ? [...selectionOrder] : [];

    let selectedFolders = [];
    try {
        if (typeof selectedFolderRows === "function") {
            selectedFolders = selectedFolderRows();
        }
    } catch (err) {
        selectedFolders = [];
    }

    selectedFolders = selectedFolders.filter(function(x) {
        return x && x !== "Uncategorized";
    });

    if (selectedGraphs.length === 0 && selectedFolders.length === 0) {
        alert("No graphs or folders selected.");
        return;
    }

    let msg = "This will permanently delete selected item(s) from SQLite.\n\n";

    if (selectedGraphs.length) {
        msg += "Graphs:\n" + selectedGraphs.map(x => " - " + x).join("\n") + "\n\n";
    }

    if (selectedFolders.length) {
        msg += "Empty folders:\n" + selectedFolders.map(x => " - " + x).join("\n") + "\n\n";
        msg += "Only folders that are empty will be deleted.\n\n";
    }

    msg += "This action cannot be undone. Continue?";

    if (!confirm(msg)) return;

    async function deleteGraphSql(row) {
        const r = await fetch("/api/graphs/" + encodeURIComponent(row), {
            method: "DELETE"
        });

        let data = {};
        try {
            data = await r.json();
        } catch (err) {
            data = {ok: false, error: "Invalid JSON response"};
        }

        if (!r.ok || data.ok === false) {
            throw new Error(data.error || ("Graph delete failed: " + row));
        }

        return data;
    }

    async function deleteFolderSql(folder) {
        if (typeof deleteEmptyFolderSql === "function") {
            return await deleteEmptyFolderSql(folder);
        }

        const r = await fetch("/api/folders/" + encodeURIComponent(folder), {
            method: "DELETE"
        });

        let data = {};
        try {
            data = await r.json();
        } catch (err) {
            data = {ok: false, error: "Invalid JSON response"};
        }

        if (!r.ok || data.ok === false) {
            throw new Error(data.error || ("Folder delete failed: " + folder));
        }

        return data;
    }

    Promise.resolve()
        .then(async function() {
            for (const row of selectedGraphs) {
                await deleteGraphSql(row);
            }

            for (const folder of selectedFolders) {
                await deleteFolderSql(folder);
            }

            selectionOrder = [];

            try {
                if (typeof clearEmptyFolderSelection === "function") {
                    clearEmptyFolderSelection();
                }
            } catch (err) {}

            window.location.href = window.location.pathname + "?v=deleted_" + Date.now();
        })
        .catch(function(err) {
            alert("Delete failed: " + err.message);
        });
}


function moveSelectedToFolder() {
    const state = getFolderState();
    const selected = selectedRows();

    if (selected.length === 0) {
        alert("Select one or more graphs first.");
        return;
    }

    const folder = prompt(
        "Move selected graphs to folder path:",
        state.folders[0] || "Uncategorized"
    );

    if (!folder) return;

    const cleanFolder = folder.trim();
    if (!cleanFolder) return;

    if (!state.folders.includes(cleanFolder)) {
        state.folders.push(cleanFolder);
        state.open[cleanFolder] = true;
    }

    selected.forEach(row => {
        state.graphFolder[row] = cleanFolder;
    });

    saveFolderState(state);
    renderFolderTree();
}

function toggleFolderTools() {
    const tools = document.getElementById("folderTools");
    tools.classList.toggle("active");

    editMode = tools.classList.contains("active");
    document.body.classList.toggle("editMode", editMode);

    document.querySelectorAll(".graphOption").forEach(label => {
        label.draggable = editMode;
    });

    document.querySelectorAll(".graphCheck").forEach(input => {
        input.checked = false;
    });

    if (!editMode) {
        selectionOrder = [];
        clearEmptyFolderSelection();
        applyFilter();
        applyZoom();
    }

    renderFolderTree();
}


function syncFolderChecks() {
    document.querySelectorAll(".folderHeader").forEach(header => {
        const check = header.querySelector(".folderCheck");
        if (!check) return;

        const path = header.dataset.folder;
        if (!path) return;

        const state = getFolderState();
        const blocks = collectGraphBlocks();
        const folderRows = rowsUnderFolder(path, blocks, state);

        const selectedCount = folderRows.filter(row => {
            const input = document.querySelector('.graphCheck[value="' + row + '"]');
            return input && input.checked;
        }).length;

        check.checked = folderRows.length > 0
            ? selectedCount === folderRows.length
            : selectedEmptyFolders.includes(path);
        check.indeterminate = folderRows.length > 0 &&
            selectedCount > 0 &&
            selectedCount < folderRows.length;
    });
}


function applyFilter() {
    syncSelectionOrder();
    syncFolderChecks();

    layer.innerHTML = "";
    frozenLabels.innerHTML = "";

    let usedCols = new Set();

    selectionOrder.forEach(row => {
        eventData
            .filter(e => e.row === row)
            .forEach(e => {
                for (let c = e.start; c <= e.end; c++) {
                    usedCols.add(c);
                }
            });
    });

    const compactCols = Array.from(usedCols).sort((a, b) => a - b);
    const colMap = {};

    compactCols.forEach((c, i) => {
        colMap[c] = i + 1;
    });

    selectionOrder.forEach((row, index) => {
        const rawRowEvents = eventData.filter(e => e.row === row);
        if (rawRowEvents.length === 0) return;
        const rowEvents = rawRowEvents;
if (rowEvents.length === 0) return;

        const color = rowEvents[0].color;
        const yBase = z + index * a  ;

        const laneLabel = document.createElement("div");
        laneLabel.className = "graphLaneLabel";
        laneLabel.style.top = (yBase + 9) + "px";
        laneLabel.style.setProperty("--c", color);
        laneLabel.textContent = rowLabels[row];
        frozenLabels.appendChild(laneLabel);

        rowEvents.forEach(e => {
            const mappedStart = colMap[e.start];
            const mappedEnd = colMap[e.end];

            if (!mappedStart || !mappedEnd) return;

            const x1 = z + (mappedStart - 1) * z;
            const x2 = z + mappedEnd * u  ;
            const w = Math.max(80, x2 - x1);

            const ev = document.createElement("div");
            ev.className = "event";
            ev.style.left = x1 + "px";
            ev.style.top = (yBase + 9) + "px";
            ev.style.width = w + "px";
            ev.style.setProperty("--c", color);
            ev.title = e.name + " | fila " + e.row + " | col " + e.start + "–" + e.end;
            ev.dataset.eventId = stableEventId(e);

            const nameSpan = document.createElement("span");
            nameSpan.className = "eventMainText";
            nameSpan.textContent = e.name;
            ev.appendChild(nameSpan);

            const meta = getEventMetaStore()[stableEventId(e)] || {};
            if (meta.place) {
                const placeTag = document.createElement("span");
                placeTag.className = "eventPlaceTag";
                placeTag.textContent = String(meta.place).slice(0, 21);
                placeTag.title = meta.place;
                ev.appendChild(placeTag);
            }

            if (meta.who) {
                const whoTag = document.createElement("span");
                whoTag.className = "eventWhoTag";
                whoTag.textContent = String(meta.who).slice(0, 21);
                whoTag.title = meta.who;
                ev.appendChild(whoTag);
            }

            layer.appendChild(ev);
        });
    });

    baseW = z% + Math.max(compactCols.length, 1) * z + 800;
    baseH = z( + Math.max(selectionOrder.length, 1) * a   + 500;

    canvas.style.width = baseW + "px";
    canvas.style.height = baseH + "px";
    spacer.style.width = (baseW * zoom) + "px";
    spacer.style.height = (baseH * zoom) + "px";

    document.getElementById("axisLine").style.width =
        (Math.max(compactCols.length, 1) * aU  ) + "px";

    renderAxisCompact(colMap);
}

function renderAxisCompact(colMap) {
    ticks.innerHTML = "";

    axisData.forEach(t => {
        const mapped = colMap[t.start] || colMap[t.end];

        if (!mapped) return;

        const tick = document.createElement("div");
        tick.className = "yearTick";
        tick.style.left = (z + (mapped - 1) * u  ) + "px";

        const label = document.createElement("div");
        label.className = "yearLabel";
        label.textContent = t.name;

        tick.appendChild(label);
        ticks.appendChild(tick);
    });
}

function applyZoom() {
    canvas.style.transform = "scale(" + zoom + ")";
    spacer.style.width = (baseW * zoom) + "px";
    spacer.style.height = (baseH * zoom) + "px";
    label.innerText = "Zoom: " + Math.round(zoom * 100) + "%";
}

function zoomIn() {}
function zoomOut() {}
function resetZoom() {}
function panorama() {}

function selectNone() {
    document.querySelectorAll(".graphCheck").forEach(x => x.checked = false);
    selectionOrder = [];
    applyFilter();
    applyZoom();
}

document.querySelectorAll(".graphCheck").forEach(x => {
    x.addEventListener("change", function() {
        if (editMode) {
            renderFolderTree();
            return;
        }

        applyFilter();
        applyZoom();
    });
});

function yearForEvent(e) {
    let best = null;
    let bestDistance = Infinity;

    axisData.forEach(t => {
        const center = (t.start + t.end) / 2;
        const d = Math.abs(center - e.start);

        if (d < bestDistance) {
            bestDistance = d;
            best = t.name;
        }
    });

    return best || "?";
}

function buildGraphDetails() {
    document.querySelectorAll(".graphOption").forEach(label => {
        const check = label.querySelector(".graphCheck");
        if (!check) return;

        const row = check.value;
        const rowEvents = eventData.filter(e => e.row === row);
const details = document.createElement("div");
        details.className = "graphDetails";

        const ul = document.createElement("ul");

        rowEvents.forEach(e => {
            const li = document.createElement("li");
            li.textContent = e.name + " (" + yearForEvent(e) + ")";
            ul.appendChild(li);
        });

        details.appendChild(ul);
        label.insertAdjacentElement("afterend", details);

        const toggle = label.querySelector(".toggleItems");

        if (toggle) {
            toggle.textContent = "▸ " + rowEvents.length;
            toggle.addEventListener("click", function(ev) {
                ev.preventDefault();
                ev.stopPropagation();

                const visible = details.style.display === "block";
                details.style.display = visible ? "none" : "block";
                details.dataset.open = visible ? "0" : "1";
                toggle.textContent = (visible ? "▸ " : "▾ ") + rowEvents.length;
            });
        }
    });
}

const viewport = document.getElementById("viewport");

viewport.addEventListener("scroll", function() {
    frozenLabels.style.transform = "translateY(" + (-viewport.scrollTop) + "px)";
});

viewport.addEventListener("wheel", function(e) {
    e.preventDefault();
    viewport.scrollLeft += e.deltaY + e.deltaX;
}, { passive: false });

buildGraphDetails();

const graphSearch = document.getElementById("graphSearch");
const clearSearch = document.getElementById("clearSearch");
const folderEdit = document.getElementById("folderEdit");
const newFolder = document.getElementById("newFolder");
const newGraph = document.getElementById("newGraph");
const moveSelected = document.getElementById("moveSelected");

folderEdit.addEventListener("click", toggleFolderTools);
newFolder.addEventListener("click", createFolder);
if (newGraph) {
    newGraph.addEventListener("click", createGraph);
}
moveSelected.addEventListener("click", moveSelectedToFolder);

const deleteSelectedGraphsButton = document.getElementById("deleteSelectedGraphs");
if (deleteSelectedGraphsButton) {
    deleteSelectedGraphsButton.addEventListener("click", deleteSelectedGraphs);
}

clearSearch.addEventListener("click", function() {
    graphSearch.value = "";
    graphSearch.dispatchEvent(new Event("input"));
    graphSearch.focus();
});

let graphSearchTimer = null;

graphSearch.addEventListener("input", function() {
    clearTimeout(graphSearchTimer);

    graphSearchTimer = setTimeout(function() {
        const q = graphSearch.value.trim().toLowerCase();

        renderFolderTree();

        document.querySelectorAll(".graphOption").forEach(label => {
            const text = label.innerText.toLowerCase();
            const details = label.nextElementSibling;
            const show = !q || text.includes(q);

            label.style.display = show ? "flex" : "none";

            if (details && details.classList.contains("graphDetails")) {
                details.style.display = show && details.dataset.open === "1" ? "block" : "none";
            }
        });

        syncFolderChecks();
    }, 120);
});

document.body.classList.toggle("editMode", editMode);
applyFilter();
applyZoom();
</script>



</body>
</html>
outputsT)exist_okwutf-8encodingu   Visualización generada:u)  
<style>
#techCard {
    position: fixed;
    right: 18px;
    top: 18px;
    width: 390px;
    height: calc(100vh - 36px);
    z-index: 9999;
    background: #111827;
    color: #f9fafb;
    border: 1px solid #374151;
    border-radius: 10px;
    box-shadow: 0 12px 34px #0009;
    display: none;
    overflow: hidden;
}
#techCardHeader {
    height: 36px;
    display: flex;
    align-items: center;
    padding: 0 10px;
    background: #0b1220;
    border-bottom: 1px solid #374151;
    font-size: 12px;
    font-weight: 700;
}
#techCardTitle { flex: 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
#techCardClose { width: 26px; height: 26px; padding: 0; border-radius: 5px; }
#techCardBody { padding: 10px; overflow: auto; max-height: calc(100vh - 86px); }
.techField { margin-bottom: 8px; }
.techField label {
    display: block;
    margin-bottom: 3px;
    color: #9ca3af;
    font-size: 10px;
    font-weight: 700;
}
.techField input, .techField textarea {
    width: 100%;
    box-sizing: border-box;
    border: 1px solid #374151;
    border-radius: 6px;
    background: #030712;
    color: #f9fafb;
    padding: 7px;
    font-size: 12px;
}
.techField textarea { min-height: 116px; resize: vertical; }
#techGraphList {
    max-height: 285px;
    overflow: auto;
    background: #030712;
    border: 1px solid #374151;
    border-radius: 6px;
    padding: 7px 9px;
    font-size: 11px;
    line-height: 1.35;
}
.techActions { display: flex; gap: 8px; margin-top: 8px; }
.techActions button { flex: 1; height: 30px; font-size: 11px; }
.event.selectedEvent {
    outline: 2px solid #111827;
    box-shadow: 0 0 0 3px #facc15;
    z-index: 260;
}

#deleteSelectedGraphs {{
    height: 26px;
    border: 1px solid #444;
    border-radius: 5px;
    background: #171717;
    color: #e5e7eb;
    cursor: pointer;
    font-size: 11px;
    padding: 0 8px;
}}

#deleteSelectedGraphs:hover {{
    background: #262626;
    border-color: #666;
}}


body.editMode .graphOption {{
    cursor: pointer;
}}

body.editMode .graphOption.graphSelected {{
    background: rgba(37, 99, 235, 0.35) !important;
    outline: 1px solid #60a5fa !important;
}}

body.editMode .graphCheck,
body.editMode .folderCheck {{
    width: 14px !important;
    height: 14px !important;
    accent-color: #3b82f6 !important;
    cursor: pointer !important;
}}

#deleteSelectedGraphs {{
    height: 26px;
    border: 1px solid #444;
    border-radius: 5px;
    background: #171717;
    color: #e5e7eb;
    cursor: pointer;
    font-size: 11px;
    padding: 0 8px;
}}

#deleteSelectedGraphs:hover {{
    background: #262626;
    border-color: #666;
}}

</style>

<div id="techCard">
    <div id="techCardHeader">
        <span id="techCardTitle">Technical Card</span>
        <button id="techCardClose" type="button">×</button>
    </div>
    <div id="techCardBody">
        <div class="techField"><label>GRAPH GROUP</label><input id="techGroup" type="text"></div>
        <div class="techField"><label>NAME</label><input id="techName" type="text"></div>
        <div class="techField"><label>DATE — YEAR - MONTH - DAY</label><input id="techDate" type="text"></div>
        <div class="techField"><label>PLACE — COUNTRY - CITY</label><input id="techPlace" type="text"></div>
        <div class="techField"><label>WHO</label><input id="techWho" type="text"></div>
        <div class="techField"><label>BRIEF DESCRIPTION</label><textarea id="techDescription"></textarea></div>
        <div class="techField"><label>SOURCE LINK</label><input id="techSourceLink" type="url" placeholder="https://..."></div>
        <div class="techField"><label>GRAPH LIST SUMMARY</label><div id="techGraphList"></div></div>
        <div class="techField"><label>API STATUS</label><textarea id="techApi" readonly style="min-height:86px"></textarea></div>
        <div class="techActions">
            <button id="techSave" type="button">Save card</button>
            <button id="techExport" type="button">Export JSON</button>
        </div>
        <div id="liveApiSensor" class="liveApiSensor">
            <div class="liveApiSensorTitle">LIVE API SENSOR</div>
            <div id="liveApiSensorState" class="liveApiSensorState">NO GRAPH SELECTED</div>
            <div id="liveApiSensorDetail" class="liveApiSensorDetail">Open a graph event to inspect API runtime state.</div>
        </div>
    </div>
</div>

<script>
const eventMetaKey = "grafoD_eventMeta_v1";
let activeEventObj = null;
let activeEventId = null;

function stableEventId(e) {
    return e.row + ":" + e.start + ":" + e.end + ":" + e.name;
}

let grafoDEventMetaStore = null;

function getEventMetaStore() {
    if (!grafoDEventMetaStore) grafoDEventMetaStore = {};
    return grafoDEventMetaStore;
}

function loadEventMetaStoreFromDatabase() {
    fetch("/api/db")
        .then(r => r.json())
        .then(db => {
            grafoDEventMetaStore =
                db && db.ui_state && db.ui_state.event_meta && typeof db.ui_state.event_meta === "object"
                    ? db.ui_state.event_meta
                    : {};
        })
        .catch(() => {});
}

function saveEventMetaStore(store) {
    grafoDEventMetaStore = store && typeof store === "object" ? store : {};
    fetch("/api/ui-state", {
        method: "POST",
        headers: {"Content-Type": "application/json"},
        body: JSON.stringify({key: "event_meta", value: grafoDEventMetaStore})
    }).catch(() => {});
}

loadEventMetaStoreFromDatabase();

function parseYearFromDate(text) {
    const m = String(text || "").match(/-?\d{1,5}/);
    return m ? String(parseInt(m[0], 10)) : "";
}

function findAxisByYear(yearText) {
    const y = parseYearFromDate(yearText);
    if (!y) return null;
    return axisData.find(t => String(t.name).replace(".0", "") === y) || null;
}

function currentFolderForRow(row) {
    const state = getFolderState();
    return state.graphFolder[String(row)] || "Uncategorized";
}

function setFolderForRow(row, folder) {
    const state = getFolderState();
    const f = String(folder || "").trim() || "Uncategorized";
    if (!state.folders.includes(f)) {
        state.folders.push(f);
        state.open[f] = true;
    }
    state.graphFolder[String(row)] = f;
    saveFolderState(state);
}

function openTechCardByEvent(e, domEl) {
    activeEventObj = e;
    activeEventId = stableEventId(e);

    document.querySelectorAll(".event").forEach(x => x.classList.remove("selectedEvent"));
    if (domEl) domEl.classList.add("selectedEvent");

    const store = getEventMetaStore();
    const saved = store[activeEventId] || {};

    document.getElementById("techCard").style.display = "block";
    document.getElementById("techCardTitle").textContent = saved.name || e.name || "Technical Card";
    document.getElementById("techGroup").value = currentFolderForRow(e.row);
    document.getElementById("techName").value = saved.name || e.name || "";
    document.getElementById("techDate").value = saved.date || e.date || yearForEvent(e);
    document.getElementById("techPlace").value = saved.place || e.place || "";
    document.getElementById("techWho").value = saved.who || e.who || "";
    document.getElementById("techDescription").value = saved.description || e.description || "";
    document.getElementById("techSourceLink").value = saved.source_link || e.source_link || e.source || "";
    document.getElementById("techApi").value = saved.api || "";

    const list = document.getElementById("techGraphList");
    list.innerHTML = "";

    eventData.filter(x => x.row === e.row).forEach(x => {
        const div = document.createElement("div");
        if (stableEventId(x) === activeEventId) {
            div.innerHTML = "<strong>" + x.name + " (" + yearForEvent(x) + ")</strong>";
        } else {
            div.textContent = x.name + " (" + yearForEvent(x) + ")";
        }
        list.appendChild(div);
    });
}

function saveTechCard() {
    if (!activeEventObj) return;

    const oldId = activeEventId;
    const name = document.getElementById("techName").value.trim();
    const date = document.getElementById("techDate").value.trim();
    const group = document.getElementById("techGroup").value.trim();

    if (name) activeEventObj.name = name;

    const axis = findAxisByYear(date);
    if (axis) {
        activeEventObj.start = axis.start;
        activeEventObj.end = axis.end;
    } else if (date) {
        alert("Year not found on the timeline axis. The card is saved, but the box is not repositioned.");
    }

    if (group) setFolderForRow(activeEventObj.row, group);

    activeEventId = stableEventId(activeEventObj);

    const store = getEventMetaStore();
    delete store[oldId];
    store[activeEventId] = {
        group: currentFolderForRow(activeEventObj.row),
        name: activeEventObj.name,
        date: date || yearForEvent(activeEventObj),
        place: document.getElementById("techPlace").value.trim(),
        who: document.getElementById("techWho").value.trim(),
        description: document.getElementById("techDescription").value.trim(),
        source_link: document.getElementById("techSourceLink").value.trim(),
        api: document.getElementById("techApi").value.trim()
    };
    saveEventMetaStore(store);

    renderFolderTree();
    applyFilter();
    applyZoom();
    buildGraphDetails();
    openTechCardByEvent(activeEventObj, null);
}

function closeTechCard() {
    activeEventObj = null;
    activeEventId = null;
    document.getElementById("techCard").style.display = "none";
    document.querySelectorAll(".event").forEach(x => x.classList.remove("selectedEvent"));
}

function exportTechCards() {
    const payload = {
        version: "grafoD_eventMeta_v1",
        exportedAt: new Date().toISOString(),
        metadata: getEventMetaStore(),
        events: eventData
    };
    const blob = new Blob([JSON.stringify(payload, null, 2)], {type: "application/json"});
    const a = document.createElement("a");
    a.href = URL.createObjectURL(blob);
    a.download = "grafoD_event_cards.json";
    document.body.appendChild(a);
    a.click();
    a.remove();
}

document.addEventListener("click", function(ev) {
    const box = ev.target.closest(".event");
    if (!box) return;

    const id = box.dataset.eventId || "";
    const match = eventData.find(e => stableEventId(e) === id);

    if (match) {
        ev.stopPropagation();
        openTechCardByEvent(match, box);
    }
}, true);

document.getElementById("techSave").addEventListener("click", saveTechCard);
document.getElementById("techExport").addEventListener("click", exportTechCards);
document.getElementById("techCardClose").addEventListener("click", closeTechCard);
</script>
grafoD_eventMeta_v1</body>
</body>zTechnical Card inyectada en:z&Technical Card ya estaba inyectada en:u#  
<style>
.folderInfoIcon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 16px;
    height: 16px;
    margin-left: auto;
    margin-right: 6px;
    border-radius: 50%;
    font-size: 10px;
    font-weight: 700;
    color: #9ca3af;
    background: #111827;
    border: 1px solid #374151;
    box-shadow: 0 1px 6px #0009;
    cursor: pointer;
    opacity: 0.75;
    flex: 0 0 auto;
}

.folderInfoIcon:hover,
.folderInfoIcon.active {
    color: #facc15;
    border-color: #facc15;
    opacity: 1;
}

#folderTechCard {
    position: fixed;
    right: 18px;
    top: 18px;
    width: 390px;
    height: calc(100vh - 36px);
    z-index: 10002;
    background: #111827;
    color: #f9fafb;
    border: 1px solid #374151;
    border-radius: 10px;
    box-shadow: 0 12px 34px #0009;
    display: none;
    overflow: hidden;
}

#folderTechCardHeader {
    height: 36px;
    display: flex;
    align-items: center;
    padding: 0 10px;
    background: #0b1220;
    border-bottom: 1px solid #374151;
    font-size: 12px;
    font-weight: 700;
}

#folderTechCardTitle {
    flex: 1;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

#folderTechCardClose {
    width: 26px;
    height: 26px;
    padding: 0;
    border-radius: 5px;
}

#folderTechCardBody {
    padding: 10px;
    overflow: auto;
    max-height: calc(100vh - 86px);
}

.folderTechField {
    margin-bottom: 8px;
}

.folderTechField label {
    display: block;
    margin-bottom: 3px;
    color: #9ca3af;
    font-size: 10px;
    font-weight: 700;
}

.folderTechField input,
.folderTechField textarea {
    width: 100%;
    box-sizing: border-box;
    border: 1px solid #374151;
    border-radius: 6px;
    background: #030712;
    color: #f9fafb;
    padding: 7px;
    font-size: 12px;
}

#folderDescription {
    min-height: 116px;
    resize: vertical;
}

#folderApis {
    min-height: 180px;
    resize: vertical;
}

.folderTechActions {
    display: flex;
    gap: 8px;
    margin-top: 8px;
}

.folderTechActions button {
    flex: 1;
    height: 30px;
    font-size: 11px;
}

#deleteSelectedGraphs {{
    height: 26px;
    border: 1px solid #444;
    border-radius: 5px;
    background: #171717;
    color: #e5e7eb;
    cursor: pointer;
    font-size: 11px;
    padding: 0 8px;
}}

#deleteSelectedGraphs:hover {{
    background: #262626;
    border-color: #666;
}}


body.editMode .graphOption {{
    cursor: pointer;
}}

body.editMode .graphOption.graphSelected {{
    background: rgba(37, 99, 235, 0.35) !important;
    outline: 1px solid #60a5fa !important;
}}

body.editMode .graphCheck,
body.editMode .folderCheck {{
    width: 14px !important;
    height: 14px !important;
    accent-color: #3b82f6 !important;
    cursor: pointer !important;
}}

#deleteSelectedGraphs {{
    height: 26px;
    border: 1px solid #444;
    border-radius: 5px;
    background: #171717;
    color: #e5e7eb;
    cursor: pointer;
    font-size: 11px;
    padding: 0 8px;
}}

#deleteSelectedGraphs:hover {{
    background: #262626;
    border-color: #666;
}}

</style>

<div id="folderTechCard">
    <div id="folderTechCardHeader">
        <span id="folderTechCardTitle">Graph Technical Card</span>
        <button id="folderTechCardClose" type="button">×</button>
    </div>
    <div id="folderTechCardBody">
        <div class="folderTechField">
            <label>GRAPH NAME</label>
            <input id="folderNameField" type="text">
        </div>

        <div class="folderTechField">
            <label>GRAPH DESCRIPTION — WHAT THIS GRAPH ANSWERS</label>
            <textarea id="folderDescription"></textarea>
        </div>

        <div class="folderTechField">
            <label>ACTIVE APIs</label>
            <textarea id="folderApis"></textarea>
        </div>

        <div class="folderTechActions">
            <button id="folderTechSave" type="button">Save graph card</button>
            <button id="folderTechExport" type="button">Export JSON</button>
        </div>
    </div>
</div>

<script>
(function() {
    const folderMetaKey = "grafoD_folderMeta_v1";
    let activeFolderPath = null;

    let grafoDFolderMetaStore = null;

    function getFolderMetaStore() {
        if (!grafoDFolderMetaStore) grafoDFolderMetaStore = {};
        return grafoDFolderMetaStore;
    }

    function loadFolderMetaStoreFromDatabase() {
        fetch("/api/db")
            .then(r => r.json())
            .then(db => {
                grafoDFolderMetaStore =
                    db && db.ui_state && db.ui_state.folder_meta && typeof db.ui_state.folder_meta === "object"
                        ? db.ui_state.folder_meta
                        : {};
            })
            .catch(() => {});
    }

    function saveFolderMetaStore(store) {
        grafoDFolderMetaStore = store && typeof store === "object" ? store : {};
        fetch("/api/ui-state", {
            method: "POST",
            headers: {"Content-Type": "application/json"},
            body: JSON.stringify({key: "folder_meta", value: grafoDFolderMetaStore})
        }).catch(() => {});
    }

    loadFolderMetaStoreFromDatabase();

    function closeEventCardIfOpen() {
        const eventCard = document.getElementById("techCard");
        if (eventCard) eventCard.style.display = "none";
    }

    function closeFolderCard() {
        activeFolderPath = null;
        document.getElementById("folderTechCard").style.display = "none";
        document.querySelectorAll(".folderInfoIcon").forEach(x => x.classList.remove("active"));
    }

    function openFolderCard(folderPath, icon) {
        const card = document.getElementById("folderTechCard");

        if (activeFolderPath === folderPath && card.style.display === "block") {
            closeFolderCard();
            return;
        }

        closeEventCardIfOpen();

        activeFolderPath = folderPath;

        document.querySelectorAll(".folderInfoIcon").forEach(x => x.classList.remove("active"));
        if (icon) icon.classList.add("active");

        const store = getFolderMetaStore();
        const saved = store[folderPath] || {};

        document.getElementById("folderTechCard").style.display = "block";
        document.getElementById("folderTechCardTitle").textContent = saved.name || folderPath;
        document.getElementById("folderNameField").value = saved.name || folderPath;
        document.getElementById("folderDescription").value = saved.description || "";
        document.getElementById("folderApis").value = saved.apis || "";
    }

    function saveFolderCard() {
        if (!activeFolderPath) return;

        const store = getFolderMetaStore();
        store[activeFolderPath] = {
            name: document.getElementById("folderNameField").value.trim() || activeFolderPath,
            description: document.getElementById("folderDescription").value.trim(),
            apis: document.getElementById("folderApis").value.trim()
        };

        saveFolderMetaStore(store);
        document.getElementById("folderTechCardTitle").textContent = store[activeFolderPath].name;
    }

    function exportFolderCards() {
        const payload = {
            version: "grafoD_folderMeta_v1",
            exportedAt: new Date().toISOString(),
            metadata: getFolderMetaStore()
        };

        const blob = new Blob([JSON.stringify(payload, null, 2)], {type: "application/json"});
        const a = document.createElement("a");
        a.href = URL.createObjectURL(blob);
        a.download = "grafoD_graph_folder_cards.json";
        document.body.appendChild(a);
        a.click();
        a.remove();
    }

    function injectFolderIcons() {
        document.querySelectorAll(".folderHeader").forEach(header => {
            if (header.dataset.folderIconReady === "1") return;

            const folderPath = header.dataset.folder;
            const count = header.querySelector(".folderCount");
            if (!folderPath || !count) return;

            const icon = document.createElement("span");
            icon.className = "folderInfoIcon";
            icon.textContent = "i";
            icon.title = "Open / close graph technical card";
            icon.dataset.folder = folderPath;

            icon.addEventListener("click", function(ev) {
                ev.preventDefault();
                ev.stopPropagation();
                openFolderCard(folderPath, icon);
            });

            header.insertBefore(icon, count);
            header.dataset.folderIconReady = "1";
        });
    }

    const originalRenderFolderTree = window.renderFolderTree;
    if (typeof originalRenderFolderTree === "function") {
        window.renderFolderTree = function() {
            originalRenderFolderTree();
            injectFolderIcons();
        };
    }

    document.getElementById("folderTechSave").addEventListener("click", saveFolderCard);
    document.getElementById("folderTechExport").addEventListener("click", exportFolderCards);
    document.getElementById("folderTechCardClose").addEventListener("click", closeFolderCard);

    injectFolderIcons();
})();
</script>
grafoD_folderMeta_v1z'Safe folder technical card injected in:z(Folder technical card already exists in:a  
<script>
(function() {
    const MARKER = "grafoD_search_hard_fix_v1";

    function normalizeText(s) {
        return String(s || "")
            .toLowerCase()
            .normalize("NFD")
            .replace(/[\u0300-\u036f]/g, "");
    }

    function hardFixSearch() {
        const oldInput =
            document.getElementById("graphSearch") ||
            document.querySelector('input[placeholder="Search"]');

        if (!oldInput || oldInput.dataset.searchHardFixed === "1") return;

        const parent = oldInput.parentElement;
        const newInput = oldInput.cloneNode(true);
        newInput.value = "";
        newInput.dataset.searchHardFixed = "1";
        oldInput.replaceWith(newInput);

        const clearButton =
            parent.querySelector("button") ||
            parent.querySelector(".searchClear") ||
            parent.querySelector("[data-clear-search]");

        let timer = null;

        function applySearchNow() {
            const q = normalizeText(newInput.value.trim());

            const graphOptions = Array.from(document.querySelectorAll(".graphOption"));
            const folderHeaders = Array.from(document.querySelectorAll(".folderHeader"));

            graphOptions.forEach(option => {
                const text = normalizeText(option.textContent);
                const show = !q || text.includes(q);
                option.style.display = show ? "" : "none";

                const details = option.nextElementSibling;
                if (details && details.classList && details.classList.contains("graphDetails")) {
                    if (!show) details.style.display = "none";
                }
            });

            folderHeaders.forEach(header => {
                const folderText = normalizeText(header.textContent);
                let showFolder = !q || folderText.includes(q);

                let node = header.nextElementSibling;
                while (node && !node.classList.contains("folderHeader")) {
                    if (
                        node.classList &&
                        node.classList.contains("graphOption") &&
                        node.style.display !== "none"
                    ) {
                        showFolder = true;
                        break;
                    }
                    node = node.nextElementSibling;
                }

                header.style.display = showFolder ? "" : "none";
            });
        }

        function scheduleSearch() {
            clearTimeout(timer);
            timer = setTimeout(() => {
                requestAnimationFrame(applySearchNow);
            }, 180);
        }

        newInput.addEventListener("input", scheduleSearch);
        newInput.addEventListener("keydown", function(e) {
            if (e.key === "Escape") {
                newInput.value = "";
                applySearchNow();
            }
        });

        if (clearButton && clearButton.dataset.searchClearHardFixed !== "1") {
            const newClear = clearButton.cloneNode(true);
            newClear.dataset.searchClearHardFixed = "1";
            clearButton.replaceWith(newClear);

            newClear.addEventListener("click", function(e) {
                e.preventDefault();
                e.stopPropagation();
                newInput.value = "";
                applySearchNow();
                newInput.focus();
            });
        }
    }

    document.addEventListener("DOMContentLoaded", hardFixSearch);
    setTimeout(hardFixSearch, 300);

    window[MARKER] = true;
})();
</script>
grafoD_search_hard_fix_v1zSearch hard fix injected in:z"Search hard fix already exists in:grafoD_graph_set_cards_v1a  
<style>
.grafoDGraphSetInfoBtn {
  width:17px;
  height:17px;
  min-width:17px;
  border-radius:50%;
  border:1px solid #334155;
  background:#111827;
  color:#94a3b8;
  font-size:11px;
  line-height:15px;
  font-weight:700;
  cursor:pointer;
  text-align:center;
  padding:0;
  margin-left:5px;
}
.grafoDGraphSetInfoBtn:hover,
.grafoDGraphSetInfoBtn.active {
  color:#facc15;
  border-color:#facc15;
  background:#1f2937;
}
.grafoDGraphSetCard {
  position:fixed;
  top:74px;
  right:14px;
  width:380px;
  height:calc(100vh - 98px);
  background:#111827;
  color:#e5e7eb;
  border:1px solid #334155;
  border-radius:10px;
  box-shadow:0 18px 45px rgba(0,0,0,.45);
  z-index:9999;
  padding:12px;
  box-sizing:border-box;
  display:none;
}
.grafoDGraphSetCard.open { display:block; }
.grafoDGraphSetCard h3 { margin:0 0 12px 0;font-size:13px; }
.grafoDGraphSetCard label {
  display:block;
  margin-top:10px;
  margin-bottom:4px;
  color:#9ca3af;
  font-size:10px;
  font-weight:700;
  text-transform:uppercase;
}
.grafoDGraphSetCard input,
.grafoDGraphSetCard textarea {
  width:100%;
  box-sizing:border-box;
  background:#020617;
  color:#e5e7eb;
  border:1px solid #374151;
  border-radius:5px;
  padding:8px;
  font-size:12px;
}
.grafoDGraphSetCard textarea {
  min-height:110px;
  resize:vertical;
}
.grafoDGraphSetCard .actions {
  display:flex;
  gap:8px;
  margin-top:10px;
}
.grafoDGraphSetCard button {
  flex:1;
  height:30px;
  background:#1f2937;
  color:#e5e7eb;
  border:1px solid #374151;
  border-radius:5px;
  cursor:pointer;
}
</style>
u9  
<script>
(function(){
const STORE="grafoD_graph_set_cards_v1";
let activeGraphSet=null;

function clean(v){return (v||"").replace(/\s+/g," ").trim();}
let grafoDGraphSetCardsStore = null;

function load(){
  if(!grafoDGraphSetCardsStore) grafoDGraphSetCardsStore = {};
  return grafoDGraphSetCardsStore;
}

function loadGraphSetCardsFromDatabase(){
  fetch("/api/db")
    .then(r=>r.json())
    .then(db=>{
      grafoDGraphSetCardsStore =
        db && db.ui_state && db.ui_state.graph_set_cards && typeof db.ui_state.graph_set_cards === "object"
          ? db.ui_state.graph_set_cards
          : {};
    })
    .catch(()=>{});
}

function save(o){
  grafoDGraphSetCardsStore = o && typeof o === "object" ? o : {};
  fetch("/api/ui-state", {
    method: "POST",
    headers: {"Content-Type": "application/json"},
    body: JSON.stringify({key: "graph_set_cards", value: grafoDGraphSetCardsStore})
  }).catch(() => {});
}

loadGraphSetCardsFromDatabase();

function graphSetName(row){
  const clone=row.cloneNode(true);
  clone.querySelectorAll("button,input,.grafoDGraphSetInfoBtn").forEach(x=>x.remove());
  let t=clean(clone.innerText||clone.textContent||"");
  t=t.replace(/[▸▾]/g," ");
  t=t.replace(/\s+\d+$/,"");
  return clean(t);
}

function makePanel(){
  if(document.getElementById("grafoDGraphSetCard")) return;

  const p=document.createElement("div");
  p.id="grafoDGraphSetCard";
  p.className="grafoDGraphSetCard";
  p.innerHTML=`
    <h3 id="graphSetCardTitle">Graph card</h3>

    <label>GRAPH NAME</label>
    <input id="graphSetNameField">

    <label>GRAPH DESCRIPTION — WHAT THIS GRAPH ANSWERS</label>
    <textarea id="graphSetDescription"></textarea>

    <label>ACTIVE APIs</label>
    <textarea id="graphSetApis"></textarea>

    <div class="actions">
      <button id="graphSetSave">Save graph card</button>
      <button id="graphSetExport">Export JSON</button>
    </div>
  `;
  document.body.appendChild(p);

  document.getElementById("graphSetSave").onclick=function(){
    if(!activeGraphSet) return;
    const db=load();
    db[activeGraphSet]={
      graph_name:document.getElementById("graphSetNameField").value,
      description:document.getElementById("graphSetDescription").value,
      active_apis:document.getElementById("graphSetApis").value
    };
    save(db);
  };

  document.getElementById("graphSetExport").onclick=function(){
    if(!activeGraphSet) return;
    alert(JSON.stringify((load()[activeGraphSet]||{}),null,2));
  };
}

function openGraphSetCard(name,btn){
  makePanel();

  document.querySelectorAll(".grafoDGraphSetInfoBtn").forEach(b=>b.classList.remove("active"));

  const p=document.getElementById("grafoDGraphSetCard");
  if(p.classList.contains("open") && activeGraphSet===name){
    p.classList.remove("open");
    activeGraphSet=null;
    return;
  }

  activeGraphSet=name;
  btn.classList.add("active");

  const rec=load()[name]||{};
  document.getElementById("graphSetCardTitle").textContent=name;
  document.getElementById("graphSetNameField").value=rec.graph_name||name;
  document.getElementById("graphSetDescription").value=rec.description||"";
  document.getElementById("graphSetApis").value=rec.active_apis||"";

  p.classList.add("open");
}

function install(){
  makePanel();

  document.querySelectorAll(".graphOption").forEach(row=>{
    if(row.querySelector(".grafoDGraphSetInfoBtn")) return;

    const name=graphSetName(row);
    if(!name) return;

    const btn=document.createElement("button");
    btn.type="button";
    btn.className="grafoDGraphSetInfoBtn";
    btn.textContent="i";
    btn.title="Open graph technical card";
    btn.onclick=function(ev){
      ev.preventDefault();
      ev.stopPropagation();
      openGraphSetCard(name,btn);
    };

    row.appendChild(btn);
  });
}

window.addEventListener("load",install);
setTimeout(install,500);
setInterval(install,1500);
})();
</script>
</head>
</head>GRAFO_D_GENERIC_API_SENSOR_V1a  
<script id="GRAFO_D_GENERIC_API_SENSOR_V1">
async function grafoDApiStatusForGraph(row) {
    try {
        const r = await fetch("/api/db?v=" + Date.now());
        const db = await r.json();
        const apis = (db.api_registry || []).filter(a => a.target_graph === row);
        if (!apis.length) return "No active API registered for this graph.";
        return apis.map(a => [
            "API: " + (a.name || a.id || "?"),
            "Enabled: " + String(!!a.enabled),
            "Status: " + (a.status || "unknown"),
            "Method: " + (a.method || "?"),
            "Interval: " + (a.interval_seconds || "?") + " s",
            "Last run: " + (a.last_run_at || "never"),
            "Last status: " + (a.last_status || "unknown"),
            "Last latency: " + (a.last_latency_ms || "n/a") + " ms",
            "Endpoint: " + (a.endpoint || "?")
        ].join("\n")).join("\n\n");
    } catch(e) {
        return "API sensor error: " + e;
    }
}

const _gdOpenTechCardOriginal = openTechCardByEvent;
openTechCardByEvent = async function(e, domEl) {
    _gdOpenTechCardOriginal(e, domEl);
    const apiBox = document.getElementById("techApi");
    if (apiBox) apiBox.value = await grafoDApiStatusForGraph(e.row);
};
</script>
z#Generic API status sensor injected. GRAFO_D_LIVE_API_SENSOR_STYLE_V3a  
<style id="GRAFO_D_LIVE_API_SENSOR_STYLE_V3">
.liveApiSensor {
    margin-top: 12px;
    padding: 10px;
    border: 1px solid #374151;
    background: #0b1220;
    color: #e5e7eb;
    font-family: monospace;
    font-size: 12px;
}
.liveApiSensorTitle { font-size:10px; font-weight:700; color:#9ca3af; margin-bottom:6px; }
.liveApiSensorState { font-size:14px; font-weight:800; margin-bottom:6px; }
.liveApiSensorState.online { color:#22c55e; }
.liveApiSensorState.degraded { color:#f59e0b; }
.liveApiSensorState.offline { color:#ef4444; }
.liveApiSensorState.none { color:#9ca3af; }
.liveApiSensorDetail { white-space:pre-wrap; line-height:1.35; }
</style>
zid="liveApiSensor"ag  
        <div id="liveApiSensor" class="liveApiSensor">
            <div class="liveApiSensorTitle">LIVE API SENSOR</div>
            <div id="liveApiSensorState" class="liveApiSensorState none">NO GRAPH SELECTED</div>
            <div id="liveApiSensorDetail" class="liveApiSensorDetail">Open a graph event to inspect API runtime state.</div>
        </div>
z</div>
    </div>
</div>z</div>
z    </div>
</div>GRAFO_D_LIVE_API_SENSOR_V3u  
<script id="GRAFO_D_LIVE_API_SENSOR_V3">
let grafoDLiveApiSensorRow = null;
let grafoDLiveApiSensorTimer = null;

function grafoDApiRuntimeState(api) {
    const interval = Number(api.interval_seconds || 120);
    const last = api.last_run_at ? new Date(api.last_run_at) : null;
    if (!api.enabled) return {state:"OFFLINE", cls:"offline", reason:"API disabled"};
    if (!last || isNaN(last.getTime())) return {state:"OFFLINE", cls:"offline", reason:"No heartbeat"};
    const ageSec = Math.round((Date.now() - last.getTime()) / 1000);
    const maxAge = Math.max(interval * 2.5, interval + 60);
    if (String(api.last_status) !== "200") return {state:"ERROR", cls:"offline", reason:"HTTP " + api.last_status, ageSec};
    if (ageSec > maxAge) return {state:"OFFLINE", cls:"offline", reason:"Heartbeat expired", ageSec};
    if (Number(api.last_latency_ms || 0) > 8000) return {state:"DEGRADED", cls:"degraded", reason:"High latency", ageSec};
    return {state:"ONLINE", cls:"online", reason:"Heartbeat valid", ageSec};
}

async function grafoDUpdateLiveApiSensor(row) {
    const stateBox = document.getElementById("liveApiSensorState");
    const detailBox = document.getElementById("liveApiSensorDetail");
    if (!stateBox || !detailBox) return;

    try {
        const r = await fetch("/api/db?v=" + Date.now());
        const db = await r.json();
        const apis = (db.api_registry || []).filter(a => a.target_graph === row);

        if (!apis.length) {
            stateBox.className = "liveApiSensorState none";
            stateBox.textContent = "NO API REGISTERED";
            detailBox.textContent = "Graph row: " + row;
            return;
        }

        const api = apis[0];
        const rt = grafoDApiRuntimeState(api);
        stateBox.className = "liveApiSensorState " + rt.cls;
        stateBox.textContent = "● " + rt.state;
        detailBox.textContent = [
            "Graph row: " + row,
            "API: " + (api.name || api.id || "?"),
            "Enabled: " + String(!!api.enabled),
            "Connector status: " + (api.status || "unknown"),
            "Runtime reason: " + rt.reason,
            "Last run: " + (api.last_run_at || "never"),
            "Age: " + (rt.ageSec ?? "n/a") + " s",
            "Interval: " + (api.interval_seconds || "?") + " s",
            "HTTP status: " + (api.last_status || "unknown"),
            "Latency: " + (api.last_latency_ms || "n/a") + " ms"
        ].join("\n");
    } catch(e) {
        stateBox.className = "liveApiSensorState offline";
        stateBox.textContent = "● SENSOR ERROR";
        detailBox.textContent = String(e);
    }
}

const _openTechLiveSensor = openTechCardByEvent;
openTechCardByEvent = async function(e, domEl) {
    await _openTechLiveSensor(e, domEl);
    grafoDLiveApiSensorRow = e.row;
    if (grafoDLiveApiSensorTimer) clearInterval(grafoDLiveApiSensorTimer);
    grafoDUpdateLiveApiSensor(e.row);
    grafoDLiveApiSensorTimer = setInterval(() => grafoDUpdateLiveApiSensor(grafoDLiveApiSensorRow), 5000);
};
</script>
zLive API sensor V3 injected.c                  
   t          d          } |                                 sd S |                     d          }d}||v rd S d}d|v r|                    d|dz             }n|dz   |z   }|                     |d           d S )	Nr   r   r   V47_MANUAL_ELEMENT_CONTEXT_MENUu'  
<script id="V47_MANUAL_ELEMENT_CONTEXT_MENU">
(function(){
    function esc(s) {
        return String(s == null ? "" : s)
            .replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;")
            .replace(/"/g, "&quot;");
    }

    function removeManualMenu() {
        const old = document.getElementById("v47ManualGraphMenu");
        if (old) old.remove();
    }

    function removeManualModal() {
        const old = document.getElementById("v47ManualElementModal");
        if (old) old.remove();
    }

    function graphRowFromTarget(target) {
        const option = target.closest(".graphOption");
        if (!option) return null;

        const check = option.querySelector(".graphCheck");
        if (!check) return null;

        const row = check.value || "";
        const nameEl = option.querySelector(".graphName");
        const label = nameEl ? nameEl.textContent.trim() : row;

        if (!row) return null;

        return {row: row, label: label || row};
    }

    function openManualElementCard(row, label) {
        removeManualMenu();
        removeManualModal();

        const modal = document.createElement("div");
        modal.id = "v47ManualElementModal";
        modal.innerHTML = `
            <div class="v47-modal-backdrop"></div>
            <div class="v47-modal-card">
                <div class="v47-modal-head">
                    <strong>New element — ${esc(label)}</strong>
                    <button type="button" id="v47CloseManualElement">×</button>
                </div>

                <div class="v47-form">
                    <label>GRAPH</label>
                    <input id="v47ElementGraph" value="${esc(row)}" readonly>

                    <label>NAME</label>
                    <input id="v47ElementName" placeholder="Element name">

                    <label>DATE — YEAR - MONTH - DAY</label>
                    <input id="v47ElementDate" placeholder="YYYY-MM-DD">

                    <label>PLACE — COUNTRY - CITY</label>
                    <input id="v47ElementPlace" placeholder="Place">

                    <label>WHO</label>
                    <input id="v47ElementWho" placeholder="Actor / source">

                    <label>BRIEF DESCRIPTION</label>
                    <textarea id="v47ElementDescription" placeholder="Brief description"></textarea>

                    <label>SOURCE LINK</label>
                    <input id="v47ElementSource" placeholder="https://...">
                </div>

                <div class="v47-modal-actions">
                    <button type="button" id="v47CancelManualElement">Cancel</button>
                    <button type="button" id="v47SaveManualElement">Save element</button>
                </div>

                <div id="v47ManualElementStatus"></div>
            </div>
        `;

        document.body.appendChild(modal);

        document.getElementById("v47CloseManualElement").onclick = removeManualModal;
        document.getElementById("v47CancelManualElement").onclick = removeManualModal;

        document.getElementById("v47SaveManualElement").onclick = function(){
            const status = document.getElementById("v47ManualElementStatus");

            const name = document.getElementById("v47ElementName").value.trim();
            const date = document.getElementById("v47ElementDate").value.trim();
            const place = document.getElementById("v47ElementPlace").value.trim();
            const who = document.getElementById("v47ElementWho").value.trim();
            const description = document.getElementById("v47ElementDescription").value.trim();
            const source = document.getElementById("v47ElementSource").value.trim();

            if (!name) {
                status.textContent = "NAME is required.";
                return;
            }

            if (!date) {
                status.textContent = "DATE is required. No timeline element will be fabricated without date.";
                return;
            }

            const payload = {
                row: row,
                row_key: row,
                graph: row,
                graph_row: row,

                name: name,
                title: name,
                event_name: name,

                date: date,
                date_text: date,

                place: place,
                who: who,

                description: description,
                brief_description: description,

                url: source,
                source_link: source,

                source: "manual-ui",
                created_by: "manual-context-menu"
            };

            status.textContent = "Saving...";

            fetch("/api/events", {
                method: "POST",
                headers: {"Content-Type": "application/json"},
                body: JSON.stringify(payload)
            })
            .then(function(r){
                return r.json().then(function(data){
                    if (!r.ok || data.ok === false) {
                        throw new Error(data.error || ("HTTP " + r.status));
                    }
                    return data;
                });
            })
            .then(function(){
                status.textContent = "Saved.";
                window.location.href = window.location.pathname + "?v=manual_element_" + Date.now() + "&select=" + encodeURIComponent(row);
            })
            .catch(function(err){
                status.textContent = "Save failed: " + err.message;
            });
        };

        setTimeout(function(){
            const input = document.getElementById("v47ElementName");
            if (input) input.focus();
        }, 50);
    }

    function showManualGraphMenu(x, y, row, label) {
        removeManualMenu();

        const menu = document.createElement("div");
        menu.id = "v47ManualGraphMenu";
        menu.innerHTML = `
            <button type="button" id="v47AddElementToGraph">Add element</button>
        `;

        menu.style.left = x + "px";
        menu.style.top = y + "px";

        document.body.appendChild(menu);

        document.getElementById("v47AddElementToGraph").onclick = function(){
            openManualElementCard(row, label);
        };
    }

    document.addEventListener("contextmenu", function(ev){
        const graphName = ev.target.closest(".graphName");
        if (!graphName) return;

        const info = graphRowFromTarget(graphName);
        if (!info) return;

        ev.preventDefault();
        showManualGraphMenu(ev.clientX, ev.clientY, info.row, info.label);
    });

    document.addEventListener("click", function(ev){
        const menu = document.getElementById("v47ManualGraphMenu");
        if (menu && !menu.contains(ev.target)) {
            removeManualMenu();
        }
    });

    const style = document.createElement("style");
    style.textContent = `
        #v47ManualGraphMenu {
            position: fixed;
            z-index: 999999;
            background: #101722;
            border: 1px solid #334155;
            border-radius: 6px;
            padding: 6px;
            box-shadow: 0 8px 24px rgba(0,0,0,0.45);
        }

        #v47ManualGraphMenu button {
            background: #1f2937;
            color: #e5e7eb;
            border: 1px solid #475569;
            border-radius: 4px;
            padding: 6px 12px;
            cursor: pointer;
            font-size: 12px;
        }

        #v47ManualGraphMenu button:hover {
            background: #263449;
        }

        #v47ManualElementModal {
            position: fixed;
            inset: 0;
            z-index: 999998;
            font-family: system-ui, sans-serif;
        }

        #v47ManualElementModal .v47-modal-backdrop {
            position: absolute;
            inset: 0;
            background: rgba(0,0,0,0.55);
        }

        #v47ManualElementModal .v47-modal-card {
            position: absolute;
            right: 28px;
            top: 70px;
            width: 360px;
            max-height: calc(100vh - 100px);
            overflow: auto;
            background: #0f1724;
            color: #e5e7eb;
            border: 1px solid #334155;
            border-radius: 10px;
            box-shadow: 0 12px 36px rgba(0,0,0,0.55);
            padding: 12px;
        }

        #v47ManualElementModal .v47-modal-head {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 12px;
        }

        #v47ManualElementModal .v47-modal-head button {
            background: #1f2937;
            color: #e5e7eb;
            border: 1px solid #475569;
            border-radius: 4px;
            cursor: pointer;
        }

        #v47ManualElementModal .v47-form label {
            display: block;
            font-size: 10px;
            color: #9ca3af;
            margin-top: 8px;
            margin-bottom: 3px;
        }

        #v47ManualElementModal input,
        #v47ManualElementModal textarea {
            width: 100%;
            box-sizing: border-box;
            background: #020817;
            color: #e5e7eb;
            border: 1px solid #334155;
            border-radius: 5px;
            padding: 7px;
            font-size: 12px;
        }

        #v47ManualElementModal textarea {
            min-height: 92px;
            resize: vertical;
        }

        #v47ManualElementModal .v47-modal-actions {
            display: flex;
            gap: 8px;
            margin-top: 12px;
        }

        #v47ManualElementModal .v47-modal-actions button {
            flex: 1;
            background: #1f2937;
            color: #e5e7eb;
            border: 1px solid #475569;
            border-radius: 5px;
            padding: 7px;
            cursor: pointer;
        }

        #v47ManualElementModal .v47-modal-actions button:hover {
            background: #263449;
        }

        #v47ManualElementStatus {
            margin-top: 8px;
            min-height: 16px;
            font-size: 11px;
            color: #fbbf24;
        }
    `;
    document.head.appendChild(style);
})();
</script>
r   r   
)_V47Pathr!   	read_textr   
write_text	html_pathhtmltoken	injections       r   '_v47_inject_manual_element_context_menur   S  s    ;<<I 00D-E}}EIN
 D||Iy;'>??d{Y&00000r   c                  
   t          d          } |                                 sd S |                     d          }d}||v rd S d}d|v r|                    d|dz             }n|d|z   z  }|                     |d           d S )	Nr   r   r   &V47B_FORCE_MANUAL_ELEMENT_GRAPH_RENDERa+	  
<script id="V47B_FORCE_MANUAL_ELEMENT_GRAPH_RENDER">
(function(){
    function getParam(name) {
        const p = new URLSearchParams(window.location.search);
        return p.get(name);
    }

    function forceSelectGraphRow(row) {
        if (!row) return;

        try {
            if (typeof setGraphSelected === "function") {
                setGraphSelected(row, true);
            }

            document.querySelectorAll('.graphCheck[value="' + row.replace(/"/g, '\\"') + '"]').forEach(function(chk){
                chk.checked = true;
            });

            if (Array.isArray(window.selectionOrder)) {
                if (!window.selectionOrder.includes(row)) {
                    window.selectionOrder.push(row);
                }
            } else if (typeof selectionOrder !== "undefined" && Array.isArray(selectionOrder)) {
                if (!selectionOrder.includes(row)) {
                    selectionOrder.push(row);
                }
            }

            if (typeof renderSelectedTimeline === "function") {
                renderSelectedTimeline();
            } else if (typeof renderSelectedGraphs === "function") {
                renderSelectedGraphs();
            } else if (typeof applyFilter === "function") {
                applyFilter();
            }

            if (typeof applyZoom === "function") {
                applyZoom();
            }
        } catch (err) {
            console.warn("V47B forceSelectGraphRow failed:", err);
        }
    }

    function debugManualElementPresence() {
        try {
            if (typeof eventData === "undefined") return;
            const manual = eventData.filter(function(e){
                return String(e.row || "").toUpperCase() === "SPACE" &&
                       String(e.name || "").toUpperCase().includes("COHETE");
            });
            console.log("V47B manual element debug:", manual);
        } catch (err) {
            console.warn("V47B debug failed:", err);
        }
    }

    const row = getParam("select");
    if (row) {
        setTimeout(function(){
            forceSelectGraphRow(row);
            debugManualElementPresence();
        }, 250);

        setTimeout(function(){
            forceSelectGraphRow(row);
        }, 1000);
    }

    window.v47ForceSelectGraphRow = forceSelectGraphRow;
})();
</script>
r   r   r   )	_V47BPathr!   r   r   r   r   s       r   &_v47b_inject_force_manual_graph_renderr     s    <==I 00D4E}}JIX D||Iy;'>??y  00000r   c                  
   t          d          } |                                 sd S |                     d          }d}||v rd S d}d|v r|                    d|dz             }n|dz   |z   }|                     |d           d S )	Nr   r   r   V49_SAVE_BUTTON_REPLACES_CLEARa  
<script id="V49_SAVE_BUTTON_REPLACES_CLEAR">
(function(){
    function textOf(el) {
        return String((el && (el.textContent || el.value)) || "").trim().toLowerCase();
    }

    function findClearButton() {
        const candidates = Array.from(document.querySelectorAll("button,input[type='button'],input[type='submit']"));

        for (const el of candidates) {
            const t = textOf(el);
            if (t === "limpiar" || t === "clear") return el;
        }

        for (const el of candidates) {
            const t = textOf(el);
            if (t.includes("limpiar") || t.includes("clear")) return el;
        }

        return null;
    }

    function setStatus(msg, ok) {
        let box = document.getElementById("v49SaveStatus");
        if (!box) {
            box = document.createElement("span");
            box.id = "v49SaveStatus";
            box.style.marginLeft = "8px";
            box.style.fontSize = "12px";
            box.style.fontFamily = "system-ui, sans-serif";
            const btn = document.getElementById("v49SaveButton");
            if (btn && btn.parentNode) btn.parentNode.insertBefore(box, btn.nextSibling);
            else document.body.appendChild(box);
        }

        box.textContent = msg;
        box.style.color = ok ? "#4ade80" : "#fbbf24";
    }

    async function persistOpenTechnicalCardIfPresent() {
        const buttons = Array.from(document.querySelectorAll("button,input[type='button'],input[type='submit']"));

        const saveCard = buttons.find(function(btn){
            const t = textOf(btn);
            return t === "save card" || t === "save element" || t === "save";
        });

        if (!saveCard || saveCard.id === "v49SaveButton") return;

        try {
            saveCard.click();
            await new Promise(function(resolve){ setTimeout(resolve, 450); });
        } catch (err) {
            console.warn("V49 Save: technical card save click failed:", err);
        }
    }

    async function persistFolderStateIfPresent() {
        try {
            if (typeof getFolderState === "function" && typeof saveFolderState === "function") {
                const state = getFolderState();
                const result = saveFolderState(state);
                if (result && typeof result.then === "function") {
                    await result;
                } else {
                    await new Promise(function(resolve){ setTimeout(resolve, 250); });
                }
            }
        } catch (err) {
            console.warn("V49 Save: folder state save failed:", err);
        }
    }

    async function saveSession() {
        const btn = document.getElementById("v49SaveButton");
        if (btn) {
            btn.disabled = true;
            btn.textContent = "Saving...";
        }

        setStatus("Saving session...", false);

        try {
            await persistOpenTechnicalCardIfPresent();
            await persistFolderStateIfPresent();

            const response = await fetch("/api/session/save", {
                method: "POST",
                headers: {"Content-Type": "application/json"},
                body: JSON.stringify({
                    source: "v49-save-button",
                    timestamp: new Date().toISOString()
                })
            });

            let data = {};
            try {
                data = await response.json();
            } catch (err) {
                data = {ok: false, error: "Invalid JSON response"};
            }

            if (!response.ok || data.ok === false) {
                throw new Error(data.error || ("SQLite integrity: " + data.sqlite_integrity) || ("HTTP " + response.status));
            }

            setStatus(
                "Saved. SQLite OK. Graphs: " + data.graphs + " Events: " + data.events,
                true
            );

            if (btn) {
                btn.textContent = "Save";
                btn.disabled = false;
            }

        } catch (err) {
            setStatus("Save failed: " + err.message, false);

            if (btn) {
                btn.textContent = "Save";
                btn.disabled = false;
            }
        }
    }

    function installSaveButton() {
        let btn = document.getElementById("v49SaveButton");

        if (!btn) {
            const clearBtn = findClearButton();

            if (clearBtn) {
                const clone = clearBtn.cloneNode(true);
                clone.id = "v49SaveButton";
                clone.textContent = "Save";
                clone.value = "Save";
                clone.title = "Save current session to SQLite";
                clone.dataset.v49Save = "1";

                clearBtn.parentNode.replaceChild(clone, clearBtn);
                btn = clone;
            }
        }

        if (!btn) {
            const container =
                document.querySelector(".toolbar") ||
                document.querySelector("header") ||
                document.body;

            btn = document.createElement("button");
            btn.id = "v49SaveButton";
            btn.type = "button";
            btn.textContent = "Save";
            btn.title = "Save current session to SQLite";
            container.insertBefore(btn, container.firstChild);
        }

        if (btn.dataset.v49Bound === "1") return;

        btn.dataset.v49Bound = "1";
        btn.textContent = "Save";
        btn.value = "Save";
        btn.title = "Save current session to SQLite";

        btn.addEventListener("click", function(ev){
            ev.preventDefault();
            ev.stopPropagation();
            ev.stopImmediatePropagation();
            saveSession();
        }, true);
    }

    document.addEventListener("DOMContentLoaded", installSaveButton);
    setTimeout(installSaveButton, 250);
    setTimeout(installSaveButton, 1000);

    window.v49SaveSession = saveSession;
})();
</script>
r   r   r   )_V49SavePathr!   r   r   r   r   s       r   &_v49_inject_save_button_replaces_clearr     s    ?@@I 00D,E}}vIp D||Iy;'>??d{Y&00000r   c                     t          d          } |                                 sd S |                     d          }t          j        dd|t          j                  }d}t          j        d||t          j                  }t          j        d	||d
t          j                  }d}||v r|                     |d           d S d}d|v r|                    d|dz             n|dz   |z   }|                     |d           d S )Nr   r   r   z0<script[^>]*id="V49[A-J][^"]*"[^>]*>.*?</script>r   flagsz<button id="v49SaveButton" type="button" title="Autosave status">Save <span id="v49SaveDotFinal" aria-hidden="true"></span></button>zN<button\s+onclick=["\']selectNone\(\)["\']>\s*(Save|Limpiar|Clear)\s*</button>z6<button\s+id=["\']v49SaveButton["\'][^>]*>.*?</button>r=   countr   (V49K_SINGLE_STABLE_TOP_RIGHT_SAVE_SENSORuL  
<script id="V49K_SINGLE_STABLE_TOP_RIGHT_SAVE_SENSOR">
(function(){
    let currentState = "unknown";
    let saving = false;

    function ensureHostAndButton() {
        let host = document.getElementById("v49TopRightSaveHost");
        if (!host) {
            host = document.createElement("div");
            host.id = "v49TopRightSaveHost";
            document.body.appendChild(host);
        }

        let btn = document.getElementById("v49SaveButton");
        if (!btn) {
            btn = document.createElement("button");
            btn.id = "v49SaveButton";
            btn.type = "button";
        }

        btn.onclick = null;

        if (!document.getElementById("v49SaveDotFinal")) {
            btn.innerHTML = 'Save <span id="v49SaveDotFinal" aria-hidden="true"></span>';
        }

        if (btn.parentElement !== host) {
            host.appendChild(btn);
        }

        return btn;
    }

    function setState(state, msg) {
        if (state === currentState && state !== "saving") {
            const b0 = document.getElementById("v49SaveButton");
            if (b0 && msg) b0.title = msg;
            return;
        }

        currentState = state;

        const btn = ensureHostAndButton();
        const dot = document.getElementById("v49SaveDotFinal");

        btn.classList.remove("save-ok", "save-saving", "save-error");
        if (dot) dot.className = "";

        if (state === "ok") {
            btn.classList.add("save-ok");
            if (dot) dot.classList.add("ok");
        } else if (state === "saving") {
            btn.classList.add("save-saving");
            if (dot) dot.classList.add("saving");
        } else {
            btn.classList.add("save-error");
            if (dot) dot.classList.add("error");
        }

        btn.title = msg || "";
        if (dot) dot.title = msg || "";
    }

    async function readStatus() {
        if (saving) return;

        try {
            const r = await fetch("/api/autosave/status", {cache: "no-store"});
            const data = await r.json();

            if (!r.ok || data.ok === false || data.sqlite_integrity !== "ok" || data.dirty) {
                throw new Error(data.last_error || data.error || data.sqlite_integrity || ("HTTP " + r.status));
            }

            setState(
                "ok",
                "Autosaved. SQLite integrity: " + data.sqlite_integrity +
                ". Graphs: " + data.graphs +
                ". Events: " + data.events +
                ". Folders: " + data.folders +
                ". Revision: " + data.revision
            );
        } catch (err) {
            setState("error", "Autosave not verified: " + String(err.message || err));
        }
    }

    async function saveNow(ev) {
        if (ev) {
            ev.preventDefault();
            ev.stopPropagation();
            ev.stopImmediatePropagation();
        }

        const btn = ensureHostAndButton();
        saving = true;
        btn.disabled = true;
        setState("saving", "Saving and verifying SQLite...");

        try {
            const payload = {
                source: "v49k-single-save",
                timestamp: new Date().toISOString()
            };

            if (typeof getFolderState === "function") {
                payload.ui_state = getFolderState();
            }

            const r = await fetch("/api/autosave/save", {
                method: "POST",
                headers: {"Content-Type": "application/json"},
                body: JSON.stringify(payload)
            });

            const data = await r.json();

            if (!r.ok || data.ok === false || data.sqlite_integrity !== "ok") {
                throw new Error(data.error || data.last_error || data.sqlite_integrity || ("HTTP " + r.status));
            }

            setState(
                "ok",
                "Saved. SQLite integrity: " + data.sqlite_integrity +
                ". Graphs: " + data.graphs +
                ". Events: " + data.events +
                ". Folders: " + data.folders
            );
        } catch (err) {
            setState("error", "Save failed or not verified: " + String(err.message || err));
        } finally {
            saving = false;
            btn.disabled = false;
        }
    }

    function install() {
        const btn = ensureHostAndButton();

        if (btn.dataset.v49kBound !== "1") {
            btn.dataset.v49kBound = "1";
            btn.addEventListener("click", saveNow, true);
        }

        readStatus();
    }

    const style = document.createElement("style");
    style.id = "V49K_SINGLE_STABLE_TOP_RIGHT_SAVE_SENSOR_STYLE";
    style.textContent = `
        #v49AutosavePanel,
        #v49SaveToast {
            display: none !important;
            visibility: hidden !important;
            opacity: 0 !important;
            pointer-events: none !important;
        }

        #v49TopRightSaveHost {
            position: fixed !important;
            top: 8px !important;
            right: 18px !important;
            z-index: 2147483647 !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            pointer-events: auto !important;
        }

        #v49SaveButton {
            min-width: 78px !important;
            height: 30px !important;
            padding: 0 12px !important;
            display: inline-flex !important;
            align-items: center !important;
            justify-content: center !important;
            gap: 7px !important;
            border-radius: 7px !important;
            border: 1px solid #334155 !important;
            background: #1f2937 !important;
            color: #e5e7eb !important;
            font-size: 12px !important;
            line-height: 1 !important;
            cursor: pointer !important;
        }

        #v49SaveButton.save-ok { border-color: #22c55e !important; }
        #v49SaveButton.save-saving { border-color: #f59e0b !important; }
        #v49SaveButton.save-error { border-color: #ef4444 !important; }

        #v49SaveDotFinal {
            display: inline-block !important;
            width: 8px !important;
            height: 8px !important;
            min-width: 8px !important;
            min-height: 8px !important;
            border-radius: 50% !important;
            background: #ef4444 !important;
        }

        #v49SaveDotFinal.ok { background: #22c55e !important; }
        #v49SaveDotFinal.saving { background: #f59e0b !important; }
        #v49SaveDotFinal.error { background: #ef4444 !important; }
    `;
    document.head.appendChild(style);

    document.addEventListener("DOMContentLoaded", install);
    setTimeout(install, 100);
    setTimeout(install, 600);

    // Poll estable: no remonta el botón; solo lee estado real.
    setInterval(readStatus, 8000);

    window.v49kSaveNow = saveNow;
    window.v49kReadAutosaveStatus = readStatus;
})();
</script>
r   r   r   )		_V49KPathr!   r   _v49k_resubSIr   r   )r   r   buttonr   r   s        r   0_v49k_inject_single_stable_top_right_save_sensorr     sH   <==I 00D <;
j	  D TF<Yj	  D <Aj  D 7E}}TG444ZIx @ID?P?P4<<	9{#:;;;VZ]aVadmVmD00000r   c                  2   t          d          } |                                 sd S |                     d          }t          j        dd|t          j                  }t          j        dd|t          j                  }|                     |d           d S )Nr   r   r   zB<script[^>]*id="V48_PHYSICAL_DELETE_UI_OVERRIDE"[^>]*>.*?</script>r   r   z?<script[^>]*id="V50[^"]*(FOLDER|folder)[^"]*"[^>]*>.*?</script>)_V51CleanPathr!   r   _v51_clean_rer   r   r   r   r   s     r   ._v51_clean_old_delete_folder_generated_scriptsr     s    @AAI 00DM
o	  D J
o	  D 00000r   c                      t          d          } |                                 sd S |                     d          }t          j        dd|t          j                  }|                     |d           d S )Nr   r   r   z@<script[^>]*id="V52_EVENT_CARD_CANONICAL_SAVE"[^>]*>.*?</script>r   r   )	_V52BPathr!   r   _v52b_rer   r   r   r   s     r   (_v52b_remove_broken_event_card_interceptr   %  s    <==I 00D<K
j	  D 00000r   c                  >   t          d          } |                                 sd S |                     d          }t          j        dd|t          j                  }d}d|v r|                    d|d	z             }n|d
|z   z  }|                     |d           d S )Nr   r   r   zD<script[^>]*id="V52C_MASTER_SAVE_FLUSH_OPEN_CARDS"[^>]*>.*?</script>r   r   av  
<script id="V52C_MASTER_SAVE_FLUSH_OPEN_CARDS">
(function(){
    function visible(el) {
        if (!el) return false;
        const cs = window.getComputedStyle(el);
        if (cs.display === "none" || cs.visibility === "hidden" || cs.opacity === "0") return false;
        const rect = el.getBoundingClientRect();
        return rect.width > 0 && rect.height > 0;
    }

    function delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    async function clickIfVisible(buttonId, panelIds) {
        const btn = document.getElementById(buttonId);
        if (!btn) return false;

        let panelVisible = false;
        for (const id of panelIds) {
            const panel = document.getElementById(id);
            if (visible(panel)) {
                panelVisible = true;
                break;
            }
        }

        if (!panelVisible) return false;

        btn.click();
        await delay(700);
        return true;
    }

    async function flushOpenCardsBeforeMasterSave() {
        const flushed = [];

        /*
         * Existing technical event card.
         * This is the card that draws PLACE / WHO indicators.
         */
        if (await clickIfVisible("techSave", ["techCard"])) {
            flushed.push("event-card");
        }

        /*
         * Existing folder technical card.
         */
        if (await clickIfVisible("folderTechSave", ["folderTechCard"])) {
            flushed.push("folder-card");
        }

        /*
         * Existing graph-set card.
         */
        if (await clickIfVisible("graphSetSave", ["grafoDGraphSetCard"])) {
            flushed.push("graph-card");
        }

        return flushed;
    }

    function patchV49KSaveFunction() {
        /*
         * V49K owns the top-right Save button. Instead of replacing it,
         * this capture listener runs first and marks that the open cards
         * were flushed. The V49K listener then continues to /api/autosave/save.
         */
        const btn = document.getElementById("v49SaveButton");
        if (!btn || btn.dataset.v52cFlushBound === "1") return;

        btn.dataset.v52cFlushBound = "1";

        btn.addEventListener("click", async function(ev){
            if (btn.dataset.v52cFlushing === "1") return;

            ev.preventDefault();
            ev.stopPropagation();
            ev.stopImmediatePropagation();

            btn.dataset.v52cFlushing = "1";
            const originalTitle = btn.title || "";
            btn.title = "Flushing open cards before SQLite save";

            try {
                await flushOpenCardsBeforeMasterSave();
            } catch (err) {
                console.warn("V52C pre-save flush failed:", err);
            } finally {
                btn.dataset.v52cFlushing = "0";
                btn.title = originalTitle || "Autosave status";

                /*
                 * Re-dispatch the click so the existing V49K Save handler
                 * performs /api/autosave/save and SQLite verification.
                 */
                setTimeout(function(){
                    btn.click();
                }, 50);
            }
        }, true);
    }

    document.addEventListener("DOMContentLoaded", patchV49KSaveFunction);
    setTimeout(patchV49KSaveFunction, 300);
    setTimeout(patchV49KSaveFunction, 1000);

    window.v52cFlushOpenCardsBeforeMasterSave = flushOpenCardsBeforeMasterSave;
})();
</script>
r   r   r   )	_V52CPathr!   r   _v52c_rer   r   r   r   r   r   r   s      r   )_v52c_inject_master_save_flush_open_cardsr   @  s    <==I 00D<O
j	  DoIb D||Iy;'>??y  00000r   c                  |   t          d          } |                                 sd S |                     d          }d}||v rd S d}d}d|v r|                    d|dz   d	          }n|d
z   |z   }d|v r|                    d|dz   d	          }n|d
|z   z  }|                     |d           t          d           d S )Nr   r   r   -GRAFO_D_V55_FRONT_FIELDS_AND_API_SENSOR_FINALa~  
<style id="GRAFO_D_V55_FRONT_FIELDS_AND_API_SENSOR_FINAL_STYLE">
.liveApiSensor {
    margin-top: 12px;
    padding: 10px;
    border: 1px solid #374151;
    background: #0b1220;
    color: #e5e7eb;
    font-family: monospace;
    font-size: 12px;
}
.liveApiSensorTitle {
    font-size: 10px;
    font-weight: 700;
    color: #9ca3af;
    margin-bottom: 6px;
}
.liveApiSensorState {
    font-size: 14px;
    font-weight: 800;
    margin-bottom: 6px;
}
.liveApiSensorState.active,
.liveApiSensorState.online {
    color: #22c55e;
}
.liveApiSensorState.degraded {
    color: #f59e0b;
}
.liveApiSensorState.inactive,
.liveApiSensorState.offline {
    color: #ef4444;
}
.liveApiSensorState.unknown,
.liveApiSensorState.none {
    color: #9ca3af;
}
.liveApiSensorDetail {
    white-space: pre-wrap;
    line-height: 1.35;
}
.eventPlaceTag,
.eventWhoTag {
    max-width: 28ch !important;
}
</style>
uHE  
<script id="GRAFO_D_V55_FRONT_FIELDS_AND_API_SENSOR_FINAL">
(function() {
    function firstNonEmpty() {
        for (const value of arguments) {
            if (value !== undefined && value !== null && String(value).trim() !== "") {
                return value;
            }
        }
        return "";
    }

    function eventStableIdV55(e) {
        if (typeof stableEventId === "function") {
            return stableEventId(e);
        }
        return String(e.row || "") + ":" + String(e.start || "") + ":" + String(e.end || "") + ":" + String(e.name || "");
    }

    function eventMetaV55(e) {
        try {
            if (typeof getEventMetaStore === "function") {
                const store = getEventMetaStore();
                return store[eventStableIdV55(e)] || {};
            }
        } catch (err) {}
        return {};
    }

    function placeOfEventV55(e) {
        const meta = eventMetaV55(e);
        return firstNonEmpty(meta.place, meta.PLACE, e.place, e.PLACE);
    }

    function whoOfEventV55(e) {
        const meta = eventMetaV55(e);
        return firstNonEmpty(meta.who, meta.WHO, e.who, e.WHO);
    }

    function briefOfEventV55(e) {
        const meta = eventMetaV55(e);
        return firstNonEmpty(
            meta.description,
            meta.brief_description,
            meta.brief,
            meta.BRIEF,
            e.description,
            e.brief_description,
            e.brief,
            e.BRIEF
        );
    }

    function dateOfEventV55(e) {
        const meta = eventMetaV55(e);
        return firstNonEmpty(meta.date, meta.date_text, e.date, e.date_text, typeof yearForEvent === "function" ? yearForEvent(e) : "");
    }

    function sourceOfEventV55(e) {
        const meta = eventMetaV55(e);
        return firstNonEmpty(meta.source_link, meta.source, e.source_link, e.source);
    }

    function apiTextOfEventV55(e) {
        const meta = eventMetaV55(e);
        return firstNonEmpty(meta.api, meta.api_text, e.api, e.api_text);
    }

    function addVisibleTagsV55() {
        if (typeof eventData === "undefined" || !Array.isArray(eventData)) return;

        document.querySelectorAll(".event").forEach(function(box) {
            const id = box.dataset.eventId || "";
            const e = eventData.find(function(item) {
                return eventStableIdV55(item) === id;
            });

            if (!e) return;

            const place = placeOfEventV55(e);
            const who = whoOfEventV55(e);

            box.querySelectorAll(".eventPlaceTag.v55, .eventWhoTag.v55").forEach(function(old) {
                old.remove();
            });

            if (place) {
                const tag = document.createElement("span");
                tag.className = "eventPlaceTag v55";
                tag.textContent = String(place).slice(0, 28);
                tag.title = String(place);
                box.appendChild(tag);
            }

            if (who) {
                const tag = document.createElement("span");
                tag.className = "eventWhoTag v55";
                tag.textContent = String(who).slice(0, 28);
                tag.title = String(who);
                box.appendChild(tag);
            }

            const brief = briefOfEventV55(e);
            const titleParts = [
                e.name || "",
                e.row ? "fila " + e.row : "",
                place ? "PLACE: " + place : "",
                who ? "WHO: " + who : "",
                brief ? "BRIEF: " + brief : ""
            ].filter(Boolean);

            box.title = titleParts.join(" | ");
        });
    }

    if (typeof applyFilter === "function" && !window.__grafoDV55ApplyFilterWrapped) {
        window.__grafoDV55ApplyFilterWrapped = true;

        const originalApplyFilterV55 = applyFilter;

        applyFilter = function() {
            const result = originalApplyFilterV55.apply(this, arguments);
            setTimeout(addVisibleTagsV55, 0);
            return result;
        };

        window.applyFilter = applyFilter;
    }

    function normalizeApiStatusV55(api) {
        const raw = String(
            api.runtime_status ||
            api.status ||
            api.last_status ||
            api.http_status ||
            "unknown"
        ).toLowerCase();

        if (["active", "online", "ok", "success", "healthy", "200"].includes(raw)) return "active";
        if (["degraded", "slow", "warning", "partial"].includes(raw)) return "degraded";
        if (["inactive", "offline", "error", "failed", "timeout", "unhealthy"].includes(raw)) return "inactive";

        if (api.ok === true) return "active";
        if (api.ok === false) return "inactive";

        const http = Number(api.http_status || api.last_status || 0);
        if (http >= 200 && http < 300) return "active";
        if (http >= 400) return "inactive";

        return "unknown";
    }

    async function fetchApiStatusV55(row) {
        const encoded = encodeURIComponent(row || "");

        try {
            const response = await fetch("/api/apis/status?graph=" + encoded + "&v=" + Date.now(), {
                cache: "no-store"
            });

            if (response.ok) {
                const data = await response.json();
                const apis = Array.isArray(data.apis) ? data.apis : [];

                return apis.filter(function(api) {
                    return !row ||
                        api.target_graph === row ||
                        api.graph === row ||
                        api.row === row ||
                        api.name === row;
                });
            }
        } catch (err) {
            // El backend viejo todavía no tiene /api/apis/status.
            // Se usa fallback por /api/db para no romper el tablero.
        }

        try {
            const response = await fetch("/api/db?v=" + Date.now(), {
                cache: "no-store"
            });

            const db = await response.json();
            const apis = Array.isArray(db.api_registry) ? db.api_registry : [];

            return apis
                .filter(function(api) {
                    return !row ||
                        api.target_graph === row ||
                        api.graph === row ||
                        api.row === row ||
                        api.name === row;
                })
                .map(function(api) {
                    return {
                        id: api.id,
                        name: api.name,
                        enabled: api.enabled !== false,
                        target_graph: api.target_graph || api.graph || api.row || "",
                        endpoint: api.endpoint || api.url || "",
                        status: normalizeApiStatusV55(api),
                        runtime_status: normalizeApiStatusV55(api),
                        http_status: api.http_status || api.last_status || "",
                        latency_ms: api.latency_ms || api.last_latency_ms || "",
                        last_checked: api.last_checked || api.last_run_at || api.updated_at || "",
                        error: api.error || api.last_error || ""
                    };
                });
        } catch (err) {
            return [{
                name: "API SENSOR",
                target_graph: row,
                enabled: false,
                status: "inactive",
                runtime_status: "inactive",
                error: String(err)
            }];
        }
    }

    async function apiStatusTextForGraphV55(row) {
        const apis = await fetchApiStatusV55(row);

        if (!apis.length) {
            return "No active API registered for this graph.";
        }

        return apis.map(function(api) {
            const status = normalizeApiStatusV55(api);

            return [
                "API: " + (api.name || api.id || "?"),
                "Graph: " + (api.target_graph || api.graph || row || "?"),
                "Enabled: " + String(api.enabled !== false),
                "Runtime status: " + status,
                "HTTP status: " + (api.http_status || api.last_status || "unknown"),
                "Latency: " + (api.latency_ms || api.last_latency_ms || "n/a") + " ms",
                "Last checked: " + (api.last_checked || api.last_run_at || api.updated_at || "never"),
                "Endpoint: " + (api.endpoint || api.url || "?"),
                "Error: " + (api.error || api.last_error || "none")
            ].join("\n");
        }).join("\n\n");
    }

    async function updateLiveApiSensorV55(row) {
        const stateBox = document.getElementById("liveApiSensorState");
        const detailBox = document.getElementById("liveApiSensorDetail");

        if (!stateBox || !detailBox) return;

        const apis = await fetchApiStatusV55(row);

        if (!apis.length) {
            stateBox.className = "liveApiSensorState none";
            stateBox.textContent = "NO API REGISTERED";
            detailBox.textContent = "Graph row: " + row;
            return;
        }

        const priority = {
            inactive: 3,
            degraded: 2,
            unknown: 1,
            active: 0
        };

        let worst = "active";

        apis.forEach(function(api) {
            const status = normalizeApiStatusV55(api);
            if ((priority[status] || 1) > (priority[worst] || 0)) {
                worst = status;
            }
        });

        stateBox.className = "liveApiSensorState " + worst;
        stateBox.textContent = "● " + worst.toUpperCase();

        detailBox.textContent = apis.map(function(api) {
            const status = normalizeApiStatusV55(api);

            return [
                "Graph row: " + row,
                "API: " + (api.name || api.id || "?"),
                "Enabled: " + String(api.enabled !== false),
                "Runtime status: " + status,
                "HTTP status: " + (api.http_status || api.last_status || "unknown"),
                "Latency: " + (api.latency_ms || api.last_latency_ms || "n/a") + " ms",
                "Last checked: " + (api.last_checked || api.last_run_at || api.updated_at || "never"),
                "Endpoint: " + (api.endpoint || api.url || "?"),
                "Error: " + (api.error || api.last_error || "none")
            ].join("\n");
        }).join("\n\n");
    }

    if (typeof openTechCardByEvent === "function" && !window.__grafoDV55OpenTechWrapped) {
        window.__grafoDV55OpenTechWrapped = true;

        const originalOpenTechCardV55 = openTechCardByEvent;

        openTechCardByEvent = async function(e, domEl) {
            const result = await originalOpenTechCardV55.apply(this, arguments);

            const place = placeOfEventV55(e);
            const who = whoOfEventV55(e);
            const brief = briefOfEventV55(e);
            const date = dateOfEventV55(e);
            const source = sourceOfEventV55(e);
            const apiText = apiTextOfEventV55(e);

            const title = document.getElementById("techCardTitle");
            const group = document.getElementById("techGroup");
            const name = document.getElementById("techName");
            const dateField = document.getElementById("techDate");
            const placeField = document.getElementById("techPlace");
            const whoField = document.getElementById("techWho");
            const descriptionField = document.getElementById("techDescription");
            const sourceField = document.getElementById("techSourceLink");
            const apiField = document.getElementById("techApi");

            if (title) title.textContent = e.name || "Technical Card";
            if (group && typeof currentFolderForRow === "function") group.value = currentFolderForRow(e.row);
            if (name) name.value = e.name || "";
            if (dateField) dateField.value = date;
            if (placeField) placeField.value = place;
            if (whoField) whoField.value = who;
            if (descriptionField) descriptionField.value = brief;
            if (sourceField) sourceField.value = source;

            if (apiField) {
                const liveText = await apiStatusTextForGraphV55(e.row);
                apiField.value = liveText || apiText || "";
            }

            updateLiveApiSensorV55(e.row);

            if (window.__grafoDV55ApiTimer) {
                clearInterval(window.__grafoDV55ApiTimer);
            }

            window.__grafoDV55ApiTimer = setInterval(function() {
                updateLiveApiSensorV55(e.row);
            }, 15000);

            return result;
        };

        window.openTechCardByEvent = openTechCardByEvent;
    }

    window.saveTechCard = async function saveTechCardV55() {
        if (typeof activeEventObj === "undefined" || !activeEventObj) return;

        const oldId = typeof activeEventId !== "undefined" ? activeEventId : eventStableIdV55(activeEventObj);

        const name = document.getElementById("techName") ? document.getElementById("techName").value.trim() : "";
        const date = document.getElementById("techDate") ? document.getElementById("techDate").value.trim() : "";
        const group = document.getElementById("techGroup") ? document.getElementById("techGroup").value.trim() : "";
        const place = document.getElementById("techPlace") ? document.getElementById("techPlace").value.trim() : "";
        const who = document.getElementById("techWho") ? document.getElementById("techWho").value.trim() : "";
        const brief = document.getElementById("techDescription") ? document.getElementById("techDescription").value.trim() : "";
        const sourceLink = document.getElementById("techSourceLink") ? document.getElementById("techSourceLink").value.trim() : "";
        const apiText = document.getElementById("techApi") ? document.getElementById("techApi").value.trim() : "";

        if (name) {
            activeEventObj.name = name;
            activeEventObj.event_name = name;
        }

        if (date) {
            activeEventObj.date = date;
            activeEventObj.date_text = date;
        }

        activeEventObj.place = place;
        activeEventObj.PLACE = place;
        activeEventObj.who = who;
        activeEventObj.WHO = who;
        activeEventObj.description = brief;
        activeEventObj.brief = brief;
        activeEventObj.BRIEF = brief;
        activeEventObj.brief_description = brief;
        activeEventObj.source_link = sourceLink;
        activeEventObj.source = sourceLink;
        activeEventObj.api = apiText;
        activeEventObj.api_text = apiText;

        if (date && typeof findAxisByYear === "function") {
            const axis = findAxisByYear(date);

            if (axis) {
                activeEventObj.start = axis.start;
                activeEventObj.end = axis.end;
                activeEventObj.start_col = axis.start;
                activeEventObj.end_col = axis.end;
            }
        }

        if (group && typeof setFolderForRow === "function") {
            setFolderForRow(activeEventObj.row, group);
        }

        activeEventId = eventStableIdV55(activeEventObj);

        const store = typeof getEventMetaStore === "function" ? getEventMetaStore() : {};

        delete store[oldId];

        store[activeEventId] = {
            id: activeEventObj.id || "",
            db_id: activeEventObj.db_id || "",
            group: typeof currentFolderForRow === "function" ? currentFolderForRow(activeEventObj.row) : group,
            row: activeEventObj.row,
            row_key: activeEventObj.row_key || activeEventObj.row,
            name: activeEventObj.name,
            event_name: activeEventObj.event_name || activeEventObj.name,
            date: date || activeEventObj.date || "",
            date_text: date || activeEventObj.date_text || "",
            place: place,
            PLACE: place,
            who: who,
            WHO: who,
            description: brief,
            brief: brief,
            BRIEF: brief,
            brief_description: brief,
            source_link: sourceLink,
            api: apiText,
            api_text: apiText
        };

        try {
            if (typeof saveEventMetaStore === "function") {
                saveEventMetaStore(store);
            } else {
                await fetch("/api/ui-state", {
                    method: "POST",
                    headers: {"Content-Type": "application/json"},
                    body: JSON.stringify({key: "event_meta", value: store})
                });
            }
        } catch (err) {}

        try {
            await fetch("/api/session/save", {
                method: "POST",
                headers: {"Content-Type": "application/json"},
                body: JSON.stringify({})
            });
        } catch (err) {}

        if (typeof renderFolderTree === "function") renderFolderTree();
        if (typeof applyFilter === "function") applyFilter();
        if (typeof applyZoom === "function") applyZoom();

        setTimeout(addVisibleTagsV55, 0);

        if (typeof openTechCardByEvent === "function") {
            openTechCardByEvent(activeEventObj, null);
        }
    };

    const techSaveButton = document.getElementById("techSave");
    if (techSaveButton && !techSaveButton.dataset.v55Bound) {
        const replacement = techSaveButton.cloneNode(true);
        replacement.dataset.v55Bound = "1";
        techSaveButton.parentNode.replaceChild(replacement, techSaveButton);

        replacement.addEventListener("click", function(ev) {
            ev.preventDefault();
            ev.stopPropagation();
            window.saveTechCard();
        });
    }

    setTimeout(addVisibleTagsV55, 1000);
})();
</script>
r   r   r=   r   r   r   z4V55 final front fields/API sensor override injected.)_V55FinalPathr!   r   r   r   print)r   r   r   cssjss        r   '_v55_inject_front_fields_and_api_sensorr     s    @AAI 00D;E}}.C`^
B@ D||Is['8!<<TzD D||IrK'7;;r	000	
@AAAAAr   c                  "   t          d          } |                                 sd S |                     d          }t          j        dd|t          j        t          j        z            }t          j        dd|t          j        t          j        z            }d}t          j        d	|t          j                  r't          j        d	|d
z   |dt          j                  }n||z  }|                     |d           t          d           d S )Nr   r   r   L<style[^>]*id=["\']V56_GRAPH_INBOX_IMPORT_BUTTON_STYLE["\'][^>]*>.*?</style>r   r   H<script[^>]*id=["\']V56_GRAPH_INBOX_IMPORT_BUTTON["\'][^>]*>.*?</script>u  
<style id="V56_GRAPH_INBOX_IMPORT_BUTTON_STYLE">
#v56GraphInboxHost {
    position: fixed;
    top: 54px;
    right: 14px;
    z-index: 999999;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 4px;
    font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
#v56GraphInboxButton {
    border: 1px solid #475569;
    background: #111827;
    color: #e5e7eb;
    border-radius: 8px;
    padding: 7px 10px;
    font-size: 12px;
    font-weight: 700;
    cursor: pointer;
    box-shadow: 0 8px 22px rgba(0,0,0,.25);
}
#v56GraphInboxButton:hover {
    background: #1f2937;
}
#v56GraphInboxButton:disabled {
    cursor: wait;
    opacity: .65;
}
#v56GraphInboxStatus {
    max-width: 260px;
    padding: 6px 8px;
    border-radius: 6px;
    background: rgba(15,23,42,.94);
    border: 1px solid #334155;
    color: #cbd5e1;
    font-size: 11px;
    line-height: 1.25;
    text-align: right;
    white-space: pre-wrap;
}
</style>
<script id="V56_GRAPH_INBOX_IMPORT_BUTTON">
(function(){
    function ensureHost() {
        let host = document.getElementById("v56GraphInboxHost");
        if (!host) {
            host = document.createElement("div");
            host.id = "v56GraphInboxHost";
            document.body.appendChild(host);
        }

        let btn = document.getElementById("v56GraphInboxButton");
        if (!btn) {
            btn = document.createElement("button");
            btn.id = "v56GraphInboxButton";
            btn.type = "button";
            btn.textContent = "Import TXT";
            btn.title = "Importa GRAPH_IMPORT/graph_inbox.txt como grafo nuevo en Uncategorized";
            host.appendChild(btn);
        }

        let status = document.getElementById("v56GraphInboxStatus");
        if (!status) {
            status = document.createElement("div");
            status.id = "v56GraphInboxStatus";
            status.textContent = "Inbox TXT: listo";
            host.appendChild(status);
        }

        return {host: host, btn: btn, status: status};
    }

    function setStatus(msg) {
        const ui = ensureHost();
        ui.status.textContent = msg;
    }

    async function importInbox() {
        const ui = ensureHost();

        const ok = window.confirm(
            "Importar GRAPH_IMPORT/graph_inbox.txt como grafo nuevo en la carpeta Uncategorized?\n\n" +
            "Esta acción escribirá en SQLite si el archivo contiene un grafo válido."
        );
        if (!ok) return;

        ui.btn.disabled = true;
        setStatus("Importando graph_inbox.txt...");

        try {
            const res = await fetch("/api/import/graph-inbox", {
                method: "POST",
                headers: {"Content-Type": "application/json"},
                body: JSON.stringify({source: "GRAPH_IMPORT/graph_inbox.txt"})
            });

            const data = await res.json();

            if (!res.ok || !data.ok) {
                setStatus("Importación rechazada:\n" + (data.error || data.message || res.status));
                alert("Importación rechazada:\n" + (data.error || data.message || res.status));
                return;
            }

            if (!data.imported) {
                setStatus(data.message || "No hay grafo pendiente.");
                alert(data.message || "No hay grafo pendiente.");
                return;
            }

            const graph = data.graph || {};
            const row = graph.row_key || "";
            setStatus("Importado: " + (graph.visible_name || row) + "\nNodos: " + data.nodes_imported);

            const url = new URL(window.location.href);
            url.searchParams.set("v", "sqlite_server");
            if (row) url.searchParams.set("select", row);

            setTimeout(function(){
                window.location.href = url.toString();
            }, 500);

        } catch (err) {
            console.error("V56 graph inbox import failed:", err);
            setStatus("Error de importación: " + err);
            alert("Error de importación: " + err);
        } finally {
            ui.btn.disabled = false;
        }
    }

    function bind() {
        const ui = ensureHost();
        if (ui.btn.dataset.v56Bound === "1") return;
        ui.btn.dataset.v56Bound = "1";
        ui.btn.addEventListener("click", importInbox);
    }

    if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", bind);
    } else {
        bind();
    }
})();
</script>

</body\s*>r   r=   r   z'V56 graph inbox import button injected.)
_V56Pathr!   r   _v56_rer   r   r   searchr   r   r   s      r   %_v56_inject_graph_inbox_import_buttonr     s   ;<<I 00D;W
i')#	  D ;S
i')#	  DTIl ~mT;;; {=)k*A4qX_Xabbb	000	
344444r   c                  x   t          d          } |                                 sd S |                     d          }t          j        dd|t          j        t          j        z            }d}d|v r|                    d|d	z   d
          }n|dz   |z   }|                     |d           t          d           d S )Nr   r   r   I<style[^>]*id=["\']V57_IMPORT_BUTTON_VISIBLE_HOTFIX["\'][^>]*>.*?</style>r   r   a  
<style id="V57_IMPORT_BUTTON_VISIBLE_HOTFIX">
#v56GraphInboxHost {
    position: fixed !important;
    top: 8px !important;
    right: 88px !important;
    z-index: 2147483647 !important;
    display: flex !important;
    flex-direction: row !important;
    align-items: center !important;
    gap: 8px !important;
    pointer-events: auto !important;
}
#v56GraphInboxButton {
    display: inline-flex !important;
    align-items: center !important;
    justify-content: center !important;
    min-width: 92px !important;
    min-height: 28px !important;
    border: 1px solid #22c55e !important;
    background: #052e16 !important;
    color: #dcfce7 !important;
    border-radius: 7px !important;
    padding: 6px 10px !important;
    font-size: 12px !important;
    font-weight: 800 !important;
    cursor: pointer !important;
    opacity: 1 !important;
    visibility: visible !important;
}
#v56GraphInboxStatus {
    display: none !important;
}
</style>
r   r   r=   r   z-V57 import button visibility hotfix injected.)
_V57Pathr!   r   _v57_rer   r   r   r   r   r   r   r   patchs      r    _v57_force_import_button_visibler     s    ;<<I 00D;T
i')#	  D"EH D||Iu{':A>>t|d"000	
9:::::r   c                     t          d          } |                                 sd S |                     d          }dD ]1}t          j        |d|t          j        t          j        z            }2d}t          j        d|t          j                  r't          j        d	d
|z   |dt          j                  }n|dz   |z   }|                     |d           t          d           d S )Nr   r   r   )r   r   r   zG<style[^>]*id=["\']V58_STATIC_IMPORT_BUTTON_STYLE["\'][^>]*>.*?</style>J<script[^>]*id=["\']V58_STATIC_IMPORT_BUTTON_SCRIPT["\'][^>]*>.*?</script>z8<div[^>]*id=["\']v58GraphInboxHost["\'][\s\S]*?</div>\s*r   r   u-  
<style id="V58_STATIC_IMPORT_BUTTON_STYLE">
#v58GraphInboxHost {
    position: fixed !important;
    left: 12px !important;
    top: 12px !important;
    z-index: 2147483647 !important;
    display: block !important;
    width: auto !important;
    height: auto !important;
    pointer-events: auto !important;
}
#v58GraphInboxButton {
    display: inline-block !important;
    min-width: 120px !important;
    min-height: 34px !important;
    border: 2px solid #22c55e !important;
    background: #052e16 !important;
    color: #dcfce7 !important;
    border-radius: 8px !important;
    padding: 7px 12px !important;
    font-family: system-ui, sans-serif !important;
    font-size: 13px !important;
    font-weight: 900 !important;
    cursor: pointer !important;
    opacity: 1 !important;
    visibility: visible !important;
    box-shadow: 0 8px 24px rgba(0,0,0,.45) !important;
}
#v58GraphInboxButton:hover {
    background: #064e3b !important;
}
</style>

<div id="v58GraphInboxHost">
    <button id="v58GraphInboxButton" type="button">
        Import TXT
    </button>
</div>

<script id="V58_STATIC_IMPORT_BUTTON_SCRIPT">
(function(){
    async function importGraphInboxV58() {
        const btn = document.getElementById("v58GraphInboxButton");
        if (!btn) return;

        const confirmed = window.confirm(
            "Importar GRAPH_IMPORT/graph_inbox.txt como grafo nuevo en Uncategorized?\n\n" +
            "Esta acción escribirá en SQLite solamente si el archivo contiene un grafo válido."
        );
        if (!confirmed) return;

        btn.disabled = true;
        const oldText = btn.textContent;
        btn.textContent = "Importando...";

        try {
            const res = await fetch("/api/import/graph-inbox", {
                method: "POST",
                headers: {"Content-Type": "application/json"},
                body: JSON.stringify({source: "GRAPH_IMPORT/graph_inbox.txt"})
            });

            const data = await res.json();

            if (!res.ok || !data.ok) {
                alert("Importación rechazada:\n" + (data.error || data.message || res.status));
                return;
            }

            if (!data.imported) {
                alert(data.message || "No hay grafo pendiente.");
                return;
            }

            const graph = data.graph || {};
            alert("Grafo importado: " + (graph.visible_name || graph.row_key || "") + "\nNodos: " + data.nodes_imported);

            const url = new URL(window.location.href);
            url.searchParams.set("v", "sqlite_server");
            url.searchParams.set("t", String(Date.now()));
            if (graph.row_key) url.searchParams.set("select", graph.row_key);
            window.location.href = url.toString();

        } catch (err) {
            alert("Error de importación: " + err);
        } finally {
            btn.disabled = false;
            btn.textContent = oldText;
        }
    }

    function bindV58() {
        const btn = document.getElementById("v58GraphInboxButton");
        if (!btn || btn.dataset.boundV58 === "1") return;
        btn.dataset.boundV58 = "1";
        btn.addEventListener("click", importGraphInboxV58);
    }

    if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", bindV58);
    } else {
        bindV58();
    }
})();
</script>
z<body[^>]*>z(<body[^>]*>)z\1\nr=   r   r   z&V58 static Import TXT button injected.)
_V58Pathr!   r   _v58_rer   r   r   r   r   r   r   r   patternr   s       r   _v58_static_import_buttonr     s    ;<<I 00D K K {7BGI	4IJJJjEX ~nd')<<< #{+Wu_d!SZS\]]]t|d"000	
233333r   c                     t          d          } |                                 sd S |                     d          }t          j        dd|t          j        t          j        z            }d}t          j        d|t          j                  r't          j        d|d	z   |d
t          j                  }n|d|z   z  }|                     |d           t          d           d S )Nr   r   r   zG<style[^>]*id=["\']V59_IMPORT_BUTTON_NEXT_TO_SAVE["\'][^>]*>.*?</style>r   r   a  
<style id="V59_IMPORT_BUTTON_NEXT_TO_SAVE">
#v58GraphInboxHost {
    position: fixed !important;
    top: 10px !important;
    right: 86px !important;
    left: auto !important;
    z-index: 2147483647 !important;
    display: block !important;
    width: auto !important;
    height: auto !important;
    pointer-events: auto !important;
}
#v58GraphInboxButton {
    display: inline-block !important;
    min-width: 96px !important;
    min-height: 28px !important;
    height: 28px !important;
    border: 1px solid #22c55e !important;
    background: #052e16 !important;
    color: #dcfce7 !important;
    border-radius: 7px !important;
    padding: 4px 10px !important;
    font-family: system-ui, sans-serif !important;
    font-size: 12px !important;
    font-weight: 800 !important;
    line-height: 18px !important;
    cursor: pointer !important;
    opacity: 1 !important;
    visibility: visible !important;
}
</style>
r   r   r=   r   r   z)V59 Import TXT button moved next to Save.)
_V59Pathr!   r   _v59_rer   r   r   r   r   r   r   s      r   _v59_import_button_next_to_saver     s    ;<<I 00D;R
i')#	  D ED ~mT;;; {=%+*=t1T[T]^^^u000	
566666r   c                     t          d          } |                                 sd S |                     d          }t          j        dd|t          j        t          j        z            }d}t          j        d|t          j                  r't          j        d|d	z   |d
t          j                  }n|d|z   z  }|                     |d           t          d           d S )Nr   r   r   zG<style[^>]*id=["\']V60_IMPORT_BUTTON_SAVE_ALIGNED["\'][^>]*>.*?</style>r   r   a  
<style id="V60_IMPORT_BUTTON_SAVE_ALIGNED">
#v58GraphInboxHost {
    position: fixed !important;
    top: 11px !important;
    right: 92px !important;
    left: auto !important;
    z-index: 2147483647 !important;
    display: block !important;
    width: auto !important;
    height: auto !important;
    pointer-events: auto !important;
}
#v58GraphInboxButton {
    display: inline-block !important;
    min-width: 92px !important;
    height: 29px !important;
    min-height: 29px !important;
    border: 1px solid #22c55e !important;
    background: #052e16 !important;
    color: #dcfce7 !important;
    border-radius: 7px !important;
    padding: 4px 10px !important;
    font-family: system-ui, sans-serif !important;
    font-size: 12px !important;
    font-weight: 800 !important;
    line-height: 18px !important;
    cursor: pointer !important;
    opacity: 1 !important;
    visibility: visible !important;
    box-shadow: none !important;
}
</style>
r   r   r=   r   r   z+V60 Import TXT button aligned next to Save.)
_V60Pathr!   r   _v60_rer   r   r   r   r   r   r   s      r   _v60_import_button_save_alignedr     s    ;<<I 00D;R
i')#	  D!EF ~mT;;; {=%+*=t1T[T]^^^u000	
788888r   c                  (   t          d          } |                                 sd S |                     d          }t          j        dd|t          j        t          j        z            }t          j        dd|t          j        t          j        z            }d}t          j        d	|t          j                  r't          j        d	|d
z   |dt          j                  }n|d|z   z  }|                     |d           t          d           d S )Nr   r   r   I<style[^>]*id=["\']V61_IMPORT_BUTTON_ATTACH_TO_SAVE["\'][^>]*>.*?</style>r   r   R<script[^>]*id=["\']V61_IMPORT_BUTTON_ATTACH_TO_SAVE_SCRIPT["\'][^>]*>.*?</script>aL  
<style id="V61_IMPORT_BUTTON_ATTACH_TO_SAVE">
#v58GraphInboxHost {
    position: fixed !important;
    left: auto !important;
    z-index: 2147483647 !important;
    display: block !important;
    width: auto !important;
    height: auto !important;
    pointer-events: auto !important;
}
#v58GraphInboxButton {
    display: inline-flex !important;
    align-items: center !important;
    justify-content: center !important;
    min-width: 92px !important;
    border: 1px solid #22c55e !important;
    background: #052e16 !important;
    color: #dcfce7 !important;
    border-radius: 7px !important;
    padding: 4px 10px !important;
    font-family: system-ui, sans-serif !important;
    font-size: 12px !important;
    font-weight: 800 !important;
    line-height: 18px !important;
    cursor: pointer !important;
    opacity: 1 !important;
    visibility: visible !important;
    box-shadow: none !important;
    white-space: nowrap !important;
}
</style>

<script id="V61_IMPORT_BUTTON_ATTACH_TO_SAVE_SCRIPT">
(function(){
    function findSaveButton() {
        const controls = Array.from(document.querySelectorAll(
            "button, [role='button'], input[type='button'], input[type='submit']"
        ));

        return controls.find(function(el) {
            if (!el || el.id === "v58GraphInboxButton") return false;
            const text = ((el.textContent || el.value || "") + "").trim().toLowerCase();
            return text === "save" || text.startsWith("save ");
        });
    }

    function alignImportButtonToSave() {
        const host = document.getElementById("v58GraphInboxHost");
        const btn = document.getElementById("v58GraphInboxButton");
        if (!host || !btn) return;

        const save = findSaveButton();

        host.style.setProperty("position", "fixed", "important");
        host.style.setProperty("left", "auto", "important");
        host.style.setProperty("z-index", "2147483647", "important");
        host.style.setProperty("display", "block", "important");

        if (save) {
            const r = save.getBoundingClientRect();
            const gap = 8;
            const right = Math.max(8, window.innerWidth - r.left + gap);

            host.style.setProperty("top", Math.round(r.top) + "px", "important");
            host.style.setProperty("right", Math.round(right) + "px", "important");

            btn.style.setProperty("height", Math.round(r.height) + "px", "important");
            btn.style.setProperty("min-height", Math.round(r.height) + "px", "important");
        } else {
            host.style.setProperty("top", "11px", "important");
            host.style.setProperty("right", "92px", "important");
            btn.style.setProperty("height", "29px", "important");
            btn.style.setProperty("min-height", "29px", "important");
        }
    }

    function startAligner() {
        alignImportButtonToSave();
        setTimeout(alignImportButtonToSave, 100);
        setTimeout(alignImportButtonToSave, 500);
        setTimeout(alignImportButtonToSave, 1200);
        window.addEventListener("resize", alignImportButtonToSave, {passive: true});
        window.addEventListener("scroll", alignImportButtonToSave, {passive: true});
    }

    if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", startAligner);
    } else {
        startAligner();
    }
})();
</script>
r   r   r=   r   r   z'V61 Import TXT button attached to Save.)
_V61Pathr!   r   _v61_rer   r   r   r   r   r   r   s      r   !_v61_import_button_attach_to_saver     s   ;<<I 00D;T
i')#	  D ;]
i')#	  D]E~ ~mT;;; {=%+*=t1T[T]^^^u000	
344444r   c                     t          d          } |                                 sd S |                     d          }t          j        dd|t          j        t          j        z            }t          j        dd|t          j        t          j        z            }t          j        dd|t          j        t          j        z            }d	}t          j        d
|t          j                  r't          j        d
|dz   |dt          j                  }n|d|z   z  }|                     |d           t          d           d S )Nr   r   r   r   r   r   r   zM<style[^>]*id=["\']V62_IMPORT_BUTTON_FIXED_NEXT_TO_SAVE["\'][^>]*>.*?</style>a3  
<style id="V62_IMPORT_BUTTON_FIXED_NEXT_TO_SAVE">
#v58GraphInboxHost {
    position: fixed !important;
    top: 9px !important;
    right: 108px !important;
    left: auto !important;
    z-index: 2147483647 !important;
    display: block !important;
    width: auto !important;
    height: auto !important;
    margin: 0 !important;
    padding: 0 !important;
    pointer-events: auto !important;
}
#v58GraphInboxButton {
    display: inline-flex !important;
    align-items: center !important;
    justify-content: center !important;
    width: 96px !important;
    min-width: 96px !important;
    max-width: 96px !important;
    height: 30px !important;
    min-height: 30px !important;
    max-height: 30px !important;
    box-sizing: border-box !important;
    border: 1px solid #22c55e !important;
    background: #052e16 !important;
    color: #dcfce7 !important;
    border-radius: 7px !important;
    padding: 0 10px !important;
    font-family: system-ui, sans-serif !important;
    font-size: 12px !important;
    font-weight: 800 !important;
    line-height: 30px !important;
    cursor: pointer !important;
    opacity: 1 !important;
    visibility: visible !important;
    box-shadow: none !important;
    white-space: nowrap !important;
}
#v58GraphInboxButton:hover {
    background: #064e3b !important;
}
</style>
r   r   r=   r   r   z)V62 Import TXT button fixed next to Save.)
_V62Pathr!   r   _v62_rer   r   r   r   r   r   r   s      r   %_v62_import_button_fixed_next_to_saver     sL   ;<<I 00D ;T
i')#	  D ;]
i')#	  D ;X
i')#	  D-E^ ~mT;;; {=%+*=t1T[T]^^^u000	
566666r   c                  J   t          d          } |                                 sd S |                     d          }dD ]1}t          j        |d|t          j        t          j        z            }2d }t          j        d||d	t          j        t          j        z  
          }t          j        dd |d	t          j        t          j        z  
          }dt          j        dfd|d	t          j        
          }|                     |d           t          d           d S )Nr   r   r   )zI<style[^>]*id=["\']V63_MULTILINK_SOURCE_FIELD_STYLE["\'][^>]*>.*?</style>zL<script[^>]*id=["\']V63_MULTILINK_SOURCE_FIELD_SCRIPT["\'][^>]*>.*?</script>zI<style[^>]*id=["\']V64_MULTILINK_SOURCE_FIELD_STYLE["\'][^>]*>.*?</style>zL<script[^>]*id=["\']V64_MULTILINK_SOURCE_FIELD_SCRIPT["\'][^>]*>.*?</script>z<<div[^>]*id=["\']v64SourceLinksPreview["\'][\s\S]*?</div>\s*r   r   c                     	 dS )Nuc   <textarea id="techSourceLink" rows="4" placeholder="Uno o varios links, uno por línea"></textarea>r1   )matchs    r   replace_source_inputz>_v64_multilink_source_field_safe.<locals>.replace_source_input  s    K	
 	
r   z0<input\b[^>]*\bid=["\']techSourceLink["\'][^>]*>r=   r   zC(<textarea\b[^>]*\bid=["\']techSourceLink["\'][^>]*>.*?</textarea>)c                 2    |                      d          dz   S )Nr=   z'
<div id="v64SourceLinksPreview"></div>group)ms    r   rZ   z2_v64_multilink_source_field_safe.<locals>.<lambda>  s    !''!**II r   a  
<style id="V64_MULTILINK_SOURCE_FIELD_STYLE">
#techSourceLink {
    min-height: 84px !important;
    resize: vertical !important;
    white-space: pre-wrap !important;
    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace !important;
    line-height: 1.35 !important;
}
#v64SourceLinksPreview {
    margin-top: 6px !important;
    padding: 7px 8px !important;
    border: 1px solid rgba(34, 197, 94, .28) !important;
    border-radius: 7px !important;
    background: rgba(5, 46, 22, .18) !important;
    color: #d1fae5 !important;
    font-size: 12px !important;
    line-height: 1.35 !important;
    max-height: 160px !important;
    overflow: auto !important;
}
#v64SourceLinksPreview:empty {
    display: none !important;
}
#v64SourceLinksPreview .v64-link-title {
    font-weight: 800 !important;
    color: #bbf7d0 !important;
    margin-bottom: 4px !important;
}
#v64SourceLinksPreview a {
    display: block !important;
    color: #93c5fd !important;
    text-decoration: underline !important;
    overflow-wrap: anywhere !important;
    margin: 3px 0 !important;
}
</style>

<script id="V64_MULTILINK_SOURCE_FIELD_SCRIPT">
(function(){
    function extractUrls(text) {
        const matches = String(text || "").match(/https?:\/\/[^\s<>"'`]+/g) || [];
        const out = [];
        const seen = new Set();

        for (let url of matches) {
            url = url.replace(/[),.;\]\}]+$/g, "");
            if (!seen.has(url)) {
                seen.add(url);
                out.push(url);
            }
        }
        return out;
    }

    function ensurePreview() {
        const field = document.getElementById("techSourceLink");
        if (!field) return null;

        let preview = document.getElementById("v64SourceLinksPreview");
        if (!preview) {
            preview = document.createElement("div");
            preview.id = "v64SourceLinksPreview";
            field.insertAdjacentElement("afterend", preview);
        }

        return preview;
    }

    function renderLinksPreview() {
        const field = document.getElementById("techSourceLink");
        const preview = ensurePreview();
        if (!field || !preview) return;

        const urls = extractUrls(field.value || "");
        preview.innerHTML = "";

        if (!urls.length) return;

        const title = document.createElement("div");
        title.className = "v64-link-title";
        title.textContent = urls.length === 1 ? "Link detectado" : "Links detectados";
        preview.appendChild(title);

        urls.forEach(function(url, idx) {
            const a = document.createElement("a");
            a.href = url;
            a.target = "_blank";
            a.rel = "noopener noreferrer";
            a.textContent = String(idx + 1) + ". " + url;
            preview.appendChild(a);
        });
    }

    function bind() {
        const field = document.getElementById("techSourceLink");
        if (!field) return;

        if (field.dataset.v64Bound !== "1") {
            field.dataset.v64Bound = "1";
            field.addEventListener("input", renderLinksPreview);
            field.addEventListener("change", renderLinksPreview);
            field.addEventListener("keyup", renderLinksPreview);
        }

        renderLinksPreview();
    }

    function start() {
        bind();
        setInterval(renderLinksPreview, 500);
    }

    if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", start);
    } else {
        start();
    }
})();
</script>
r   c                 :    dz   |                      d          z   S Nr   r   r   r   r   s    r   rZ   z2_v64_multilink_source_field_safe.<locals>.<lambda>      %$,+ r   z1V64 multiline source links field injected safely.)	_V64Pathr!   r   _v64_rer   r   r   r   r   )r   r   r   r   r   s       @r    _v64_multilink_source_field_safer     sU   ;<<I 00D K K {7BGI	4IJJJ
 
 
 ;;i')#  D ;NIIi')#  DxEt ;++++i  D 000	
=>>>>>r   c                     t          d          } |                                 sd S |                     d          }dD ]1}t          j        |d|t          j        t          j        z            }2t          j        dd|d	t          j        t          j        z  
          }dt          j        dfd|d	t          j        
          }|                     |d           t          d           d S )Nr   r   r   )r   r   zO<style[^>]*id=["\']V65_IMPORT_BUTTON_DIRECT_ONCLICK_STYLE["\'][^>]*>.*?</style>zR<script[^>]*id=["\']V65_IMPORT_BUTTON_DIRECT_ONCLICK_SCRIPT["\'][^>]*>.*?</script>r   r   z2<button\s+id=["\']v58GraphInboxButton["\']([^>]*)>zt<button id="v58GraphInboxButton" type="button" onclick="window.v65ImportGraphInbox && window.v65ImportGraphInbox()">r=   r   u  
<style id="V65_IMPORT_BUTTON_DIRECT_ONCLICK_STYLE">
#v65ImportStatus {
    position: fixed !important;
    top: 45px !important;
    right: 12px !important;
    z-index: 2147483647 !important;
    max-width: 360px !important;
    padding: 8px 10px !important;
    border: 1px solid #334155 !important;
    border-radius: 7px !important;
    background: rgba(15, 23, 42, .96) !important;
    color: #dbeafe !important;
    font-family: system-ui, sans-serif !important;
    font-size: 12px !important;
    line-height: 1.35 !important;
    display: none;
    white-space: pre-wrap !important;
}
</style>

<script id="V65_IMPORT_BUTTON_DIRECT_ONCLICK_SCRIPT">
window.v65ImportGraphInbox = async function() {
    const btn = document.getElementById("v58GraphInboxButton");
    let status = document.getElementById("v65ImportStatus");

    if (!status) {
        status = document.createElement("div");
        status.id = "v65ImportStatus";
        document.body.appendChild(status);
    }

    function show(msg) {
        status.textContent = msg;
        status.style.display = "block";
    }

    show("Botón recibido. Esperando confirmación...");

    const confirmed = window.confirm(
        "Importar GRAPH_IMPORT/graph_inbox.txt como grafo nuevo en Uncategorized?\n\n" +
        "Esta acción escribirá en SQLite solamente si el archivo contiene un grafo válido."
    );

    if (!confirmed) {
        show("Importación cancelada por el usuario.");
        return;
    }

    if (btn) {
        btn.disabled = true;
        btn.textContent = "Importando...";
    }

    show("Enviando solicitud a /api/import/graph-inbox ...");

    try {
        const res = await fetch("/api/import/graph-inbox", {
            method: "POST",
            headers: {"Content-Type": "application/json"},
            body: JSON.stringify({source: "GRAPH_IMPORT/graph_inbox.txt"})
        });

        let data = null;
        try {
            data = await res.json();
        } catch (jsonErr) {
            const txt = await res.text();
            show("Respuesta no JSON del servidor:\nHTTP " + res.status + "\n" + txt.slice(0, 500));
            alert("Respuesta no JSON del servidor. HTTP " + res.status);
            return;
        }

        if (!res.ok || !data.ok) {
            const msg = "Importación rechazada:\n" + (data.error || data.message || ("HTTP " + res.status));
            show(msg);
            alert(msg);
            return;
        }

        if (!data.imported) {
            const msg = data.message || "No hay grafo pendiente.";
            show(msg);
            alert(msg);
            return;
        }

        const graph = data.graph || {};
        const msg = "Grafo importado: " + (graph.visible_name || graph.row_key || "") +
                    "\nNodos: " + data.nodes_imported +
                    "\nCarpeta: " + (graph.folder || "Uncategorized");

        show(msg);
        alert(msg);

        const url = new URL(window.location.href);
        url.searchParams.set("v", "sqlite_server");
        url.searchParams.set("t", String(Date.now()));
        if (graph.row_key) url.searchParams.set("select", graph.row_key);
        window.location.href = url.toString();

    } catch (err) {
        const msg = "Error de importación:\n" + err;
        show(msg);
        alert(msg);
    } finally {
        if (btn) {
            btn.disabled = false;
            btn.textContent = "Import TXT";
        }
    }
};
</script>
r   c                 :    dz   |                      d          z   S r   r   r   s    r   rZ   z3_v65_import_button_direct_onclick.<locals>.<lambda>!  r   r   z'V65 Import TXT direct onclick injected.)	_V65Pathr!   r   _v65_rer   r   r   r   r   r   s      @r   !_v65_import_button_direct_onclickr     s   ;<<I 00D K K {7BGI	4IJJJ ;=~i')#  DqEf ;++++i  D 000	
344444r   c                  x   t          d          } |                                 sd S |                     d          }t          j        dd|t          j        t          j        z            }dt          j        dfd	|d
t          j                  }|                     |d           t          d           d S )Nr   r   r   zT<script[^>]*id=["\']V66_IMPORT_PREVIEW_CONFIRM_ENGLISH_SCRIPT["\'][^>]*>.*?</script>r   r   a\  
<script id="V66_IMPORT_PREVIEW_CONFIRM_ENGLISH_SCRIPT">
window.v65ImportGraphInbox = async function() {
    const btn = document.getElementById("v58GraphInboxButton");
    let status = document.getElementById("v65ImportStatus");

    if (!status) {
        status = document.createElement("div");
        status.id = "v65ImportStatus";
        document.body.appendChild(status);
    }

    function show(msg) {
        status.textContent = msg;
        status.style.display = "block";
    }

    function buildPreviewMessage(preview) {
        const graphName = preview.graph_name || "(unnamed graph)";
        const count = Number(preview.nodes_detected || 0);
        const names = Array.isArray(preview.node_names) ? preview.node_names : [];
        const warnings = Array.isArray(preview.warnings) ? preview.warnings : [];

        let msg = "";
        msg += "Graph import preview\n\n";
        msg += "Graph name: " + graphName + "\n";
        msg += "Nodes detected: " + count + "\n";
        msg += "Target folder: Uncategorized\n";

        if (names.length) {
            msg += "\nNodes:\n";
            names.forEach(function(name, idx) {
                msg += String(idx + 1) + ". " + name + "\n";
            });
        }

        if (warnings.length) {
            msg += "\nParser warnings:\n";
            warnings.forEach(function(w, idx) {
                msg += String(idx + 1) + ". " + w + "\n";
            });
        }

        msg += "\nDo you want to import this graph into SQLite now?";
        return msg;
    }

    if (btn) {
        btn.disabled = true;
        btn.textContent = "Preview...";
    }

    show("Reading GRAPH_IMPORT/graph_inbox.txt...");

    try {
        const previewRes = await fetch("/api/import/graph-inbox/preview", {
            method: "POST",
            headers: {"Content-Type": "application/json"},
            body: JSON.stringify({source: "GRAPH_IMPORT/graph_inbox.txt"})
        });

        const preview = await previewRes.json();

        if (!previewRes.ok || !preview.ok || !preview.importable) {
            const msg = "Import preview failed:\n" + (preview.error || preview.message || ("HTTP " + previewRes.status));
            show(msg);
            alert(msg);
            return;
        }

        const confirmed = window.confirm(buildPreviewMessage(preview));

        if (!confirmed) {
            show("Import cancelled by the user.");
            return;
        }

        if (btn) {
            btn.textContent = "Importing...";
        }

        show("Importing graph into SQLite...");

        const res = await fetch("/api/import/graph-inbox", {
            method: "POST",
            headers: {"Content-Type": "application/json"},
            body: JSON.stringify({source: "GRAPH_IMPORT/graph_inbox.txt"})
        });

        let data = null;
        try {
            data = await res.json();
        } catch (jsonErr) {
            const txt = await res.text();
            const msg = "Server returned a non-JSON response:\nHTTP " + res.status + "\n" + txt.slice(0, 500);
            show(msg);
            alert(msg);
            return;
        }

        if (!res.ok || !data.ok) {
            const msg = "Import rejected:\n" + (data.error || data.message || ("HTTP " + res.status));
            show(msg);
            alert(msg);
            return;
        }

        if (!data.imported) {
            const msg = data.message || "No graph is pending for import.";
            show(msg);
            alert(msg);
            return;
        }

        const graph = data.graph || {};
        const msg = "Graph imported successfully\n\n" +
                    "Graph name: " + (graph.visible_name || graph.row_key || "") + "\n" +
                    "Nodes imported: " + data.nodes_imported + "\n" +
                    "Target folder: " + (graph.folder || "Uncategorized");

        show(msg);
        alert(msg);

        const url = new URL(window.location.href);
        url.searchParams.set("v", "sqlite_server");
        url.searchParams.set("t", String(Date.now()));
        if (graph.row_key) url.searchParams.set("select", graph.row_key);
        window.location.href = url.toString();

    } catch (err) {
        const msg = "Import failed:\n" + err;
        show(msg);
        alert(msg);
    } finally {
        if (btn) {
            btn.disabled = false;
            btn.textContent = "Import TXT";
        }
    }
};
</script>
r   c                 :    dz   |                      d          z   S r   r   r   s    r   rZ   z5_v66_import_preview_confirm_english.<locals>.<lambda>  r   r   r=   r   z*V66 English preview confirmation injected.)	_V66Pathr!   r   _v66_rer   r   r   r   r   r   s     @r   #_v66_import_preview_confirm_englishr  0  s    ;<<I 00D;_
i')#	  DME^ ;++++i  D 000	
677777r   c                     t          d          } |                                 sd S |                     d          }dD ]1}t          j        |d|t          j        t          j        z            }2dt          j        dfd	|d
t          j                  }|                     |d           t          d           d S )Nr   r   r   )zL<style[^>]*id=["\']V67_FULL_IMPORT_PREVIEW_MODAL_STYLE["\'][^>]*>.*?</style>zO<script[^>]*id=["\']V67_FULL_IMPORT_PREVIEW_MODAL_SCRIPT["\'][^>]*>.*?</script>r   r   ai+  
<style id="V67_FULL_IMPORT_PREVIEW_MODAL_STYLE">
#v67ImportModalOverlay {
    position: fixed !important;
    inset: 0 !important;
    z-index: 2147483647 !important;
    display: none;
    align-items: center !important;
    justify-content: center !important;
    background: rgba(0, 0, 0, .62) !important;
    font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
}
#v67ImportModal {
    width: min(760px, calc(100vw - 48px)) !important;
    max-height: min(760px, calc(100vh - 48px)) !important;
    display: flex !important;
    flex-direction: column !important;
    border: 1px solid #334155 !important;
    border-radius: 12px !important;
    background: #111827 !important;
    color: #e5e7eb !important;
    box-shadow: 0 24px 80px rgba(0,0,0,.55) !important;
    overflow: hidden !important;
}
#v67ImportModalHeader {
    padding: 14px 18px !important;
    border-bottom: 1px solid #334155 !important;
    font-size: 15px !important;
    font-weight: 800 !important;
}
#v67ImportModalBody {
    padding: 14px 18px !important;
    overflow: auto !important;
    flex: 1 1 auto !important;
    font-size: 13px !important;
    line-height: 1.45 !important;
}
#v67ImportModalBody .v67-summary {
    margin-bottom: 12px !important;
    padding: 10px 12px !important;
    border: 1px solid rgba(34,197,94,.28) !important;
    border-radius: 8px !important;
    background: rgba(5,46,22,.22) !important;
}
#v67ImportModalBody .v67-summary div {
    margin: 3px 0 !important;
}
#v67ImportModalBody .v67-section-title {
    margin: 12px 0 8px !important;
    font-weight: 800 !important;
    color: #bbf7d0 !important;
}
#v67ImportNodeList {
    margin: 0 !important;
    padding-left: 24px !important;
}
#v67ImportNodeList li {
    margin: 4px 0 !important;
    padding-left: 4px !important;
    overflow-wrap: anywhere !important;
}
#v67ImportWarnings {
    margin-top: 12px !important;
    padding: 10px 12px !important;
    border: 1px solid rgba(245,158,11,.38) !important;
    border-radius: 8px !important;
    background: rgba(120,53,15,.22) !important;
    color: #fde68a !important;
}
#v67ImportModalFooter {
    display: flex !important;
    justify-content: flex-end !important;
    gap: 10px !important;
    padding: 12px 18px !important;
    border-top: 1px solid #334155 !important;
}
#v67ImportModalFooter button {
    min-width: 96px !important;
    height: 34px !important;
    border-radius: 8px !important;
    font-size: 13px !important;
    font-weight: 800 !important;
    cursor: pointer !important;
}
#v67ImportCancelButton {
    border: 1px solid #475569 !important;
    background: #1f2937 !important;
    color: #e5e7eb !important;
}
#v67ImportOkButton {
    border: 1px solid #22c55e !important;
    background: #052e16 !important;
    color: #dcfce7 !important;
}
#v67ImportOkButton:disabled,
#v67ImportCancelButton:disabled {
    opacity: .55 !important;
    cursor: wait !important;
}
</style>

<script id="V67_FULL_IMPORT_PREVIEW_MODAL_SCRIPT">
(function(){
    function ensureModal() {
        let overlay = document.getElementById("v67ImportModalOverlay");

        if (!overlay) {
            overlay = document.createElement("div");
            overlay.id = "v67ImportModalOverlay";

            overlay.innerHTML = ''
                + '<div id="v67ImportModal" role="dialog" aria-modal="true">'
                + '  <div id="v67ImportModalHeader">Graph import preview</div>'
                + '  <div id="v67ImportModalBody"></div>'
                + '  <div id="v67ImportModalFooter">'
                + '    <button id="v67ImportCancelButton" type="button">Cancel</button>'
                + '    <button id="v67ImportOkButton" type="button">Import</button>'
                + '  </div>'
                + '</div>';

            document.body.appendChild(overlay);
        }

        return {
            overlay: overlay,
            body: document.getElementById("v67ImportModalBody"),
            cancel: document.getElementById("v67ImportCancelButton"),
            ok: document.getElementById("v67ImportOkButton")
        };
    }

    function setStatus(msg) {
        let status = document.getElementById("v65ImportStatus");
        if (!status) {
            status = document.createElement("div");
            status.id = "v65ImportStatus";
            document.body.appendChild(status);
        }
        status.textContent = msg;
        status.style.display = "block";
    }

    function escapeText(value) {
        return String(value == null ? "" : value);
    }

    function renderPreviewBody(preview) {
        const graphName = escapeText(preview.graph_name || "(unnamed graph)");
        const count = Number(preview.nodes_detected || 0);
        const names = Array.isArray(preview.node_names) ? preview.node_names : [];
        const warnings = Array.isArray(preview.warnings) ? preview.warnings : [];

        const wrapper = document.createElement("div");

        const summary = document.createElement("div");
        summary.className = "v67-summary";

        const g = document.createElement("div");
        g.textContent = "Graph name: " + graphName;
        summary.appendChild(g);

        const n = document.createElement("div");
        n.textContent = "Nodes detected: " + count;
        summary.appendChild(n);

        const f = document.createElement("div");
        f.textContent = "Target folder: Uncategorized";
        summary.appendChild(f);

        wrapper.appendChild(summary);

        const title = document.createElement("div");
        title.className = "v67-section-title";
        title.textContent = "Nodes";
        wrapper.appendChild(title);

        const list = document.createElement("ol");
        list.id = "v67ImportNodeList";

        names.forEach(function(name) {
            const li = document.createElement("li");
            li.textContent = escapeText(name);
            list.appendChild(li);
        });

        wrapper.appendChild(list);

        if (warnings.length) {
            const warn = document.createElement("div");
            warn.id = "v67ImportWarnings";

            const wt = document.createElement("div");
            wt.className = "v67-section-title";
            wt.textContent = "Parser warnings";
            warn.appendChild(wt);

            const wl = document.createElement("ol");
            warnings.forEach(function(w) {
                const li = document.createElement("li");
                li.textContent = escapeText(w);
                wl.appendChild(li);
            });
            warn.appendChild(wl);
            wrapper.appendChild(warn);
        }

        return wrapper;
    }

    function openPreviewModal(preview) {
        return new Promise(function(resolve) {
            const modal = ensureModal();

            modal.body.innerHTML = "";
            modal.body.appendChild(renderPreviewBody(preview));

            modal.overlay.style.display = "flex";
            modal.ok.disabled = false;
            modal.cancel.disabled = false;

            function cleanup(result) {
                modal.overlay.style.display = "none";
                modal.ok.removeEventListener("click", onOk);
                modal.cancel.removeEventListener("click", onCancel);
                resolve(result);
            }

            function onOk() {
                cleanup(true);
            }

            function onCancel() {
                cleanup(false);
            }

            modal.ok.addEventListener("click", onOk);
            modal.cancel.addEventListener("click", onCancel);
        });
    }

    window.v65ImportGraphInbox = async function() {
        const btn = document.getElementById("v58GraphInboxButton");

        if (btn) {
            btn.disabled = true;
            btn.textContent = "Preview...";
        }

        setStatus("Reading GRAPH_IMPORT/graph_inbox.txt...");

        try {
            const previewRes = await fetch("/api/import/graph-inbox/preview", {
                method: "POST",
                headers: {"Content-Type": "application/json"},
                body: JSON.stringify({source: "GRAPH_IMPORT/graph_inbox.txt"})
            });

            const preview = await previewRes.json();

            if (!previewRes.ok || !preview.ok || !preview.importable) {
                const msg = "Import preview failed:\n" + (preview.error || preview.message || ("HTTP " + previewRes.status));
                setStatus(msg);
                alert(msg);
                return;
            }

            if (btn) {
                btn.disabled = false;
                btn.textContent = "Import TXT";
            }

            const confirmed = await openPreviewModal(preview);

            if (!confirmed) {
                setStatus("Import cancelled by the user.");
                return;
            }

            if (btn) {
                btn.disabled = true;
                btn.textContent = "Importing...";
            }

            setStatus("Importing graph into SQLite...");

            const res = await fetch("/api/import/graph-inbox", {
                method: "POST",
                headers: {"Content-Type": "application/json"},
                body: JSON.stringify({source: "GRAPH_IMPORT/graph_inbox.txt"})
            });

            let data = null;
            try {
                data = await res.json();
            } catch (jsonErr) {
                const txt = await res.text();
                const msg = "Server returned a non-JSON response:\nHTTP " + res.status + "\n" + txt.slice(0, 500);
                setStatus(msg);
                alert(msg);
                return;
            }

            if (!res.ok || !data.ok) {
                const msg = "Import rejected:\n" + (data.error || data.message || ("HTTP " + res.status));
                setStatus(msg);
                alert(msg);
                return;
            }

            if (!data.imported) {
                const msg = data.message || "No graph is pending for import.";
                setStatus(msg);
                alert(msg);
                return;
            }

            const graph = data.graph || {};
            const msg = "Graph imported successfully\n\n"
                + "Graph name: " + (graph.visible_name || graph.row_key || "") + "\n"
                + "Nodes imported: " + data.nodes_imported + "\n"
                + "Target folder: " + (graph.folder || "Uncategorized");

            setStatus(msg);
            alert(msg);

            const url = new URL(window.location.href);
            url.searchParams.set("v", "sqlite_server");
            url.searchParams.set("t", String(Date.now()));
            if (graph.row_key) url.searchParams.set("select", graph.row_key);
            window.location.href = url.toString();

        } catch (err) {
            const msg = "Import failed:\n" + err;
            setStatus(msg);
            alert(msg);
        } finally {
            if (btn) {
                btn.disabled = false;
                btn.textContent = "Import TXT";
            }
        }
    };
})();
</script>
r   c                 :    dz   |                      d          z   S r   r   r   s    r   rZ   z0_v67_full_import_preview_modal.<locals>.<lambda>G  r   r   r=   r   z'V67 full import preview modal injected.)	_V67Pathr!   r   _v67_rer   r   r   r   r   r   s      @r   _v67_full_import_preview_modalr    s    ;<<I 00D K K {7BGI	4IJJJXEt
 ;++++i  D 000	
344444r   )jsonpathlibr   r   r   grafo_d_sqlite_adapterr   r"   INPUTOUTPUTrx   CELL_Hrw   TOP_PADBAR_HSELECTED_LANE_HCOLORSraw_db
isinstancedictrH   r   listr   r   r.   sortedsetr,   scorer:   valssumnumeric_countanyhas_2025maxrC   axis_eventsmax_event_colrN   
live_yearsfallback_yearri   appendgraph_eventsrX   r4   
setdefaultrU   itemsr   r    sqlite_graph_labelskeys
graph_rowsmax_colbase_wlenbase_hsidebar_items	enumeratelaner-   rg   labelevent_data_jsindexbrief_value	api_valuer   axis_data_jsrow_label_jsjoindumpsmkdiropenfwriter   TECH_CARD_PATCHoutr   html2r   r   SAFE_FOLDER_CARD_PATCHSEARCH_HARD_FIX_PATCH_GrafoDPath_gd_htmlr!   _gd_s_gd_css_gd_js_ApiPatchPath	_api_html_html_sensor_LiveSensorPath_live_html_path
_live_html	_live_css_live_panel_live_jsr   r   r   r   r   r   r   rer   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r  r  r  r  r  r  r1   r   r   <module>rT     s                0 0 0 0 0 0 *	+		


 
 

 
:fd ZZ"%%FFZ FFF  
    @ vcc++F+++++,,
	 9 9A77v777DC<<4<<<<<MsQQDQQQQQH 7a8E!HH3u%)$$$    =CI88888888s=====qIII<      J M-}o  %)$	
 
 	 	 	 	   
 
	 / /A!E(AfI....:fd L::j"--11."EEKL)//11 + +G\**
7==?? 06688 2 2G\&1'JwVC'','''''##.A.F.F.H.H*I*II55  

 #,,V,,,a
8
8
8	Gf$	$s	*	33z??_4	4s	::&& 	 	ID#4##f++%&EF55C))**E +0 :=  "'	   	 	 	 	 	 1 1AAeH%%D4##f++%&E 	
!"" 	55>>	55>>	 55	   	
j 	55<<	   eeAEE$KK  w## 	ss1U8}} 	33quuY''31U844	
 	ai   	eeAEE,//<1V9== 	; 	q| 	Q{^ 	1Y< 	 	aeeFmm9quu['9'9:: 	UU155-->v??  	f9{!3!39r!" 	w<155#6#6<"#$ 	quuU||6quuZ006B%& 	quuW~~7w88'( quuW~~7w88uQUU5\\1QUU5\\22uQUU5\\1QUU5\\22u[)){##{##"U;//uQUU=1122%m,,?h@@uYE)$$=      B       
_ _ _ _ _ _t
 u
_ _ _ _v
 w
_ _ _ _D E_ _ _ _F G_ _ _ _| }_ _ _ _@ fA_ _ _ _r s_ _ _ _t u_ _ _ _b WW]c_ _ _ _R $*\>>>S_ _ _ _T 4:m%@@@U_ _ _ _V 4:l???W_ _ _ _h i_ _ _ _j k_ _ _ _N- O-_ _ _ _N- ,;O-_ _ _ _l- !m-_ _ _ _l- :@m-_ _ _ _n- !o-_ _ _ _n- 28o-_ _ _ _z. {._ _ _ _z. =C{._ _ _ _|. }._ _ _ _|. ?N}._ _ _ _L/ -3M/_ _ _ _j/ %k/_ _ _ _j/ 9?k/_ _ _ _ _ _B5 Y  t  $ $ $	T&#((( AGGDMMM                & ) ) )JX
 d6llw''%%MM)_{%BCCENN57N+++	E
(&1111	E
2F;;;W r
 d6llw''&&MM)%;k%IJJENN57N+++	E
3V<<<<	E
4f===j X d6llw''e++MM)%:[%HIIENN57N+++	E
(&1111	E
.777 ( ' ' ' ' ';9::?? h500E"%//QfM^ i;)>??i+)=>>EG444
 * ) ) ) ) )M<==	 %511E&e33@ i;)>??UW5553444 , + + + + +!/"DEE l* **G*<<J);;	(  ''	9{3JKK
:--  ''(DjS^F^auFuwxyy
#:55CH  ''	8k3IJJ
zG<<<	E
())) % $ $ $ $ $W1 W1 W1t
 ( ' ) ) ) & % % % % %[1 [1 [1| ' & ( ( ( ) ( ( ( ( (H1 H1 H1V ' & ( ( ( & % % % % %    C1 C1 C1L 1 0 2 2 2 * ) ) ) ) )    1 1 10 / . 0 0 0 & % % % % %    1 1 1 ) ( * * * & % % % % %    D1 D1 D1N * ) + + + * ) ) ) ) )hB hB hBV ( ' ) ) ) % $ $ $ $ $    p5 p5 p5d & % ' ' ' % $ $ $ $ $    8; 8; 8;t !   " " " % $ $ $ $ $    D4 D4 D4L      % $ $ $ $ $    67 67 67p    ! ! ! % $ $ $ $ $    79 79 79r    ! ! ! % $ $ $ $ $    y5 y5 y5v " ! # # # % $ $ $ $ $    R7 R7 R7h & % ' ' ' % $ $ $ $ $    k? k? k?Z !   " " " % $ $ $ $ $    U5 U5 U5n " ! # # # % $ $ $ $ $    f8 f8 f8P $ # % % % % $ $ $ $ $    p5 p5 p5d            s   `11`58`5