Jose Salazar commited on
Commit
de1d694
·
1 Parent(s): 0ef022d

Maqueta de layout para UI con mockup data, actualizo a ultimas versiones todas las dependencias de los package.json

Browse files
backend/package.json CHANGED
@@ -14,13 +14,13 @@
14
  "db:studio": "prisma studio"
15
  },
16
  "dependencies": {
17
- "express": "^4.21.0",
18
- "socket.io": "^4.8.0",
19
- "node-cron": "^3.0.3",
20
- "@prisma/client": "^6.0.0",
21
- "prisma": "^6.0.0"
22
  },
23
  "engines": {
24
- "node": ">=22.0.0"
25
  }
26
  }
 
14
  "db:studio": "prisma studio"
15
  },
16
  "dependencies": {
17
+ "express": "^5.2.1",
18
+ "socket.io": "^4.8.3",
19
+ "node-cron": "^4.2.1",
20
+ "@prisma/client": "^7.8.0",
21
+ "prisma": "^7.8.0"
22
  },
23
  "engines": {
24
+ "node": ">=26.0.0"
25
  }
26
  }
frontend/dist/assets/index-A_KP_t7E.js ADDED
The diff for this file is too large to render. See raw diff
 
frontend/dist/assets/index-CHfdQIIe.js DELETED
The diff for this file is too large to render. See raw diff
 
frontend/dist/assets/index-CIGW-MKW.css DELETED
@@ -1 +0,0 @@
1
- .leaflet-pane,.leaflet-tile,.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-tile-container,.leaflet-pane>svg,.leaflet-pane>canvas,.leaflet-zoom-box,.leaflet-image-layer,.leaflet-layer{position:absolute;left:0;top:0}.leaflet-container{overflow:hidden}.leaflet-tile,.leaflet-marker-icon,.leaflet-marker-shadow{-webkit-user-select:none;-moz-user-select:none;user-select:none;-webkit-user-drag:none}.leaflet-tile::selection{background:transparent}.leaflet-safari .leaflet-tile{image-rendering:-webkit-optimize-contrast}.leaflet-safari .leaflet-tile-container{width:1600px;height:1600px;-webkit-transform-origin:0 0}.leaflet-marker-icon,.leaflet-marker-shadow{display:block}.leaflet-container .leaflet-overlay-pane svg{max-width:none!important;max-height:none!important}.leaflet-container .leaflet-marker-pane img,.leaflet-container .leaflet-shadow-pane img,.leaflet-container .leaflet-tile-pane img,.leaflet-container img.leaflet-image-layer,.leaflet-container .leaflet-tile{max-width:none!important;max-height:none!important;width:auto;padding:0}.leaflet-container img.leaflet-tile{mix-blend-mode:plus-lighter}.leaflet-container.leaflet-touch-zoom{-ms-touch-action:pan-x pan-y;touch-action:pan-x pan-y}.leaflet-container.leaflet-touch-drag{-ms-touch-action:pinch-zoom;touch-action:none;touch-action:pinch-zoom}.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom{-ms-touch-action:none;touch-action:none}.leaflet-container{-webkit-tap-highlight-color:transparent}.leaflet-container a{-webkit-tap-highlight-color:rgba(51,181,229,.4)}.leaflet-tile{filter:inherit;visibility:hidden}.leaflet-tile-loaded{visibility:inherit}.leaflet-zoom-box{width:0;height:0;-moz-box-sizing:border-box;box-sizing:border-box;z-index:800}.leaflet-overlay-pane svg{-moz-user-select:none}.leaflet-pane{z-index:400}.leaflet-tile-pane{z-index:200}.leaflet-overlay-pane{z-index:400}.leaflet-shadow-pane{z-index:500}.leaflet-marker-pane{z-index:600}.leaflet-tooltip-pane{z-index:650}.leaflet-popup-pane{z-index:700}.leaflet-map-pane canvas{z-index:100}.leaflet-map-pane svg{z-index:200}.leaflet-vml-shape{width:1px;height:1px}.lvml{behavior:url(#default#VML);display:inline-block;position:absolute}.leaflet-control{position:relative;z-index:800;pointer-events:visiblePainted;pointer-events:auto}.leaflet-top,.leaflet-bottom{position:absolute;z-index:1000;pointer-events:none}.leaflet-top{top:0}.leaflet-right{right:0}.leaflet-bottom{bottom:0}.leaflet-left{left:0}.leaflet-control{float:left;clear:both}.leaflet-right .leaflet-control{float:right}.leaflet-top .leaflet-control{margin-top:10px}.leaflet-bottom .leaflet-control{margin-bottom:10px}.leaflet-left .leaflet-control{margin-left:10px}.leaflet-right .leaflet-control{margin-right:10px}.leaflet-fade-anim .leaflet-popup{opacity:0;-webkit-transition:opacity .2s linear;-moz-transition:opacity .2s linear;transition:opacity .2s linear}.leaflet-fade-anim .leaflet-map-pane .leaflet-popup{opacity:1}.leaflet-zoom-animated{-webkit-transform-origin:0 0;-ms-transform-origin:0 0;transform-origin:0 0}svg.leaflet-zoom-animated{will-change:transform}.leaflet-zoom-anim .leaflet-zoom-animated{-webkit-transition:-webkit-transform .25s cubic-bezier(0,0,.25,1);-moz-transition:-moz-transform .25s cubic-bezier(0,0,.25,1);transition:transform .25s cubic-bezier(0,0,.25,1)}.leaflet-zoom-anim .leaflet-tile,.leaflet-pan-anim .leaflet-tile{-webkit-transition:none;-moz-transition:none;transition:none}.leaflet-zoom-anim .leaflet-zoom-hide{visibility:hidden}.leaflet-interactive{cursor:pointer}.leaflet-grab{cursor:-webkit-grab;cursor:-moz-grab;cursor:grab}.leaflet-crosshair,.leaflet-crosshair .leaflet-interactive{cursor:crosshair}.leaflet-popup-pane,.leaflet-control{cursor:auto}.leaflet-dragging .leaflet-grab,.leaflet-dragging .leaflet-grab .leaflet-interactive,.leaflet-dragging .leaflet-marker-draggable{cursor:move;cursor:-webkit-grabbing;cursor:-moz-grabbing;cursor:grabbing}.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-image-layer,.leaflet-pane>svg path,.leaflet-tile-container{pointer-events:none}.leaflet-marker-icon.leaflet-interactive,.leaflet-image-layer.leaflet-interactive,.leaflet-pane>svg path.leaflet-interactive,svg.leaflet-image-layer.leaflet-interactive path{pointer-events:visiblePainted;pointer-events:auto}.leaflet-container{background:#ddd;outline-offset:1px}.leaflet-container a{color:#0078a8}.leaflet-zoom-box{border:2px dotted #38f;background:#ffffff80}.leaflet-container{font-family:Helvetica Neue,Arial,Helvetica,sans-serif;font-size:12px;font-size:.75rem;line-height:1.5}.leaflet-bar{box-shadow:0 1px 5px #000000a6;border-radius:4px}.leaflet-bar a{background-color:#fff;border-bottom:1px solid #ccc;width:26px;height:26px;line-height:26px;display:block;text-align:center;text-decoration:none;color:#000}.leaflet-bar a,.leaflet-control-layers-toggle{background-position:50% 50%;background-repeat:no-repeat;display:block}.leaflet-bar a:hover,.leaflet-bar a:focus{background-color:#f4f4f4}.leaflet-bar a:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.leaflet-bar a:last-child{border-bottom-left-radius:4px;border-bottom-right-radius:4px;border-bottom:none}.leaflet-bar a.leaflet-disabled{cursor:default;background-color:#f4f4f4;color:#bbb}.leaflet-touch .leaflet-bar a{width:30px;height:30px;line-height:30px}.leaflet-touch .leaflet-bar a:first-child{border-top-left-radius:2px;border-top-right-radius:2px}.leaflet-touch .leaflet-bar a:last-child{border-bottom-left-radius:2px;border-bottom-right-radius:2px}.leaflet-control-zoom-in,.leaflet-control-zoom-out{font:700 18px Lucida Console,Monaco,monospace;text-indent:1px}.leaflet-touch .leaflet-control-zoom-in,.leaflet-touch .leaflet-control-zoom-out{font-size:22px}.leaflet-control-layers{box-shadow:0 1px 5px #0006;background:#fff;border-radius:5px}.leaflet-control-layers-toggle{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAQAAAADQ4RFAAACf0lEQVR4AY1UM3gkARTePdvdoTxXKc+qTl3aU5U6b2Kbkz3Gtq3Zw6ziLGNPzrYx7946Tr6/ee/XeCQ4D3ykPtL5tHno4n0d/h3+xfuWHGLX81cn7r0iTNzjr7LrlxCqPtkbTQEHeqOrTy4Yyt3VCi/IOB0v7rVC7q45Q3Gr5K6jt+3Gl5nCoDD4MtO+j96Wu8atmhGqcNGHObuf8OM/x3AMx38+4Z2sPqzCxRFK2aF2e5Jol56XTLyggAMTL56XOMoS1W4pOyjUcGGQdZxU6qRh7B9Zp+PfpOFlqt0zyDZckPi1ttmIp03jX8gyJ8a/PG2yutpS/Vol7peZIbZcKBAEEheEIAgFbDkz5H6Zrkm2hVWGiXKiF4Ycw0RWKdtC16Q7qe3X4iOMxruonzegJzWaXFrU9utOSsLUmrc0YjeWYjCW4PDMADElpJSSQ0vQvA1Tm6/JlKnqFs1EGyZiFCqnRZTEJJJiKRYzVYzJck2Rm6P4iH+cmSY0YzimYa8l0EtTODFWhcMIMVqdsI2uiTvKmTisIDHJ3od5GILVhBCarCfVRmo4uTjkhrhzkiBV7SsaqS+TzrzM1qpGGUFt28pIySQHR6h7F6KSwGWm97ay+Z+ZqMcEjEWebE7wxCSQwpkhJqoZA5ivCdZDjJepuJ9IQjGGUmuXJdBFUygxVqVsxFsLMbDe8ZbDYVCGKxs+W080max1hFCarCfV+C1KATwcnvE9gRRuMP2prdbWGowm1KB1y+zwMMENkM755cJ2yPDtqhTI6ED1M/82yIDtC/4j4BijjeObflpO9I9MwXTCsSX8jWAFeHr05WoLTJ5G8IQVS/7vwR6ohirYM7f6HzYpogfS3R2OAAAAAElFTkSuQmCC);width:36px;height:36px}.leaflet-retina .leaflet-control-layers-toggle{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADQAAAA0CAQAAABvcdNgAAAEsklEQVR4AWL4TydIhpZK1kpWOlg0w3ZXP6D2soBtG42jeI6ZmQTHzAxiTbSJsYLjO9HhP+WOmcuhciVnmHVQcJnp7DFvScowZorad/+V/fVzMdMT2g9Cv9guXGv/7pYOrXh2U+RRR3dSd9JRx6bIFc/ekqHI29JC6pJ5ZEh1yWkhkbcFeSjxgx3L2m1cb1C7bceyxA+CNjT/Ifff+/kDk2u/w/33/IeCMOSaWZ4glosqT3DNnNZQ7Cs58/3Ce5HL78iZH/vKVIaYlqzfdLu8Vi7dnvUbEza5Idt36tquZFldl6N5Z/POLof0XLK61mZCmJSWjVF9tEjUluu74IUXvgttuVIHE7YxSkaYhJZam7yiM9Pv82JYfl9nptxZaxMJE4YSPty+vF0+Y2up9d3wwijfjZbabqm/3bZ9ecKHsiGmRflnn1MW4pjHf9oLufyn2z3y1D6n8g8TZhxyzipLNPnAUpsOiuWimg52psrTZYnOWYNDTMuWBWa0tJb4rgq1UvmutpaYEbZlwU3CLJm/ayYjHW5/h7xWLn9Hh1vepDkyf7dE7MtT5LR4e7yYpHrkhOUpEfssBLq2pPhAqoSWKUkk7EDqkmK6RrCEzqDjhNDWNE+XSMvkJRDWlZTmCW0l0PHQGRZY5t1L83kT0Y3l2SItk5JAWHl2dCOBm+fPu3fo5/3v61RMCO9Jx2EEYYhb0rmNQMX/vm7gqOEJLcXTGw3CAuRNeyaPWwjR8PRqKQ1PDA/dpv+on9Shox52WFnx0KY8onHayrJzm87i5h9xGw/tfkev0jGsQizqezUKjk12hBMKJ4kbCqGPVNXudyyrShovGw5CgxsRICxF6aRmSjlBnHRzg7Gx8fKqEubI2rahQYdR1YgDIRQO7JvQyD52hoIQx0mxa0ODtW2Iozn1le2iIRdzwWewedyZzewidueOGqlsn1MvcnQpuVwLGG3/IR1hIKxCjelIDZ8ldqWz25jWAsnldEnK0Zxro19TGVb2ffIZEsIO89EIEDvKMPrzmBOQcKQ+rroye6NgRRxqR4U8EAkz0CL6uSGOm6KQCdWjvjRiSP1BPalCRS5iQYiEIvxuBMJEWgzSoHADcVMuN7IuqqTeyUPq22qFimFtxDyBBJEwNyt6TM88blFHao/6tWWhuuOM4SAK4EI4QmFHA+SEyWlp4EQoJ13cYGzMu7yszEIBOm2rVmHUNqwAIQabISNMRstmdhNWcFLsSm+0tjJH1MdRxO5Nx0WDMhCtgD6OKgZeljJqJKc9po8juskR9XN0Y1lZ3mWjLR9JCO1jRDMd0fpYC2VnvjBSEFg7wBENc0R9HFlb0xvF1+TBEpF68d+DHR6IOWVv2BECtxo46hOFUBd/APU57WIoEwJhIi2CdpyZX0m93BZicktMj1AS9dClteUFAUNUIEygRZCtik5zSxI9MubTBH1GOiHsiLJ3OCoSZkILa9PxiN0EbvhsAo8tdAf9Seepd36lGWHmtNANTv5Jd0z4QYyeo/UEJqxKRpg5LZx6btLPsOaEmdMyxYdlc8LMaJnikDlhclqmPiQnTEpLUIZEwkRagjYkEibQErwhkTAKCLQEbUgkzJQWc/0PstHHcfEdQ+UAAAAASUVORK5CYII=);background-size:26px 26px}.leaflet-touch .leaflet-control-layers-toggle{width:44px;height:44px}.leaflet-control-layers .leaflet-control-layers-list,.leaflet-control-layers-expanded .leaflet-control-layers-toggle{display:none}.leaflet-control-layers-expanded .leaflet-control-layers-list{display:block;position:relative}.leaflet-control-layers-expanded{padding:6px 10px 6px 6px;color:#333;background:#fff}.leaflet-control-layers-scrollbar{overflow-y:scroll;overflow-x:hidden;padding-right:5px}.leaflet-control-layers-selector{margin-top:2px;position:relative;top:1px}.leaflet-control-layers label{display:block;font-size:13px;font-size:1.08333em}.leaflet-control-layers-separator{height:0;border-top:1px solid #ddd;margin:5px -10px 5px -6px}.leaflet-default-icon-path{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAApCAYAAADAk4LOAAAFgUlEQVR4Aa1XA5BjWRTN2oW17d3YaZtr2962HUzbDNpjszW24mRt28p47v7zq/bXZtrp/lWnXr337j3nPCe85NcypgSFdugCpW5YoDAMRaIMqRi6aKq5E3YqDQO3qAwjVWrD8Ncq/RBpykd8oZUb/kaJutow8r1aP9II0WmLKLIsJyv1w/kqw9Ch2MYdB++12Onxee/QMwvf4/Dk/Lfp/i4nxTXtOoQ4pW5Aj7wpici1A9erdAN2OH64x8OSP9j3Ft3b7aWkTg/Fm91siTra0f9on5sQr9INejH6CUUUpavjFNq1B+Oadhxmnfa8RfEmN8VNAsQhPqF55xHkMzz3jSmChWU6f7/XZKNH+9+hBLOHYozuKQPxyMPUKkrX/K0uWnfFaJGS1QPRtZsOPtr3NsW0uyh6NNCOkU3Yz+bXbT3I8G3xE5EXLXtCXbbqwCO9zPQYPRTZ5vIDXD7U+w7rFDEoUUf7ibHIR4y6bLVPXrz8JVZEql13trxwue/uDivd3fkWRbS6/IA2bID4uk0UpF1N8qLlbBlXs4Ee7HLTfV1j54APvODnSfOWBqtKVvjgLKzF5YdEk5ewRkGlK0i33Eofffc7HT56jD7/6U+qH3Cx7SBLNntH5YIPvODnyfIXZYRVDPqgHtLs5ABHD3YzLuespb7t79FY34DjMwrVrcTuwlT55YMPvOBnRrJ4VXTdNnYug5ucHLBjEpt30701A3Ts+HEa73u6dT3FNWwflY86eMHPk+Yu+i6pzUpRrW7SNDg5JHR4KapmM5Wv2E8Tfcb1HoqqHMHU+uWDD7zg54mz5/2BSnizi9T1Dg4QQXLToGNCkb6tb1NU+QAlGr1++eADrzhn/u8Q2YZhQVlZ5+CAOtqfbhmaUCS1ezNFVm2imDbPmPng5wmz+gwh+oHDce0eUtQ6OGDIyR0uUhUsoO3vfDmmgOezH0mZN59x7MBi++WDL1g/eEiU3avlidO671bkLfwbw5XV2P8Pzo0ydy4t2/0eu33xYSOMOD8hTf4CrBtGMSoXfPLchX+J0ruSePw3LZeK0juPJbYzrhkH0io7B3k164hiGvawhOKMLkrQLyVpZg8rHFW7E2uHOL888IBPlNZ1FPzstSJM694fWr6RwpvcJK60+0HCILTBzZLFNdtAzJaohze60T8qBzyh5ZuOg5e7uwQppofEmf2++DYvmySqGBuKaicF1blQjhuHdvCIMvp8whTTfZzI7RldpwtSzL+F1+wkdZ2TBOW2gIF88PBTzD/gpeREAMEbxnJcaJHNHrpzji0gQCS6hdkEeYt9DF/2qPcEC8RM28Hwmr3sdNyht00byAut2k3gufWNtgtOEOFGUwcXWNDbdNbpgBGxEvKkOQsxivJx33iow0Vw5S6SVTrpVq11ysA2Rp7gTfPfktc6zhtXBBC+adRLshf6sG2RfHPZ5EAc4sVZ83yCN00Fk/4kggu40ZTvIEm5g24qtU4KjBrx/BTTH8ifVASAG7gKrnWxJDcU7x8X6Ecczhm3o6YicvsLXWfh3Ch1W0k8x0nXF+0fFxgt4phz8QvypiwCCFKMqXCnqXExjq10beH+UUA7+nG6mdG/Pu0f3LgFcGrl2s0kNNjpmoJ9o4B29CMO8dMT4Q5ox8uitF6fqsrJOr8qnwNbRzv6hSnG5wP+64C7h9lp30hKNtKdWjtdkbuPA19nJ7Tz3zR/ibgARbhb4AlhavcBebmTHcFl2fvYEnW0ox9xMxKBS8btJ+KiEbq9zA4RthQXDhPa0T9TEe69gWupwc6uBUphquXgf+/FrIjweHQS4/pduMe5ERUMHUd9xv8ZR98CxkS4F2n3EUrUZ10EYNw7BWm9x1GiPssi3GgiGRDKWRYZfXlON+dfNbM+GgIwYdwAAAAASUVORK5CYII=)}.leaflet-container .leaflet-control-attribution{background:#fff;background:#fffc;margin:0}.leaflet-control-attribution,.leaflet-control-scale-line{padding:0 5px;color:#333;line-height:1.4}.leaflet-control-attribution a{text-decoration:none}.leaflet-control-attribution a:hover,.leaflet-control-attribution a:focus{text-decoration:underline}.leaflet-attribution-flag{display:inline!important;vertical-align:baseline!important;width:1em;height:.6669em}.leaflet-left .leaflet-control-scale{margin-left:5px}.leaflet-bottom .leaflet-control-scale{margin-bottom:5px}.leaflet-control-scale-line{border:2px solid #777;border-top:none;line-height:1.1;padding:2px 5px 1px;white-space:nowrap;-moz-box-sizing:border-box;box-sizing:border-box;background:#fffc;text-shadow:1px 1px #fff}.leaflet-control-scale-line:not(:first-child){border-top:2px solid #777;border-bottom:none;margin-top:-2px}.leaflet-control-scale-line:not(:first-child):not(:last-child){border-bottom:2px solid #777}.leaflet-touch .leaflet-control-attribution,.leaflet-touch .leaflet-control-layers,.leaflet-touch .leaflet-bar{box-shadow:none}.leaflet-touch .leaflet-control-layers,.leaflet-touch .leaflet-bar{border:2px solid rgba(0,0,0,.2);background-clip:padding-box}.leaflet-popup{position:absolute;text-align:center;margin-bottom:20px}.leaflet-popup-content-wrapper{padding:1px;text-align:left;border-radius:12px}.leaflet-popup-content{margin:13px 24px 13px 20px;line-height:1.3;font-size:13px;font-size:1.08333em;min-height:1px}.leaflet-popup-content p{margin:1.3em 0}.leaflet-popup-tip-container{width:40px;height:20px;position:absolute;left:50%;margin-top:-1px;margin-left:-20px;overflow:hidden;pointer-events:none}.leaflet-popup-tip{width:17px;height:17px;padding:1px;margin:-10px auto 0;pointer-events:auto;-webkit-transform:rotate(45deg);-moz-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.leaflet-popup-content-wrapper,.leaflet-popup-tip{background:#fff;color:#333;box-shadow:0 3px 14px #0006}.leaflet-container a.leaflet-popup-close-button{position:absolute;top:0;right:0;border:none;text-align:center;width:24px;height:24px;font:16px/24px Tahoma,Verdana,sans-serif;color:#757575;text-decoration:none;background:transparent}.leaflet-container a.leaflet-popup-close-button:hover,.leaflet-container a.leaflet-popup-close-button:focus{color:#585858}.leaflet-popup-scrolled{overflow:auto}.leaflet-oldie .leaflet-popup-content-wrapper{-ms-zoom:1}.leaflet-oldie .leaflet-popup-tip{width:24px;margin:0 auto;-ms-filter:"progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";filter:progid:DXImageTransform.Microsoft.Matrix(M11=.70710678,M12=.70710678,M21=-.70710678,M22=.70710678)}.leaflet-oldie .leaflet-control-zoom,.leaflet-oldie .leaflet-control-layers,.leaflet-oldie .leaflet-popup-content-wrapper,.leaflet-oldie .leaflet-popup-tip{border:1px solid #999}.leaflet-div-icon{background:#fff;border:1px solid #666}.leaflet-tooltip{position:absolute;padding:6px;background-color:#fff;border:1px solid #fff;border-radius:3px;color:#222;white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none;box-shadow:0 1px 3px #0006}.leaflet-tooltip.leaflet-interactive{cursor:pointer;pointer-events:auto}.leaflet-tooltip-top:before,.leaflet-tooltip-bottom:before,.leaflet-tooltip-left:before,.leaflet-tooltip-right:before{position:absolute;pointer-events:none;border:6px solid transparent;background:transparent;content:""}.leaflet-tooltip-bottom{margin-top:6px}.leaflet-tooltip-top{margin-top:-6px}.leaflet-tooltip-bottom:before,.leaflet-tooltip-top:before{left:50%;margin-left:-6px}.leaflet-tooltip-top:before{bottom:0;margin-bottom:-12px;border-top-color:#fff}.leaflet-tooltip-bottom:before{top:0;margin-top:-12px;margin-left:-6px;border-bottom-color:#fff}.leaflet-tooltip-left{margin-left:-6px}.leaflet-tooltip-right{margin-left:6px}.leaflet-tooltip-left:before,.leaflet-tooltip-right:before{top:50%;margin-top:-6px}.leaflet-tooltip-left:before{right:0;margin-right:-12px;border-left-color:#fff}.leaflet-tooltip-right:before{left:0;margin-left:-12px;border-right-color:#fff}@media print{.leaflet-control{-webkit-print-color-adjust:exact;print-color-adjust:exact}}
 
 
frontend/dist/assets/index-xtYPhhTl.css ADDED
@@ -0,0 +1 @@
 
 
1
+ @import "https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Syne:wght@400;500;600;700&display=swap";:root{--bg:#0a0c10;--bg2:#111318;--bg3:#181c24;--bg4:#1e232d;--border:#ffffff14;--border2:#ffffff21;--text:#e8eaf0;--text2:#8b90a0;--text3:#555b6e;--green:#22d37a;--green2:#0d6e3a;--green3:#052a17;--red:#f04040;--red2:#7a1a1a;--red3:#2d0808;--blue:#4a9eff;--blue2:#1a4a80;--blue3:#081830;--amber:#f0a020;--amber2:#7a4e08;--amber3:#2d1c02;--accent:#4a9eff;--sidebar-width:240px;--sidebar-collapsed:56px;--topbar-height:56px;--panel-gap:16px;--radius:10px;--radius-sm:6px;--fs-p:clamp(16px, 1.15vw, 18px);--fs-h1:clamp(28px, 2.8vw, 36px);--fs-h2:clamp(24px, 2.4vw, 30px);--fs-h3:clamp(20px, 1.9vw, 24px);--fs-h4:clamp(18px, 1.6vw, 20px);--fs-h5:clamp(16px, 1.3vw, 18px);--fs-h6:clamp(14px, 1.1vw, 16px)}*{box-sizing:border-box;margin:0;padding:0}html,body,#app{width:100%;height:100%;overflow:hidden}body{background:var(--bg);color:var(--text);-webkit-font-smoothing:antialiased;font-family:Syne,sans-serif;font-size:16px;line-height:1.5}p{font-size:var(--fs-p);line-height:1.55}h1{font-size:var(--fs-h1);font-weight:700;line-height:1.15}h2{font-size:var(--fs-h2);font-weight:700;line-height:1.2}h3{font-size:var(--fs-h3);font-weight:600;line-height:1.25}h4{font-size:var(--fs-h4);font-weight:600;line-height:1.3}h5{font-size:var(--fs-h5);font-weight:500;line-height:1.35}h6{font-size:var(--fs-h6);font-weight:500;line-height:1.4}.layout{grid-template-areas:"topbar topbar""sidebar main";grid-template-columns:var(--sidebar-width) 1fr;grid-template-rows:var(--topbar-height) 1fr;width:100vw;height:100vh;transition:grid-template-columns .25s;display:grid}.layout.collapsed{grid-template-columns:var(--sidebar-collapsed) 1fr}.sidebar{background:var(--bg2);border-right:1px solid var(--border);flex-direction:column;grid-area:sidebar;transition:width .25s;display:flex;position:relative;overflow:hidden}.sidebar-toggle{background:var(--bg3);border:1px solid var(--border2);width:24px;height:24px;color:var(--text2);cursor:pointer;z-index:10;border-radius:50%;justify-content:center;align-items:center;font-size:14px;transition:transform .2s;display:flex;position:absolute;top:12px;right:-10px}.layout.collapsed .sidebar-toggle{right:-10px;transform:rotate(180deg)}.topbar-logo{align-items:center;gap:10px;display:flex;position:absolute;top:0;bottom:0;left:16px}.topbar-logo .logo-dot{background:var(--blue);border-radius:50%;flex-shrink:0;width:10px;height:10px;animation:2s ease-in-out infinite pulse}.topbar-logo .logo-text{color:var(--text);letter-spacing:-.3px;white-space:nowrap;font-size:18px;font-weight:700}.sidebar-nav{flex-direction:column;flex:1;gap:4px;padding:14px 8px;display:flex;overflow-y:auto}.nav-item{border-radius:var(--radius-sm);cursor:pointer;color:var(--text2);white-space:nowrap;align-items:center;gap:14px;padding:14px;font-size:16px;font-weight:500;transition:background .15s,color .15s;display:flex;overflow:hidden}.nav-item:hover{background:var(--bg3);color:var(--text)}.nav-item.active{background:var(--blue3);color:var(--blue);border:.5px solid var(--blue2)}.nav-icon{text-align:center;flex-shrink:0;width:20px;font-size:16px}.nav-label{opacity:1;transition:opacity .15s}.layout.collapsed .nav-label{opacity:0;width:0}.sidebar-footer{border-top:1px solid var(--border);color:var(--text3);text-align:center;white-space:nowrap;padding:14px;font-family:DM Mono,monospace;font-size:14px;overflow:hidden}.layout.collapsed .sidebar-footer{opacity:0}.topbar{background:var(--bg2);border-bottom:1px solid var(--border);padding:0 16px;padding-left:calc(var(--sidebar-width) + var(--panel-gap));grid-area:topbar;align-items:center;gap:16px;transition:padding-left .25s;display:flex;position:relative;overflow:hidden}.layout.collapsed .topbar{padding-left:calc(var(--sidebar-collapsed) + var(--panel-gap))}.live-badge{background:var(--green3);color:var(--green);border:.5px solid var(--green2);border-radius:4px;flex-shrink:0;align-items:center;gap:4px;padding:6px 12px;font-family:DM Mono,monospace;font-size:14px;display:flex}.live-dot{background:var(--green);border-radius:50%;width:8px;height:8px;animation:1.5s ease-in-out infinite pulse}.topbar-stats{flex:1;align-items:center;gap:20px;display:flex;overflow:hidden}.stat{flex-shrink:0;align-items:center;gap:14px;display:flex}.stat-label{color:var(--text3);text-transform:uppercase;letter-spacing:.06em;font-family:DM Mono,monospace;font-size:14px}.stat-val{color:var(--text);font-family:DM Mono,monospace;font-size:16px;font-weight:600}.stat-delta{font-family:DM Mono,monospace;font-size:14px}.stat-delta.up{color:var(--green)}.stat-delta.dn{color:var(--red)}.stat-delta.neutral{color:var(--text3)}.topbar-actions{flex-shrink:0;align-items:center;gap:16px;margin-left:auto;display:flex}.btn-ghost{color:var(--blue);border:.5px solid var(--blue2);background:var(--blue3);border-radius:var(--radius-sm);cursor:pointer;padding:8px 16px;font-family:Syne,sans-serif;font-size:15px;font-weight:500;transition:opacity .15s}.btn-ghost:hover{opacity:.85}.icon-btn{background:var(--bg3);border:.5px solid var(--border2);width:32px;height:32px;color:var(--text2);cursor:pointer;border-radius:50%;justify-content:center;align-items:center;font-size:16px;transition:color .15s,border-color .15s;display:flex}.icon-btn:hover{color:var(--text);border-color:var(--border2)}.main{padding:var(--panel-gap);background:var(--bg);grid-area:main;overflow:auto}.view{display:none}.view.active{height:100%;display:block}.dashboard-grid{gap:var(--panel-gap);grid-template-rows:1fr minmax(280px,40%);grid-template-columns:1fr 280px;height:100%;min-height:0;display:grid}.panel{background:var(--bg2);border:.5px solid var(--border);border-radius:var(--radius);flex-direction:column;min-height:0;display:flex;overflow:hidden}.panel-header{border-bottom:.5px solid var(--border);cursor:pointer;-webkit-user-select:none;user-select:none;justify-content:space-between;align-items:center;padding:14px;transition:background .15s;display:flex}.panel-header:hover{background:#ffffff05}.panel-title{color:var(--text3);text-transform:uppercase;letter-spacing:.08em;align-items:center;gap:14px;font-family:DM Mono,monospace;font-size:14px;display:flex}.panel-toggle{color:var(--text3);font-size:14px;transition:transform .2s}.panel.collapsed .panel-toggle{transform:rotate(-90deg)}.panel-body{flex:1;min-height:0;padding:12px;overflow:auto}.panel.collapsed .panel-body{display:none}.panel-subtitle{color:var(--text3);font-size:14px}.positions-separator{border-top:.5px solid var(--border);margin-top:10px;padding-top:10px}.panel-title.mb-sm{margin-bottom:8px}.panel.full-height{height:100%}.hidden{display:none}.map-panel{grid-area:1/1/2/2}#map-container{background:var(--bg3);border-radius:var(--radius-sm);width:100%;height:100%;min-height:300px;overflow:hidden}.signals-panel{grid-area:1/2/3/3}.signals-list{flex-direction:column;gap:16px;display:flex}.market-card{background:var(--bg3);border:.5px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;padding:14px;transition:border-color .15s,background .15s}.market-card:hover{border-color:var(--border2)}.market-card.active{border-color:var(--blue2);background:var(--blue3)}.market-cat{color:var(--text3);text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px;font-family:DM Mono,monospace;font-size:14px}.market-q{color:var(--text);margin-bottom:8px;font-size:15px;font-weight:500;line-height:1.4}.market-footer{justify-content:space-between;align-items:center;gap:16px;display:flex}.prob-bar-wrap{flex:1;min-width:0}.prob-bar-bg{background:var(--bg4);border-radius:2px;height:5px;overflow:hidden}.prob-bar-fill{height:100%;width:var(--prob-width,0%);border-radius:2px;transition:width .8s}.prob-val{flex-shrink:0;font-family:DM Mono,monospace;font-size:15px;font-weight:600}.signal-badge{letter-spacing:.04em;border-radius:4px;flex-shrink:0;padding:2px 6px;font-family:DM Mono,monospace;font-size:14px;font-weight:600}.sig-bull{background:var(--green3);color:var(--green);border:.5px solid var(--green2)}.sig-bear{background:var(--red3);color:var(--red);border:.5px solid var(--red2)}.sig-neut{background:var(--bg4);color:var(--text2);border:.5px solid var(--border2)}.detail-panel{grid-area:2/1/3/2;max-height:100%;overflow:hidden}.detail-panel .panel-body{overflow-y:auto}.detail-header{justify-content:space-between;align-items:flex-start;gap:16px;margin-bottom:12px;display:flex}.detail-tag{color:var(--blue);text-transform:uppercase;letter-spacing:.08em;margin-bottom:4px;font-family:DM Mono,monospace;font-size:14px}.detail-q{color:var(--text);font-size:16px;font-weight:600;line-height:1.5}.detail-meta{color:var(--text3);margin-top:2px;font-family:DM Mono,monospace;font-size:14px}.detail-metrics{flex-shrink:0;align-items:center;gap:16px;display:flex}.metric{text-align:right}.metric-label{color:var(--text3);margin-bottom:2px;font-family:DM Mono,monospace;font-size:14px}.metric-value{font-family:DM Mono,monospace;font-size:14px;font-weight:700}.metric-sep{background:var(--border);width:1px;height:40px}.outcomes-row{gap:16px;margin-bottom:12px;display:flex}.outcome-card{background:var(--bg3);border:.5px solid var(--border);border-radius:var(--radius-sm);text-align:center;flex:1;padding:14px}.outcome-name{color:var(--text2);margin-bottom:4px;font-family:DM Mono,monospace;font-size:14px}.outcome-price{color:var(--text);font-family:DM Mono,monospace;font-size:20px;font-weight:700}.outcome-delta{margin-top:2px;font-family:DM Mono,monospace;font-size:14px}.outcome-card.yes .outcome-price{color:var(--green)}.outcome-card.no .outcome-price{color:var(--red)}.chart-container{background:var(--bg3);border:.5px solid var(--border);border-radius:var(--radius-sm);flex:2;height:160px;min-height:120px;max-height:160px;padding:14px;overflow:hidden}.chart-container canvas{max-height:130px}.chart-label{color:var(--text3);margin-bottom:6px;font-family:DM Mono,monospace;font-size:14px}.ai-box{background:var(--bg3);border:.5px solid var(--blue2);border-radius:var(--radius-sm);align-items:flex-start;gap:14px;margin-bottom:10px;padding:14px;display:flex}.ai-icon{background:var(--blue3);border-radius:6px;flex-shrink:0;justify-content:center;align-items:center;width:32px;height:32px;font-size:14px;display:flex}.ai-label{color:var(--blue);text-transform:uppercase;letter-spacing:.06em;margin-bottom:3px;font-family:DM Mono,monospace;font-size:14px}.ai-text{color:var(--text2);font-size:15px;line-height:1.5}.sim-row{flex-wrap:wrap;align-items:center;gap:16px;display:flex}.sim-label{color:var(--text3);font-family:DM Mono,monospace;font-size:15px}.sim-input{background:var(--bg3);border:.5px solid var(--border2);border-radius:var(--radius-sm);color:var(--text);outline:none;width:100px;padding:8px 14px;font-family:DM Mono,monospace;font-size:16px}.sim-input:focus{border-color:var(--blue2)}.sim-btn-yes{background:var(--green3);border:.5px solid var(--green2);color:var(--green);border-radius:var(--radius-sm);cursor:pointer;padding:8px 16px;font-family:DM Mono,monospace;font-size:15px;font-weight:600;transition:opacity .15s}.sim-btn-no{background:var(--red3);border:.5px solid var(--red2);color:var(--red);border-radius:var(--radius-sm);cursor:pointer;padding:8px 16px;font-family:DM Mono,monospace;font-size:15px;font-weight:600;transition:opacity .15s}.sim-btn-yes:hover,.sim-btn-no:hover{opacity:.85}.sim-disclaimer{color:var(--text3);font-family:DM Mono,monospace;font-size:14px}.legend{align-items:center;gap:16px;display:flex}.legend-item{color:var(--text3);align-items:center;gap:5px;font-family:DM Mono,monospace;font-size:14px;display:flex}.legend-dot{border-radius:50%;width:10px;height:10px}.legend-dot.green{background:var(--green)}.legend-dot.red{background:var(--red)}.legend-dot.gray{background:var(--text3)}.legend.end{margin-left:auto}.table-wrap{border:.5px solid var(--border);border-radius:var(--radius-sm);background:var(--bg3);overflow:auto}table{border-collapse:collapse;width:100%;font-size:16px}th,td{text-align:left;border-bottom:.5px solid var(--border);padding:14px}th{color:var(--text3);text-transform:uppercase;letter-spacing:.06em;background:var(--bg4);font-family:DM Mono,monospace;font-size:14px;font-weight:500;position:sticky;top:0}tr:hover td{background:#ffffff05}td{color:var(--text)}.td-mono{font-family:DM Mono,monospace}.td-green{color:var(--green)}.td-red{color:var(--red)}.td-blue{color:var(--blue)}.empty-state{text-align:center;color:var(--text3);padding:40px;font-size:16px}#app .leaflet-container{background:var(--bg3);font-family:DM Mono,monospace}#app .leaflet-popup-content-wrapper{background:var(--bg2);color:var(--text);border:.5px solid var(--border);border-radius:var(--radius-sm)}#app .leaflet-popup-tip{background:var(--bg2)}.sparkline{align-items:flex-end;gap:2px;height:32px;margin-top:6px;display:flex}.spark-bar{background:var(--blue2);border-radius:1px;width:3px;transition:height .3s}@keyframes pulse{0%,to{opacity:1}50%{opacity:.4}}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:var(--bg4);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:var(--text3)}.flex-between{justify-content:space-between;align-items:center;display:flex}.flex-start{justify-content:space-between;align-items:flex-start;display:flex}.flex-row{align-items:center;display:flex}.flex-wrap{flex-wrap:wrap}.gap-6{gap:14px}.gap-8{gap:16px}.text-green{color:var(--green)}.text-red{color:var(--red)}.text-blue{color:var(--blue)}.text-amber{color:var(--amber)}.text-neutral{color:var(--text3)}.bg-green{background:var(--green)}.bg-red{background:var(--red)}.bg-amber{background:var(--amber)}.flex-1{flex:1}.font-mono{font-family:DM Mono,monospace}.text-xs,.text-sm{font-size:14px}.text-base{font-size:15px}.text-lg{font-size:16px}.text-xl{font-size:18px}.font-semibold{font-weight:600}.font-bold{font-weight:700}.mb-4{margin-bottom:4px}.mb-6{margin-bottom:6px}.mb-8{margin-bottom:8px}.mt-4{margin-top:4px}.mt-6{margin-top:6px}.ml-auto{margin-left:auto}.divider{background:var(--border);height:1px;margin:8px 0}.empty-state-sm{text-align:center;color:var(--text3);padding:16px;font-size:16px}.map-popup{color:var(--text);max-width:200px;font-family:Syne,sans-serif;font-size:15px}.map-popup-cat{color:var(--text3);text-transform:uppercase;margin-bottom:4px;font-family:DM Mono,monospace;font-size:14px}.map-popup-q{margin-bottom:4px;font-weight:600;line-height:1.3}.map-popup-prices{gap:16px;font-family:DM Mono,monospace;font-size:15px;display:flex}.map-label-text{color:var(--text2);text-shadow:0 1px 2px #000;font-family:DM Mono,monospace;font-size:14px}@media (width<=1024px){.dashboard-grid{grid-template-rows:auto auto auto;grid-template-columns:1fr}.signals-panel{grid-area:auto;max-height:400px}.map-panel{grid-area:auto;min-height:300px}.detail-panel{grid-area:auto}}@media (width<=640px){.layout{grid-template-columns:0 1fr}.sidebar,.topbar-stats{display:none}.outcomes-row{flex-direction:column}}.leaflet-pane,.leaflet-tile,.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-tile-container,.leaflet-pane>svg,.leaflet-pane>canvas,.leaflet-zoom-box,.leaflet-image-layer,.leaflet-layer{position:absolute;top:0;left:0}.leaflet-container{overflow:hidden}.leaflet-tile,.leaflet-marker-icon,.leaflet-marker-shadow{-webkit-user-select:none;user-select:none;-webkit-user-drag:none}.leaflet-tile::selection{background:0 0}.leaflet-safari .leaflet-tile{image-rendering:-webkit-optimize-contrast}.leaflet-safari .leaflet-tile-container{-webkit-transform-origin:0 0;width:1600px;height:1600px}.leaflet-marker-icon,.leaflet-marker-shadow{display:block}.leaflet-container .leaflet-overlay-pane svg{max-width:none!important;max-height:none!important}.leaflet-container .leaflet-marker-pane img,.leaflet-container .leaflet-shadow-pane img,.leaflet-container .leaflet-tile-pane img,.leaflet-container img.leaflet-image-layer,.leaflet-container .leaflet-tile{width:auto;padding:0;max-width:none!important;max-height:none!important}.leaflet-container img.leaflet-tile{mix-blend-mode:plus-lighter}.leaflet-container.leaflet-touch-zoom{-ms-touch-action:pan-x pan-y;touch-action:pan-x pan-y}.leaflet-container.leaflet-touch-drag{-ms-touch-action:pinch-zoom;touch-action:none;touch-action:pinch-zoom}.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom{-ms-touch-action:none;touch-action:none}.leaflet-container{-webkit-tap-highlight-color:transparent}.leaflet-container a{-webkit-tap-highlight-color:#33b5e566}.leaflet-tile{filter:inherit;visibility:hidden}.leaflet-tile-loaded{visibility:inherit}.leaflet-zoom-box{box-sizing:border-box;z-index:800;width:0;height:0}.leaflet-overlay-pane svg{-moz-user-select:none}.leaflet-pane{z-index:400}.leaflet-tile-pane{z-index:200}.leaflet-overlay-pane{z-index:400}.leaflet-shadow-pane{z-index:500}.leaflet-marker-pane{z-index:600}.leaflet-tooltip-pane{z-index:650}.leaflet-popup-pane{z-index:700}.leaflet-map-pane canvas{z-index:100}.leaflet-map-pane svg{z-index:200}.leaflet-vml-shape{width:1px;height:1px}.lvml{behavior:url(#default#VML);display:inline-block;position:absolute}.leaflet-control{z-index:800;pointer-events:visiblePainted;pointer-events:auto;position:relative}.leaflet-top,.leaflet-bottom{z-index:1000;pointer-events:none;position:absolute}.leaflet-top{top:0}.leaflet-right{right:0}.leaflet-bottom{bottom:0}.leaflet-left{left:0}.leaflet-control{float:left;clear:both}.leaflet-right .leaflet-control{float:right}.leaflet-top .leaflet-control{margin-top:10px}.leaflet-bottom .leaflet-control{margin-bottom:10px}.leaflet-left .leaflet-control{margin-left:10px}.leaflet-right .leaflet-control{margin-right:10px}.leaflet-fade-anim .leaflet-popup{opacity:0;transition:opacity .2s linear}.leaflet-fade-anim .leaflet-map-pane .leaflet-popup{opacity:1}.leaflet-zoom-animated{transform-origin:0 0}svg.leaflet-zoom-animated{will-change:transform}.leaflet-zoom-anim .leaflet-zoom-animated{-webkit-transition:-webkit-transform .25s cubic-bezier(0,0,.25,1);-moz-transition:-moz-transform .25s cubic-bezier(0,0,.25,1);transition:transform .25s cubic-bezier(0,0,.25,1)}.leaflet-zoom-anim .leaflet-tile,.leaflet-pan-anim .leaflet-tile{transition:none}.leaflet-zoom-anim .leaflet-zoom-hide{visibility:hidden}.leaflet-interactive{cursor:pointer}.leaflet-grab{cursor:-webkit-grab;cursor:-moz-grab;cursor:grab}.leaflet-crosshair,.leaflet-crosshair .leaflet-interactive{cursor:crosshair}.leaflet-popup-pane,.leaflet-control{cursor:auto}.leaflet-dragging .leaflet-grab,.leaflet-dragging .leaflet-grab .leaflet-interactive,.leaflet-dragging .leaflet-marker-draggable{cursor:move;cursor:-webkit-grabbing;cursor:-moz-grabbing;cursor:grabbing}.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-image-layer,.leaflet-pane>svg path,.leaflet-tile-container{pointer-events:none}.leaflet-marker-icon.leaflet-interactive,.leaflet-image-layer.leaflet-interactive,.leaflet-pane>svg path.leaflet-interactive,svg.leaflet-image-layer.leaflet-interactive path{pointer-events:visiblePainted;pointer-events:auto}.leaflet-container{outline-offset:1px;background:#ddd}.leaflet-container a{color:#0078a8}.leaflet-zoom-box{background:#ffffff80;border:2px dotted #38f}.leaflet-container{font-family:Helvetica Neue,Arial,Helvetica,sans-serif;font-size:.75rem;line-height:1.5}.leaflet-bar{border-radius:4px;box-shadow:0 1px 5px #000000a6}.leaflet-bar a{text-align:center;color:#000;background-color:#fff;border-bottom:1px solid #ccc;width:26px;height:26px;line-height:26px;text-decoration:none;display:block}.leaflet-bar a,.leaflet-control-layers-toggle{background-position:50%;background-repeat:no-repeat;display:block}.leaflet-bar a:hover,.leaflet-bar a:focus{background-color:#f4f4f4}.leaflet-bar a:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.leaflet-bar a:last-child{border-bottom:none;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.leaflet-bar a.leaflet-disabled{cursor:default;color:#bbb;background-color:#f4f4f4}.leaflet-touch .leaflet-bar a{width:30px;height:30px;line-height:30px}.leaflet-touch .leaflet-bar a:first-child{border-top-left-radius:2px;border-top-right-radius:2px}.leaflet-touch .leaflet-bar a:last-child{border-bottom-right-radius:2px;border-bottom-left-radius:2px}.leaflet-control-zoom-in,.leaflet-control-zoom-out{text-indent:1px;font:700 18px Lucida Console,Monaco,monospace}.leaflet-touch .leaflet-control-zoom-in,.leaflet-touch .leaflet-control-zoom-out{font-size:22px}.leaflet-control-layers{background:#fff;border-radius:5px;box-shadow:0 1px 5px #0006}.leaflet-control-layers-toggle{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAQAAAADQ4RFAAACf0lEQVR4AY1UM3gkARTePdvdoTxXKc+qTl3aU5U6b2Kbkz3Gtq3Zw6ziLGNPzrYx7946Tr6/ee/XeCQ4D3ykPtL5tHno4n0d/h3+xfuWHGLX81cn7r0iTNzjr7LrlxCqPtkbTQEHeqOrTy4Yyt3VCi/IOB0v7rVC7q45Q3Gr5K6jt+3Gl5nCoDD4MtO+j96Wu8atmhGqcNGHObuf8OM/x3AMx38+4Z2sPqzCxRFK2aF2e5Jol56XTLyggAMTL56XOMoS1W4pOyjUcGGQdZxU6qRh7B9Zp+PfpOFlqt0zyDZckPi1ttmIp03jX8gyJ8a/PG2yutpS/Vol7peZIbZcKBAEEheEIAgFbDkz5H6Zrkm2hVWGiXKiF4Ycw0RWKdtC16Q7qe3X4iOMxruonzegJzWaXFrU9utOSsLUmrc0YjeWYjCW4PDMADElpJSSQ0vQvA1Tm6/JlKnqFs1EGyZiFCqnRZTEJJJiKRYzVYzJck2Rm6P4iH+cmSY0YzimYa8l0EtTODFWhcMIMVqdsI2uiTvKmTisIDHJ3od5GILVhBCarCfVRmo4uTjkhrhzkiBV7SsaqS+TzrzM1qpGGUFt28pIySQHR6h7F6KSwGWm97ay+Z+ZqMcEjEWebE7wxCSQwpkhJqoZA5ivCdZDjJepuJ9IQjGGUmuXJdBFUygxVqVsxFsLMbDe8ZbDYVCGKxs+W080max1hFCarCfV+C1KATwcnvE9gRRuMP2prdbWGowm1KB1y+zwMMENkM755cJ2yPDtqhTI6ED1M/82yIDtC/4j4BijjeObflpO9I9MwXTCsSX8jWAFeHr05WoLTJ5G8IQVS/7vwR6ohirYM7f6HzYpogfS3R2OAAAAAElFTkSuQmCC);width:36px;height:36px}.leaflet-retina .leaflet-control-layers-toggle{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADQAAAA0CAQAAABvcdNgAAAEsklEQVR4AWL4TydIhpZK1kpWOlg0w3ZXP6D2soBtG42jeI6ZmQTHzAxiTbSJsYLjO9HhP+WOmcuhciVnmHVQcJnp7DFvScowZorad/+V/fVzMdMT2g9Cv9guXGv/7pYOrXh2U+RRR3dSd9JRx6bIFc/ekqHI29JC6pJ5ZEh1yWkhkbcFeSjxgx3L2m1cb1C7bceyxA+CNjT/Ifff+/kDk2u/w/33/IeCMOSaWZ4glosqT3DNnNZQ7Cs58/3Ce5HL78iZH/vKVIaYlqzfdLu8Vi7dnvUbEza5Idt36tquZFldl6N5Z/POLof0XLK61mZCmJSWjVF9tEjUluu74IUXvgttuVIHE7YxSkaYhJZam7yiM9Pv82JYfl9nptxZaxMJE4YSPty+vF0+Y2up9d3wwijfjZbabqm/3bZ9ecKHsiGmRflnn1MW4pjHf9oLufyn2z3y1D6n8g8TZhxyzipLNPnAUpsOiuWimg52psrTZYnOWYNDTMuWBWa0tJb4rgq1UvmutpaYEbZlwU3CLJm/ayYjHW5/h7xWLn9Hh1vepDkyf7dE7MtT5LR4e7yYpHrkhOUpEfssBLq2pPhAqoSWKUkk7EDqkmK6RrCEzqDjhNDWNE+XSMvkJRDWlZTmCW0l0PHQGRZY5t1L83kT0Y3l2SItk5JAWHl2dCOBm+fPu3fo5/3v61RMCO9Jx2EEYYhb0rmNQMX/vm7gqOEJLcXTGw3CAuRNeyaPWwjR8PRqKQ1PDA/dpv+on9Shox52WFnx0KY8onHayrJzm87i5h9xGw/tfkev0jGsQizqezUKjk12hBMKJ4kbCqGPVNXudyyrShovGw5CgxsRICxF6aRmSjlBnHRzg7Gx8fKqEubI2rahQYdR1YgDIRQO7JvQyD52hoIQx0mxa0ODtW2Iozn1le2iIRdzwWewedyZzewidueOGqlsn1MvcnQpuVwLGG3/IR1hIKxCjelIDZ8ldqWz25jWAsnldEnK0Zxro19TGVb2ffIZEsIO89EIEDvKMPrzmBOQcKQ+rroye6NgRRxqR4U8EAkz0CL6uSGOm6KQCdWjvjRiSP1BPalCRS5iQYiEIvxuBMJEWgzSoHADcVMuN7IuqqTeyUPq22qFimFtxDyBBJEwNyt6TM88blFHao/6tWWhuuOM4SAK4EI4QmFHA+SEyWlp4EQoJ13cYGzMu7yszEIBOm2rVmHUNqwAIQabISNMRstmdhNWcFLsSm+0tjJH1MdRxO5Nx0WDMhCtgD6OKgZeljJqJKc9po8juskR9XN0Y1lZ3mWjLR9JCO1jRDMd0fpYC2VnvjBSEFg7wBENc0R9HFlb0xvF1+TBEpF68d+DHR6IOWVv2BECtxo46hOFUBd/APU57WIoEwJhIi2CdpyZX0m93BZicktMj1AS9dClteUFAUNUIEygRZCtik5zSxI9MubTBH1GOiHsiLJ3OCoSZkILa9PxiN0EbvhsAo8tdAf9Seepd36lGWHmtNANTv5Jd0z4QYyeo/UEJqxKRpg5LZx6btLPsOaEmdMyxYdlc8LMaJnikDlhclqmPiQnTEpLUIZEwkRagjYkEibQErwhkTAKCLQEbUgkzJQWc/0PstHHcfEdQ+UAAAAASUVORK5CYII=);background-size:26px 26px}.leaflet-touch .leaflet-control-layers-toggle{width:44px;height:44px}.leaflet-control-layers .leaflet-control-layers-list,.leaflet-control-layers-expanded .leaflet-control-layers-toggle{display:none}.leaflet-control-layers-expanded .leaflet-control-layers-list{display:block;position:relative}.leaflet-control-layers-expanded{color:#333;background:#fff;padding:6px 10px 6px 6px}.leaflet-control-layers-scrollbar{padding-right:5px;overflow:hidden scroll}.leaflet-control-layers-selector{margin-top:2px;position:relative;top:1px}.leaflet-control-layers label{font-size:1.08333em;display:block}.leaflet-control-layers-separator{border-top:1px solid #ddd;height:0;margin:5px -10px 5px -6px}.leaflet-default-icon-path{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAApCAYAAADAk4LOAAAFgUlEQVR4Aa1XA5BjWRTN2oW17d3YaZtr2962HUzbDNpjszW24mRt28p47v7zq/bXZtrp/lWnXr337j3nPCe85NcypgSFdugCpW5YoDAMRaIMqRi6aKq5E3YqDQO3qAwjVWrD8Ncq/RBpykd8oZUb/kaJutow8r1aP9II0WmLKLIsJyv1w/kqw9Ch2MYdB++12Onxee/QMwvf4/Dk/Lfp/i4nxTXtOoQ4pW5Aj7wpici1A9erdAN2OH64x8OSP9j3Ft3b7aWkTg/Fm91siTra0f9on5sQr9INejH6CUUUpavjFNq1B+Oadhxmnfa8RfEmN8VNAsQhPqF55xHkMzz3jSmChWU6f7/XZKNH+9+hBLOHYozuKQPxyMPUKkrX/K0uWnfFaJGS1QPRtZsOPtr3NsW0uyh6NNCOkU3Yz+bXbT3I8G3xE5EXLXtCXbbqwCO9zPQYPRTZ5vIDXD7U+w7rFDEoUUf7ibHIR4y6bLVPXrz8JVZEql13trxwue/uDivd3fkWRbS6/IA2bID4uk0UpF1N8qLlbBlXs4Ee7HLTfV1j54APvODnSfOWBqtKVvjgLKzF5YdEk5ewRkGlK0i33Eofffc7HT56jD7/6U+qH3Cx7SBLNntH5YIPvODnyfIXZYRVDPqgHtLs5ABHD3YzLuespb7t79FY34DjMwrVrcTuwlT55YMPvOBnRrJ4VXTdNnYug5ucHLBjEpt30701A3Ts+HEa73u6dT3FNWwflY86eMHPk+Yu+i6pzUpRrW7SNDg5JHR4KapmM5Wv2E8Tfcb1HoqqHMHU+uWDD7zg54mz5/2BSnizi9T1Dg4QQXLToGNCkb6tb1NU+QAlGr1++eADrzhn/u8Q2YZhQVlZ5+CAOtqfbhmaUCS1ezNFVm2imDbPmPng5wmz+gwh+oHDce0eUtQ6OGDIyR0uUhUsoO3vfDmmgOezH0mZN59x7MBi++WDL1g/eEiU3avlidO671bkLfwbw5XV2P8Pzo0ydy4t2/0eu33xYSOMOD8hTf4CrBtGMSoXfPLchX+J0ruSePw3LZeK0juPJbYzrhkH0io7B3k164hiGvawhOKMLkrQLyVpZg8rHFW7E2uHOL888IBPlNZ1FPzstSJM694fWr6RwpvcJK60+0HCILTBzZLFNdtAzJaohze60T8qBzyh5ZuOg5e7uwQppofEmf2++DYvmySqGBuKaicF1blQjhuHdvCIMvp8whTTfZzI7RldpwtSzL+F1+wkdZ2TBOW2gIF88PBTzD/gpeREAMEbxnJcaJHNHrpzji0gQCS6hdkEeYt9DF/2qPcEC8RM28Hwmr3sdNyht00byAut2k3gufWNtgtOEOFGUwcXWNDbdNbpgBGxEvKkOQsxivJx33iow0Vw5S6SVTrpVq11ysA2Rp7gTfPfktc6zhtXBBC+adRLshf6sG2RfHPZ5EAc4sVZ83yCN00Fk/4kggu40ZTvIEm5g24qtU4KjBrx/BTTH8ifVASAG7gKrnWxJDcU7x8X6Ecczhm3o6YicvsLXWfh3Ch1W0k8x0nXF+0fFxgt4phz8QvypiwCCFKMqXCnqXExjq10beH+UUA7+nG6mdG/Pu0f3LgFcGrl2s0kNNjpmoJ9o4B29CMO8dMT4Q5ox8uitF6fqsrJOr8qnwNbRzv6hSnG5wP+64C7h9lp30hKNtKdWjtdkbuPA19nJ7Tz3zR/ibgARbhb4AlhavcBebmTHcFl2fvYEnW0ox9xMxKBS8btJ+KiEbq9zA4RthQXDhPa0T9TEe69gWupwc6uBUphquXgf+/FrIjweHQS4/pduMe5ERUMHUd9xv8ZR98CxkS4F2n3EUrUZ10EYNw7BWm9x1GiPssi3GgiGRDKWRYZfXlON+dfNbM+GgIwYdwAAAAASUVORK5CYII=)}.leaflet-container .leaflet-control-attribution{background:#fffc;margin:0}.leaflet-control-attribution,.leaflet-control-scale-line{color:#333;padding:0 5px;line-height:1.4}.leaflet-control-attribution a{text-decoration:none}.leaflet-control-attribution a:hover,.leaflet-control-attribution a:focus{text-decoration:underline}.leaflet-attribution-flag{width:1em;height:.6669em;vertical-align:baseline!important;display:inline!important}.leaflet-left .leaflet-control-scale{margin-left:5px}.leaflet-bottom .leaflet-control-scale{margin-bottom:5px}.leaflet-control-scale-line{white-space:nowrap;box-sizing:border-box;text-shadow:1px 1px #fff;background:#fffc;border:2px solid #777;border-top:none;padding:2px 5px 1px;line-height:1.1}.leaflet-control-scale-line:not(:first-child){border-top:2px solid #777;border-bottom:none;margin-top:-2px}.leaflet-control-scale-line:not(:first-child):not(:last-child){border-bottom:2px solid #777}.leaflet-touch .leaflet-control-attribution,.leaflet-touch .leaflet-control-layers,.leaflet-touch .leaflet-bar{box-shadow:none}.leaflet-touch .leaflet-control-layers,.leaflet-touch .leaflet-bar{background-clip:padding-box;border:2px solid #0003}.leaflet-popup{text-align:center;margin-bottom:20px;position:absolute}.leaflet-popup-content-wrapper{text-align:left;border-radius:12px;padding:1px}.leaflet-popup-content{min-height:1px;margin:13px 24px 13px 20px;font-size:1.08333em;line-height:1.3}.leaflet-popup-content p{margin:1.3em 0}.leaflet-popup-tip-container{pointer-events:none;width:40px;height:20px;margin-top:-1px;margin-left:-20px;position:absolute;left:50%;overflow:hidden}.leaflet-popup-tip{pointer-events:auto;width:17px;height:17px;margin:-10px auto 0;padding:1px;transform:rotate(45deg)}.leaflet-popup-content-wrapper,.leaflet-popup-tip{color:#333;background:#fff;box-shadow:0 3px 14px #0006}.leaflet-container a.leaflet-popup-close-button{text-align:center;color:#757575;background:0 0;border:none;width:24px;height:24px;font:16px/24px Tahoma,Verdana,sans-serif;text-decoration:none;position:absolute;top:0;right:0}.leaflet-container a.leaflet-popup-close-button:hover,.leaflet-container a.leaflet-popup-close-button:focus{color:#585858}.leaflet-popup-scrolled{overflow:auto}.leaflet-oldie .leaflet-popup-content-wrapper{-ms-zoom:1}.leaflet-oldie .leaflet-popup-tip{-ms-filter:"progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";width:24px;filter:progid:DXImageTransform.Microsoft.Matrix(M11=.707107, M12=.707107, M21=-.707107, M22=.707107);margin:0 auto}.leaflet-oldie .leaflet-control-zoom,.leaflet-oldie .leaflet-control-layers,.leaflet-oldie .leaflet-popup-content-wrapper,.leaflet-oldie .leaflet-popup-tip{border:1px solid #999}.leaflet-div-icon{background:#fff;border:1px solid #666}.leaflet-tooltip{color:#222;white-space:nowrap;-webkit-user-select:none;user-select:none;pointer-events:none;background-color:#fff;border:1px solid #fff;border-radius:3px;padding:6px;position:absolute;box-shadow:0 1px 3px #0006}.leaflet-tooltip.leaflet-interactive{cursor:pointer;pointer-events:auto}.leaflet-tooltip-top:before,.leaflet-tooltip-bottom:before,.leaflet-tooltip-left:before,.leaflet-tooltip-right:before{pointer-events:none;content:"";background:0 0;border:6px solid #0000;position:absolute}.leaflet-tooltip-bottom{margin-top:6px}.leaflet-tooltip-top{margin-top:-6px}.leaflet-tooltip-bottom:before,.leaflet-tooltip-top:before{margin-left:-6px;left:50%}.leaflet-tooltip-top:before{border-top-color:#fff;margin-bottom:-12px;bottom:0}.leaflet-tooltip-bottom:before{border-bottom-color:#fff;margin-top:-12px;margin-left:-6px;top:0}.leaflet-tooltip-left{margin-left:-6px}.leaflet-tooltip-right{margin-left:6px}.leaflet-tooltip-left:before,.leaflet-tooltip-right:before{margin-top:-6px;top:50%}.leaflet-tooltip-left:before{border-left-color:#fff;margin-right:-12px;right:0}.leaflet-tooltip-right:before{border-right-color:#fff;margin-left:-12px;left:0}@media print{.leaflet-control{-webkit-print-color-adjust:exact;print-color-adjust:exact}}
frontend/dist/index.html CHANGED
@@ -5,13 +5,222 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <title>PolySignal — Dashboard de Inteligencia de Mercados</title>
7
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
8
- <script type="module" crossorigin src="/assets/index-CHfdQIIe.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-CIGW-MKW.css">
10
  </head>
11
  <body>
12
- <div id="app">
13
- <h1>PolySignal Dashboard</h1>
14
- <p>Carga el entorno de desarrollo con <code>npm run dev</code> en la carpeta <code>frontend</code>.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  </div>
16
 
17
  </body>
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <title>PolySignal — Dashboard de Inteligencia de Mercados</title>
7
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
8
+ <script type="module" crossorigin src="/assets/index-A_KP_t7E.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-xtYPhhTl.css">
10
  </head>
11
  <body>
12
+ <div id="app" class="layout">
13
+
14
+ <!-- Sidebar -->
15
+ <aside class="sidebar" id="sidebar">
16
+ <div class="sidebar-toggle" id="sidebar-toggle" title="Colapsar sidebar">◀</div>
17
+ <nav class="sidebar-nav">
18
+ <div class="nav-item active" data-view="dashboard">
19
+ <span class="nav-icon">◈</span>
20
+ <span class="nav-label">Panel</span>
21
+ </div>
22
+ <div class="nav-item" data-view="positions">
23
+ <span class="nav-icon">◫</span>
24
+ <span class="nav-label">Posiciones</span>
25
+ </div>
26
+ <div class="nav-item" data-view="watchlist">
27
+ <span class="nav-icon">☆</span>
28
+ <span class="nav-label">Seguimiento</span>
29
+ </div>
30
+ <div class="nav-item" data-view="alerts">
31
+ <span class="nav-icon">⚡</span>
32
+ <span class="nav-label">Alertas</span>
33
+ </div>
34
+ </nav>
35
+ <div class="sidebar-footer">
36
+ v0.1.0 · HF Spaces
37
+ </div>
38
+ </aside>
39
+
40
+ <!-- Topbar -->
41
+ <header class="topbar" id="topbar">
42
+ <div class="topbar-logo">
43
+ <div class="logo-dot"></div>
44
+ <span class="logo-text">PolySignal</span>
45
+ </div>
46
+ <div class="live-badge">
47
+ <div class="live-dot"></div>
48
+ EN VIVO
49
+ </div>
50
+ <div class="topbar-stats">
51
+ <div class="stat">
52
+ <span class="stat-label">Mercados</span>
53
+ <span class="stat-val" id="stat-markets">2.847</span>
54
+ </div>
55
+ <div class="stat">
56
+ <span class="stat-label">Volumen 24h</span>
57
+ <span class="stat-val" id="stat-volume">€4,2M</span>
58
+ <span class="stat-delta up" id="stat-volume-delta">+12,4%</span>
59
+ </div>
60
+ <div class="stat">
61
+ <span class="stat-label">Señales IA</span>
62
+ <span class="stat-val" id="stat-signals">183</span>
63
+ <span class="stat-delta up" id="stat-signals-delta">alcista</span>
64
+ </div>
65
+ <div class="stat">
66
+ <span class="stat-label">Alertas enviadas</span>
67
+ <span class="stat-val" id="stat-alerts">47</span>
68
+ <span class="stat-delta neutral">hoy</span>
69
+ </div>
70
+ <div class="legend end">
71
+ <div class="legend-item"><div class="legend-dot green"></div>alcista</div>
72
+ <div class="legend-item"><div class="legend-dot red"></div>bajista</div>
73
+ <div class="legend-item"><div class="legend-dot gray"></div>neutral</div>
74
+ </div>
75
+ </div>
76
+ <div class="topbar-actions">
77
+ <button class="btn-ghost" id="btn-telegram">Alertas Telegram</button>
78
+ <button class="icon-btn" id="btn-notif" title="Notificaciones">◉</button>
79
+ </div>
80
+ </header>
81
+
82
+ <!-- Main content area -->
83
+ <main class="main" id="main">
84
+
85
+ <!-- DASHBOARD VIEW -->
86
+ <section class="view active" id="view-dashboard">
87
+ <div class="dashboard-grid">
88
+
89
+ <!-- Map Panel -->
90
+ <div class="panel map-panel" id="panel-map">
91
+ <div class="panel-header" data-panel="map">
92
+ <div class="panel-title">
93
+ <span>◈</span>
94
+ Mapa global de predicciones
95
+ <span class="panel-subtitle">· tamaño = volumen · color = señal IA</span>
96
+ </div>
97
+ <span class="panel-toggle">▼</span>
98
+ </div>
99
+ <div class="panel-body">
100
+ <div id="map-container"></div>
101
+ </div>
102
+ </div>
103
+
104
+ <!-- Signals Panel -->
105
+ <div class="panel signals-panel" id="panel-signals">
106
+ <div class="panel-header" data-panel="signals">
107
+ <div class="panel-title">
108
+ <span>◈</span>
109
+ Señales IA — mercados top
110
+ </div>
111
+ <span class="panel-toggle">▼</span>
112
+ </div>
113
+ <div class="panel-body">
114
+ <div class="signals-list" id="signals-list"></div>
115
+ <div class="positions-separator">
116
+ <div class="panel-title mb-sm">Mis posiciones</div>
117
+ <div id="mini-positions"></div>
118
+ </div>
119
+ </div>
120
+ </div>
121
+
122
+ <!-- Detail Panel -->
123
+ <div class="panel detail-panel" id="panel-detail">
124
+ <div class="panel-header" data-panel="detail">
125
+ <div class="panel-title">
126
+ <span>◈</span>
127
+ Detalle del mercado
128
+ </div>
129
+ <span class="panel-toggle">▼</span>
130
+ </div>
131
+ <div class="panel-body" id="detail-body">
132
+ <!-- Dynamic content -->
133
+ </div>
134
+ </div>
135
+
136
+ </div>
137
+ </section>
138
+
139
+ <!-- POSITIONS VIEW -->
140
+ <section class="view" id="view-positions">
141
+ <div class="panel full-height">
142
+ <div class="panel-header">
143
+ <div class="panel-title"><span>◫</span> Simulador — Posiciones abiertas</div>
144
+ </div>
145
+ <div class="panel-body">
146
+ <div class="table-wrap">
147
+ <table id="positions-table">
148
+ <thead>
149
+ <tr>
150
+ <th>Mercado</th>
151
+ <th>Resultado</th>
152
+ <th>Cantidad</th>
153
+ <th>Entrada</th>
154
+ <th>Actual</th>
155
+ <th>G&amp;P</th>
156
+ <th>Kelly</th>
157
+ <th>Abierta</th>
158
+ <th></th>
159
+ </tr>
160
+ </thead>
161
+ <tbody></tbody>
162
+ </table>
163
+ </div>
164
+ <div class="empty-state hidden" id="positions-empty">No hay posiciones abiertas. Ve al Panel para simular una operación.</div>
165
+ </div>
166
+ </div>
167
+ </section>
168
+
169
+ <!-- WATCHLIST VIEW -->
170
+ <section class="view" id="view-watchlist">
171
+ <div class="panel full-height">
172
+ <div class="panel-header">
173
+ <div class="panel-title"><span>☆</span> Lista de seguimiento</div>
174
+ </div>
175
+ <div class="panel-body">
176
+ <div class="table-wrap">
177
+ <table id="watchlist-table">
178
+ <thead>
179
+ <tr>
180
+ <th>Mercado</th>
181
+ <th>Categoría</th>
182
+ <th>Sí</th>
183
+ <th>No</th>
184
+ <th>Señal</th>
185
+ <th>Volumen</th>
186
+ <th>Umbral de alerta</th>
187
+ <th></th>
188
+ </tr>
189
+ </thead>
190
+ <tbody></tbody>
191
+ </table>
192
+ </div>
193
+ <div class="empty-state hidden" id="watchlist-empty">Tu lista de seguimiento está vacía. Añade mercados desde el Panel.</div>
194
+ </div>
195
+ </div>
196
+ </section>
197
+
198
+ <!-- ALERTS VIEW -->
199
+ <section class="view" id="view-alerts">
200
+ <div class="panel full-height">
201
+ <div class="panel-header">
202
+ <div class="panel-title"><span>⚡</span> Historial de alertas</div>
203
+ </div>
204
+ <div class="panel-body">
205
+ <div class="table-wrap">
206
+ <table id="alerts-table">
207
+ <thead>
208
+ <tr>
209
+ <th>Hora</th>
210
+ <th>Mercado</th>
211
+ <th>Tipo</th>
212
+ <th>Mensaje</th>
213
+ </tr>
214
+ </thead>
215
+ <tbody></tbody>
216
+ </table>
217
+ </div>
218
+ <div class="empty-state hidden" id="alerts-empty">Aún no se han enviado alertas.</div>
219
+ </div>
220
+ </div>
221
+ </section>
222
+
223
+ </main>
224
  </div>
225
 
226
  </body>
frontend/index.html CHANGED
@@ -7,9 +7,218 @@
7
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
8
  </head>
9
  <body>
10
- <div id="app">
11
- <h1>PolySignal Dashboard</h1>
12
- <p>Carga el entorno de desarrollo con <code>npm run dev</code> en la carpeta <code>frontend</code>.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  </div>
14
 
15
  <script type="module" src="/src/main.js"></script>
 
7
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
8
  </head>
9
  <body>
10
+ <div id="app" class="layout">
11
+
12
+ <!-- Sidebar -->
13
+ <aside class="sidebar" id="sidebar">
14
+ <div class="sidebar-toggle" id="sidebar-toggle" title="Colapsar sidebar">◀</div>
15
+ <nav class="sidebar-nav">
16
+ <div class="nav-item active" data-view="dashboard">
17
+ <span class="nav-icon">◈</span>
18
+ <span class="nav-label">Panel</span>
19
+ </div>
20
+ <div class="nav-item" data-view="positions">
21
+ <span class="nav-icon">◫</span>
22
+ <span class="nav-label">Posiciones</span>
23
+ </div>
24
+ <div class="nav-item" data-view="watchlist">
25
+ <span class="nav-icon">☆</span>
26
+ <span class="nav-label">Seguimiento</span>
27
+ </div>
28
+ <div class="nav-item" data-view="alerts">
29
+ <span class="nav-icon">⚡</span>
30
+ <span class="nav-label">Alertas</span>
31
+ </div>
32
+ </nav>
33
+ <div class="sidebar-footer">
34
+ v0.1.0 · HF Spaces
35
+ </div>
36
+ </aside>
37
+
38
+ <!-- Topbar -->
39
+ <header class="topbar" id="topbar">
40
+ <div class="topbar-logo">
41
+ <div class="logo-dot"></div>
42
+ <span class="logo-text">PolySignal</span>
43
+ </div>
44
+ <div class="live-badge">
45
+ <div class="live-dot"></div>
46
+ EN VIVO
47
+ </div>
48
+ <div class="topbar-stats">
49
+ <div class="stat">
50
+ <span class="stat-label">Mercados</span>
51
+ <span class="stat-val" id="stat-markets">2.847</span>
52
+ </div>
53
+ <div class="stat">
54
+ <span class="stat-label">Volumen 24h</span>
55
+ <span class="stat-val" id="stat-volume">€4,2M</span>
56
+ <span class="stat-delta up" id="stat-volume-delta">+12,4%</span>
57
+ </div>
58
+ <div class="stat">
59
+ <span class="stat-label">Señales IA</span>
60
+ <span class="stat-val" id="stat-signals">183</span>
61
+ <span class="stat-delta up" id="stat-signals-delta">alcista</span>
62
+ </div>
63
+ <div class="stat">
64
+ <span class="stat-label">Alertas enviadas</span>
65
+ <span class="stat-val" id="stat-alerts">47</span>
66
+ <span class="stat-delta neutral">hoy</span>
67
+ </div>
68
+ <div class="legend end">
69
+ <div class="legend-item"><div class="legend-dot green"></div>alcista</div>
70
+ <div class="legend-item"><div class="legend-dot red"></div>bajista</div>
71
+ <div class="legend-item"><div class="legend-dot gray"></div>neutral</div>
72
+ </div>
73
+ </div>
74
+ <div class="topbar-actions">
75
+ <button class="btn-ghost" id="btn-telegram">Alertas Telegram</button>
76
+ <button class="icon-btn" id="btn-notif" title="Notificaciones">◉</button>
77
+ </div>
78
+ </header>
79
+
80
+ <!-- Main content area -->
81
+ <main class="main" id="main">
82
+
83
+ <!-- DASHBOARD VIEW -->
84
+ <section class="view active" id="view-dashboard">
85
+ <div class="dashboard-grid">
86
+
87
+ <!-- Map Panel -->
88
+ <div class="panel map-panel" id="panel-map">
89
+ <div class="panel-header" data-panel="map">
90
+ <div class="panel-title">
91
+ <span>◈</span>
92
+ Mapa global de predicciones
93
+ <span class="panel-subtitle">· tamaño = volumen · color = señal IA</span>
94
+ </div>
95
+ <span class="panel-toggle">▼</span>
96
+ </div>
97
+ <div class="panel-body">
98
+ <div id="map-container"></div>
99
+ </div>
100
+ </div>
101
+
102
+ <!-- Signals Panel -->
103
+ <div class="panel signals-panel" id="panel-signals">
104
+ <div class="panel-header" data-panel="signals">
105
+ <div class="panel-title">
106
+ <span>◈</span>
107
+ Señales IA — mercados top
108
+ </div>
109
+ <span class="panel-toggle">▼</span>
110
+ </div>
111
+ <div class="panel-body">
112
+ <div class="signals-list" id="signals-list"></div>
113
+ <div class="positions-separator">
114
+ <div class="panel-title mb-sm">Mis posiciones</div>
115
+ <div id="mini-positions"></div>
116
+ </div>
117
+ </div>
118
+ </div>
119
+
120
+ <!-- Detail Panel -->
121
+ <div class="panel detail-panel" id="panel-detail">
122
+ <div class="panel-header" data-panel="detail">
123
+ <div class="panel-title">
124
+ <span>◈</span>
125
+ Detalle del mercado
126
+ </div>
127
+ <span class="panel-toggle">▼</span>
128
+ </div>
129
+ <div class="panel-body" id="detail-body">
130
+ <!-- Dynamic content -->
131
+ </div>
132
+ </div>
133
+
134
+ </div>
135
+ </section>
136
+
137
+ <!-- POSITIONS VIEW -->
138
+ <section class="view" id="view-positions">
139
+ <div class="panel full-height">
140
+ <div class="panel-header">
141
+ <div class="panel-title"><span>◫</span> Simulador — Posiciones abiertas</div>
142
+ </div>
143
+ <div class="panel-body">
144
+ <div class="table-wrap">
145
+ <table id="positions-table">
146
+ <thead>
147
+ <tr>
148
+ <th>Mercado</th>
149
+ <th>Resultado</th>
150
+ <th>Cantidad</th>
151
+ <th>Entrada</th>
152
+ <th>Actual</th>
153
+ <th>G&amp;P</th>
154
+ <th>Kelly</th>
155
+ <th>Abierta</th>
156
+ <th></th>
157
+ </tr>
158
+ </thead>
159
+ <tbody></tbody>
160
+ </table>
161
+ </div>
162
+ <div class="empty-state hidden" id="positions-empty">No hay posiciones abiertas. Ve al Panel para simular una operación.</div>
163
+ </div>
164
+ </div>
165
+ </section>
166
+
167
+ <!-- WATCHLIST VIEW -->
168
+ <section class="view" id="view-watchlist">
169
+ <div class="panel full-height">
170
+ <div class="panel-header">
171
+ <div class="panel-title"><span>☆</span> Lista de seguimiento</div>
172
+ </div>
173
+ <div class="panel-body">
174
+ <div class="table-wrap">
175
+ <table id="watchlist-table">
176
+ <thead>
177
+ <tr>
178
+ <th>Mercado</th>
179
+ <th>Categoría</th>
180
+ <th>Sí</th>
181
+ <th>No</th>
182
+ <th>Señal</th>
183
+ <th>Volumen</th>
184
+ <th>Umbral de alerta</th>
185
+ <th></th>
186
+ </tr>
187
+ </thead>
188
+ <tbody></tbody>
189
+ </table>
190
+ </div>
191
+ <div class="empty-state hidden" id="watchlist-empty">Tu lista de seguimiento está vacía. Añade mercados desde el Panel.</div>
192
+ </div>
193
+ </div>
194
+ </section>
195
+
196
+ <!-- ALERTS VIEW -->
197
+ <section class="view" id="view-alerts">
198
+ <div class="panel full-height">
199
+ <div class="panel-header">
200
+ <div class="panel-title"><span>⚡</span> Historial de alertas</div>
201
+ </div>
202
+ <div class="panel-body">
203
+ <div class="table-wrap">
204
+ <table id="alerts-table">
205
+ <thead>
206
+ <tr>
207
+ <th>Hora</th>
208
+ <th>Mercado</th>
209
+ <th>Tipo</th>
210
+ <th>Mensaje</th>
211
+ </tr>
212
+ </thead>
213
+ <tbody></tbody>
214
+ </table>
215
+ </div>
216
+ <div class="empty-state hidden" id="alerts-empty">Aún no se han enviado alertas.</div>
217
+ </div>
218
+ </div>
219
+ </section>
220
+
221
+ </main>
222
  </div>
223
 
224
  <script type="module" src="/src/main.js"></script>
frontend/package.json CHANGED
@@ -9,11 +9,14 @@
9
  "preview": "vite preview"
10
  },
11
  "devDependencies": {
12
- "vite": "^6.0.0"
 
 
 
13
  },
14
  "dependencies": {
15
- "chart.js": "^4.4.8",
16
  "leaflet": "^1.9.4",
17
- "socket.io-client": "^4.8.1"
18
  }
19
  }
 
9
  "preview": "vite preview"
10
  },
11
  "devDependencies": {
12
+ "vite": "^8.0.13"
13
+ },
14
+ "engines": {
15
+ "node": ">=26.0.0"
16
  },
17
  "dependencies": {
18
+ "chart.js": "^4.5.1",
19
  "leaflet": "^1.9.4",
20
+ "socket.io-client": "^4.8.3"
21
  }
22
  }
frontend/src/api.js CHANGED
@@ -1,7 +1,7 @@
1
  /**
2
  * Cliente HTTP para consumir la API REST del backend.
3
  *
4
- * Funciones fetch encapsuladas para cada endpoint:
5
  * - getMarkets(), getMarket(id), getSignal(id)
6
  * - createPosition(data), getPositions(), closePosition(id)
7
  * - addToWatchlist(marketId), removeFromWatchlist(marketId), getWatchlist()
@@ -9,3 +9,75 @@
9
  *
10
  * Base URL: /api/v1 (mismo dominio, sin CORS en producción HF Spaces).
11
  */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  /**
2
  * Cliente HTTP para consumir la API REST del backend.
3
  *
4
+ * Endpoints:
5
  * - getMarkets(), getMarket(id), getSignal(id)
6
  * - createPosition(data), getPositions(), closePosition(id)
7
  * - addToWatchlist(marketId), removeFromWatchlist(marketId), getWatchlist()
 
9
  *
10
  * Base URL: /api/v1 (mismo dominio, sin CORS en producción HF Spaces).
11
  */
12
+
13
+ const BASE = '/api/v1'
14
+
15
+ async function fetchJson(url, opts = {}) {
16
+ const res = await fetch(url, {
17
+ headers: { 'Content-Type': 'application/json', ...opts.headers },
18
+ ...opts,
19
+ })
20
+ if (!res.ok) {
21
+ const text = await res.text().catch(() => '')
22
+ throw new Error(`HTTP ${res.status}: ${text}`)
23
+ }
24
+ if (res.status === 204) return null
25
+ return res.json()
26
+ }
27
+
28
+ /* ─── Markets ─── */
29
+ export async function getMarkets(params = {}) {
30
+ const qs = new URLSearchParams(params).toString()
31
+ return fetchJson(`${BASE}/markets${qs ? '?' + qs : ''}`)
32
+ }
33
+
34
+ export async function getMarket(id) {
35
+ return fetchJson(`${BASE}/markets/${id}`)
36
+ }
37
+
38
+ /* ─── Signals ─── */
39
+ export async function getSignal(marketId) {
40
+ return fetchJson(`${BASE}/markets/${marketId}/signal`)
41
+ }
42
+
43
+ /* ─── Positions ─── */
44
+ export async function getPositions() {
45
+ return fetchJson(`${BASE}/positions`)
46
+ }
47
+
48
+ export async function createPosition(data) {
49
+ return fetchJson(`${BASE}/positions`, {
50
+ method: 'POST',
51
+ body: JSON.stringify(data),
52
+ })
53
+ }
54
+
55
+ export async function closePosition(id) {
56
+ return fetchJson(`${BASE}/positions/${id}`, { method: 'DELETE' })
57
+ }
58
+
59
+ /* ─── Watchlist ─── */
60
+ export async function getWatchlist() {
61
+ return fetchJson(`${BASE}/watchlist`)
62
+ }
63
+
64
+ export async function addToWatchlist(marketId, alertThreshold) {
65
+ return fetchJson(`${BASE}/watchlist`, {
66
+ method: 'POST',
67
+ body: JSON.stringify({ marketId, alertThreshold }),
68
+ })
69
+ }
70
+
71
+ export async function removeFromWatchlist(marketId) {
72
+ return fetchJson(`${BASE}/watchlist/${marketId}`, { method: 'DELETE' })
73
+ }
74
+
75
+ /* ─── Alerts ─── */
76
+ export async function getAlerts() {
77
+ return fetchJson(`${BASE}/alerts`)
78
+ }
79
+
80
+ /* ─── Stats ─── */
81
+ export async function getStats() {
82
+ return fetchJson(`${BASE}/stats`)
83
+ }
frontend/src/app.js CHANGED
@@ -1,13 +1,11 @@
1
  /**
2
- * Punto de entrada del frontend (Vanilla JS).
3
  *
4
  * Responsabilidades:
5
- * - Inicializar conexion Socket.io y escuchar eventos:
6
- * 'market_update', 'ai_signal', 'price_alert'
7
- * - Orchestrar la carga inicial de datos (mercados, posiciones, watchlist)
8
- * - Conectar los modulos: map.js, charts.js, simulator.js, api.js
9
- *
10
- * Se ejecuta al cargar index.html.
11
  */
12
 
13
  import { io } from 'socket.io-client'
@@ -16,22 +14,626 @@ import * as charts from './charts.js'
16
  import * as map from './map.js'
17
  import * as simulator from './simulator.js'
18
 
19
- const socket = io()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
- socket.on('connect', () => {
22
- console.log('Socket.io conectado')
23
- })
24
 
25
- socket.on('market_update', (data) => {
26
- console.log('market_update', data)
27
- })
 
 
 
 
 
 
28
 
29
- socket.on('ai_signal', (data) => {
30
- console.log('ai_signal', data)
31
- })
 
 
 
 
32
 
33
- socket.on('price_alert', (data) => {
34
- console.log('price_alert', data)
35
- })
 
36
 
37
- // TODO: inicializar modulos y cargar datos iniciales
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  /**
2
+ * Lógica principal de la SPA PolySignal.
3
  *
4
  * Responsabilidades:
5
+ * - Routing de vistas (dashboard, positions, watchlist, alerts)
6
+ * - Sidebar y paneles colapsables
7
+ * - Carga inicial de datos y actualizaciones en tiempo real
8
+ * - Integración con api.js, map.js, charts.js, simulator.js
 
 
9
  */
10
 
11
  import { io } from 'socket.io-client'
 
14
  import * as map from './map.js'
15
  import * as simulator from './simulator.js'
16
 
17
+ /* ─── Estado global ─── */
18
+ let state = {
19
+ view: 'dashboard',
20
+ activeMarketId: null,
21
+ markets: [],
22
+ signals: [],
23
+ positions: [],
24
+ watchlist: [],
25
+ alerts: [],
26
+ collapsedPanels: new Set(),
27
+ sidebarCollapsed: false,
28
+ }
29
+
30
+ /* ─── Datos mock para demo (fallback si backend no responde) ─── */
31
+ const MOCK_MARKETS = [
32
+ {
33
+ id: 'usa-001',
34
+ question: '¿Trump firmará la ley fiscal antes de junio de 2026?',
35
+ category: 'política',
36
+ countryCode: 'US',
37
+ yesPrice: 0.73,
38
+ noPrice: 0.27,
39
+ volumeEur: 1240000,
40
+ liquidityEur: 340000,
41
+ status: 'active',
42
+ closesAt: '2026-06-01T00:00:00Z',
43
+ },
44
+ {
45
+ id: 'eur-001',
46
+ question: '¿El BCE recortará tipos en junio de 2026?',
47
+ category: 'economía',
48
+ countryCode: 'DE',
49
+ yesPrice: 0.34,
50
+ noPrice: 0.66,
51
+ volumeEur: 890000,
52
+ liquidityEur: 210000,
53
+ status: 'active',
54
+ closesAt: '2026-06-12T00:00:00Z',
55
+ },
56
+ {
57
+ id: 'chn-001',
58
+ question: '¿Se alcanzará un acuerdo arancelario EE.UU.-China antes del T3 2026?',
59
+ category: 'comercio',
60
+ countryCode: 'CN',
61
+ yesPrice: 0.51,
62
+ noPrice: 0.49,
63
+ volumeEur: 2100000,
64
+ liquidityEur: 580000,
65
+ status: 'active',
66
+ closesAt: '2026-07-01T00:00:00Z',
67
+ },
68
+ {
69
+ id: 'bra-001',
70
+ question: '¿Bitcoin superará los $120.000 antes de julio de 2026?',
71
+ category: 'cripto',
72
+ countryCode: 'BR',
73
+ yesPrice: 0.61,
74
+ noPrice: 0.39,
75
+ volumeEur: 3400000,
76
+ liquidityEur: 920000,
77
+ status: 'active',
78
+ closesAt: '2026-07-01T00:00:00Z',
79
+ },
80
+ {
81
+ id: 'uk-001',
82
+ question: '¿La inflación del Reino Unido bajará del 2% en 2026?',
83
+ category: 'economía',
84
+ countryCode: 'GB',
85
+ yesPrice: 0.45,
86
+ noPrice: 0.55,
87
+ volumeEur: 620000,
88
+ liquidityEur: 180000,
89
+ status: 'active',
90
+ closesAt: '2026-09-01T00:00:00Z',
91
+ },
92
+ {
93
+ id: 'ind-001',
94
+ question: '¿El crecimiento del PIB de India superará el 7% en el AF2026?',
95
+ category: 'economía',
96
+ countryCode: 'IN',
97
+ yesPrice: 0.68,
98
+ noPrice: 0.32,
99
+ volumeEur: 450000,
100
+ liquidityEur: 120000,
101
+ status: 'active',
102
+ closesAt: '2026-03-31T00:00:00Z',
103
+ },
104
+ ]
105
+
106
+ const MOCK_SIGNALS = {
107
+ 'usa-001': {
108
+ signal: 'bullish',
109
+ confidence: 0.87,
110
+ summary: 'El liderazgo republicano confirmó 3 de 4 votos de comité asegurados. El sentimiento de noticias de 8 fuentes es positivo (+0,72). La línea temporal del Congreso sugiere que la ventana de firma se abre del 28 al 31 de mayo.',
111
+ keyRisk: 'El proceso de enmiendas en el Senado podría retrasar más allá del 1 de junio.',
112
+ },
113
+ 'eur-001': {
114
+ signal: 'bearish',
115
+ confidence: 0.74,
116
+ summary: 'La inflación de la zona euro sorprendió al alza al 2,4% en abril. Las recientes declaraciones de los miembros del consejo de gobierno del BCE señalan que es más probable una pausa.',
117
+ keyRisk: 'La valoración del mercado de bonos implica solo un 22% de probabilidad de recorte en junio.',
118
+ },
119
+ 'chn-001': {
120
+ signal: 'neutral',
121
+ confidence: 0.52,
122
+ summary: 'Las negociaciones continúan pero no se ha acordado un marco formal. Los flujos comerciales muestran patrones tempranos de evasión arancelaria, sugiriendo que ambas partes ganan tiempo.',
123
+ keyRisk: 'Demasiadas variables geopolíticas. Se monitorea la declaración del USTR como señal clave.',
124
+ },
125
+ 'bra-001': {
126
+ signal: 'bullish',
127
+ confidence: 0.79,
128
+ summary: 'BTC actualmente en $103.400. Las métricas on-chain muestran acumulación de grandes carteras. Entradas en ETF +$820M esta semana. El mercado de opciones implica 68% de probabilidad de $120K a finales de junio.',
129
+ keyRisk: 'Las condiciones de liquidez macro podrían cambiar si la Fed mantiene una postura hawkish.',
130
+ },
131
+ 'uk-001': {
132
+ signal: 'neutral',
133
+ confidence: 0.58,
134
+ summary: 'Las previsiones del BoE sugieren una caída gradual pero los shocks externos de precios de energía siguen siendo una incógnita. La inflación de servicios se mantiene en el 5,2%.',
135
+ keyRisk: 'Las interrupciones geopolíticas en la cadena de suministro podrían reavivar la inflación de bienes.',
136
+ },
137
+ 'ind-001': {
138
+ signal: 'bullish',
139
+ confidence: 0.71,
140
+ summary: 'Dato del T4 AF25 en 7,1% con expansión del PMI manufacturero. El impulso del gasto público en infraestructura probablemente sostenga el momentum en el S1 AF26.',
141
+ keyRisk: 'El impacto de El Niño en la producción agrícola podría reducir 30-40 pb del dato principal.',
142
+ },
143
+ }
144
+
145
+ const MOCK_POSITIONS = [
146
+ {
147
+ id: 1,
148
+ marketId: 'usa-001',
149
+ outcome: 'SÍ',
150
+ amountEur: 100,
151
+ entryPrice: 0.68,
152
+ currentPrice: 0.73,
153
+ pnl: 14.20,
154
+ kellyFraction: 0.25,
155
+ openedAt: '2026-05-10T14:30:00Z',
156
+ },
157
+ {
158
+ id: 2,
159
+ marketId: 'eur-001',
160
+ outcome: 'NO',
161
+ amountEur: 80,
162
+ entryPrice: 0.62,
163
+ currentPrice: 0.66,
164
+ pnl: -3.80,
165
+ kellyFraction: 0.18,
166
+ openedAt: '2026-05-11T09:15:00Z',
167
+ },
168
+ ]
169
+
170
+ const MOCK_ALERTS = [
171
+ {
172
+ id: 1,
173
+ marketId: 'usa-001',
174
+ type: 'cambio_señal',
175
+ message: 'La señal cambió a ALCISTA (87%) en ley fiscal de Trump',
176
+ sentAt: '2026-05-14T08:23:00Z',
177
+ },
178
+ {
179
+ id: 2,
180
+ marketId: 'bra-001',
181
+ type: 'umbral_precio',
182
+ message: 'El mercado BTC $120K SÍ cruzó el umbral de 60¢',
183
+ sentAt: '2026-05-14T07:45:00Z',
184
+ },
185
+ ]
186
+
187
+ /* ─── Helpers ─── */
188
+ function formatCurrency(n) {
189
+ if (n >= 1e6) return '€' + (n / 1e6).toFixed(1) + 'M'
190
+ if (n >= 1e3) return '€' + (n / 1e3).toFixed(1) + 'K'
191
+ return '€' + n.toFixed(0)
192
+ }
193
+
194
+ function formatPrice(p) {
195
+ return Math.round(p * 100) + '¢'
196
+ }
197
+
198
+ function formatDate(iso) {
199
+ const d = new Date(iso)
200
+ return d.toLocaleDateString('es-ES', { month: 'short', day: 'numeric', year: 'numeric' })
201
+ }
202
+
203
+ function signalColorClass(signal) {
204
+ if (signal === 'bullish') return 'green'
205
+ if (signal === 'bearish') return 'red'
206
+ return 'amber'
207
+ }
208
+
209
+ function getSignalBadgeClass(signal) {
210
+ if (signal === 'bullish') return 'sig-bull'
211
+ if (signal === 'bearish') return 'sig-bear'
212
+ return 'sig-neut'
213
+ }
214
+
215
+ function getSignalLabel(signal) {
216
+ if (signal === 'bullish') return 'ALC'
217
+ if (signal === 'bearish') return 'BAJ'
218
+ return 'NEUT'
219
+ }
220
+
221
+ function translateSignal(signal) {
222
+ if (signal === 'bullish') return 'alcista'
223
+ if (signal === 'bearish') return 'bajista'
224
+ return 'neutral'
225
+ }
226
+
227
+ /* ─── Routing de vistas ─── */
228
+ function switchView(viewName) {
229
+ state.view = viewName
230
+ document.querySelectorAll('.view').forEach((el) => el.classList.toggle('active', el.id === `view-${viewName}`))
231
+ document.querySelectorAll('.nav-item').forEach((el) => el.classList.toggle('active', el.dataset.view === viewName))
232
+ if (viewName === 'positions') renderPositions()
233
+ if (viewName === 'watchlist') renderWatchlist()
234
+ if (viewName === 'alerts') renderAlerts()
235
+ }
236
+
237
+ /* ─── Sidebar toggle ─── */
238
+ function toggleSidebar() {
239
+ state.sidebarCollapsed = !state.sidebarCollapsed
240
+ document.getElementById('app').classList.toggle('collapsed', state.sidebarCollapsed)
241
+ }
242
+
243
+ /* ─── Panel toggle ─── */
244
+ function togglePanel(panelId) {
245
+ const panel = document.getElementById(`panel-${panelId}`)
246
+ if (!panel) return
247
+ const isCollapsed = panel.classList.toggle('collapsed')
248
+ if (isCollapsed) state.collapsedPanels.add(panelId)
249
+ else state.collapsedPanels.delete(panelId)
250
+ }
251
+
252
+ /* ─── Render signals list ─── */
253
+ function renderSignals() {
254
+ const container = document.getElementById('signals-list')
255
+ if (!container) return
256
+ container.innerHTML = state.markets
257
+ .map((m) => {
258
+ const sig = state.signals.find((s) => s.marketId === m.id) || MOCK_SIGNALS[m.id] || { signal: 'neutral', confidence: 0.5 }
259
+ const isActive = state.activeMarketId === m.id
260
+ const cls = signalColorClass(sig.signal)
261
+ const badgeClass = getSignalBadgeClass(sig.signal)
262
+ return `
263
+ <div class="market-card ${isActive ? 'active' : ''}" data-market="${m.id}">
264
+ <div class="market-cat">${m.category || 'General'} · ${m.countryCode || 'GL'}</div>
265
+ <div class="market-q">${m.question}</div>
266
+ <div class="market-footer">
267
+ <div class="prob-bar-wrap">
268
+ <div class="prob-bar-bg">
269
+ <div class="prob-bar-fill bg-${cls}" style="--prob-width:${Math.round(m.yesPrice * 100)}%"></div>
270
+ </div>
271
+ </div>
272
+ <span class="prob-val text-${cls}">${formatPrice(m.yesPrice)}</span>
273
+ <span class="signal-badge ${badgeClass}">${getSignalLabel(sig.signal)}</span>
274
+ </div>
275
+ </div>
276
+ `
277
+ })
278
+ .join('')
279
+
280
+ container.querySelectorAll('.market-card').forEach((card) => {
281
+ card.addEventListener('click', () => selectMarket(card.dataset.market))
282
+ })
283
+ }
284
+
285
+ /* ─── Render mini positions in sidebar ─── */
286
+ function renderMiniPositions() {
287
+ const container = document.getElementById('mini-positions')
288
+ if (!container) return
289
+ if (state.positions.length === 0) {
290
+ container.innerHTML = '<div class="empty-state empty-state-sm">Aún sin posiciones</div>'
291
+ return
292
+ }
293
+ let netPnl = 0
294
+ const items = state.positions
295
+ .map((p) => {
296
+ const m = state.markets.find((x) => x.id === p.marketId) || { question: p.marketId }
297
+ netPnl += p.pnl
298
+ const cls = p.pnl >= 0 ? 'green' : 'red'
299
+ const sign = p.pnl >= 0 ? '+' : ''
300
+ return `
301
+ <div class="flex-between mb-6">
302
+ <span class="text-sm text-neutral font-mono">${m.question.substring(0, 32)}${m.question.length > 32 ? '…' : ''} ${p.outcome}</span>
303
+ <span class="text-base font-semibold text-${cls} font-mono">${sign}€${p.pnl.toFixed(2)}</span>
304
+ </div>
305
+ `
306
+ })
307
+ .join('')
308
+
309
+ const netCls = netPnl >= 0 ? 'green' : 'red'
310
+ const netSign = netPnl >= 0 ? '+' : ''
311
+ container.innerHTML = `
312
+ ${items}
313
+ <div class="divider"></div>
314
+ <div class="flex-between">
315
+ <span class="text-sm text-neutral font-mono">G&amp;P Neto</span>
316
+ <span class="text-lg font-bold text-${netCls} font-mono">${netSign}€${netPnl.toFixed(2)}</span>
317
+ </div>
318
+ `
319
+ }
320
+
321
+ /* ─── Render detail panel ─── */
322
+ function renderDetail() {
323
+ const container = document.getElementById('detail-body')
324
+ if (!container) return
325
+ const m = state.markets.find((x) => x.id === state.activeMarketId)
326
+ if (!m) {
327
+ container.innerHTML = '<div class="empty-state">Selecciona un mercado para ver detalles</div>'
328
+ return
329
+ }
330
+
331
+ const sig = state.signals.find((s) => s.marketId === m.id) || MOCK_SIGNALS[m.id] || { signal: 'neutral', confidence: 0.5, summary: 'Aún no hay análisis de IA disponible.', keyRisk: '' }
332
+ const delta = ((m.yesPrice - 0.5) * 20).toFixed(1)
333
+ const deltaCls = m.yesPrice > 0.5 ? 'green' : 'red'
334
+ const deltaSign = m.yesPrice > 0.5 ? '+' : ''
335
+
336
+ container.innerHTML = `
337
+ <div class="detail-header">
338
+ <div>
339
+ <div class="detail-tag">${m.countryCode || 'GL'} · ${m.category || 'General'} · Polymarket</div>
340
+ <div class="detail-q">${m.question}</div>
341
+ <div class="detail-meta">Vol: ${formatCurrency(m.volumeEur || 0)} · Liq: ${formatCurrency(m.liquidityEur || 0)} · Cierra: ${formatDate(m.closesAt)}</div>
342
+ </div>
343
+ <div class="detail-metrics">
344
+ <div class="metric">
345
+ <div class="metric-label">Cambio 24h</div>
346
+ <div class="metric-value text-${deltaCls}">${deltaSign}${delta}%</div>
347
+ </div>
348
+ <div class="metric-sep"></div>
349
+ <div class="metric">
350
+ <div class="metric-label">Confianza</div>
351
+ <div class="metric-value text-blue">${Math.round(sig.confidence * 100)}%</div>
352
+ </div>
353
+ </div>
354
+ </div>
355
+
356
+ <div class="outcomes-row">
357
+ <div class="outcome-card yes">
358
+ <div class="outcome-name">SÍ</div>
359
+ <div class="outcome-price">${formatPrice(m.yesPrice)}</div>
360
+ <div class="outcome-delta td-green">▲ ${(m.yesPrice * 0.05).toFixed(1)}¢</div>
361
+ <div class="sparkline" id="spark-yes"></div>
362
+ </div>
363
+ <div class="outcome-card no">
364
+ <div class="outcome-name">NO</div>
365
+ <div class="outcome-price">${formatPrice(m.noPrice)}</div>
366
+ <div class="outcome-delta td-red">▼ ${(m.noPrice * 0.05).toFixed(1)}¢</div>
367
+ <div class="sparkline" id="spark-no"></div>
368
+ </div>
369
+ <div class="chart-container">
370
+ <div class="chart-label">Historial de precios 7d</div>
371
+ <canvas id="detail-chart"></canvas>
372
+ </div>
373
+ </div>
374
+
375
+ <div class="ai-box">
376
+ <div class="ai-icon">◈</div>
377
+ <div class="flex-1">
378
+ <div class="flex-row gap-8 mb-4 flex-wrap">
379
+ <div class="ai-label">Análisis IA · HuggingFace Qwen3-8B</div>
380
+ <span class="signal-badge ${getSignalBadgeClass(sig.signal)}">${translateSignal(sig.signal).toUpperCase()} · ${Math.round(sig.confidence * 100)}%</span>
381
+ <span class="text-xs text-neutral font-mono ml-auto">actualizado hace 2m</span>
382
+ </div>
383
+ <div class="ai-text">${sig.summary}${sig.keyRisk ? ' <strong>Riesgo clave:</strong> ' + sig.keyRisk : ''}</div>
384
+ </div>
385
+ </div>
386
+
387
+ <div class="sim-row">
388
+ <span class="sim-label">Simular posición →</span>
389
+ <input class="sim-input" type="number" id="sim-amount" value="100" min="1" placeholder="€"/>
390
+ <button class="sim-btn-yes" id="sim-yes">COMPRAR SÍ ↗</button>
391
+ <button class="sim-btn-no" id="sim-no">COMPRAR NO</button>
392
+ <span class="sim-disclaimer">Simulado · sin trading real</span>
393
+ </div>
394
+ `
395
+
396
+ // Bind simulator buttons
397
+ document.getElementById('sim-yes')?.addEventListener('click', () => simulator.openPosition(m.id, 'SÍ', document.getElementById('sim-amount').value))
398
+ document.getElementById('sim-no')?.addEventListener('click', () => simulator.openPosition(m.id, 'NO', document.getElementById('sim-amount').value))
399
+
400
+ // Render chart
401
+ charts.renderDetailChart('detail-chart', m.yesPrice)
402
+ charts.renderSparkline('spark-yes', m.yesPrice, 'yes')
403
+ charts.renderSparkline('spark-no', m.noPrice, 'no')
404
+ }
405
+
406
+ /* ─── Select market ─── */
407
+ function selectMarket(marketId) {
408
+ state.activeMarketId = marketId
409
+ renderSignals()
410
+ renderDetail()
411
+ map.highlightMarket(marketId)
412
+ }
413
+
414
+ /* ─── Render positions view ─── */
415
+ function renderPositions() {
416
+ const tbody = document.querySelector('#positions-table tbody')
417
+ const empty = document.getElementById('positions-empty')
418
+ if (!tbody) return
419
+ if (state.positions.length === 0) {
420
+ tbody.innerHTML = ''
421
+ empty.classList.remove('hidden')
422
+ return
423
+ }
424
+ empty.classList.add('hidden')
425
+ tbody.innerHTML = state.positions
426
+ .map((p) => {
427
+ const m = state.markets.find((x) => x.id === p.marketId) || { question: p.marketId }
428
+ const pnlColor = p.pnl >= 0 ? 'td-green' : 'td-red'
429
+ const sign = p.pnl >= 0 ? '+' : ''
430
+ return `
431
+ <tr>
432
+ <td>${m.question.substring(0, 40)}${m.question.length > 40 ? '…' : ''}</td>
433
+ <td class="td-mono ${p.outcome === 'SÍ' ? 'td-green' : 'td-red'}">${p.outcome}</td>
434
+ <td class="td-mono">€${p.amountEur.toFixed(0)}</td>
435
+ <td class="td-mono">${formatPrice(p.entryPrice)}</td>
436
+ <td class="td-mono">${formatPrice(p.currentPrice)}</td>
437
+ <td class="td-mono ${pnlColor}">${sign}€${p.pnl.toFixed(2)}</td>
438
+ <td class="td-mono td-blue">${((p.kellyFraction || 0) * 100).toFixed(0)}%</td>
439
+ <td class="td-mono">${formatDate(p.openedAt)}</td>
440
+ <td><button class="btn-ghost" onclick="closePositionById(${p.id})">Cerrar</button></td>
441
+ </tr>
442
+ `
443
+ })
444
+ .join('')
445
+ }
446
+
447
+ window.closePositionById = async (id) => {
448
+ await simulator.closePosition(id)
449
+ await loadPositions()
450
+ renderPositions()
451
+ renderMiniPositions()
452
+ }
453
+
454
+ /* ─── Render watchlist view ─── */
455
+ function renderWatchlist() {
456
+ const tbody = document.querySelector('#watchlist-table tbody')
457
+ const empty = document.getElementById('watchlist-empty')
458
+ if (!tbody) return
459
+ if (state.watchlist.length === 0) {
460
+ tbody.innerHTML = ''
461
+ empty.classList.remove('hidden')
462
+ return
463
+ }
464
+ empty.classList.add('hidden')
465
+ tbody.innerHTML = state.watchlist
466
+ .map((w) => {
467
+ const m = state.markets.find((x) => x.id === w.marketId) || { question: w.marketId, category: '-', yesPrice: 0, noPrice: 0, volumeEur: 0 }
468
+ const sig = state.signals.find((s) => s.marketId === w.marketId) || { signal: 'neutral' }
469
+ return `
470
+ <tr>
471
+ <td>${m.question.substring(0, 40)}${m.question.length > 40 ? '…' : ''}</td>
472
+ <td>${m.category || '-'}</td>
473
+ <td class="td-mono td-green">${formatPrice(m.yesPrice)}</td>
474
+ <td class="td-mono td-red">${formatPrice(m.noPrice)}</td>
475
+ <td><span class="signal-badge ${getSignalBadgeClass(sig.signal)}">${getSignalLabel(sig.signal)}</span></td>
476
+ <td class="td-mono">${formatCurrency(m.volumeEur || 0)}</td>
477
+ <td class="td-mono">${w.alertThreshold ? formatPrice(w.alertThreshold) : '-'}</td>
478
+ <td><button class="btn-ghost" onclick="removeFromWatchlistById('${w.marketId}')">Eliminar</button></td>
479
+ </tr>
480
+ `
481
+ })
482
+ .join('')
483
+ }
484
+
485
+ window.removeFromWatchlistById = async (marketId) => {
486
+ try { await api.removeFromWatchlist(marketId) } catch (e) { console.warn(e) }
487
+ state.watchlist = state.watchlist.filter((w) => w.marketId !== marketId)
488
+ renderWatchlist()
489
+ }
490
+
491
+ /* ─── Render alerts view ─── */
492
+ function renderAlerts() {
493
+ const tbody = document.querySelector('#alerts-table tbody')
494
+ const empty = document.getElementById('alerts-empty')
495
+ if (!tbody) return
496
+ if (state.alerts.length === 0) {
497
+ tbody.innerHTML = ''
498
+ empty.classList.remove('hidden')
499
+ return
500
+ }
501
+ empty.classList.add('hidden')
502
+ tbody.innerHTML = state.alerts
503
+ .map((a) => {
504
+ const m = state.markets.find((x) => x.id === a.marketId) || { question: a.marketId }
505
+ return `
506
+ <tr>
507
+ <td class="td-mono">${new Date(a.sentAt).toLocaleString('es-ES')}</td>
508
+ <td>${m.question.substring(0, 35)}${m.question.length > 35 ? '…' : ''}</td>
509
+ <td><span class="signal-badge sig-neut">${a.type}</span></td>
510
+ <td>${a.message}</td>
511
+ </tr>
512
+ `
513
+ })
514
+ .join('')
515
+ }
516
+
517
+ /* ─── Carga de datos ─── */
518
+ async function loadMarkets() {
519
+ try {
520
+ const data = await api.getMarkets({ limit: 50 })
521
+ state.markets = data.length ? data : MOCK_MARKETS
522
+ } catch (e) {
523
+ console.warn('API mercados no disponible, usando datos de prueba')
524
+ state.markets = MOCK_MARKETS
525
+ }
526
+ }
527
+
528
+ async function loadSignals() {
529
+ try {
530
+ const promises = state.markets.map((m) => api.getSignal(m.id).catch(() => null))
531
+ const results = await Promise.all(promises)
532
+ state.signals = results.filter(Boolean).map((r, i) => ({ ...r, marketId: state.markets[i].id }))
533
+ } catch (e) {
534
+ console.warn('API señales no disponible, usando datos de prueba')
535
+ state.signals = Object.entries(MOCK_SIGNALS).map(([marketId, s]) => ({ ...s, marketId }))
536
+ }
537
+ }
538
+
539
+ async function loadPositions() {
540
+ try {
541
+ state.positions = await api.getPositions()
542
+ } catch (e) {
543
+ state.positions = MOCK_POSITIONS
544
+ }
545
+ }
546
+
547
+ async function loadWatchlist() {
548
+ try {
549
+ state.watchlist = await api.getWatchlist()
550
+ } catch (e) {
551
+ state.watchlist = []
552
+ }
553
+ }
554
+
555
+ async function loadAlerts() {
556
+ try {
557
+ state.alerts = await api.getAlerts()
558
+ } catch (e) {
559
+ state.alerts = MOCK_ALERTS
560
+ }
561
+ }
562
+
563
+ /* ─── Inicialización ─── */
564
+ export async function init() {
565
+ // Sidebar toggle
566
+ document.getElementById('sidebar-toggle')?.addEventListener('click', toggleSidebar)
567
+
568
+ // Nav routing
569
+ document.querySelectorAll('.nav-item').forEach((el) => {
570
+ el.addEventListener('click', () => switchView(el.dataset.view))
571
+ })
572
+
573
+ // Panel toggles
574
+ document.querySelectorAll('.panel-header[data-panel]').forEach((el) => {
575
+ el.addEventListener('click', (e) => {
576
+ // Evitar colapsar al clicar elementos interactivos
577
+ if (e.target.closest('button, input, a')) return
578
+ togglePanel(el.dataset.panel)
579
+ })
580
+ })
581
+
582
+ // Load initial data
583
+ await loadMarkets()
584
+ await loadSignals()
585
+ await loadPositions()
586
+ await loadWatchlist()
587
+ await loadAlerts()
588
+
589
+ // Init modules
590
+ map.init('map-container', state.markets, state.signals, selectMarket)
591
+ simulator.init(state)
592
+
593
+ // Initial render
594
+ state.activeMarketId = state.markets[0]?.id || null
595
+ renderSignals()
596
+ renderDetail()
597
+ renderMiniPositions()
598
 
599
+ // Socket.io
600
+ const socket = io()
601
+ socket.on('connect', () => console.log('Socket.io conectado'))
602
 
603
+ socket.on('market_update', (data) => {
604
+ const m = state.markets.find((x) => x.id === data.marketId)
605
+ if (m) {
606
+ Object.assign(m, data)
607
+ if (state.activeMarketId === data.marketId) renderDetail()
608
+ renderSignals()
609
+ map.updateBubble(data.marketId, data.yesPrice)
610
+ }
611
+ })
612
 
613
+ socket.on('ai_signal', (data) => {
614
+ const idx = state.signals.findIndex((s) => s.marketId === data.marketId)
615
+ if (idx >= 0) state.signals[idx] = data
616
+ else state.signals.push(data)
617
+ renderSignals()
618
+ if (state.activeMarketId === data.marketId) renderDetail()
619
+ })
620
 
621
+ socket.on('price_alert', (data) => {
622
+ state.alerts.unshift(data)
623
+ if (state.view === 'alerts') renderAlerts()
624
+ })
625
 
626
+ // Stats animation mock
627
+ setInterval(() => {
628
+ const el = document.getElementById('stat-markets')
629
+ if (el) {
630
+ const n = parseInt(el.textContent.replace(/\./g, '')) + Math.floor(Math.random() * 3)
631
+ el.textContent = n.toLocaleString('es-ES')
632
+ }
633
+ const el2 = document.getElementById('stat-signals')
634
+ if (el2 && Math.random() > 0.7) {
635
+ const n = parseInt(el2.textContent) + 1
636
+ el2.textContent = n
637
+ }
638
+ }, 3000)
639
+ }
frontend/src/charts.js CHANGED
@@ -1,14 +1,72 @@
1
  /**
2
- * Módulo de gráficos de historial de precios.
3
- *
4
- * Tecnología: Chart.js
5
  *
6
  * Funciones:
7
- * - Renderizar línea de precios YES/NO de un mercado seleccionado
8
- * - Actualizar datos en tiempo real cuando llega 'market_update'
9
- * - Opcional: mostrar punto de entrada de posiciones abiertas
10
- *
11
- * Se vincula al panel de detalle de mercado.
12
  */
13
 
14
  import { Chart } from 'chart.js/auto'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  /**
2
+ * Módulo de gráficos usando Chart.js.
 
 
3
  *
4
  * Funciones:
5
+ * - renderDetailChart(canvasId, currentPrice)
6
+ * - renderSparkline(containerId, price, side)
 
 
 
7
  */
8
 
9
  import { Chart } from 'chart.js/auto'
10
+
11
+ let detailChartInstance = null
12
+
13
+ export function renderDetailChart(canvasId, currentPrice) {
14
+ const ctx = document.getElementById(canvasId)
15
+ if (!ctx) return
16
+ if (detailChartInstance) detailChartInstance.destroy()
17
+
18
+ const base = currentPrice * 100
19
+ const pts = Array.from({ length: 8 }, (_, i) => {
20
+ const noise = (Math.random() - 0.5) * 8
21
+ return Math.max(5, Math.min(95, base - 12 + (i / 7) * 12 + noise))
22
+ })
23
+ pts[pts.length - 1] = base
24
+
25
+ const col = base > 50 ? '#22d37a' : base < 40 ? '#f04040' : '#f0a020'
26
+
27
+ detailChartInstance = new Chart(ctx, {
28
+ type: 'line',
29
+ data: {
30
+ labels: ['7d', '6d', '5d', '4d', '3d', '2d', '1d', 'now'],
31
+ datasets: [
32
+ {
33
+ data: pts,
34
+ borderColor: col,
35
+ borderWidth: 1.5,
36
+ pointRadius: 0,
37
+ fill: false,
38
+ tension: 0.4,
39
+ },
40
+ ],
41
+ },
42
+ options: {
43
+ responsive: true,
44
+ maintainAspectRatio: false,
45
+ plugins: { legend: { display: false }, tooltip: { enabled: false } },
46
+ scales: { x: { display: false }, y: { display: false } },
47
+ animation: { duration: 600 },
48
+ },
49
+ })
50
+ }
51
+
52
+ export function renderSparkline(containerId, price, side) {
53
+ const el = document.getElementById(containerId)
54
+ if (!el) return
55
+ el.innerHTML = ''
56
+ el.className = 'sparkline'
57
+
58
+ const base = price * 100
59
+ for (let i = 0; i < 12; i++) {
60
+ const h = Math.max(4, Math.min(24, base / 4 + (Math.random() - 0.5) * 8))
61
+ const d = document.createElement('div')
62
+ d.className = 'spark-bar'
63
+ d.style.height = h + 'px'
64
+ d.style.background = side === 'yes' ? '#0d6e3a' : '#7a1a1a'
65
+ el.appendChild(d)
66
+ }
67
+ const last = document.createElement('div')
68
+ last.className = 'spark-bar'
69
+ last.style.height = Math.min(28, base / 3.5) + 'px'
70
+ last.style.background = side === 'yes' ? '#22d37a' : '#f04040'
71
+ el.appendChild(last)
72
+ }
frontend/src/main.js CHANGED
@@ -1,2 +1,4 @@
1
  import './style.css'
2
- import './app.js'
 
 
 
1
  import './style.css'
2
+ import { init } from './app.js'
3
+
4
+ init().catch((err) => console.error('Failed to initialize app:', err))
frontend/src/map.js CHANGED
@@ -1,17 +1,142 @@
1
  /**
2
- * Módulo de visualización del mapa mundial interactivo.
3
- *
4
- * Tecnología: Leaflet.js
5
  *
6
  * Funciones:
7
- * - Renderizar mapa base centrado en coordenadas por defecto
8
- * - Dibujar burbujas (circle markers) por país usando countryCode (ISO2)
9
- * - Color de burbuja según señal IA dominante (verde/amarillo/rojo)
10
- * - Tamaño proporcional al volumen o liquidez del mercado
11
- * - Popup al hacer click con detalle del mercado
12
- *
13
- * Recibe actualizaciones en tiempo real vía Socket.io desde app.js.
14
  */
15
 
16
  import L from 'leaflet'
17
  import 'leaflet/dist/leaflet.css'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  /**
2
+ * Módulo de visualización del mapa mundial interactivo con Leaflet.js.
 
 
3
  *
4
  * Funciones:
5
+ * - init(containerId, markets, signals, onSelect)
6
+ * - updateBubble(marketId, newPrice)
7
+ * - highlightMarket(marketId)
 
 
 
 
8
  */
9
 
10
  import L from 'leaflet'
11
  import 'leaflet/dist/leaflet.css'
12
+
13
+ // Coordenadas aproximadas por countryCode ISO2
14
+ const COORDS = {
15
+ US: [37.09, -95.71],
16
+ DE: [51.16, 10.45],
17
+ GB: [55.37, -3.43],
18
+ BR: [-14.23, -51.92],
19
+ CN: [35.86, 104.19],
20
+ IN: [20.59, 78.96],
21
+ KR: [35.90, 127.76],
22
+ SA: [23.88, 45.07],
23
+ FR: [46.22, 2.21],
24
+ JP: [36.20, 138.25],
25
+ AU: [-25.27, 133.77],
26
+ CA: [56.13, -106.34],
27
+ RU: [61.52, 105.31],
28
+ MX: [23.63, -102.55],
29
+ ZA: [-30.55, 22.93],
30
+ }
31
+
32
+ let mapInstance = null
33
+ let bubbles = {} // marketId -> circle marker
34
+
35
+ function getCoords(countryCode) {
36
+ return COORDS[countryCode?.toUpperCase()] || [20, 0]
37
+ }
38
+
39
+ function getSignalColor(signal) {
40
+ if (signal === 'bullish') return '#22d37a'
41
+ if (signal === 'bearish') return '#f04040'
42
+ return '#f0a020'
43
+ }
44
+
45
+ function getRadius(volumeEur) {
46
+ const v = volumeEur || 0
47
+ if (v > 2e6) return 18
48
+ if (v > 1e6) return 14
49
+ if (v > 500000) return 11
50
+ if (v > 200000) return 8
51
+ return 6
52
+ }
53
+
54
+ export function init(containerId, markets, signals, onSelect) {
55
+ const container = document.getElementById(containerId)
56
+ if (!container) return
57
+
58
+ mapInstance = L.map(container, {
59
+ zoomControl: false,
60
+ attributionControl: false,
61
+ minZoom: 2,
62
+ maxZoom: 6,
63
+ worldCopyJump: true,
64
+ }).setView([25, 10], 2)
65
+
66
+ // Dark tile layer (CartoDB Dark Matter)
67
+ L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
68
+ attribution: '&copy;OpenStreetMap &copy;CartoDB',
69
+ subdomains: 'abcd',
70
+ maxZoom: 19,
71
+ }).addTo(mapInstance)
72
+
73
+ markets.forEach((m) => {
74
+ const sig = signals.find((s) => s.marketId === m.id) || { signal: 'neutral' }
75
+ const color = getSignalColor(sig.signal)
76
+ const coords = getCoords(m.countryCode)
77
+ const radius = getRadius(m.volumeEur)
78
+
79
+ const circle = L.circleMarker(coords, {
80
+ radius,
81
+ fillColor: color,
82
+ color: color,
83
+ weight: 1.5,
84
+ opacity: 0.6,
85
+ fillOpacity: 0.22,
86
+ }).addTo(mapInstance)
87
+
88
+ const inner = L.circleMarker(coords, {
89
+ radius: Math.max(3, radius * 0.45),
90
+ fillColor: color,
91
+ color: 'transparent',
92
+ fillOpacity: 0.8,
93
+ }).addTo(mapInstance)
94
+
95
+ const label = L.marker(coords, {
96
+ icon: L.divIcon({
97
+ className: 'map-label',
98
+ html: `<span class="map-label-text">${m.countryCode || 'GL'}</span>`,
99
+ iconSize: [40, 14],
100
+ iconAnchor: [20, -radius - 4],
101
+ }),
102
+ interactive: false,
103
+ }).addTo(mapInstance)
104
+
105
+ const popupContent = `
106
+ <div class="map-popup">
107
+ <div class="map-popup-cat">${m.category || 'General'} · ${m.countryCode || 'GL'}</div>
108
+ <div class="map-popup-q">${m.question}</div>
109
+ <div class="map-popup-prices">
110
+ <span class="text-green">SÍ ${Math.round((m.yesPrice || 0) * 100)}¢</span>
111
+ <span class="text-red">NO ${Math.round((m.noPrice || 0) * 100)}¢</span>
112
+ </div>
113
+ </div>
114
+ `
115
+ circle.bindPopup(popupContent, { closeButton: false, offset: [0, -4] })
116
+
117
+ circle.on('click', () => {
118
+ onSelect(m.id)
119
+ })
120
+
121
+ bubbles[m.id] = { circle, inner, label, color }
122
+ })
123
+ }
124
+
125
+ export function updateBubble(marketId, newPrice) {
126
+ const b = bubbles[marketId]
127
+ if (!b) return
128
+ // Slightly adjust radius based on new activity (mock behavior)
129
+ const newRadius = b.circle.getRadius() + (Math.random() > 0.5 ? 0.5 : -0.5)
130
+ b.circle.setRadius(Math.max(5, Math.min(22, newRadius)))
131
+ }
132
+
133
+ export function highlightMarket(marketId) {
134
+ Object.values(bubbles).forEach((b) => {
135
+ b.circle.setStyle({ weight: 1.5, opacity: 0.6 })
136
+ })
137
+ const b = bubbles[marketId]
138
+ if (b) {
139
+ b.circle.setStyle({ weight: 3, opacity: 1, color: '#4a9eff' })
140
+ b.circle.openPopup()
141
+ }
142
+ }
frontend/src/simulator.js CHANGED
@@ -1,11 +1,67 @@
1
  /**
2
- * Módulo de interfaz del simulador de posiciones.
3
  *
4
  * Funciones:
5
- * - Mostrar formulario para abrir posición virtual (YES/NO, capital)
6
- * - Solicitar fracción de Kelly desde services/kelly.js vía API
7
- * - Renderizar lista de posiciones abiertas con P&L en vivo
8
- * - Botón de cierre de posición y cálculo de resultado final
9
- *
10
- * Usa userId=1 fijo para la demo.
11
  */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  /**
2
+ * Módulo del simulador de posiciones virtuales.
3
  *
4
  * Funciones:
5
+ * - init(state)
6
+ * - openPosition(marketId, outcome, amount)
7
+ * - closePosition(positionId)
 
 
 
8
  */
9
+
10
+ import * as api from './api.js'
11
+
12
+ let appState = null
13
+
14
+ export function init(state) {
15
+ appState = state
16
+ }
17
+
18
+ export async function openPosition(marketId, outcome, amount) {
19
+ const amt = parseFloat(amount)
20
+ if (!amt || amt <= 0) {
21
+ alert('Introduce una cantidad válida')
22
+ return
23
+ }
24
+
25
+ const m = appState.markets.find((x) => x.id === marketId)
26
+ if (!m) return
27
+
28
+ const entryPrice = outcome === 'YES' ? m.yesPrice : m.noPrice
29
+ const data = {
30
+ marketId,
31
+ outcome,
32
+ amountEur: amt,
33
+ entryPrice,
34
+ }
35
+
36
+ try {
37
+ const created = await api.createPosition(data)
38
+ appState.positions.push(created)
39
+ } catch (e) {
40
+ // Fallback: create locally if API unavailable
41
+ const fakeId = Date.now()
42
+ appState.positions.push({
43
+ id: fakeId,
44
+ marketId,
45
+ outcome,
46
+ amountEur: amt,
47
+ entryPrice,
48
+ currentPrice: entryPrice,
49
+ pnl: 0,
50
+ kellyFraction: 0.2,
51
+ openedAt: new Date().toISOString(),
52
+ })
53
+ }
54
+
55
+ // Trigger re-render via app.js if needed
56
+ document.dispatchEvent(new CustomEvent('positions:changed'))
57
+ }
58
+
59
+ export async function closePosition(positionId) {
60
+ try {
61
+ await api.closePosition(positionId)
62
+ } catch (e) {
63
+ console.warn('API closePosition failed, removing locally')
64
+ }
65
+ appState.positions = appState.positions.filter((p) => p.id !== positionId)
66
+ document.dispatchEvent(new CustomEvent('positions:changed'))
67
+ }
frontend/src/style.css CHANGED
@@ -1,12 +1,1061 @@
1
- /*
2
- * Estilos visuales del dashboard PolySignal.
3
- *
4
- * Incluye:
5
- * - Layout principal y grid del dashboard
6
- * - Estilos del mapa Leaflet y burbujas de mercado por país
7
- * - Paneles de señales IA (bullish / bearish / neutral)
8
- * - Componentes del simulador de posiciones y watchlist
9
- * - Diseño responsive básico para hackathon
10
- *
11
- * Puro CSS vanilla, sin preprocesadores.
12
- */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Syne:wght@400;500;600;700&display=swap');
2
+
3
+ :root {
4
+ --bg: #0a0c10;
5
+ --bg2: #111318;
6
+ --bg3: #181c24;
7
+ --bg4: #1e232d;
8
+ --border: rgba(255,255,255,0.08);
9
+ --border2: rgba(255,255,255,0.13);
10
+ --text: #e8eaf0;
11
+ --text2: #8b90a0;
12
+ --text3: #555b6e;
13
+ --green: #22d37a;
14
+ --green2: #0d6e3a;
15
+ --green3: #052a17;
16
+ --red: #f04040;
17
+ --red2: #7a1a1a;
18
+ --red3: #2d0808;
19
+ --blue: #4a9eff;
20
+ --blue2: #1a4a80;
21
+ --blue3: #081830;
22
+ --amber: #f0a020;
23
+ --amber2: #7a4e08;
24
+ --amber3: #2d1c02;
25
+ --accent: #4a9eff;
26
+
27
+ --sidebar-width: 240px;
28
+ --sidebar-collapsed: 56px;
29
+ --topbar-height: 56px;
30
+ --panel-gap: 16px;
31
+ --radius: 10px;
32
+ --radius-sm: 6px;
33
+
34
+ /* Fluid typography scale */
35
+ --fs-p: clamp(16px, 1.15vw, 18px);
36
+ --fs-h1: clamp(28px, 2.8vw, 36px);
37
+ --fs-h2: clamp(24px, 2.4vw, 30px);
38
+ --fs-h3: clamp(20px, 1.9vw, 24px);
39
+ --fs-h4: clamp(18px, 1.6vw, 20px);
40
+ --fs-h5: clamp(16px, 1.3vw, 18px);
41
+ --fs-h6: clamp(14px, 1.1vw, 16px);
42
+ }
43
+
44
+ * {
45
+ box-sizing: border-box;
46
+ margin: 0;
47
+ padding: 0;
48
+ }
49
+
50
+ html, body, #app {
51
+ height: 100%;
52
+ width: 100%;
53
+ overflow: hidden;
54
+ }
55
+
56
+ body {
57
+ background: var(--bg);
58
+ color: var(--text);
59
+ font-family: 'Syne', sans-serif;
60
+ font-size: 16px;
61
+ line-height: 1.5;
62
+ -webkit-font-smoothing: antialiased;
63
+ }
64
+
65
+ /* Semantic type hierarchy (fluid) */
66
+ p { font-size: var(--fs-p); line-height: 1.55; }
67
+ h1 { font-size: var(--fs-h1); line-height: 1.15; font-weight: 700; }
68
+ h2 { font-size: var(--fs-h2); line-height: 1.2; font-weight: 700; }
69
+ h3 { font-size: var(--fs-h3); line-height: 1.25; font-weight: 600; }
70
+ h4 { font-size: var(--fs-h4); line-height: 1.3; font-weight: 600; }
71
+ h5 { font-size: var(--fs-h5); line-height: 1.35; font-weight: 500; }
72
+ h6 { font-size: var(--fs-h6); line-height: 1.4; font-weight: 500; }
73
+
74
+ /* ─── Layout principal ─── */
75
+ .layout {
76
+ display: grid;
77
+ grid-template-areas:
78
+ "topbar topbar"
79
+ "sidebar main";
80
+ grid-template-columns: var(--sidebar-width) 1fr;
81
+ grid-template-rows: var(--topbar-height) 1fr;
82
+ height: 100vh;
83
+ width: 100vw;
84
+ transition: grid-template-columns 0.25s ease;
85
+ }
86
+
87
+ .layout.collapsed {
88
+ grid-template-columns: var(--sidebar-collapsed) 1fr;
89
+ }
90
+
91
+ /* ─── Sidebar ─── */
92
+ .sidebar {
93
+ grid-area: sidebar;
94
+ background: var(--bg2);
95
+ border-right: 1px solid var(--border);
96
+ display: flex;
97
+ flex-direction: column;
98
+ overflow: hidden;
99
+ transition: width 0.25s ease;
100
+ position: relative;
101
+ }
102
+
103
+ .sidebar-toggle {
104
+ position: absolute;
105
+ top: 12px;
106
+ right: -10px;
107
+ width: 24px;
108
+ height: 24px;
109
+ border-radius: 50%;
110
+ background: var(--bg3);
111
+ border: 1px solid var(--border2);
112
+ color: var(--text2);
113
+ display: flex;
114
+ align-items: center;
115
+ justify-content: center;
116
+ cursor: pointer;
117
+ font-size: 14px;
118
+ z-index: 10;
119
+ transition: transform 0.2s;
120
+ }
121
+
122
+ .layout.collapsed .sidebar-toggle {
123
+ transform: rotate(180deg);
124
+ right: -10px;
125
+ }
126
+
127
+ /* ─── Topbar Logo ─── */
128
+ .topbar-logo {
129
+ position: absolute;
130
+ left: 16px;
131
+ top: 0;
132
+ bottom: 0;
133
+ display: flex;
134
+ align-items: center;
135
+ gap: 10px;
136
+ }
137
+
138
+ .topbar-logo .logo-dot {
139
+ width: 10px;
140
+ height: 10px;
141
+ border-radius: 50%;
142
+ background: var(--blue);
143
+ animation: pulse 2s ease-in-out infinite;
144
+ flex-shrink: 0;
145
+ }
146
+
147
+ .topbar-logo .logo-text {
148
+ font-size: 18px;
149
+ font-weight: 700;
150
+ color: var(--text);
151
+ letter-spacing: -0.3px;
152
+ white-space: nowrap;
153
+ }
154
+
155
+ .sidebar-nav {
156
+ flex: 1;
157
+ padding: 14px 8px;
158
+ display: flex;
159
+ flex-direction: column;
160
+ gap: 4px;
161
+ overflow-y: auto;
162
+ }
163
+
164
+ .nav-item {
165
+ display: flex;
166
+ align-items: center;
167
+ gap: 14px;
168
+ padding: 14px 14px;
169
+ border-radius: var(--radius-sm);
170
+ cursor: pointer;
171
+ color: var(--text2);
172
+ font-size: 16px;
173
+ font-weight: 500;
174
+ transition: background 0.15s, color 0.15s;
175
+ white-space: nowrap;
176
+ overflow: hidden;
177
+ }
178
+
179
+ .nav-item:hover {
180
+ background: var(--bg3);
181
+ color: var(--text);
182
+ }
183
+
184
+ .nav-item.active {
185
+ background: var(--blue3);
186
+ color: var(--blue);
187
+ border: 0.5px solid var(--blue2);
188
+ }
189
+
190
+ .nav-icon {
191
+ font-size: 16px;
192
+ width: 20px;
193
+ text-align: center;
194
+ flex-shrink: 0;
195
+ }
196
+
197
+ .nav-label {
198
+ opacity: 1;
199
+ transition: opacity 0.15s;
200
+ }
201
+
202
+ .layout.collapsed .nav-label {
203
+ opacity: 0;
204
+ width: 0;
205
+ }
206
+
207
+ .sidebar-footer {
208
+ padding: 14px;
209
+ border-top: 1px solid var(--border);
210
+ font-size: 14px;
211
+ color: var(--text3);
212
+ font-family: 'DM Mono', monospace;
213
+ text-align: center;
214
+ white-space: nowrap;
215
+ overflow: hidden;
216
+ }
217
+
218
+ .layout.collapsed .sidebar-footer {
219
+ opacity: 0;
220
+ }
221
+
222
+ /* ─── Topbar ─── */
223
+ .topbar {
224
+ grid-area: topbar;
225
+ background: var(--bg2);
226
+ border-bottom: 1px solid var(--border);
227
+ display: flex;
228
+ align-items: center;
229
+ padding: 0 16px;
230
+ padding-left: calc(var(--sidebar-width) + var(--panel-gap));
231
+ gap: 16px;
232
+ overflow: hidden;
233
+ position: relative;
234
+ transition: padding-left 0.25s ease;
235
+ }
236
+
237
+ .layout.collapsed .topbar {
238
+ padding-left: calc(var(--sidebar-collapsed) + var(--panel-gap));
239
+ }
240
+
241
+ .live-badge {
242
+ font-family: 'DM Mono', monospace;
243
+ font-size: 14px;
244
+ background: var(--green3);
245
+ color: var(--green);
246
+ border: 0.5px solid var(--green2);
247
+ padding: 6px 12px;
248
+ border-radius: 4px;
249
+ display: flex;
250
+ align-items: center;
251
+ gap: 4px;
252
+ flex-shrink: 0;
253
+ }
254
+
255
+ .live-dot {
256
+ width: 8px;
257
+ height: 8px;
258
+ border-radius: 50%;
259
+ background: var(--green);
260
+ animation: pulse 1.5s ease-in-out infinite;
261
+ }
262
+
263
+ .topbar-stats {
264
+ display: flex;
265
+ align-items: center;
266
+ gap: 20px;
267
+ flex: 1;
268
+ overflow: hidden;
269
+ }
270
+
271
+ .stat {
272
+ display: flex;
273
+ align-items: center;
274
+ gap: 14px;
275
+ flex-shrink: 0;
276
+ }
277
+
278
+ .stat-label {
279
+ font-size: 14px;
280
+ color: var(--text3);
281
+ font-family: 'DM Mono', monospace;
282
+ text-transform: uppercase;
283
+ letter-spacing: 0.06em;
284
+ }
285
+
286
+ .stat-val {
287
+ font-size: 16px;
288
+ font-weight: 600;
289
+ color: var(--text);
290
+ font-family: 'DM Mono', monospace;
291
+ }
292
+
293
+ .stat-delta {
294
+ font-size: 14px;
295
+ font-family: 'DM Mono', monospace;
296
+ }
297
+
298
+ .stat-delta.up { color: var(--green); }
299
+ .stat-delta.dn { color: var(--red); }
300
+ .stat-delta.neutral { color: var(--text3); }
301
+
302
+ .topbar-actions {
303
+ margin-left: auto;
304
+ display: flex;
305
+ align-items: center;
306
+ gap: 16px;
307
+ flex-shrink: 0;
308
+ }
309
+
310
+ .btn-ghost {
311
+ font-size: 15px;
312
+ font-weight: 500;
313
+ color: var(--blue);
314
+ border: 0.5px solid var(--blue2);
315
+ background: var(--blue3);
316
+ padding: 8px 16px;
317
+ border-radius: var(--radius-sm);
318
+ cursor: pointer;
319
+ font-family: 'Syne', sans-serif;
320
+ transition: opacity 0.15s;
321
+ }
322
+
323
+ .btn-ghost:hover {
324
+ opacity: 0.85;
325
+ }
326
+
327
+ .icon-btn {
328
+ width: 32px;
329
+ height: 32px;
330
+ border-radius: 50%;
331
+ background: var(--bg3);
332
+ border: 0.5px solid var(--border2);
333
+ color: var(--text2);
334
+ display: flex;
335
+ align-items: center;
336
+ justify-content: center;
337
+ cursor: pointer;
338
+ font-size: 16px;
339
+ transition: color 0.15s, border-color 0.15s;
340
+ }
341
+
342
+ .icon-btn:hover {
343
+ color: var(--text);
344
+ border-color: var(--border2);
345
+ }
346
+
347
+ /* ─── Main content ─── */
348
+ .main {
349
+ grid-area: main;
350
+ overflow: auto;
351
+ padding: var(--panel-gap);
352
+ background: var(--bg);
353
+ }
354
+
355
+ /* Vistas */
356
+ .view { display: none; }
357
+ .view.active { display: block; height: 100%; }
358
+
359
+ /* ─── Dashboard layout ─── */
360
+ .dashboard-grid {
361
+ display: grid;
362
+ grid-template-columns: 1fr 280px;
363
+ grid-template-rows: 1fr minmax(280px, 40%);
364
+ gap: var(--panel-gap);
365
+ height: 100%;
366
+ min-height: 0;
367
+ }
368
+
369
+ .panel {
370
+ background: var(--bg2);
371
+ border: 0.5px solid var(--border);
372
+ border-radius: var(--radius);
373
+ display: flex;
374
+ flex-direction: column;
375
+ overflow: hidden;
376
+ min-height: 0;
377
+ }
378
+
379
+ .panel-header {
380
+ display: flex;
381
+ align-items: center;
382
+ justify-content: space-between;
383
+ padding: 14px 14px;
384
+ border-bottom: 0.5px solid var(--border);
385
+ cursor: pointer;
386
+ user-select: none;
387
+ transition: background 0.15s;
388
+ }
389
+
390
+ .panel-header:hover {
391
+ background: rgba(255,255,255,0.02);
392
+ }
393
+
394
+ .panel-title {
395
+ font-size: 14px;
396
+ color: var(--text3);
397
+ font-family: 'DM Mono', monospace;
398
+ text-transform: uppercase;
399
+ letter-spacing: 0.08em;
400
+ display: flex;
401
+ align-items: center;
402
+ gap: 14px;
403
+ }
404
+
405
+ .panel-toggle {
406
+ font-size: 14px;
407
+ color: var(--text3);
408
+ transition: transform 0.2s;
409
+ }
410
+
411
+ .panel.collapsed .panel-toggle {
412
+ transform: rotate(-90deg);
413
+ }
414
+
415
+ .panel-body {
416
+ flex: 1;
417
+ overflow: auto;
418
+ padding: 12px;
419
+ min-height: 0;
420
+ }
421
+
422
+ .panel.collapsed .panel-body {
423
+ display: none;
424
+ }
425
+
426
+ .panel-subtitle {
427
+ font-size: 14px;
428
+ color: var(--text3);
429
+ }
430
+
431
+ .positions-separator {
432
+ margin-top: 10px;
433
+ padding-top: 10px;
434
+ border-top: 0.5px solid var(--border);
435
+ }
436
+
437
+ .panel-title.mb-sm {
438
+ margin-bottom: 8px;
439
+ }
440
+
441
+ .panel.full-height {
442
+ height: 100%;
443
+ }
444
+
445
+ .hidden {
446
+ display: none;
447
+ }
448
+
449
+ /* Panel mapa */
450
+ .map-panel {
451
+ grid-row: 1 / 2;
452
+ grid-column: 1 / 2;
453
+ }
454
+
455
+ #map-container {
456
+ width: 100%;
457
+ height: 100%;
458
+ min-height: 300px;
459
+ background: var(--bg3);
460
+ border-radius: var(--radius-sm);
461
+ overflow: hidden;
462
+ }
463
+
464
+ /* Panel señales */
465
+ .signals-panel {
466
+ grid-row: 1 / 3;
467
+ grid-column: 2 / 3;
468
+ }
469
+
470
+ .signals-list {
471
+ display: flex;
472
+ flex-direction: column;
473
+ gap: 16px;
474
+ }
475
+
476
+ .market-card {
477
+ background: var(--bg3);
478
+ border: 0.5px solid var(--border);
479
+ border-radius: var(--radius-sm);
480
+ padding: 14px;
481
+ cursor: pointer;
482
+ transition: border-color 0.15s, background 0.15s;
483
+ }
484
+
485
+ .market-card:hover {
486
+ border-color: var(--border2);
487
+ }
488
+
489
+ .market-card.active {
490
+ border-color: var(--blue2);
491
+ background: var(--blue3);
492
+ }
493
+
494
+ .market-cat {
495
+ font-size: 14px;
496
+ font-family: 'DM Mono', monospace;
497
+ color: var(--text3);
498
+ text-transform: uppercase;
499
+ letter-spacing: 0.06em;
500
+ margin-bottom: 4px;
501
+ }
502
+
503
+ .market-q {
504
+ font-size: 15px;
505
+ color: var(--text);
506
+ line-height: 1.4;
507
+ font-weight: 500;
508
+ margin-bottom: 8px;
509
+ }
510
+
511
+ .market-footer {
512
+ display: flex;
513
+ align-items: center;
514
+ justify-content: space-between;
515
+ gap: 16px;
516
+ }
517
+
518
+ .prob-bar-wrap {
519
+ flex: 1;
520
+ min-width: 0;
521
+ }
522
+
523
+ .prob-bar-bg {
524
+ height: 5px;
525
+ background: var(--bg4);
526
+ border-radius: 2px;
527
+ overflow: hidden;
528
+ }
529
+
530
+ .prob-bar-fill {
531
+ height: 100%;
532
+ border-radius: 2px;
533
+ width: var(--prob-width, 0%);
534
+ transition: width 0.8s ease;
535
+ }
536
+
537
+ .prob-val {
538
+ font-size: 15px;
539
+ font-weight: 600;
540
+ font-family: 'DM Mono', monospace;
541
+ flex-shrink: 0;
542
+ }
543
+
544
+ .signal-badge {
545
+ font-size: 14px;
546
+ font-weight: 600;
547
+ padding: 2px 6px;
548
+ border-radius: 4px;
549
+ font-family: 'DM Mono', monospace;
550
+ letter-spacing: 0.04em;
551
+ flex-shrink: 0;
552
+ }
553
+
554
+ .sig-bull { background: var(--green3); color: var(--green); border: 0.5px solid var(--green2); }
555
+ .sig-bear { background: var(--red3); color: var(--red); border: 0.5px solid var(--red2); }
556
+ .sig-neut { background: var(--bg4); color: var(--text2); border: 0.5px solid var(--border2); }
557
+
558
+ /* Panel detalle */
559
+ .detail-panel {
560
+ grid-row: 2 / 3;
561
+ grid-column: 1 / 2;
562
+ max-height: 100%;
563
+ overflow: hidden;
564
+ }
565
+
566
+ .detail-panel .panel-body {
567
+ overflow-y: auto;
568
+ }
569
+
570
+ .detail-header {
571
+ display: flex;
572
+ align-items: flex-start;
573
+ justify-content: space-between;
574
+ margin-bottom: 12px;
575
+ gap: 16px;
576
+ }
577
+
578
+ .detail-tag {
579
+ font-size: 14px;
580
+ color: var(--blue);
581
+ font-family: 'DM Mono', monospace;
582
+ text-transform: uppercase;
583
+ letter-spacing: 0.08em;
584
+ margin-bottom: 4px;
585
+ }
586
+
587
+ .detail-q {
588
+ font-size: 16px;
589
+ font-weight: 600;
590
+ color: var(--text);
591
+ line-height: 1.5;
592
+ }
593
+
594
+ .detail-meta {
595
+ font-size: 14px;
596
+ color: var(--text3);
597
+ font-family: 'DM Mono', monospace;
598
+ margin-top: 2px;
599
+ }
600
+
601
+ .detail-metrics {
602
+ display: flex;
603
+ gap: 16px;
604
+ align-items: center;
605
+ flex-shrink: 0;
606
+ }
607
+
608
+ .metric {
609
+ text-align: right;
610
+ }
611
+
612
+ .metric-label {
613
+ font-size: 14px;
614
+ color: var(--text3);
615
+ font-family: 'DM Mono', monospace;
616
+ margin-bottom: 2px;
617
+ }
618
+
619
+ .metric-value {
620
+ font-size: 14px;
621
+ font-weight: 700;
622
+ font-family: 'DM Mono', monospace;
623
+ }
624
+
625
+ .metric-sep {
626
+ width: 1px;
627
+ height: 40px;
628
+ background: var(--border);
629
+ }
630
+
631
+ .outcomes-row {
632
+ display: flex;
633
+ gap: 16px;
634
+ margin-bottom: 12px;
635
+ }
636
+
637
+ .outcome-card {
638
+ flex: 1;
639
+ background: var(--bg3);
640
+ border: 0.5px solid var(--border);
641
+ border-radius: var(--radius-sm);
642
+ padding: 14px;
643
+ text-align: center;
644
+ }
645
+
646
+ .outcome-name {
647
+ font-size: 14px;
648
+ color: var(--text2);
649
+ margin-bottom: 4px;
650
+ font-family: 'DM Mono', monospace;
651
+ }
652
+
653
+ .outcome-price {
654
+ font-size: 20px;
655
+ font-weight: 700;
656
+ color: var(--text);
657
+ font-family: 'DM Mono', monospace;
658
+ }
659
+
660
+ .outcome-delta {
661
+ font-size: 14px;
662
+ font-family: 'DM Mono', monospace;
663
+ margin-top: 2px;
664
+ }
665
+
666
+ .outcome-card.yes .outcome-price { color: var(--green); }
667
+ .outcome-card.no .outcome-price { color: var(--red); }
668
+
669
+ .chart-container {
670
+ flex: 2;
671
+ background: var(--bg3);
672
+ border: 0.5px solid var(--border);
673
+ border-radius: var(--radius-sm);
674
+ padding: 14px;
675
+ min-height: 120px;
676
+ height: 160px;
677
+ max-height: 160px;
678
+ overflow: hidden;
679
+ }
680
+
681
+ .chart-container canvas {
682
+ max-height: 130px;
683
+ }
684
+
685
+ .chart-label {
686
+ font-size: 14px;
687
+ color: var(--text3);
688
+ font-family: 'DM Mono', monospace;
689
+ margin-bottom: 6px;
690
+ }
691
+
692
+ .ai-box {
693
+ background: var(--bg3);
694
+ border: 0.5px solid var(--blue2);
695
+ border-radius: var(--radius-sm);
696
+ padding: 14px;
697
+ display: flex;
698
+ gap: 14px;
699
+ align-items: flex-start;
700
+ margin-bottom: 10px;
701
+ }
702
+
703
+ .ai-icon {
704
+ width: 32px;
705
+ height: 32px;
706
+ border-radius: 6px;
707
+ background: var(--blue3);
708
+ display: flex;
709
+ align-items: center;
710
+ justify-content: center;
711
+ flex-shrink: 0;
712
+ font-size: 14px;
713
+ }
714
+
715
+ .ai-label {
716
+ font-size: 14px;
717
+ color: var(--blue);
718
+ font-family: 'DM Mono', monospace;
719
+ text-transform: uppercase;
720
+ letter-spacing: 0.06em;
721
+ margin-bottom: 3px;
722
+ }
723
+
724
+ .ai-text {
725
+ font-size: 15px;
726
+ color: var(--text2);
727
+ line-height: 1.5;
728
+ }
729
+
730
+ .sim-row {
731
+ display: flex;
732
+ gap: 16px;
733
+ align-items: center;
734
+ flex-wrap: wrap;
735
+ }
736
+
737
+ .sim-label {
738
+ font-size: 15px;
739
+ color: var(--text3);
740
+ font-family: 'DM Mono', monospace;
741
+ }
742
+
743
+ .sim-input {
744
+ background: var(--bg3);
745
+ border: 0.5px solid var(--border2);
746
+ border-radius: var(--radius-sm);
747
+ padding: 8px 14px;
748
+ color: var(--text);
749
+ font-size: 16px;
750
+ font-family: 'DM Mono', monospace;
751
+ width: 100px;
752
+ outline: none;
753
+ }
754
+
755
+ .sim-input:focus {
756
+ border-color: var(--blue2);
757
+ }
758
+
759
+ .sim-btn-yes {
760
+ background: var(--green3);
761
+ border: 0.5px solid var(--green2);
762
+ color: var(--green);
763
+ font-size: 15px;
764
+ font-weight: 600;
765
+ padding: 8px 16px;
766
+ border-radius: var(--radius-sm);
767
+ cursor: pointer;
768
+ font-family: 'DM Mono', monospace;
769
+ transition: opacity 0.15s;
770
+ }
771
+
772
+ .sim-btn-no {
773
+ background: var(--red3);
774
+ border: 0.5px solid var(--red2);
775
+ color: var(--red);
776
+ font-size: 15px;
777
+ font-weight: 600;
778
+ padding: 8px 16px;
779
+ border-radius: var(--radius-sm);
780
+ cursor: pointer;
781
+ font-family: 'DM Mono', monospace;
782
+ transition: opacity 0.15s;
783
+ }
784
+
785
+ .sim-btn-yes:hover, .sim-btn-no:hover {
786
+ opacity: 0.85;
787
+ }
788
+
789
+ .sim-disclaimer {
790
+ font-size: 14px;
791
+ color: var(--text3);
792
+ font-family: 'DM Mono', monospace;
793
+ }
794
+
795
+ /* ─── Legend inline ─── */
796
+ .legend {
797
+ display: flex;
798
+ gap: 16px;
799
+ align-items: center;
800
+ }
801
+
802
+ .legend-item {
803
+ display: flex;
804
+ align-items: center;
805
+ gap: 5px;
806
+ font-size: 14px;
807
+ color: var(--text3);
808
+ font-family: 'DM Mono', monospace;
809
+ }
810
+
811
+ .legend-dot {
812
+ width: 10px;
813
+ height: 10px;
814
+ border-radius: 50%;
815
+ }
816
+
817
+ .legend-dot.green { background: var(--green); }
818
+ .legend-dot.red { background: var(--red); }
819
+ .legend-dot.gray { background: var(--text3); }
820
+
821
+ .legend.end { margin-left: auto; }
822
+
823
+ /* ─── Positions / Watchlist views ─── */
824
+ .table-wrap {
825
+ overflow: auto;
826
+ border: 0.5px solid var(--border);
827
+ border-radius: var(--radius-sm);
828
+ background: var(--bg3);
829
+ }
830
+
831
+ table {
832
+ width: 100%;
833
+ border-collapse: collapse;
834
+ font-size: 16px;
835
+ }
836
+
837
+ th, td {
838
+ padding: 14px 14px;
839
+ text-align: left;
840
+ border-bottom: 0.5px solid var(--border);
841
+ }
842
+
843
+ th {
844
+ font-family: 'DM Mono', monospace;
845
+ font-size: 14px;
846
+ color: var(--text3);
847
+ text-transform: uppercase;
848
+ letter-spacing: 0.06em;
849
+ font-weight: 500;
850
+ background: var(--bg4);
851
+ position: sticky;
852
+ top: 0;
853
+ }
854
+
855
+ tr:hover td {
856
+ background: rgba(255,255,255,0.02);
857
+ }
858
+
859
+ td {
860
+ color: var(--text);
861
+ }
862
+
863
+ .td-mono {
864
+ font-family: 'DM Mono', monospace;
865
+ }
866
+
867
+ .td-green { color: var(--green); }
868
+ .td-red { color: var(--red); }
869
+ .td-blue { color: var(--blue); }
870
+
871
+ .empty-state {
872
+ padding: 40px;
873
+ text-align: center;
874
+ color: var(--text3);
875
+ font-size: 16px;
876
+ }
877
+
878
+ /* ─── Leaflet overrides ─── */
879
+ #app .leaflet-container {
880
+ background: var(--bg3);
881
+ font-family: 'DM Mono', monospace;
882
+ }
883
+
884
+ #app .leaflet-popup-content-wrapper {
885
+ background: var(--bg2);
886
+ color: var(--text);
887
+ border: 0.5px solid var(--border);
888
+ border-radius: var(--radius-sm);
889
+ }
890
+
891
+ #app .leaflet-popup-tip {
892
+ background: var(--bg2);
893
+ }
894
+
895
+ /* ─── Sparklines ─── */
896
+ .sparkline {
897
+ display: flex;
898
+ align-items: flex-end;
899
+ gap: 2px;
900
+ height: 32px;
901
+ margin-top: 6px;
902
+ }
903
+
904
+ .spark-bar {
905
+ width: 3px;
906
+ border-radius: 1px;
907
+ background: var(--blue2);
908
+ transition: height 0.3s;
909
+ }
910
+
911
+ /* ─── Animations ─── */
912
+ @keyframes pulse {
913
+ 0%, 100% { opacity: 1; }
914
+ 50% { opacity: 0.4; }
915
+ }
916
+
917
+ /* ─── Scrollbar ─── */
918
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
919
+ ::-webkit-scrollbar-track { background: transparent; }
920
+ ::-webkit-scrollbar-thumb { background: var(--bg4); border-radius: 3px; }
921
+ ::-webkit-scrollbar-thumb:hover { background: var(--text3); }
922
+
923
+ /* ─── Utilities ─── */
924
+ .flex-between {
925
+ display: flex;
926
+ justify-content: space-between;
927
+ align-items: center;
928
+ }
929
+
930
+ .flex-start {
931
+ display: flex;
932
+ align-items: flex-start;
933
+ justify-content: space-between;
934
+ }
935
+
936
+ .flex-row {
937
+ display: flex;
938
+ align-items: center;
939
+ }
940
+
941
+ .flex-wrap {
942
+ flex-wrap: wrap;
943
+ }
944
+
945
+ .gap-6 {
946
+ gap: 14px;
947
+ }
948
+
949
+ .gap-8 {
950
+ gap: 16px;
951
+ }
952
+
953
+ .text-green { color: var(--green); }
954
+ .text-red { color: var(--red); }
955
+ .text-blue { color: var(--blue); }
956
+ .text-amber { color: var(--amber); }
957
+ .text-neutral { color: var(--text3); }
958
+
959
+ .bg-green { background: var(--green); }
960
+ .bg-red { background: var(--red); }
961
+ .bg-amber { background: var(--amber); }
962
+
963
+ .flex-1 { flex: 1; }
964
+
965
+ .font-mono { font-family: 'DM Mono', monospace; }
966
+
967
+ .text-xs { font-size: 14px; }
968
+ .text-sm { font-size: 14px; }
969
+ .text-base { font-size: 15px; }
970
+ .text-lg { font-size: 16px; }
971
+ .text-xl { font-size: 18px; }
972
+
973
+ .font-semibold { font-weight: 600; }
974
+ .font-bold { font-weight: 700; }
975
+
976
+ .mb-4 { margin-bottom: 4px; }
977
+ .mb-6 { margin-bottom: 6px; }
978
+ .mb-8 { margin-bottom: 8px; }
979
+ .mt-4 { margin-top: 4px; }
980
+ .mt-6 { margin-top: 6px; }
981
+ .ml-auto { margin-left: auto; }
982
+
983
+ .divider {
984
+ height: 1px;
985
+ background: var(--border);
986
+ margin: 8px 0;
987
+ }
988
+
989
+ .empty-state-sm {
990
+ padding: 16px;
991
+ text-align: center;
992
+ color: var(--text3);
993
+ font-size: 16px;
994
+ }
995
+
996
+ /* Map popup */
997
+ .map-popup {
998
+ font-family: 'Syne', sans-serif;
999
+ font-size: 15px;
1000
+ color: var(--text);
1001
+ max-width: 200px;
1002
+ }
1003
+
1004
+ .map-popup-cat {
1005
+ font-size: 14px;
1006
+ color: var(--text3);
1007
+ font-family: 'DM Mono', monospace;
1008
+ margin-bottom: 4px;
1009
+ text-transform: uppercase;
1010
+ }
1011
+
1012
+ .map-popup-q {
1013
+ font-weight: 600;
1014
+ margin-bottom: 4px;
1015
+ line-height: 1.3;
1016
+ }
1017
+
1018
+ .map-popup-prices {
1019
+ display: flex;
1020
+ gap: 16px;
1021
+ font-family: 'DM Mono', monospace;
1022
+ font-size: 15px;
1023
+ }
1024
+
1025
+ .map-label-text {
1026
+ color: var(--text2);
1027
+ font-family: 'DM Mono', monospace;
1028
+ font-size: 14px;
1029
+ text-shadow: 0 1px 2px #000;
1030
+ }
1031
+
1032
+ /* ─── Responsive ─── */
1033
+ @media (max-width: 1024px) {
1034
+ .dashboard-grid {
1035
+ grid-template-columns: 1fr;
1036
+ grid-template-rows: auto auto auto;
1037
+ }
1038
+ .signals-panel {
1039
+ grid-row: auto;
1040
+ grid-column: auto;
1041
+ max-height: 400px;
1042
+ }
1043
+ .map-panel {
1044
+ grid-row: auto;
1045
+ grid-column: auto;
1046
+ min-height: 300px;
1047
+ }
1048
+ .detail-panel {
1049
+ grid-row: auto;
1050
+ grid-column: auto;
1051
+ }
1052
+ }
1053
+
1054
+ @media (max-width: 640px) {
1055
+ .layout {
1056
+ grid-template-columns: 0 1fr;
1057
+ }
1058
+ .sidebar { display: none; }
1059
+ .topbar-stats { display: none; }
1060
+ .outcomes-row { flex-direction: column; }
1061
+ }
package.json CHANGED
@@ -19,7 +19,7 @@
19
  "db:studio": "npm run db:studio --workspace=backend"
20
  },
21
  "engines": {
22
- "node": ">=22.0.0"
23
  },
24
  "keywords": [
25
  "polymarket",
 
19
  "db:studio": "npm run db:studio --workspace=backend"
20
  },
21
  "engines": {
22
+ "node": ">=26.0.0"
23
  },
24
  "keywords": [
25
  "polymarket",
polysignal_app_mockup.html ADDED
@@ -0,0 +1,450 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ <h2 class="sr-only">Mockup interactivo de PolySignal — dashboard con mapa global de predicciones y panel de señales IA</h2>
3
+ <style>
4
+ @import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Syne:wght@500;600;700&display=swap');
5
+ *{box-sizing:border-box;margin:0;padding:0}
6
+ :root{
7
+ --bg:#0a0c10;--bg2:#111318;--bg3:#181c24;--bg4:#1e232d;
8
+ --border:#ffffff14;--border2:#ffffff22;
9
+ --text:#e8eaf0;--text2:#8b90a0;--text3:#555b6e;
10
+ --green:#22d37a;--green2:#0d6e3a;--green3:#052a17;
11
+ --red:#f04040;--red2:#7a1a1a;--red3:#2d0808;
12
+ --blue:#4a9eff;--blue2:#1a4a80;--blue3:#081830;
13
+ --amber:#f0a020;--amber2:#7a4e08;--amber3:#2d1c02;
14
+ --accent:#4a9eff;
15
+ }
16
+ .app{background:var(--bg);border-radius:16px;border:0.5px solid var(--border2);overflow:hidden;font-family:'Syne',sans-serif}
17
+ .topbar{background:var(--bg2);border-bottom:0.5px solid var(--border);padding:10px 16px;display:flex;align-items:center;gap:12px}
18
+ .logo{font-size:15px;font-weight:700;color:var(--text);letter-spacing:-0.3px;display:flex;align-items:center;gap:6px}
19
+ .logo-dot{width:8px;height:8px;border-radius:50%;background:var(--blue);animation:pulse 2s ease-in-out infinite}
20
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}
21
+ .live-badge{font-family:'DM Mono',monospace;font-size:10px;background:var(--green3);color:var(--green);border:0.5px solid var(--green2);padding:3px 8px;border-radius:4px;display:flex;align-items:center;gap:4px}
22
+ .live-dot{width:5px;height:5px;border-radius:50%;background:var(--green);animation:pulse 1.5s ease-in-out infinite}
23
+ .topbar-right{margin-left:auto;display:flex;align-items:center;gap:8px}
24
+ .tg-btn{font-size:11px;font-weight:500;color:var(--blue);border:0.5px solid var(--blue2);background:var(--blue3);padding:4px 10px;border-radius:6px;cursor:pointer;font-family:'Syne',sans-serif}
25
+ .stat-row{background:var(--bg2);border-bottom:0.5px solid var(--border);padding:8px 16px;display:flex;gap:24px}
26
+ .stat{display:flex;align-items:center;gap:6px}
27
+ .stat-label{font-size:10px;color:var(--text3);font-family:'DM Mono',monospace;text-transform:uppercase;letter-spacing:0.06em}
28
+ .stat-val{font-size:13px;font-weight:600;color:var(--text);font-family:'DM Mono',monospace}
29
+ .stat-delta{font-size:10px;font-family:'DM Mono',monospace}
30
+ .stat-delta.up{color:var(--green)}
31
+ .stat-delta.dn{color:var(--red)}
32
+ .main{display:grid;grid-template-columns:1fr 220px}
33
+ .map-area{padding:16px;border-right:0.5px solid var(--border)}
34
+ .map-title{font-size:11px;color:var(--text3);font-family:'DM Mono',monospace;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:10px;display:flex;align-items:center;gap:8px}
35
+ .map-wrap{background:var(--bg3);border-radius:10px;border:0.5px solid var(--border);overflow:hidden;position:relative}
36
+ .map-svg{width:100%;display:block}
37
+ .signal-panel{padding:12px}
38
+ .panel-title{font-size:10px;color:var(--text3);font-family:'DM Mono',monospace;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:10px}
39
+ .market-card{background:var(--bg3);border:0.5px solid var(--border);border-radius:8px;padding:10px;margin-bottom:8px;cursor:pointer;transition:border-color 0.15s}
40
+ .market-card:hover{border-color:var(--border2)}
41
+ .market-card.active{border-color:var(--blue2);background:var(--blue3)}
42
+ .market-cat{font-size:9px;font-family:'DM Mono',monospace;color:var(--text3);text-transform:uppercase;letter-spacing:0.06em;margin-bottom:4px}
43
+ .market-q{font-size:11px;color:var(--text);line-height:1.4;font-weight:500;margin-bottom:8px}
44
+ .market-footer{display:flex;align-items:center;justify-content:space-between}
45
+ .prob-bar-wrap{flex:1;margin-right:8px}
46
+ .prob-bar-bg{height:3px;background:var(--bg4);border-radius:2px;overflow:hidden}
47
+ .prob-bar-fill{height:100%;border-radius:2px;transition:width 0.8s ease}
48
+ .prob-val{font-size:11px;font-weight:600;font-family:'DM Mono',monospace}
49
+ .signal-badge{font-size:9px;font-weight:600;padding:2px 6px;border-radius:4px;font-family:'DM Mono',monospace;letter-spacing:0.04em}
50
+ .sig-bull{background:var(--green3);color:var(--green);border:0.5px solid var(--green2)}
51
+ .sig-bear{background:var(--red3);color:var(--red);border:0.5px solid var(--red2)}
52
+ .sig-neut{background:var(--bg4);color:var(--text2);border:0.5px solid var(--border2)}
53
+ .detail-section{background:var(--bg2);border-top:0.5px solid var(--border);padding:14px 16px}
54
+ .detail-header{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:12px}
55
+ .detail-q{font-size:13px;font-weight:600;color:var(--text);line-height:1.5;max-width:380px}
56
+ .detail-meta{font-size:10px;color:var(--text3);font-family:'DM Mono',monospace;margin-top:2px}
57
+ .outcomes-row{display:flex;gap:8px;margin-bottom:12px}
58
+ .outcome-card{flex:1;background:var(--bg3);border:0.5px solid var(--border);border-radius:8px;padding:10px;text-align:center}
59
+ .outcome-name{font-size:10px;color:var(--text2);margin-bottom:4px;font-family:'DM Mono',monospace}
60
+ .outcome-price{font-size:20px;font-weight:700;color:var(--text);font-family:'DM Mono',monospace}
61
+ .outcome-delta{font-size:10px;font-family:'DM Mono',monospace;margin-top:2px}
62
+ .outcome-card.yes .outcome-price{color:var(--green)}
63
+ .outcome-card.no .outcome-price{color:var(--red)}
64
+ .ai-box{background:var(--bg3);border:0.5px solid var(--blue2);border-radius:8px;padding:10px;display:flex;gap:10px;align-items:flex-start}
65
+ .ai-icon{width:28px;height:28px;border-radius:6px;background:var(--blue3);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:14px}
66
+ .ai-label{font-size:9px;color:var(--blue);font-family:'DM Mono',monospace;text-transform:uppercase;letter-spacing:0.06em;margin-bottom:3px}
67
+ .ai-text{font-size:11px;color:var(--text2);line-height:1.5}
68
+ .sim-row{display:flex;gap:8px;margin-top:10px;align-items:center}
69
+ .sim-input{background:var(--bg3);border:0.5px solid var(--border2);border-radius:6px;padding:6px 10px;color:var(--text);font-size:12px;font-family:'DM Mono',monospace;width:80px;outline:none}
70
+ .sim-btn-yes{background:var(--green3);border:0.5px solid var(--green2);color:var(--green);font-size:11px;font-weight:600;padding:6px 14px;border-radius:6px;cursor:pointer;font-family:'DM Mono',monospace}
71
+ .sim-btn-no{background:var(--red3);border:0.5px solid var(--red2);color:var(--red);font-size:11px;font-weight:600;padding:6px 14px;border-radius:6px;cursor:pointer;font-family:'DM Mono',monospace}
72
+ .sim-label{font-size:11px;color:var(--text3);font-family:'DM Mono',monospace}
73
+ .legend{display:flex;gap:12px;align-items:center;margin-top:6px}
74
+ .legend-item{display:flex;align-items:center;gap:5px;font-size:10px;color:var(--text3);font-family:'DM Mono',monospace}
75
+ .legend-dot{width:8px;height:8px;border-radius:50%}
76
+ @keyframes float{0%,100%{transform:translateY(0) scale(1)}50%{transform:translateY(-3px) scale(1.05)}}
77
+ .bubble{cursor:pointer;transition:opacity 0.2s}
78
+ .bubble:hover{opacity:0.85}
79
+ .bubble.active-b circle{stroke-width:2;stroke:#4a9eff}
80
+ @keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
81
+ .sparkline{display:flex;align-items:flex-end;gap:1.5px;height:28px;margin-top:4px}
82
+ .spark-bar{width:3px;border-radius:1px;background:var(--blue2);transition:height 0.3s}
83
+ </style>
84
+
85
+ <div class="app">
86
+
87
+ <div class="topbar">
88
+ <div class="logo">
89
+ <div class="logo-dot"></div>
90
+ PolySignal
91
+ </div>
92
+ <div class="live-badge">
93
+ <div class="live-dot"></div>
94
+ LIVE
95
+ </div>
96
+ <div style="font-size:10px;color:var(--text3);font-family:'DM Mono',monospace">Polymarket · Finnhub · HuggingFace</div>
97
+ <div class="topbar-right">
98
+ <div class="tg-btn"><i class="ti ti-send" style="font-size:11px;margin-right:3px" aria-hidden="true"></i>Telegram alerts</div>
99
+ <div style="width:28px;height:28px;border-radius:50%;background:var(--bg3);border:0.5px solid var(--border2);display:flex;align-items:center;justify-content:center">
100
+ <i class="ti ti-bell" style="font-size:13px;color:var(--text2)" aria-hidden="true"></i>
101
+ </div>
102
+ </div>
103
+ </div>
104
+
105
+ <div class="stat-row">
106
+ <div class="stat">
107
+ <span class="stat-label">Markets</span>
108
+ <span class="stat-val" id="mkt-count">2,847</span>
109
+ </div>
110
+ <div class="stat">
111
+ <span class="stat-label">Volume 24h</span>
112
+ <span class="stat-val">$4.2M</span>
113
+ <span class="stat-delta up">+12.4%</span>
114
+ </div>
115
+ <div class="stat">
116
+ <span class="stat-label">AI signals</span>
117
+ <span class="stat-val" id="sig-count">183</span>
118
+ <span class="stat-delta up">bullish</span>
119
+ </div>
120
+ <div class="stat">
121
+ <span class="stat-label">Alerts sent</span>
122
+ <span class="stat-val">47</span>
123
+ <span class="stat-delta" style="color:var(--text3)">today</span>
124
+ </div>
125
+ <div style="margin-left:auto;display:flex;align-items:center;gap:6px">
126
+ <div class="legend">
127
+ <div class="legend-item"><div class="legend-dot" style="background:var(--green)"></div>bullish</div>
128
+ <div class="legend-item"><div class="legend-dot" style="background:var(--red)"></div>bearish</div>
129
+ <div class="legend-item"><div class="legend-dot" style="background:var(--text3)"></div>neutral</div>
130
+ </div>
131
+ </div>
132
+ </div>
133
+
134
+ <div class="main">
135
+ <div class="map-area">
136
+ <div class="map-title">
137
+ <i class="ti ti-world" style="font-size:13px" aria-hidden="true"></i>
138
+ Global prediction map
139
+ <span style="font-size:9px;color:var(--text3)">· bubble size = volume · color = AI signal</span>
140
+ </div>
141
+ <div class="map-wrap">
142
+ <svg class="map-svg" viewBox="0 0 440 240" id="world-map">
143
+ <rect width="440" height="240" fill="#0e1218"/>
144
+ <g fill="#1e2530" stroke="#2a3040" stroke-width="0.4">
145
+ <path d="M35 90 Q55 75 90 80 Q120 85 130 100 Q140 115 125 125 Q110 135 85 130 Q60 125 45 115 Q30 105 35 90Z"/>
146
+ <path d="M88 100 Q115 95 135 105 Q155 115 160 130 Q165 145 145 155 Q125 165 105 158 Q85 150 80 135 Q75 120 88 100Z"/>
147
+ <path d="M148 88 Q175 80 195 92 Q215 104 218 118 Q221 132 205 140 Q189 148 168 142 Q147 136 142 122 Q137 108 148 88Z"/>
148
+ <path d="M195 78 Q225 68 255 76 Q280 84 288 98 Q296 112 285 124 Q274 136 248 138 Q222 140 204 128 Q186 116 185 102 Q184 88 195 78Z"/>
149
+ <path d="M250 80 Q278 70 305 80 Q330 90 335 108 Q340 126 320 136 Q300 146 275 140 Q250 134 242 118 Q234 102 250 80Z"/>
150
+ <path d="M310 72 Q345 62 375 75 Q400 88 405 105 Q410 122 392 132 Q374 142 345 138 Q316 134 305 118 Q294 102 310 72Z"/>
151
+ <path d="M45 145 Q70 138 90 148 Q110 158 112 172 Q114 186 96 192 Q78 198 58 190 Q38 182 36 168 Q34 154 45 145Z"/>
152
+ <path d="M100 140 Q130 132 155 145 Q175 158 176 175 Q177 192 158 198 Q139 204 116 196 Q93 188 90 173 Q87 158 100 140Z"/>
153
+ <path d="M320 135 Q345 128 365 140 Q382 152 380 168 Q378 184 360 190 Q342 196 325 187 Q308 178 308 163 Q308 148 320 135Z"/>
154
+ <path d="M200 150 Q228 142 250 155 Q268 168 266 185 Q264 202 244 208 Q224 214 206 204 Q188 194 187 177 Q186 160 200 150Z"/>
155
+ <path d="M380 65 Q400 60 415 68 Q428 76 425 88 Q422 100 408 104 Q394 108 382 100 Q370 92 372 80 Q374 68 380 65Z"/>
156
+ </g>
157
+ <g id="bubbles">
158
+ <g class="bubble active-b" id="b-usa" onclick="selectMarket('usa')">
159
+ <circle cx="95" cy="105" r="14" fill="#22d37a" fill-opacity="0.22" stroke="#22d37a" stroke-width="1" stroke-opacity="0.6"/>
160
+ <circle cx="95" cy="105" r="7" fill="#22d37a" fill-opacity="0.8"/>
161
+ <text x="95" y="128" text-anchor="middle" font-size="8" fill="#8b90a0" font-family="DM Mono,monospace">USA</text>
162
+ </g>
163
+ <g class="bubble" id="b-eur" onclick="selectMarket('eur')">
164
+ <circle cx="215" cy="98" r="11" fill="#f04040" fill-opacity="0.22" stroke="#f04040" stroke-width="1" stroke-opacity="0.6"/>
165
+ <circle cx="215" cy="98" r="5.5" fill="#f04040" fill-opacity="0.8"/>
166
+ <text x="215" y="118" text-anchor="middle" font-size="8" fill="#8b90a0" font-family="DM Mono,monospace">EU</text>
167
+ </g>
168
+ <g class="bubble" id="b-uk" onclick="selectMarket('uk')">
169
+ <circle cx="172" cy="85" r="8" fill="#8b90a0" fill-opacity="0.22" stroke="#8b90a0" stroke-width="1" stroke-opacity="0.5"/>
170
+ <circle cx="172" cy="85" r="4" fill="#8b90a0" fill-opacity="0.7"/>
171
+ <text x="172" y="102" text-anchor="middle" font-size="7" fill="#8b90a0" font-family="DM Mono,monospace">UK</text>
172
+ </g>
173
+ <g class="bubble" id="b-bra" onclick="selectMarket('bra')">
174
+ <circle cx="130" cy="175" r="9" fill="#22d37a" fill-opacity="0.22" stroke="#22d37a" stroke-width="1" stroke-opacity="0.5"/>
175
+ <circle cx="130" cy="175" r="4.5" fill="#22d37a" fill-opacity="0.75"/>
176
+ <text x="130" y="193" text-anchor="middle" font-size="7" fill="#8b90a0" font-family="DM Mono,monospace">BRA</text>
177
+ </g>
178
+ <g class="bubble" id="b-chn" onclick="selectMarket('chn')">
179
+ <circle cx="340" cy="105" r="16" fill="#f0a020" fill-opacity="0.22" stroke="#f0a020" stroke-width="1" stroke-opacity="0.6"/>
180
+ <circle cx="340" cy="105" r="8" fill="#f0a020" fill-opacity="0.8"/>
181
+ <text x="340" y="130" text-anchor="middle" font-size="8" fill="#8b90a0" font-family="DM Mono,monospace">CHN</text>
182
+ </g>
183
+ <g class="bubble" id="b-ind" onclick="selectMarket('ind')">
184
+ <circle cx="295" cy="130" r="10" fill="#22d37a" fill-opacity="0.22" stroke="#22d37a" stroke-width="1" stroke-opacity="0.5"/>
185
+ <circle cx="295" cy="130" r="5" fill="#22d37a" fill-opacity="0.75"/>
186
+ <text x="295" y="149" text-anchor="middle" font-size="7" fill="#8b90a0" font-family="DM Mono,monospace">IND</text>
187
+ </g>
188
+ <g class="bubble" id="b-kor" onclick="selectMarket('kor')">
189
+ <circle cx="378" cy="98" r="7" fill="#f04040" fill-opacity="0.2" stroke="#f04040" stroke-width="1" stroke-opacity="0.5"/>
190
+ <circle cx="378" cy="98" r="3.5" fill="#f04040" fill-opacity="0.75"/>
191
+ <text x="378" y="113" text-anchor="middle" font-size="7" fill="#8b90a0" font-family="DM Mono,monospace">KOR</text>
192
+ </g>
193
+ <g class="bubble" id="b-sau" onclick="selectMarket('sau')">
194
+ <circle cx="248" cy="128" r="8" fill="#8b90a0" fill-opacity="0.2" stroke="#8b90a0" stroke-width="1" stroke-opacity="0.4"/>
195
+ <circle cx="248" cy="128" r="4" fill="#8b90a0" fill-opacity="0.65"/>
196
+ <text x="248" y="144" text-anchor="middle" font-size="7" fill="#8b90a0" font-family="DM Mono,monospace">SAU</text>
197
+ </g>
198
+ </g>
199
+ <g>
200
+ <rect x="8" y="8" width="62" height="14" rx="3" fill="#0a0c10" fill-opacity="0.8"/>
201
+ <text x="12" y="18" font-size="8" fill="#555b6e" font-family="DM Mono,monospace">SELECTED: USA</text>
202
+ </g>
203
+ </svg>
204
+ </div>
205
+ </div>
206
+
207
+ <div class="signal-panel">
208
+ <div class="panel-title">AI signals — top markets</div>
209
+
210
+ <div class="market-card active" onclick="selectMarket('usa')">
211
+ <div class="market-cat">Politics · USA</div>
212
+ <div class="market-q">Will Trump sign the tax bill before June 2026?</div>
213
+ <div class="market-footer">
214
+ <div class="prob-bar-wrap">
215
+ <div class="prob-bar-bg"><div class="prob-bar-fill" style="width:73%;background:var(--green)"></div></div>
216
+ </div>
217
+ <span class="prob-val" style="color:var(--green);margin-right:6px">73¢</span>
218
+ <span class="signal-badge sig-bull">BULL</span>
219
+ </div>
220
+ </div>
221
+
222
+ <div class="market-card" onclick="selectMarket('eur')">
223
+ <div class="market-cat">Economy · EU</div>
224
+ <div class="market-q">ECB rate cut in June 2026?</div>
225
+ <div class="market-footer">
226
+ <div class="prob-bar-wrap">
227
+ <div class="prob-bar-bg"><div class="prob-bar-fill" style="width:34%;background:var(--red)"></div></div>
228
+ </div>
229
+ <span class="prob-val" style="color:var(--red);margin-right:6px">34¢</span>
230
+ <span class="signal-badge sig-bear">BEAR</span>
231
+ </div>
232
+ </div>
233
+
234
+ <div class="market-card" onclick="selectMarket('chn')">
235
+ <div class="market-cat">Trade · CHN</div>
236
+ <div class="market-q">US-China tariff deal before Q3 2026?</div>
237
+ <div class="market-footer">
238
+ <div class="prob-bar-wrap">
239
+ <div class="prob-bar-bg"><div class="prob-bar-fill" style="width:51%;background:var(--amber)"></div></div>
240
+ </div>
241
+ <span class="prob-val" style="color:var(--amber);margin-right:6px">51¢</span>
242
+ <span class="signal-badge sig-neut">NEUT</span>
243
+ </div>
244
+ </div>
245
+
246
+ <div class="market-card" onclick="selectMarket('bra')">
247
+ <div class="market-cat">Crypto · Global</div>
248
+ <div class="market-q">Bitcoin above $120k before July?</div>
249
+ <div class="market-footer">
250
+ <div class="prob-bar-wrap">
251
+ <div class="prob-bar-bg"><div class="prob-bar-fill" style="width:61%;background:var(--green)"></div></div>
252
+ </div>
253
+ <span class="prob-val" style="color:var(--green);margin-right:6px">61¢</span>
254
+ <span class="signal-badge sig-bull">BULL</span>
255
+ </div>
256
+ </div>
257
+
258
+ <div style="margin-top:10px;padding-top:10px;border-top:0.5px solid var(--border)">
259
+ <div class="panel-title">My positions</div>
260
+ <div style="background:var(--bg3);border-radius:8px;border:0.5px solid var(--border);padding:10px">
261
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
262
+ <span style="font-size:10px;color:var(--text2);font-family:'DM Mono',monospace">Trump tax bill YES</span>
263
+ <span style="font-size:11px;font-weight:600;color:var(--green);font-family:'DM Mono',monospace">+$14.20</span>
264
+ </div>
265
+ <div style="display:flex;justify-content:space-between;align-items:center">
266
+ <span style="font-size:10px;color:var(--text2);font-family:'DM Mono',monospace">ECB cut NO</span>
267
+ <span style="font-size:11px;font-weight:600;color:var(--red);font-family:'DM Mono',monospace">-$3.80</span>
268
+ </div>
269
+ <div style="height:1px;background:var(--border);margin:8px 0"></div>
270
+ <div style="display:flex;justify-content:space-between;align-items:center">
271
+ <span style="font-size:10px;color:var(--text3);font-family:'DM Mono',monospace">Net P&L</span>
272
+ <span style="font-size:13px;font-weight:700;color:var(--green);font-family:'DM Mono',monospace">+$10.40</span>
273
+ </div>
274
+ </div>
275
+ </div>
276
+ </div>
277
+ </div>
278
+
279
+ <div class="detail-section">
280
+ <div class="detail-header">
281
+ <div>
282
+ <div style="font-size:9px;color:var(--blue);font-family:'DM Mono',monospace;text-transform:uppercase;letter-spacing:0.08em;margin-bottom:4px">
283
+ <i class="ti ti-map-pin" style="font-size:10px" aria-hidden="true"></i> USA · Politics · Polymarket
284
+ </div>
285
+ <div class="detail-q" id="detail-q">Will Trump sign the tax bill before June 2026?</div>
286
+ <div class="detail-meta" id="detail-meta">Vol: $1.24M · Liq: $340K · Closes: Jun 1 2026</div>
287
+ </div>
288
+ <div style="display:flex;gap:6px;align-items:center">
289
+ <div style="text-align:right">
290
+ <div style="font-size:9px;color:var(--text3);font-family:'DM Mono',monospace;margin-bottom:2px">24h change</div>
291
+ <div style="font-size:14px;font-weight:700;color:var(--green);font-family:'DM Mono',monospace">+4.2%</div>
292
+ </div>
293
+ <div style="width:1px;height:32px;background:var(--border);margin:0 4px"></div>
294
+ <div style="text-align:right">
295
+ <div style="font-size:9px;color:var(--text3);font-family:'DM Mono',monospace;margin-bottom:2px">Confidence</div>
296
+ <div style="font-size:14px;font-weight:700;color:var(--blue);font-family:'DM Mono',monospace">87%</div>
297
+ </div>
298
+ </div>
299
+ </div>
300
+
301
+ <div class="outcomes-row">
302
+ <div class="outcome-card yes">
303
+ <div class="outcome-name">YES</div>
304
+ <div class="outcome-price">73¢</div>
305
+ <div class="outcome-delta up" style="color:var(--green)">▲ 3.1¢</div>
306
+ <div class="sparkline" id="spark-yes"></div>
307
+ </div>
308
+ <div class="outcome-card no">
309
+ <div class="outcome-name">NO</div>
310
+ <div class="outcome-price">27¢</div>
311
+ <div class="outcome-delta" style="color:var(--red)">▼ 3.1¢</div>
312
+ <div class="sparkline" id="spark-no"></div>
313
+ </div>
314
+ <div style="flex:2;background:var(--bg3);border:0.5px solid var(--border);border-radius:8px;padding:10px">
315
+ <div style="font-size:9px;color:var(--text3);font-family:'DM Mono',monospace;margin-bottom:6px">Price history 7d</div>
316
+ <canvas id="mini-chart" height="48"></canvas>
317
+ </div>
318
+ </div>
319
+
320
+ <div class="ai-box">
321
+ <div class="ai-icon">
322
+ <i class="ti ti-brain" style="font-size:15px;color:var(--blue)" aria-hidden="true"></i>
323
+ </div>
324
+ <div style="flex:1">
325
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">
326
+ <div class="ai-label">AI analysis · HuggingFace Mistral-7B</div>
327
+ <span class="signal-badge sig-bull">BULLISH · 87%</span>
328
+ <span style="font-size:9px;color:var(--text3);font-family:'DM Mono',monospace;margin-left:auto">updated 2m ago</span>
329
+ </div>
330
+ <div class="ai-text" id="ai-text">Republican leadership confirmed 3 of 4 committee votes secured. News sentiment from 8 sources is positive (+0.72). Congressional timeline suggests signing window opens May 28-31. Key risk: Senate amendment process could delay beyond June 1.</div>
331
+ </div>
332
+ </div>
333
+
334
+ <div class="sim-row">
335
+ <span class="sim-label">Simulate position →</span>
336
+ <input class="sim-input" type="number" value="100" min="1" placeholder="$USDC"/>
337
+ <button class="sim-btn-yes" onclick="sendPrompt('¿Cuánto ganaría si pongo $100 en YES del mercado Trump tax bill al precio actual de 73¢?')">BUY YES ↗</button>
338
+ <button class="sim-btn-no">BUY NO</button>
339
+ <span style="font-size:10px;color:var(--text3);font-family:'DM Mono',monospace;margin-left:4px">Simulated · no real trading</span>
340
+ </div>
341
+ </div>
342
+
343
+ </div>
344
+
345
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
346
+ <script>
347
+ const markets = {
348
+ usa: {
349
+ q: 'Will Trump sign the tax bill before June 2026?',
350
+ meta: 'Vol: $1.24M · Liq: $340K · Closes: Jun 1 2026',
351
+ cat: 'USA · Politics · Polymarket',
352
+ yes: 73, no: 27, delta: '+4.2%', conf: '87%', signal: 'BULLISH',
353
+ ai: 'Republican leadership confirmed 3 of 4 committee votes secured. News sentiment from 8 sources is positive (+0.72). Congressional timeline suggests signing window opens May 28–31. Key risk: Senate amendment process could delay beyond June 1.'
354
+ },
355
+ eur: {
356
+ q: 'Will the ECB cut rates in June 2026?',
357
+ meta: 'Vol: $890K · Liq: $210K · Closes: Jun 12 2026',
358
+ cat: 'EU · Economy · Polymarket',
359
+ yes: 34, no: 66, delta: '-2.1%', conf: '74%', signal: 'BEARISH',
360
+ ai: 'Eurozone inflation surprised to the upside at 2.4% in April. ECB governing council members\'s recent statements signal a pause is more likely. Bond market pricing implies only 22% probability of a June cut vs Polymarket\'s 34% — divergence is notable.'
361
+ },
362
+ chn: {
363
+ q: 'Will a US-China tariff deal be reached before Q3 2026?',
364
+ meta: 'Vol: $2.1M · Liq: $580K · Closes: Jul 1 2026',
365
+ cat: 'CHN · Trade · Polymarket',
366
+ yes: 51, no: 49, delta: '+0.8%', conf: '52%', signal: 'NEUTRAL',
367
+ ai: 'Negotiations ongoing but no formal framework agreed. Trade flows show early tariff evasion patterns suggesting both sides are playing for time. AI confidence is low — too many geopolitical variables. Monitoring for USTR statement as key signal.'
368
+ },
369
+ bra: {
370
+ q: 'Will Bitcoin exceed $120,000 before July 2026?',
371
+ meta: 'Vol: $3.4M · Liq: $920K · Closes: Jul 1 2026',
372
+ cat: 'Crypto · Global · Polymarket',
373
+ yes: 61, no: 39, delta: '+6.3%', conf: '79%', signal: 'BULLISH',
374
+ ai: 'BTC currently at $103,400. On-chain metrics show accumulation from large wallets. ETF inflows +$820M this week. Options market implies 68% probability of $120K by end of June. Sentiment score: 0.81 (strongly positive across 14 news sources).'
375
+ }
376
+ };
377
+
378
+ let activeMarket = 'usa';
379
+ let chartInst = null;
380
+
381
+ function selectMarket(id) {
382
+ activeMarket = id;
383
+ const m = markets[id];
384
+ document.getElementById('detail-q').textContent = m.q;
385
+ document.getElementById('detail-meta').textContent = m.meta;
386
+ document.getElementById('ai-text').textContent = m.ai;
387
+ document.querySelectorAll('.market-card').forEach((c,i) => {
388
+ const ids = ['usa','eur','chn','bra'];
389
+ c.classList.toggle('active', ids[i] === id);
390
+ });
391
+ document.querySelectorAll('.bubble').forEach(b => b.classList.remove('active-b'));
392
+ const bel = document.getElementById('b-'+id);
393
+ if(bel) bel.classList.add('active-b');
394
+ document.querySelector('#world-map text').textContent = 'SELECTED: '+id.toUpperCase();
395
+ buildChart(m);
396
+ }
397
+
398
+ function buildChart(m) {
399
+ const ctx = document.getElementById('mini-chart').getContext('2d');
400
+ if(chartInst) chartInst.destroy();
401
+ const base = m.yes;
402
+ const pts = Array.from({length:8},(_,i)=>{
403
+ const noise = (Math.random()-0.5)*8;
404
+ return Math.max(5,Math.min(95, base - 12 + (i/7)*12 + noise));
405
+ });
406
+ pts[pts.length-1] = base;
407
+ const col = base > 50 ? '#22d37a' : base < 40 ? '#f04040' : '#f0a020';
408
+ chartInst = new Chart(ctx,{
409
+ type:'line',
410
+ data:{
411
+ labels:['7d','6d','5d','4d','3d','2d','1d','now'],
412
+ datasets:[{data:pts,borderColor:col,borderWidth:1.5,pointRadius:0,fill:false,tension:0.4}]
413
+ },
414
+ options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false},tooltip:{enabled:false}},scales:{x:{display:false},y:{display:false}},animation:{duration:600}}
415
+ });
416
+ buildSparklines(m);
417
+ }
418
+
419
+ function buildSparklines(m) {
420
+ ['yes','no'].forEach(side => {
421
+ const el = document.getElementById('spark-'+side);
422
+ el.innerHTML = '';
423
+ const base = side==='yes' ? m.yes : m.no;
424
+ for(let i=0;i<12;i++){
425
+ const h = Math.max(4, Math.min(24, base/4 + (Math.random()-0.5)*8));
426
+ const d = document.createElement('div');
427
+ d.className='spark-bar';
428
+ d.style.height = h+'px';
429
+ d.style.background = side==='yes'?'#0d6e3a':'#7a1a1a';
430
+ el.appendChild(d);
431
+ }
432
+ const last = document.createElement('div');
433
+ last.className='spark-bar';
434
+ last.style.height = Math.min(28,base/3.5)+'px';
435
+ last.style.background = side==='yes'?'#22d37a':'#f04040';
436
+ el.appendChild(last);
437
+ });
438
+ }
439
+
440
+ let counter = 2847;
441
+ let sigCounter = 183;
442
+ setInterval(()=>{
443
+ counter += Math.floor(Math.random()*3);
444
+ sigCounter += Math.random()>0.7 ? 1 : 0;
445
+ document.getElementById('mkt-count').textContent = counter.toLocaleString();
446
+ document.getElementById('sig-count').textContent = sigCounter;
447
+ }, 3000);
448
+
449
+ buildChart(markets[activeMarket]);
450
+ </script>