nebula-notebook 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +110 -16
  2. package/dist/assets/errorwidget-Wy5ajPiM.js +5 -0
  3. package/dist/assets/fa-brands-400-CEJbCg16.woff +0 -0
  4. package/dist/assets/fa-brands-400-CSYNqBb_.ttf +0 -0
  5. package/dist/assets/fa-brands-400-DnkPfk3o.eot +0 -0
  6. package/dist/assets/fa-brands-400-UxlILjvJ.woff2 +0 -0
  7. package/dist/assets/fa-brands-400-cH1MgKbP.svg +3717 -0
  8. package/dist/assets/fa-regular-400-BhTwtT8w.eot +0 -0
  9. package/dist/assets/fa-regular-400-D1vz6WBx.ttf +0 -0
  10. package/dist/assets/fa-regular-400-DFnMcJPd.woff +0 -0
  11. package/dist/assets/fa-regular-400-DGzu1beS.woff2 +0 -0
  12. package/dist/assets/fa-regular-400-gwj8Pxq-.svg +801 -0
  13. package/dist/assets/fa-solid-900-B4ZZ7kfP.svg +5034 -0
  14. package/dist/assets/fa-solid-900-B6Axprfb.eot +0 -0
  15. package/dist/assets/fa-solid-900-BUswJgRo.woff2 +0 -0
  16. package/dist/assets/fa-solid-900-DOXgCApm.woff +0 -0
  17. package/dist/assets/fa-solid-900-mxuxnBEa.ttf +0 -0
  18. package/dist/assets/index-B_Bs5koo.js +1 -0
  19. package/dist/assets/index-BtWv4MIT.css +7 -0
  20. package/dist/assets/index-DK4nS74r.js +829 -0
  21. package/dist/assets/index-DeQN03nW.js +81 -0
  22. package/dist/assets/index-DzjpmUak.css +32 -0
  23. package/dist/assets/index-g6MjT-VG.js +1 -0
  24. package/dist/assets/services-shim-BirHVsih.js +33 -0
  25. package/dist/assets/viewlist-uomDf7I7.js +1 -0
  26. package/dist/assets/widgets-X7J3NxEn.css +1 -0
  27. package/dist/index.html +2 -2
  28. package/node-server/dist/auth/auth-middleware.js +22 -4
  29. package/node-server/dist/auth/auth-service.js +37 -7
  30. package/node-server/dist/cluster/client-registration.d.ts +12 -1
  31. package/node-server/dist/cluster/client-registration.js +38 -2
  32. package/node-server/dist/cluster/kernel-proxy.js +43 -12
  33. package/node-server/dist/cluster/server-registry.d.ts +17 -0
  34. package/node-server/dist/cluster/server-registry.js +57 -7
  35. package/node-server/dist/discovery/conda-locations.d.ts +73 -0
  36. package/node-server/dist/discovery/conda-locations.js +427 -0
  37. package/node-server/dist/discovery/discovery-service.d.ts +73 -4
  38. package/node-server/dist/discovery/discovery-service.js +211 -127
  39. package/node-server/dist/discovery/types.d.ts +0 -1
  40. package/node-server/dist/discovery/types.js +1 -2
  41. package/node-server/dist/environment.d.ts +12 -0
  42. package/node-server/dist/environment.js +104 -0
  43. package/node-server/dist/fs/fs-service.d.ts +86 -8
  44. package/node-server/dist/fs/fs-service.js +640 -99
  45. package/node-server/dist/fs/notebook-formats/percent.d.ts +25 -0
  46. package/node-server/dist/fs/notebook-formats/percent.js +286 -0
  47. package/node-server/dist/fs/notebook-formats/qmd.d.ts +29 -0
  48. package/node-server/dist/fs/notebook-formats/qmd.js +307 -0
  49. package/node-server/dist/fs/notebook-formats/registry.d.ts +12 -0
  50. package/node-server/dist/fs/notebook-formats/registry.js +77 -0
  51. package/node-server/dist/fs/notebook-formats/types.d.ts +37 -0
  52. package/node-server/dist/fs/notebook-formats/types.js +13 -0
  53. package/node-server/dist/fs/types.d.ts +9 -0
  54. package/node-server/dist/fs/types.js +8 -0
  55. package/node-server/dist/idle-exit.d.ts +52 -0
  56. package/node-server/dist/idle-exit.js +83 -0
  57. package/node-server/dist/index.js +198 -8
  58. package/node-server/dist/kernel/kernel-service.d.ts +130 -2
  59. package/node-server/dist/kernel/kernel-service.js +834 -64
  60. package/node-server/dist/kernel/kernelspec.d.ts +27 -0
  61. package/node-server/dist/kernel/kernelspec.js +75 -4
  62. package/node-server/dist/notebook/headless-handler.d.ts +12 -0
  63. package/node-server/dist/notebook/headless-handler.js +125 -29
  64. package/node-server/dist/notebook/operation-router.js +7 -2
  65. package/node-server/dist/notebook/undoRedoManager.d.ts +12 -1
  66. package/node-server/dist/notebook/undoRedoManager.js +49 -18
  67. package/node-server/dist/output/display-data.js +2 -0
  68. package/node-server/dist/private-tmp.d.ts +14 -0
  69. package/node-server/dist/private-tmp.js +70 -0
  70. package/node-server/dist/routes/autocomplete.d.ts +19 -0
  71. package/node-server/dist/routes/autocomplete.js +294 -0
  72. package/node-server/dist/routes/cluster.js +2 -2
  73. package/node-server/dist/routes/compute.d.ts +8 -0
  74. package/node-server/dist/routes/compute.js +136 -0
  75. package/node-server/dist/routes/fs.js +23 -6
  76. package/node-server/dist/routes/kernel.js +156 -3
  77. package/node-server/dist/routes/notebook.js +37 -3
  78. package/node-server/dist/routes/python.d.ts +1 -2
  79. package/node-server/dist/routes/python.js +139 -3
  80. package/node-server/dist/scheduler/allocation-service.d.ts +63 -0
  81. package/node-server/dist/scheduler/allocation-service.js +285 -0
  82. package/node-server/dist/scheduler/job-template.d.ts +30 -0
  83. package/node-server/dist/scheduler/job-template.js +85 -0
  84. package/node-server/dist/scheduler/mock-scheduler.d.ts +30 -0
  85. package/node-server/dist/scheduler/mock-scheduler.js +125 -0
  86. package/node-server/dist/scheduler/slurm-scheduler.d.ts +31 -0
  87. package/node-server/dist/scheduler/slurm-scheduler.js +406 -0
  88. package/node-server/dist/scheduler/types.d.ts +118 -0
  89. package/node-server/dist/scheduler/types.js +8 -0
  90. package/node-server/dist/scheduler/util.d.ts +7 -0
  91. package/node-server/dist/scheduler/util.js +20 -0
  92. package/node-server/dist/terminal/agent-registry.d.ts +87 -0
  93. package/node-server/dist/terminal/agent-registry.js +246 -0
  94. package/node-server/dist/terminal/binding-store.d.ts +57 -0
  95. package/node-server/dist/terminal/binding-store.js +0 -0
  96. package/node-server/dist/terminal/pty-manager.d.ts +23 -6
  97. package/node-server/dist/terminal/pty-manager.js +138 -19
  98. package/node-server/dist/terminal/server.js +155 -23
  99. package/node-server/dist/terminal/types.d.ts +2 -0
  100. package/node-server/dist/update-check.d.ts +20 -0
  101. package/node-server/dist/update-check.js +114 -0
  102. package/node-server/package.json +2 -0
  103. package/node_modules/nebula-autocomplete/README.md +145 -0
  104. package/node_modules/nebula-autocomplete/dist/backends/claude.d.ts +69 -0
  105. package/node_modules/nebula-autocomplete/dist/backends/claude.js +449 -0
  106. package/node_modules/nebula-autocomplete/dist/backends/codex.d.ts +45 -0
  107. package/node_modules/nebula-autocomplete/dist/backends/codex.js +108 -0
  108. package/node_modules/nebula-autocomplete/dist/client/fetcher.d.ts +16 -0
  109. package/node_modules/nebula-autocomplete/dist/client/fetcher.js +41 -0
  110. package/node_modules/nebula-autocomplete/dist/codemirror/ghostText.d.ts +43 -0
  111. package/node_modules/nebula-autocomplete/dist/codemirror/ghostText.js +175 -0
  112. package/node_modules/nebula-autocomplete/dist/core/engine.d.ts +17 -0
  113. package/node_modules/nebula-autocomplete/dist/core/engine.js +135 -0
  114. package/node_modules/nebula-autocomplete/dist/core/lru.d.ts +7 -0
  115. package/node_modules/nebula-autocomplete/dist/core/lru.js +26 -0
  116. package/node_modules/nebula-autocomplete/dist/core/prompt.d.ts +12 -0
  117. package/node_modules/nebula-autocomplete/dist/core/prompt.js +0 -0
  118. package/node_modules/nebula-autocomplete/dist/core/text.d.ts +53 -0
  119. package/node_modules/nebula-autocomplete/dist/core/text.js +169 -0
  120. package/node_modules/nebula-autocomplete/dist/index.d.ts +7 -0
  121. package/node_modules/nebula-autocomplete/dist/index.js +5 -0
  122. package/node_modules/nebula-autocomplete/dist/server/fastify.d.ts +40 -0
  123. package/node_modules/nebula-autocomplete/dist/server/fastify.js +52 -0
  124. package/node_modules/nebula-autocomplete/dist/transport.d.ts +55 -0
  125. package/node_modules/nebula-autocomplete/dist/transport.js +66 -0
  126. package/node_modules/nebula-autocomplete/dist/types.d.ts +103 -0
  127. package/node_modules/nebula-autocomplete/dist/types.js +1 -0
  128. package/node_modules/nebula-autocomplete/package.json +58 -0
  129. package/node_modules/nebula-autocomplete/server.d.ts +3 -0
  130. package/node_modules/nebula-autocomplete/server.js +3 -0
  131. package/package.json +18 -5
  132. package/scripts/postinstall.cjs +25 -5
  133. package/dist/assets/index-BvrHu37J.js +0 -648
  134. package/dist/assets/index-Dfj_xsKU.css +0 -32
  135. package/node-server/dist/llm/index.d.ts +0 -5
  136. package/node-server/dist/llm/index.js +0 -21
  137. package/node-server/dist/llm/llm-service.d.ts +0 -77
  138. package/node-server/dist/llm/llm-service.js +0 -454
  139. package/node-server/dist/llm/types.d.ts +0 -40
  140. package/node-server/dist/llm/types.js +0 -15
  141. package/node-server/dist/routes/llm.d.ts +0 -8
  142. package/node-server/dist/routes/llm.js +0 -105
@@ -0,0 +1 @@
1
+ class c{constructor(e,s,h){this.initialize(e,s,h)}initialize(e,s,h){this._handler_context=h||this,this._models=[],this.views=[],this._create_view=e,this._remove_view=s||function(t){t.remove()}}update(e,s,h,t){const v=h||this._remove_view,n=s||this._create_view;t=t||this._handler_context;let i=0;for(;i<e.length&&!(i>=this._models.length||e[i]!==this._models[i]);i++);const r=i,o=this.views.splice(r,this.views.length-r);for(let l=0;l<o.length;l++)o[l].then(function(_){v.call(t,_)});for(;i<e.length;i++)this.views.push(Promise.resolve(n.call(t,e[i],i)));return this._models=e.slice(),Promise.all(this.views)}remove(){return Promise.all(this.views).then(e=>{e.forEach(s=>this._remove_view.call(this._handler_context,s)),this.views=[],this._models=[]})}dispose(){this.views=null,this._models=null}}export{c as V};
@@ -0,0 +1 @@
1
+ :root{--md-red-50: #ffebee;--md-red-100: #ffcdd2;--md-red-200: #ef9a9a;--md-red-300: #e57373;--md-red-400: #ef5350;--md-red-500: #f44336;--md-red-600: #e53935;--md-red-700: #d32f2f;--md-red-800: #c62828;--md-red-900: #b71c1c;--md-red-A100: #ff8a80;--md-red-A200: #ff5252;--md-red-A400: #ff1744;--md-red-A700: #d50000;--md-pink-50: #fce4ec;--md-pink-100: #f8bbd0;--md-pink-200: #f48fb1;--md-pink-300: #f06292;--md-pink-400: #ec407a;--md-pink-500: #e91e63;--md-pink-600: #d81b60;--md-pink-700: #c2185b;--md-pink-800: #ad1457;--md-pink-900: #880e4f;--md-pink-A100: #ff80ab;--md-pink-A200: #ff4081;--md-pink-A400: #f50057;--md-pink-A700: #c51162;--md-purple-50: #f3e5f5;--md-purple-100: #e1bee7;--md-purple-200: #ce93d8;--md-purple-300: #ba68c8;--md-purple-400: #ab47bc;--md-purple-500: #9c27b0;--md-purple-600: #8e24aa;--md-purple-700: #7b1fa2;--md-purple-800: #6a1b9a;--md-purple-900: #4a148c;--md-purple-A100: #ea80fc;--md-purple-A200: #e040fb;--md-purple-A400: #d500f9;--md-purple-A700: #aa00ff;--md-deep-purple-50: #ede7f6;--md-deep-purple-100: #d1c4e9;--md-deep-purple-200: #b39ddb;--md-deep-purple-300: #9575cd;--md-deep-purple-400: #7e57c2;--md-deep-purple-500: #673ab7;--md-deep-purple-600: #5e35b1;--md-deep-purple-700: #512da8;--md-deep-purple-800: #4527a0;--md-deep-purple-900: #311b92;--md-deep-purple-A100: #b388ff;--md-deep-purple-A200: #7c4dff;--md-deep-purple-A400: #651fff;--md-deep-purple-A700: #6200ea;--md-indigo-50: #e8eaf6;--md-indigo-100: #c5cae9;--md-indigo-200: #9fa8da;--md-indigo-300: #7986cb;--md-indigo-400: #5c6bc0;--md-indigo-500: #3f51b5;--md-indigo-600: #3949ab;--md-indigo-700: #303f9f;--md-indigo-800: #283593;--md-indigo-900: #1a237e;--md-indigo-A100: #8c9eff;--md-indigo-A200: #536dfe;--md-indigo-A400: #3d5afe;--md-indigo-A700: #304ffe;--md-blue-50: #e3f2fd;--md-blue-100: #bbdefb;--md-blue-200: #90caf9;--md-blue-300: #64b5f6;--md-blue-400: #42a5f5;--md-blue-500: #2196f3;--md-blue-600: #1e88e5;--md-blue-700: #1976d2;--md-blue-800: #1565c0;--md-blue-900: #0d47a1;--md-blue-A100: #82b1ff;--md-blue-A200: #448aff;--md-blue-A400: #2979ff;--md-blue-A700: #2962ff;--md-light-blue-50: #e1f5fe;--md-light-blue-100: #b3e5fc;--md-light-blue-200: #81d4fa;--md-light-blue-300: #4fc3f7;--md-light-blue-400: #29b6f6;--md-light-blue-500: #03a9f4;--md-light-blue-600: #039be5;--md-light-blue-700: #0288d1;--md-light-blue-800: #0277bd;--md-light-blue-900: #01579b;--md-light-blue-A100: #80d8ff;--md-light-blue-A200: #40c4ff;--md-light-blue-A400: #00b0ff;--md-light-blue-A700: #0091ea;--md-cyan-50: #e0f7fa;--md-cyan-100: #b2ebf2;--md-cyan-200: #80deea;--md-cyan-300: #4dd0e1;--md-cyan-400: #26c6da;--md-cyan-500: #00bcd4;--md-cyan-600: #00acc1;--md-cyan-700: #0097a7;--md-cyan-800: #00838f;--md-cyan-900: #006064;--md-cyan-A100: #84ffff;--md-cyan-A200: #18ffff;--md-cyan-A400: #00e5ff;--md-cyan-A700: #00b8d4;--md-teal-50: #e0f2f1;--md-teal-100: #b2dfdb;--md-teal-200: #80cbc4;--md-teal-300: #4db6ac;--md-teal-400: #26a69a;--md-teal-500: #009688;--md-teal-600: #00897b;--md-teal-700: #00796b;--md-teal-800: #00695c;--md-teal-900: #004d40;--md-teal-A100: #a7ffeb;--md-teal-A200: #64ffda;--md-teal-A400: #1de9b6;--md-teal-A700: #00bfa5;--md-green-50: #e8f5e9;--md-green-100: #c8e6c9;--md-green-200: #a5d6a7;--md-green-300: #81c784;--md-green-400: #66bb6a;--md-green-500: #4caf50;--md-green-600: #43a047;--md-green-700: #388e3c;--md-green-800: #2e7d32;--md-green-900: #1b5e20;--md-green-A100: #b9f6ca;--md-green-A200: #69f0ae;--md-green-A400: #00e676;--md-green-A700: #00c853;--md-light-green-50: #f1f8e9;--md-light-green-100: #dcedc8;--md-light-green-200: #c5e1a5;--md-light-green-300: #aed581;--md-light-green-400: #9ccc65;--md-light-green-500: #8bc34a;--md-light-green-600: #7cb342;--md-light-green-700: #689f38;--md-light-green-800: #558b2f;--md-light-green-900: #33691e;--md-light-green-A100: #ccff90;--md-light-green-A200: #b2ff59;--md-light-green-A400: #76ff03;--md-light-green-A700: #64dd17;--md-lime-50: #f9fbe7;--md-lime-100: #f0f4c3;--md-lime-200: #e6ee9c;--md-lime-300: #dce775;--md-lime-400: #d4e157;--md-lime-500: #cddc39;--md-lime-600: #c0ca33;--md-lime-700: #afb42b;--md-lime-800: #9e9d24;--md-lime-900: #827717;--md-lime-A100: #f4ff81;--md-lime-A200: #eeff41;--md-lime-A400: #c6ff00;--md-lime-A700: #aeea00;--md-yellow-50: #fffde7;--md-yellow-100: #fff9c4;--md-yellow-200: #fff59d;--md-yellow-300: #fff176;--md-yellow-400: #ffee58;--md-yellow-500: #ffeb3b;--md-yellow-600: #fdd835;--md-yellow-700: #fbc02d;--md-yellow-800: #f9a825;--md-yellow-900: #f57f17;--md-yellow-A100: #ffff8d;--md-yellow-A200: #ffff00;--md-yellow-A400: #ffea00;--md-yellow-A700: #ffd600;--md-amber-50: #fff8e1;--md-amber-100: #ffecb3;--md-amber-200: #ffe082;--md-amber-300: #ffd54f;--md-amber-400: #ffca28;--md-amber-500: #ffc107;--md-amber-600: #ffb300;--md-amber-700: #ffa000;--md-amber-800: #ff8f00;--md-amber-900: #ff6f00;--md-amber-A100: #ffe57f;--md-amber-A200: #ffd740;--md-amber-A400: #ffc400;--md-amber-A700: #ffab00;--md-orange-50: #fff3e0;--md-orange-100: #ffe0b2;--md-orange-200: #ffcc80;--md-orange-300: #ffb74d;--md-orange-400: #ffa726;--md-orange-500: #ff9800;--md-orange-600: #fb8c00;--md-orange-700: #f57c00;--md-orange-800: #ef6c00;--md-orange-900: #e65100;--md-orange-A100: #ffd180;--md-orange-A200: #ffab40;--md-orange-A400: #ff9100;--md-orange-A700: #ff6d00;--md-deep-orange-50: #fbe9e7;--md-deep-orange-100: #ffccbc;--md-deep-orange-200: #ffab91;--md-deep-orange-300: #ff8a65;--md-deep-orange-400: #ff7043;--md-deep-orange-500: #ff5722;--md-deep-orange-600: #f4511e;--md-deep-orange-700: #e64a19;--md-deep-orange-800: #d84315;--md-deep-orange-900: #bf360c;--md-deep-orange-A100: #ff9e80;--md-deep-orange-A200: #ff6e40;--md-deep-orange-A400: #ff3d00;--md-deep-orange-A700: #dd2c00;--md-brown-50: #efebe9;--md-brown-100: #d7ccc8;--md-brown-200: #bcaaa4;--md-brown-300: #a1887f;--md-brown-400: #8d6e63;--md-brown-500: #795548;--md-brown-600: #6d4c41;--md-brown-700: #5d4037;--md-brown-800: #4e342e;--md-brown-900: #3e2723;--md-grey-50: #fafafa;--md-grey-100: #f5f5f5;--md-grey-200: #eeeeee;--md-grey-300: #e0e0e0;--md-grey-400: #bdbdbd;--md-grey-500: #9e9e9e;--md-grey-600: #757575;--md-grey-700: #616161;--md-grey-800: #424242;--md-grey-900: #212121;--md-blue-grey-50: #eceff1;--md-blue-grey-100: #cfd8dc;--md-blue-grey-200: #b0bec5;--md-blue-grey-300: #90a4ae;--md-blue-grey-400: #78909c;--md-blue-grey-500: #607d8b;--md-blue-grey-600: #546e7a;--md-blue-grey-700: #455a64;--md-blue-grey-800: #37474f;--md-blue-grey-900: #263238}:root{--jp-icon-search: none;--jp-ui-select-caret: none}:root{--jp-border-width: 1px;--jp-border-color0: var(--md-grey-700);--jp-border-color1: var(--md-grey-500);--jp-border-color2: var(--md-grey-300);--jp-border-color3: var(--md-grey-100);--jp-ui-icon-font-size: 14px;--jp-ui-font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;--jp-ui-font-color0: rgba(0, 0, 0, 1);--jp-ui-font-color1: rgba(0, 0, 0, .8);--jp-ui-font-color2: rgba(0, 0, 0, .5);--jp-ui-font-color3: rgba(0, 0, 0, .3);--jp-ui-inverse-font-color0: rgba(255, 255, 255, 1);--jp-ui-inverse-font-color1: rgba(255, 255, 255, 1);--jp-ui-inverse-font-color2: rgba(255, 255, 255, .7);--jp-ui-inverse-font-color3: rgba(255, 255, 255, .5);--jp-content-font-size: 13px;--jp-content-line-height: 1.5;--jp-content-font-color0: black;--jp-content-font-color1: black;--jp-content-font-color2: var(--md-grey-700);--jp-content-font-color3: var(--md-grey-500);--jp-ui-font-scale-factor: 1.2;--jp-ui-font-size0: calc( var(--jp-ui-font-size1) / var(--jp-ui-font-scale-factor) );--jp-ui-font-size1: 13px;--jp-ui-font-size2: calc( var(--jp-ui-font-size1) * var(--jp-ui-font-scale-factor) );--jp-ui-font-size3: calc( var(--jp-ui-font-size2) * var(--jp-ui-font-scale-factor) );--jp-code-font-size: 13px;--jp-code-line-height: 1.307;--jp-code-padding: 5px;--jp-code-font-family: monospace;--jp-layout-color0: white;--jp-layout-color1: white;--jp-layout-color2: var(--md-grey-200);--jp-layout-color3: var(--md-grey-400);--jp-brand-color0: var(--md-blue-700);--jp-brand-color1: var(--md-blue-500);--jp-brand-color2: var(--md-blue-300);--jp-brand-color3: var(--md-blue-100);--jp-accent-color0: var(--md-green-700);--jp-accent-color1: var(--md-green-500);--jp-accent-color2: var(--md-green-300);--jp-accent-color3: var(--md-green-100);--jp-warn-color0: var(--md-orange-700);--jp-warn-color1: var(--md-orange-500);--jp-warn-color2: var(--md-orange-300);--jp-warn-color3: var(--md-orange-100);--jp-error-color0: var(--md-red-700);--jp-error-color1: var(--md-red-500);--jp-error-color2: var(--md-red-300);--jp-error-color3: var(--md-red-100);--jp-success-color0: var(--md-green-700);--jp-success-color1: var(--md-green-500);--jp-success-color2: var(--md-green-300);--jp-success-color3: var(--md-green-100);--jp-info-color0: var(--md-cyan-700);--jp-info-color1: var(--md-cyan-500);--jp-info-color2: var(--md-cyan-300);--jp-info-color3: var(--md-cyan-100);--jp-cell-padding: 5px;--jp-cell-editor-background: #f7f7f7;--jp-cell-editor-border-color: #cfcfcf;--jp-cell-editor-background-edit: var(--jp-ui-layout-color1);--jp-cell-editor-border-color-edit: var(--jp-brand-color1);--jp-cell-prompt-width: 100px;--jp-cell-prompt-font-family: "Roboto Mono", monospace;--jp-cell-prompt-letter-spacing: 0px;--jp-cell-prompt-opacity: 1;--jp-cell-prompt-opacity-not-active: .4;--jp-cell-prompt-font-color-not-active: var(--md-grey-700);--jp-cell-inprompt-font-color: #307fc1;--jp-cell-outprompt-font-color: #bf5b3d;--jp-notebook-padding: 10px;--jp-notebook-scroll-padding: 100px;--jp-console-background: var(--md-grey-100);--jp-toolbar-border-color: var(--md-grey-400);--jp-toolbar-micro-height: 8px;--jp-toolbar-background: var(--jp-layout-color0);--jp-toolbar-box-shadow: 0px 0px 2px 0px rgba(0, 0, 0, .24);--jp-toolbar-header-margin: 4px 4px 0px 4px;--jp-toolbar-active-background: var(--md-grey-300)}.jupyter-widgets.widget-tab>.p-TabBar,.jupyter-widgets.jupyter-widget-tab>.p-TabBar,.jupyter-widgets.jupyter-widget-tab>.lm-TabBar{display:flex;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.jupyter-widgets.widget-tab>.p-TabBar[data-orientation=horizontal],.jupyter-widgets.jupyter-widget-tab>.p-TabBar[data-orientation=horizontal],.jupyter-widgets.jupyter-widget-tab>.lm-TabBar[data-orientation=horizontal]{flex-direction:row}.jupyter-widgets.widget-tab>.p-TabBar[data-orientation=vertical],.jupyter-widgets.jupyter-widget-tab>.p-TabBar[data-orientation=vertical],.jupyter-widgets.jupyter-widget-tab>.lm-TabBar[data-orientation=vertical]{flex-direction:column}.jupyter-widgets.widget-tab>.p-TabBar>.p-TabBar-content,.jupyter-widgets.jupyter-widget-tab>.p-TabBar>.p-TabBar-content,.jupyter-widgets.jupyter-widget-tab>.lm-TabBar>.lm-TabBar-content{margin:0;padding:0;display:flex;flex:1 1 auto;list-style-type:none}.jupyter-widgets.widget-tab>.p-TabBar[data-orientation=horizontal]>.p-TabBar-content,.jupyter-widgets.jupyter-widget-tab>.p-TabBar[data-orientation=horizontal]>.p-TabBar-content,.jupyter-widgets.jupyter-widget-tab>.lm-TabBar[data-orientation=horizontal]>.lm-TabBar-content{flex-direction:row}.jupyter-widgets.widget-tab>.p-TabBar[data-orientation=vertical]>.p-TabBar-content,.jupyter-widgets.jupyter-widget-tab>.p-TabBar[data-orientation=vertical]>.p-TabBar-content,.jupyter-widgets.jupyter-widget-tab>.lm-TabBar[data-orientation=vertical]>.lm-TabBar-content{flex-direction:column}.jupyter-widgets.widget-tab>.p-TabBar .p-TabBar-tab,.jupyter-widgets.jupyter-widget-tab>.p-TabBar .p-TabBar-tab,.jupyter-widgets.jupyter-widget-tab>.lm-TabBar .lm-TabBar-tab{display:flex;flex-direction:row;box-sizing:border-box;overflow:hidden}.jupyter-widgets.widget-tab>.p-TabBar .p-TabBar-tabIcon,.jupyter-widgets.widget-tab>.p-TabBar .p-TabBar-tabCloseIcon,.jupyter-widgets.jupyter-widget-tab>.p-TabBar .p-TabBar-tabIcon,.jupyter-widgets.jupyter-widget-tab>.p-TabBar .p-TabBar-tabCloseIcon,.jupyter-widgets.jupyter-widget-tab>.lm-TabBar .lm-TabBar-tabIcon,.jupyter-widgets.jupyter-widget-tab>.lm-TabBar .lm-TabBar-tabCloseIcon{flex:0 0 auto}.jupyter-widgets.widget-tab>.p-TabBar .p-TabBar-tabLabel,.jupyter-widgets.jupyter-widget-tab>.p-TabBar .p-TabBar-tabLabel,.jupyter-widgets.jupyter-widget-tab>.lm-TabBar .lm-TabBar-tabLabel{flex:1 1 auto;overflow:hidden;white-space:nowrap}.jupyter-widgets.widget-tab>.p-TabBar .p-TabBar-tab.p-mod-hidden,.jupyter-widgets.jupyter-widget-tab>.p-TabBar .p-TabBar-tab.p-mod-hidden,.jupyter-widgets.jupyter-widget-tab>.lm-TabBar .lm-TabBar-tab.lm-mod-hidden{display:none!important}.jupyter-widgets.widget-tab>.p-TabBar.p-mod-dragging .p-TabBar-tab,.jupyter-widgets.jupyter-widget-tab>.p-TabBar.p-mod-dragging .p-TabBar-tab,.jupyter-widgets.jupyter-widget-tab>.lm-TabBar.lm-mod-dragging .lm-TabBar-tab{position:relative}.jupyter-widgets.widget-tab>.p-TabBar.p-mod-dragging[data-orientation=horizontal] .p-TabBar-tab,.jupyter-widgets.jupyter-widget-tab>.p-TabBar.p-mod-dragging[data-orientation=horizontal] .p-TabBar-tab,.jupyter-widgets.jupyter-widget-tab>.lm-TabBar.lm-mod-dragging[data-orientation=horizontal] .lm-TabBar-tab{left:0;transition:left .15s ease}.jupyter-widgets.widget-tab>.p-TabBar.p-mod-dragging[data-orientation=vertical] .p-TabBar-tab,.jupyter-widgets.jupyter-widget-tab>.p-TabBar.p-mod-dragging[data-orientation=vertical] .p-TabBar-tab,.jupyter-widgets.jupyter-widget-tab>.lm-TabBar.lm-mod-dragging[data-orientation=vertical] .lm-TabBar-tab{top:0;transition:top .15s ease}.jupyter-widgets.widget-tab>.p-TabBar.p-mod-dragging .p-TabBar-tab.p-mod-dragging,.jupyter-widgets.jupyter-widget-tab>.p-TabBar.p-mod-dragging .p-TabBar-tab.p-mod-dragging,.jupyter-widgets.jupyter-widget-tab>.lm-TabBar.lm-mod-dragging .lm-TabBar-tab.lm-mod-dragging{transition:none}.widget-slider .noUi-target,.jupyter-widget-slider .noUi-target,.widget-slider .noUi-target *,.jupyter-widget-slider .noUi-target *{-webkit-touch-callout:none;-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-user-select:none;-ms-touch-action:none;touch-action:none;-ms-user-select:none;-moz-user-select:none;user-select:none;-moz-box-sizing:border-box;box-sizing:border-box}.widget-slider .noUi-target,.jupyter-widget-slider .noUi-target{position:relative}.widget-slider .noUi-base,.jupyter-widget-slider .noUi-base,.widget-slider .noUi-connects,.jupyter-widget-slider .noUi-connects{width:100%;height:100%;position:relative;z-index:1}.widget-slider .noUi-connects,.jupyter-widget-slider .noUi-connects{overflow:hidden;z-index:0}.widget-slider .noUi-connect,.jupyter-widget-slider .noUi-connect,.widget-slider .noUi-origin,.jupyter-widget-slider .noUi-origin{will-change:transform;position:absolute;z-index:1;top:0;right:0;-ms-transform-origin:0 0;-webkit-transform-origin:0 0;-webkit-transform-style:preserve-3d;transform-origin:0 0;transform-style:flat}.widget-slider .noUi-connect,.jupyter-widget-slider .noUi-connect{height:100%;width:100%}.widget-slider .noUi-origin,.jupyter-widget-slider .noUi-origin{height:10%;width:10%}.widget-slider .noUi-txt-dir-rtl.noUi-horizontal .noUi-origin,.jupyter-widget-slider .noUi-txt-dir-rtl.noUi-horizontal .noUi-origin{left:0;right:auto}.widget-slider .noUi-vertical .noUi-origin,.jupyter-widget-slider .noUi-vertical .noUi-origin{width:0}.widget-slider .noUi-horizontal .noUi-origin,.jupyter-widget-slider .noUi-horizontal .noUi-origin{height:0}.widget-slider .noUi-handle,.jupyter-widget-slider .noUi-handle{-webkit-backface-visibility:hidden;backface-visibility:hidden;position:absolute}.widget-slider .noUi-touch-area,.jupyter-widget-slider .noUi-touch-area{height:100%;width:100%}.widget-slider .noUi-state-tap .noUi-connect,.jupyter-widget-slider .noUi-state-tap .noUi-connect,.widget-slider .noUi-state-tap .noUi-origin,.jupyter-widget-slider .noUi-state-tap .noUi-origin{-webkit-transition:transform .3s;transition:transform .3s}.widget-slider .noUi-state-drag *,.jupyter-widget-slider .noUi-state-drag *{cursor:inherit!important}.widget-slider .noUi-horizontal,.jupyter-widget-slider .noUi-horizontal{height:18px}.widget-slider .noUi-horizontal .noUi-handle,.jupyter-widget-slider .noUi-horizontal .noUi-handle{width:34px;height:28px;right:-17px;top:-6px}.widget-slider .noUi-vertical,.jupyter-widget-slider .noUi-vertical{width:18px}.widget-slider .noUi-vertical .noUi-handle,.jupyter-widget-slider .noUi-vertical .noUi-handle{width:28px;height:34px;right:-6px;top:-17px}.widget-slider .noUi-txt-dir-rtl.noUi-horizontal .noUi-handle,.jupyter-widget-slider .noUi-txt-dir-rtl.noUi-horizontal .noUi-handle{left:-17px;right:auto}.widget-slider .noUi-target,.jupyter-widget-slider .noUi-target{background:#fafafa;border-radius:4px;border:1px solid #D3D3D3;box-shadow:inset 0 1px 1px #f0f0f0,0 3px 6px -5px #bbb}.widget-slider .noUi-connects,.jupyter-widget-slider .noUi-connects{border-radius:3px}.widget-slider .noUi-connect,.jupyter-widget-slider .noUi-connect{background:#3fb8af}.widget-slider .noUi-draggable,.jupyter-widget-slider .noUi-draggable{cursor:ew-resize}.widget-slider .noUi-vertical .noUi-draggable,.jupyter-widget-slider .noUi-vertical .noUi-draggable{cursor:ns-resize}.widget-slider .noUi-handle,.jupyter-widget-slider .noUi-handle{border:1px solid #D9D9D9;border-radius:3px;background:#fff;cursor:default;box-shadow:inset 0 0 1px #fff,inset 0 1px 7px #ebebeb,0 3px 6px -3px #bbb}.widget-slider .noUi-active,.jupyter-widget-slider .noUi-active{box-shadow:inset 0 0 1px #fff,inset 0 1px 7px #ddd,0 3px 6px -3px #bbb}.widget-slider .noUi-handle:before,.jupyter-widget-slider .noUi-handle:before,.widget-slider .noUi-handle:after,.jupyter-widget-slider .noUi-handle:after{content:"";display:block;position:absolute;height:14px;width:1px;background:#e8e7e6;left:14px;top:6px}.widget-slider .noUi-handle:after,.jupyter-widget-slider .noUi-handle:after{left:17px}.widget-slider .noUi-vertical .noUi-handle:before,.jupyter-widget-slider .noUi-vertical .noUi-handle:before,.widget-slider .noUi-vertical .noUi-handle:after,.jupyter-widget-slider .noUi-vertical .noUi-handle:after{width:14px;height:1px;left:6px;top:14px}.widget-slider .noUi-vertical .noUi-handle:after,.jupyter-widget-slider .noUi-vertical .noUi-handle:after{top:17px}.widget-slider [disabled] .noUi-connect,.jupyter-widget-slider [disabled] .noUi-connect{background:#b8b8b8}.widget-slider [disabled].noUi-target,.jupyter-widget-slider [disabled].noUi-target,.widget-slider [disabled].noUi-handle,.jupyter-widget-slider [disabled].noUi-handle,.widget-slider [disabled] .noUi-handle,.jupyter-widget-slider [disabled] .noUi-handle{cursor:not-allowed}.widget-slider .noUi-pips,.jupyter-widget-slider .noUi-pips,.widget-slider .noUi-pips *,.jupyter-widget-slider .noUi-pips *{-moz-box-sizing:border-box;box-sizing:border-box}.widget-slider .noUi-pips,.jupyter-widget-slider .noUi-pips{position:absolute;color:#999}.widget-slider .noUi-value,.jupyter-widget-slider .noUi-value{position:absolute;white-space:nowrap;text-align:center}.widget-slider .noUi-value-sub,.jupyter-widget-slider .noUi-value-sub{color:#ccc;font-size:10px}.widget-slider .noUi-marker,.jupyter-widget-slider .noUi-marker{position:absolute;background:#ccc}.widget-slider .noUi-marker-sub,.jupyter-widget-slider .noUi-marker-sub,.widget-slider .noUi-marker-large,.jupyter-widget-slider .noUi-marker-large{background:#aaa}.widget-slider .noUi-pips-horizontal,.jupyter-widget-slider .noUi-pips-horizontal{padding:10px 0;height:80px;top:100%;left:0;width:100%}.widget-slider .noUi-value-horizontal,.jupyter-widget-slider .noUi-value-horizontal{-webkit-transform:translate(-50%,50%);transform:translate(-50%,50%)}.noUi-rtl .widget-slider .noUi-value-horizontal,.noUi-rtl .jupyter-widget-slider .noUi-value-horizontal{-webkit-transform:translate(50%,50%);transform:translate(50%,50%)}.widget-slider .noUi-marker-horizontal.noUi-marker,.jupyter-widget-slider .noUi-marker-horizontal.noUi-marker{margin-left:-1px;width:2px;height:5px}.widget-slider .noUi-marker-horizontal.noUi-marker-sub,.jupyter-widget-slider .noUi-marker-horizontal.noUi-marker-sub{height:10px}.widget-slider .noUi-marker-horizontal.noUi-marker-large,.jupyter-widget-slider .noUi-marker-horizontal.noUi-marker-large{height:15px}.widget-slider .noUi-pips-vertical,.jupyter-widget-slider .noUi-pips-vertical{padding:0 10px;height:100%;top:0;left:100%}.widget-slider .noUi-value-vertical,.jupyter-widget-slider .noUi-value-vertical{-webkit-transform:translate(0,-50%);transform:translateY(-50%);padding-left:25px}.noUi-rtl .widget-slider .noUi-value-vertical,.noUi-rtl .jupyter-widget-slider .noUi-value-vertical{-webkit-transform:translate(0,50%);transform:translateY(50%)}.widget-slider .noUi-marker-vertical.noUi-marker,.jupyter-widget-slider .noUi-marker-vertical.noUi-marker{width:5px;height:2px;margin-top:-1px}.widget-slider .noUi-marker-vertical.noUi-marker-sub,.jupyter-widget-slider .noUi-marker-vertical.noUi-marker-sub{width:10px}.widget-slider .noUi-marker-vertical.noUi-marker-large,.jupyter-widget-slider .noUi-marker-vertical.noUi-marker-large{width:15px}.widget-slider .noUi-tooltip,.jupyter-widget-slider .noUi-tooltip{display:block;position:absolute;border:1px solid #D9D9D9;border-radius:3px;background:#fff;color:#000;padding:5px;text-align:center;white-space:nowrap}.widget-slider .noUi-horizontal .noUi-tooltip,.jupyter-widget-slider .noUi-horizontal .noUi-tooltip{-webkit-transform:translate(-50%,0);transform:translate(-50%);left:50%;bottom:120%}.widget-slider .noUi-vertical .noUi-tooltip,.jupyter-widget-slider .noUi-vertical .noUi-tooltip{-webkit-transform:translate(0,-50%);transform:translateY(-50%);top:50%;right:120%}.widget-slider .noUi-horizontal .noUi-origin>.noUi-tooltip,.jupyter-widget-slider .noUi-horizontal .noUi-origin>.noUi-tooltip{-webkit-transform:translate(50%,0);transform:translate(50%);left:auto;bottom:10px}.widget-slider .noUi-vertical .noUi-origin>.noUi-tooltip,.jupyter-widget-slider .noUi-vertical .noUi-origin>.noUi-tooltip{-webkit-transform:translate(0,-18px);transform:translateY(-18px);top:auto;right:28px}.widget-slider .noUi-connect,.jupyter-widget-slider .noUi-connect{background:#2196f3}.widget-slider .noUi-horizontal,.jupyter-widget-slider .noUi-horizontal{height:var(--jp-widgets-slider-track-thickness)}.widget-slider .noUi-vertical,.jupyter-widget-slider .noUi-vertical{width:var(--jp-widgets-slider-track-thickness);height:100%}.widget-slider .noUi-horizontal .noUi-handle,.jupyter-widget-slider .noUi-horizontal .noUi-handle{width:var(--jp-widgets-slider-handle-size);height:var(--jp-widgets-slider-handle-size);border-radius:50%;top:calc((var(--jp-widgets-slider-track-thickness) - var(--jp-widgets-slider-handle-size)) / 2);right:calc(var(--jp-widgets-slider-handle-size) / -2)}.widget-slider .noUi-vertical .noUi-handle,.jupyter-widget-slider .noUi-vertical .noUi-handle{height:var(--jp-widgets-slider-handle-size);width:var(--jp-widgets-slider-handle-size);border-radius:50%;right:calc((var(--jp-widgets-slider-handle-size) - var(--jp-widgets-slider-track-thickness)) / -2);top:calc(var(--jp-widgets-slider-handle-size) / -2)}.widget-slider .noUi-handle:after,.jupyter-widget-slider .noUi-handle:after{content:none}.widget-slider .noUi-handle:before,.jupyter-widget-slider .noUi-handle:before{content:none}.widget-slider .noUi-target,.jupyter-widget-slider .noUi-target{background:#fafafa;border-radius:4px;border:1px}.widget-slider .ui-slider,.jupyter-widget-slider .ui-slider{border:var(--jp-widgets-slider-border-width) solid var(--jp-layout-color3);background:var(--jp-layout-color3);box-sizing:border-box;position:relative;border-radius:0}.widget-slider .noUi-handle,.jupyter-widget-slider .noUi-handle{width:var(--jp-widgets-slider-handle-size);border:1px solid #d9d9d9;border-radius:3px;background:#fff;cursor:default;box-shadow:none;outline:none}.widget-slider .noUi-target:not([disabled]) .noUi-handle:hover,.jupyter-widget-slider .noUi-target:not([disabled]) .noUi-handle:hover,.widget-slider .noUi-target:not([disabled]) .noUi-handle:focus,.jupyter-widget-slider .noUi-target:not([disabled]) .noUi-handle:focus{background-color:var(--jp-widgets-slider-active-handle-color);border:var(--jp-widgets-slider-border-width) solid var(--jp-widgets-slider-active-handle-color)}.widget-slider [disabled].noUi-target,.jupyter-widget-slider [disabled].noUi-target{opacity:.35}.widget-slider .noUi-connects,.jupyter-widget-slider .noUi-connects{overflow:visible;z-index:0;background:var(--jp-layout-color3)}.widget-slider .noUi-vertical .noUi-connect,.jupyter-widget-slider .noUi-vertical .noUi-connect{width:calc(100% + 2px);right:-1px}.widget-slider .noUi-horizontal .noUi-connect,.jupyter-widget-slider .noUi-horizontal .noUi-connect{height:calc(100% + 2px);top:-1px}:root{--jp-widgets-color: var(--jp-content-font-color1);--jp-widgets-label-color: var(--jp-widgets-color);--jp-widgets-readout-color: var(--jp-widgets-color);--jp-widgets-font-size: var(--jp-ui-font-size1);--jp-widgets-margin: 2px;--jp-widgets-inline-height: 28px;--jp-widgets-inline-width: 300px;--jp-widgets-inline-width-short: calc( var(--jp-widgets-inline-width) / 2 - var(--jp-widgets-margin) );--jp-widgets-inline-width-tiny: calc( var(--jp-widgets-inline-width-short) / 2 - var(--jp-widgets-margin) );--jp-widgets-inline-margin: 4px;--jp-widgets-inline-label-width: 80px;--jp-widgets-border-width: var(--jp-border-width);--jp-widgets-vertical-height: 200px;--jp-widgets-horizontal-tab-height: 24px;--jp-widgets-horizontal-tab-width: 144px;--jp-widgets-horizontal-tab-top-border: 2px;--jp-widgets-progress-thickness: 20px;--jp-widgets-container-padding: 15px;--jp-widgets-input-padding: 4px;--jp-widgets-radio-item-height-adjustment: 8px;--jp-widgets-radio-item-height: calc( var(--jp-widgets-inline-height) - var(--jp-widgets-radio-item-height-adjustment) );--jp-widgets-slider-track-thickness: 4px;--jp-widgets-slider-border-width: var(--jp-widgets-border-width);--jp-widgets-slider-handle-size: 16px;--jp-widgets-slider-handle-border-color: var(--jp-border-color1);--jp-widgets-slider-handle-background-color: var(--jp-layout-color1);--jp-widgets-slider-active-handle-color: var(--jp-brand-color1);--jp-widgets-menu-item-height: 24px;--jp-widgets-dropdown-arrow: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE5LjIuMSwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxOCAxOCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTggMTg7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDpub25lO30KPC9zdHlsZT4KPHBhdGggZD0iTTUuMiw1LjlMOSw5LjdsMy44LTMuOGwxLjIsMS4ybC00LjksNWwtNC45LTVMNS4yLDUuOXoiLz4KPHBhdGggY2xhc3M9InN0MCIgZD0iTTAtMC42aDE4djE4SDBWLTAuNnoiLz4KPC9zdmc+Cg);--jp-widgets-input-color: var(--jp-ui-font-color1);--jp-widgets-input-background-color: var(--jp-layout-color1);--jp-widgets-input-border-color: var(--jp-border-color1);--jp-widgets-input-focus-border-color: var(--jp-brand-color2);--jp-widgets-input-border-width: var(--jp-widgets-border-width);--jp-widgets-disabled-opacity: .6;--md-shadow-key-umbra-opacity: .2;--md-shadow-key-penumbra-opacity: .14;--md-shadow-ambient-shadow-opacity: .12}.jupyter-widgets{margin:var(--jp-widgets-margin);box-sizing:border-box;color:var(--jp-widgets-color);overflow:visible}.jp-Output-result>.jupyter-widgets{margin-left:0;margin-right:0}.widget-inline-hbox,.jupyter-widget-inline-hbox{box-sizing:border-box;display:flex;flex-direction:row;align-items:baseline}.widget-inline-vbox,.jupyter-widget-inline-vbox{box-sizing:border-box;display:flex;flex-direction:column;align-items:center}.widget-box,.jupyter-widget-box{box-sizing:border-box;display:flex;margin:0;overflow:auto}.widget-gridbox,.jupyter-widget-gridbox{box-sizing:border-box;display:grid;margin:0;overflow:auto}.widget-hbox,.jupyter-widget-hbox{flex-direction:row}.widget-vbox,.jupyter-widget-vbox{flex-direction:column}.jupyter-widget-tagsinput{display:flex;flex-direction:row;flex-wrap:wrap;align-items:center;overflow:auto;cursor:text}.jupyter-widget-tag{padding:0 10px;display:inline-block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:center;font-size:var(--jp-widgets-font-size);height:calc(var(--jp-widgets-inline-height) - 2px);border:0px solid;line-height:calc(var(--jp-widgets-inline-height) - 2px);box-shadow:none;color:var(--jp-ui-font-color1);background-color:var(--jp-layout-color2);border-color:var(--jp-border-color2);border:none;-webkit-user-select:none;user-select:none;cursor:grab;transition:margin-left .2s;margin:1px}.jupyter-widget-tag.mod-active{box-shadow:0 4px 5px rgba(0,0,0,var(--md-shadow-key-penumbra-opacity)),0 1px 10px rgba(0,0,0,var(--md-shadow-ambient-shadow-opacity)),0 2px 4px -1px rgba(0,0,0,var(--md-shadow-key-umbra-opacity));color:var(--jp-ui-font-color1);background-color:var(--jp-layout-color3)}.jupyter-widget-colortag{color:var(--jp-inverse-ui-font-color1)}.jupyter-widget-colortag.mod-active{color:var(--jp-inverse-ui-font-color0)}.jupyter-widget-taginput{color:var(--jp-ui-font-color0);background-color:var(--jp-layout-color0);cursor:text;text-align:left}.jupyter-widget-taginput:focus{outline:none}.jupyter-widget-tag-close{margin-left:var(--jp-widgets-inline-margin);padding:2px 0 2px 2px}.jupyter-widget-tag-close:hover{cursor:pointer}.jupyter-widget-tag.mod-primary{color:var(--jp-inverse-ui-font-color1);background-color:var(--jp-brand-color1)}.jupyter-widget-tag.mod-primary.mod-active{color:var(--jp-inverse-ui-font-color0);background-color:var(--jp-brand-color0)}.jupyter-widget-tag.mod-success{color:var(--jp-inverse-ui-font-color1);background-color:var(--jp-success-color1)}.jupyter-widget-tag.mod-success.mod-active{color:var(--jp-inverse-ui-font-color0);background-color:var(--jp-success-color0)}.jupyter-widget-tag.mod-info{color:var(--jp-inverse-ui-font-color1);background-color:var(--jp-info-color1)}.jupyter-widget-tag.mod-info.mod-active{color:var(--jp-inverse-ui-font-color0);background-color:var(--jp-info-color0)}.jupyter-widget-tag.mod-warning{color:var(--jp-inverse-ui-font-color1);background-color:var(--jp-warn-color1)}.jupyter-widget-tag.mod-warning.mod-active{color:var(--jp-inverse-ui-font-color0);background-color:var(--jp-warn-color0)}.jupyter-widget-tag.mod-danger{color:var(--jp-inverse-ui-font-color1);background-color:var(--jp-error-color1)}.jupyter-widget-tag.mod-danger.mod-active{color:var(--jp-inverse-ui-font-color0);background-color:var(--jp-error-color0)}.jupyter-button{padding:0 10px;display:inline-block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:center;font-size:var(--jp-widgets-font-size);cursor:pointer;height:var(--jp-widgets-inline-height);border:0px solid;line-height:var(--jp-widgets-inline-height);box-shadow:none;color:var(--jp-ui-font-color1);background-color:var(--jp-layout-color2);border-color:var(--jp-border-color2);border:none;-webkit-user-select:none;user-select:none}.jupyter-button i.fa{margin-right:var(--jp-widgets-inline-margin);pointer-events:none}.jupyter-button:empty:before{content:"​"}.jupyter-widgets.jupyter-button:disabled{opacity:var(--jp-widgets-disabled-opacity)}.jupyter-button i.fa.center{margin-right:0}.jupyter-button:hover:enabled,.jupyter-button:focus:enabled{box-shadow:0 2px 2px rgba(0,0,0,var(--md-shadow-key-penumbra-opacity)),0 3px 1px -2px rgba(0,0,0,var(--md-shadow-key-umbra-opacity)),0 1px 5px rgba(0,0,0,var(--md-shadow-ambient-shadow-opacity))}.jupyter-button:active,.jupyter-button.mod-active{box-shadow:0 4px 5px rgba(0,0,0,var(--md-shadow-key-penumbra-opacity)),0 1px 10px rgba(0,0,0,var(--md-shadow-ambient-shadow-opacity)),0 2px 4px -1px rgba(0,0,0,var(--md-shadow-key-umbra-opacity));color:var(--jp-ui-font-color1);background-color:var(--jp-layout-color3)}.jupyter-button:focus:enabled{outline:1px solid var(--jp-widgets-input-focus-border-color)}.jupyter-button.mod-primary{color:var(--jp-ui-inverse-font-color1);background-color:var(--jp-brand-color1)}.jupyter-button.mod-primary.mod-active,.jupyter-button.mod-primary:active{color:var(--jp-ui-inverse-font-color0);background-color:var(--jp-brand-color0)}.jupyter-button.mod-success{color:var(--jp-ui-inverse-font-color1);background-color:var(--jp-success-color1)}.jupyter-button.mod-success.mod-active,.jupyter-button.mod-success:active{color:var(--jp-ui-inverse-font-color0);background-color:var(--jp-success-color0)}.jupyter-button.mod-info{color:var(--jp-ui-inverse-font-color1);background-color:var(--jp-info-color1)}.jupyter-button.mod-info.mod-active,.jupyter-button.mod-info:active{color:var(--jp-ui-inverse-font-color0);background-color:var(--jp-info-color0)}.jupyter-button.mod-warning{color:var(--jp-ui-inverse-font-color1);background-color:var(--jp-warn-color1)}.jupyter-button.mod-warning.mod-active,.jupyter-button.mod-warning:active{color:var(--jp-ui-inverse-font-color0);background-color:var(--jp-warn-color0)}.jupyter-button.mod-danger{color:var(--jp-ui-inverse-font-color1);background-color:var(--jp-error-color1)}.jupyter-button.mod-danger.mod-active,.jupyter-button.mod-danger:active{color:var(--jp-ui-inverse-font-color0);background-color:var(--jp-error-color0)}.widget-button,.widget-toggle-button,.widget-upload,.jupyter-widget-button,.jupyter-widget-toggle-button,.jupyter-widget-upload{width:var(--jp-widgets-inline-width-short)}.jupyter-widgets label{margin-bottom:initial}.widget-label-basic,.jupyter-widget-label-basic,.widget-label,.jupyter-widget-label{color:var(--jp-widgets-label-color);font-size:var(--jp-widgets-font-size);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:var(--jp-widgets-inline-height)}.widget-inline-hbox .widget-label,.jupyter-widget-inline-hbox .jupyter-widget-label{color:var(--jp-widgets-label-color);text-align:right;margin-right:calc(var(--jp-widgets-inline-margin) * 2);width:var(--jp-widgets-inline-label-width);flex-shrink:0}.widget-inline-vbox .widget-label,.jupyter-widget-inline-vbox .jupyter-widget-label{color:var(--jp-widgets-label-color);text-align:center;line-height:var(--jp-widgets-inline-height)}.widget-readout,.jupyter-widget-readout{color:var(--jp-widgets-readout-color);font-size:var(--jp-widgets-font-size);height:var(--jp-widgets-inline-height);line-height:var(--jp-widgets-inline-height);overflow:hidden;white-space:nowrap;text-align:center}.widget-readout.overflow,.jupyter-widget-readout.overflow{-webkit-box-shadow:0 2px 2px 0 rgba(0,0,0,.2),0 3px 1px -2px rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12);-moz-box-shadow:0 2px 2px 0 rgba(0,0,0,.2),0 3px 1px -2px rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12);box-shadow:0 2px 2px #0003,0 3px 1px -2px #00000024,0 1px 5px #0000001f}.widget-inline-hbox .widget-readout,.jupyter-widget-inline-hbox .jupyter-widget-readout{text-align:center;max-width:var(--jp-widgets-inline-width-short);min-width:var(--jp-widgets-inline-width-tiny);margin-left:var(--jp-widgets-inline-margin)}.widget-inline-vbox .widget-readout,.jupyter-widget-inline-vbox .jupyter-widget-readout{margin-top:var(--jp-widgets-inline-margin);width:inherit}.widget-checkbox,.jupyter-widget-checkbox{width:var(--jp-widgets-inline-width);height:var(--jp-widgets-inline-height);line-height:var(--jp-widgets-inline-height)}.widget-checkbox input[type=checkbox],.jupyter-widget-checkbox input[type=checkbox]{margin:0px calc(var(--jp-widgets-inline-margin) * 2) 0px 0px;line-height:var(--jp-widgets-inline-height);font-size:large;flex-grow:1;flex-shrink:0;align-self:center}.widget-valid,.jupyter-widget-valid{height:var(--jp-widgets-inline-height);line-height:var(--jp-widgets-inline-height);width:var(--jp-widgets-inline-width-short);font-size:var(--jp-widgets-font-size)}.widget-valid i,.jupyter-widget-valid i{line-height:var(--jp-widgets-inline-height);margin-right:var(--jp-widgets-inline-margin);margin-left:var(--jp-widgets-inline-margin)}.widget-valid.mod-valid i,.jupyter-widget-valid.mod-valid i{color:green}.widget-valid.mod-invalid i,.jupyter-widget-valid.mod-invalid i{color:red}.widget-valid.mod-valid .widget-valid-readout,.jupyter-widget-valid.mod-valid .jupyter-widget-valid-readout{display:none}.widget-textarea,.widget-text,.jupyter-widget-textarea,.jupyter-widget-text{width:var(--jp-widgets-inline-width)}.widget-text input[type=text],.widget-text input[type=number],.widget-text input[type=password],.jupyter-widget-text input[type=text],.jupyter-widget-text input[type=number],.jupyter-widget-text input[type=password]{height:var(--jp-widgets-inline-height)}.widget-text input[type=text]:disabled,.widget-text input[type=number]:disabled,.widget-text input[type=password]:disabled,.widget-textarea textarea:disabled,.jupyter-widget-text input[type=text]:disabled,.jupyter-widget-text input[type=number]:disabled,.jupyter-widget-text input[type=password]:disabled,.jupyter-widget-textarea textarea:disabled{opacity:var(--jp-widgets-disabled-opacity)}.widget-text input[type=text],.widget-text input[type=number],.widget-text input[type=password],.widget-textarea textarea,.jupyter-widget-text input[type=text],.jupyter-widget-text input[type=number],.jupyter-widget-text input[type=password],.jupyter-widget-textarea textarea{box-sizing:border-box;border:var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);background-color:var(--jp-widgets-input-background-color);color:var(--jp-widgets-input-color);font-size:var(--jp-widgets-font-size);flex-grow:1;min-width:0;flex-shrink:1;outline:none!important}.widget-text input[type=text],.widget-text input[type=password],.widget-textarea textarea,.jupyter-widget-text input[type=text],.jupyter-widget-text input[type=password],.jupyter-widget-textarea textarea{padding:var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding) * 2)}.widget-text input[type=number],.jupyter-widget-text input[type=number]{padding:var(--jp-widgets-input-padding) 0 var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding) * 2)}.widget-textarea textarea,.jupyter-widget-textarea textarea{height:inherit;width:inherit}.widget-text input:focus,.widget-textarea textarea:focus,.jupyter-widget-text input:focus,.jupyter-widget-textarea textarea:focus{border-color:var(--jp-widgets-input-focus-border-color)}.widget-hslider,.jupyter-widget-hslider{width:var(--jp-widgets-inline-width);height:var(--jp-widgets-inline-height);line-height:var(--jp-widgets-inline-height);align-items:center}.widgets-slider .slider-container,.jupyter-widgets-slider .slider-container{overflow:visible}.widget-hslider .slider-container,.jupyter-widget-hslider .slider-container{margin-left:calc(var(--jp-widgets-slider-handle-size) / 2 - 2 * var(--jp-widgets-slider-border-width));margin-right:calc(var(--jp-widgets-slider-handle-size) / 2 - 2 * var(--jp-widgets-slider-border-width));flex:1 1 var(--jp-widgets-inline-width-short)}.widget-vbox .widget-label,.jupyter-widget-vbox .jupyter-widget-label{height:var(--jp-widgets-inline-height);line-height:var(--jp-widgets-inline-height)}.widget-vslider,.jupyter-widget-vslider{height:var(--jp-widgets-vertical-height);width:var(--jp-widgets-inline-width-tiny)}.widget-vslider .slider-container,.jupyter-widget-vslider .slider-container{flex:1 1 var(--jp-widgets-inline-width-short);margin-left:auto;margin-right:auto;margin-bottom:calc(var(--jp-widgets-slider-handle-size) / 2 - 2 * var(--jp-widgets-slider-border-width));margin-top:calc(var(--jp-widgets-slider-handle-size) / 2 - 2 * var(--jp-widgets-slider-border-width));display:flex;flex-direction:column}.progress-bar{-webkit-transition:none;-moz-transition:none;-ms-transition:none;-o-transition:none;transition:none}.progress-bar{height:var(--jp-widgets-inline-height)}.progress-bar{background-color:var(--jp-brand-color1)}.progress-bar-success{background-color:var(--jp-success-color1)}.progress-bar-info{background-color:var(--jp-info-color1)}.progress-bar-warning{background-color:var(--jp-warn-color1)}.progress-bar-danger{background-color:var(--jp-error-color1)}.progress{background-color:var(--jp-layout-color2);border:none;box-shadow:none}.widget-hprogress,.jupyter-widget-hprogress{height:var(--jp-widgets-inline-height);line-height:var(--jp-widgets-inline-height);width:var(--jp-widgets-inline-width);align-items:center}.widget-hprogress .progress,.jupyter-widget-hprogress .progress{flex-grow:1;margin-top:var(--jp-widgets-input-padding);margin-bottom:var(--jp-widgets-input-padding);align-self:stretch;height:initial}.widget-vprogress,.jupyter-widget-vprogress{height:var(--jp-widgets-vertical-height);width:var(--jp-widgets-inline-width-tiny)}.widget-vprogress .progress,.jupyter-widget-vprogress .progress{flex-grow:1;width:var(--jp-widgets-progress-thickness);margin-left:auto;margin-right:auto;margin-bottom:0}.widget-dropdown,.jupyter-widget-dropdown{height:var(--jp-widgets-inline-height);width:var(--jp-widgets-inline-width);line-height:var(--jp-widgets-inline-height)}.widget-dropdown>select,.jupyter-widget-dropdown>select{padding-right:20px;border:var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);border-radius:0;height:inherit;flex:1 1 var(--jp-widgets-inline-width-short);min-width:0;box-sizing:border-box;outline:none!important;box-shadow:none;background-color:var(--jp-widgets-input-background-color);color:var(--jp-widgets-input-color);font-size:var(--jp-widgets-font-size);vertical-align:top;padding-left:calc(var(--jp-widgets-input-padding) * 2);appearance:none;-webkit-appearance:none;-moz-appearance:none;background-repeat:no-repeat;background-size:20px;background-position:right center;background-image:var(--jp-widgets-dropdown-arrow)}.widget-dropdown>select:focus,.jupyter-widget-dropdown>select:focus{border-color:var(--jp-widgets-input-focus-border-color)}.widget-dropdown>select:disabled,.jupyter-widget-dropdown>select:disabled{opacity:var(--jp-widgets-disabled-opacity)}.widget-dropdown>select:-moz-focusring,.jupyter-widget-dropdown>select:-moz-focusring{color:transparent;text-shadow:0 0 0 #000}.widget-select,.jupyter-widget-select{width:var(--jp-widgets-inline-width);line-height:var(--jp-widgets-inline-height);align-items:flex-start}.widget-select>select,.jupyter-widget-select>select{border:var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);background-color:var(--jp-widgets-input-background-color);color:var(--jp-widgets-input-color);font-size:var(--jp-widgets-font-size);flex:1 1 var(--jp-widgets-inline-width-short);outline:none!important;overflow:auto;height:inherit;padding-top:5px}.widget-select>select:focus,.jupyter-widget-select>select:focus{border-color:var(--jp-widgets-input-focus-border-color)}.wiget-select>select>option,.jupyter-wiget-select>select>option{padding-left:var(--jp-widgets-input-padding);line-height:var(--jp-widgets-inline-height);padding-top:calc(var(--jp-widgets-inline-height) - var(--jp-widgets-font-size) / 2);padding-bottom:calc(var(--jp-widgets-inline-height) - var(--jp-widgets-font-size) / 2)}.widget-toggle-buttons,.jupyter-widget-toggle-buttons{line-height:var(--jp-widgets-inline-height)}.widget-toggle-buttons .widget-toggle-button,.jupyter-widget-toggle-buttons .jupyter-widget-toggle-button{margin-left:var(--jp-widgets-margin);margin-right:var(--jp-widgets-margin)}.widget-toggle-buttons .jupyter-button:disabled,.jupyter-widget-toggle-buttons .jupyter-button:disabled{opacity:var(--jp-widgets-disabled-opacity)}.widget-radio,.jupyter-widget-radio{width:var(--jp-widgets-inline-width);line-height:var(--jp-widgets-inline-height)}.widget-radio-box,.jupyter-widget-radio-box{display:flex;flex-direction:column;align-items:stretch;box-sizing:border-box;flex-grow:1;margin-bottom:var(--jp-widgets-radio-item-height-adjustment)}.widget-radio-box-vertical,.jupyter-widget-radio-box-vertical{flex-direction:column}.widget-radio-box-horizontal,.jupyter-widget-radio-box-horizontal{flex-direction:row}.widget-radio-box label,.jupyter-widget-radio-box label{height:var(--jp-widgets-radio-item-height);line-height:var(--jp-widgets-radio-item-height);font-size:var(--jp-widgets-font-size)}.widget-radio-box-horizontal label,.jupyter-widget-radio-box-horizontal label{margin:0 calc(var(--jp-widgets-input-padding) * 2) 0 0}.widget-radio-box input,.jupyter-widget-radio-box input{height:var(--jp-widgets-radio-item-height);line-height:var(--jp-widgets-radio-item-height);margin:0 calc(var(--jp-widgets-input-padding) * 2) 0 1px;float:left}.widget-colorpicker,.jupyter-widget-colorpicker{width:var(--jp-widgets-inline-width);height:var(--jp-widgets-inline-height);line-height:var(--jp-widgets-inline-height)}.widget-colorpicker>.widget-colorpicker-input,.jupyter-widget-colorpicker>.jupyter-widget-colorpicker-input{flex-grow:1;flex-shrink:1;min-width:var(--jp-widgets-inline-width-tiny)}.widget-colorpicker input[type=color],.jupyter-widget-colorpicker input[type=color]{width:var(--jp-widgets-inline-height);height:var(--jp-widgets-inline-height);padding:0 2px;background:var(--jp-widgets-input-background-color);color:var(--jp-widgets-input-color);border:var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);border-left:none;flex-grow:0;flex-shrink:0;box-sizing:border-box;align-self:stretch;outline:none!important}.widget-colorpicker.concise input[type=color],.jupyter-widget-colorpicker.concise input[type=color]{border-left:var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color)}.widget-colorpicker input[type=color]:focus,.widget-colorpicker input[type=text]:focus,.jupyter-widget-colorpicker input[type=color]:focus,.jupyter-widget-colorpicker input[type=text]:focus{border-color:var(--jp-widgets-input-focus-border-color)}.widget-colorpicker input[type=text],.jupyter-widget-colorpicker input[type=text]{flex-grow:1;outline:none!important;height:var(--jp-widgets-inline-height);line-height:var(--jp-widgets-inline-height);background:var(--jp-widgets-input-background-color);color:var(--jp-widgets-input-color);border:var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);font-size:var(--jp-widgets-font-size);padding:var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding) * 2);min-width:0;flex-shrink:1;box-sizing:border-box}.widget-colorpicker input[type=text]:disabled,.jupyter-widget-colorpicker input[type=text]:disabled{opacity:var(--jp-widgets-disabled-opacity)}.widget-datepicker,.jupyter-widget-datepicker{width:var(--jp-widgets-inline-width);height:var(--jp-widgets-inline-height);line-height:var(--jp-widgets-inline-height)}.widget-datepicker input[type=date],.jupyter-widget-datepicker input[type=date]{flex-grow:1;flex-shrink:1;min-width:0;outline:none!important;height:var(--jp-widgets-inline-height);border:var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color);background-color:var(--jp-widgets-input-background-color);color:var(--jp-widgets-input-color);font-size:var(--jp-widgets-font-size);padding:var(--jp-widgets-input-padding) calc(var(--jp-widgets-input-padding) * 2);box-sizing:border-box}.widget-datepicker input[type=date]:focus,.jupyter-widget-datepicker input[type=date]:focus{border-color:var(--jp-widgets-input-focus-border-color)}.widget-datepicker input[type=date]:invalid,.jupyter-widget-datepicker input[type=date]:invalid{border-color:var(--jp-warn-color1)}.widget-datepicker input[type=date]:disabled,.jupyter-widget-datepicker input[type=date]:disabled{opacity:var(--jp-widgets-disabled-opacity)}.widget-play,.jupyter-widget-play{width:var(--jp-widgets-inline-width-short);display:flex;align-items:stretch}.widget-play .jupyter-button,.jupyter-widget-play .jupyter-button{flex-grow:1;height:auto}.widget-play .jupyter-button:disabled,.jupyter-widget-play .jupyter-button:disabled{opacity:var(--jp-widgets-disabled-opacity)}.jupyter-widgets.widget-tab,.jupyter-widgets.jupyter-widget-tab{display:flex;flex-direction:column}.jupyter-widgets.widget-tab>.p-TabBar,.jupyter-widgets.jupyter-widget-tab>.p-TabBar,.jupyter-widgets.jupyter-widget-tab>.lm-TabBar{overflow-x:visible;overflow-y:visible}.jupyter-widgets.widget-tab>.p-TabBar>.p-TabBar-content,.jupyter-widgets.jupyter-widget-tab>.p-TabBar>.p-TabBar-content,.jupyter-widgets.jupyter-widget-tab>.lm-TabBar>.lm-TabBar-content{align-items:flex-end;min-width:0;min-height:0}.jupyter-widgets.widget-tab>.widget-tab-contents,.jupyter-widgets.jupyter-widget-tab>.widget-tab-contents{width:100%;box-sizing:border-box;margin:0;background:var(--jp-layout-color1);color:var(--jp-ui-font-color1);border:var(--jp-border-width) solid var(--jp-border-color1);padding:var(--jp-widgets-container-padding);flex-grow:1;overflow:auto}.jupyter-widgets.widget-tab>.p-TabBar,.jupyter-widgets.jupyter-widget-tab>.p-TabBar,.jupyter-widgets.jupyter-widget-tab>.lm-TabBar{font:var(--jp-widgets-font-size) Helvetica,Arial,sans-serif;min-height:calc(var(--jp-widgets-horizontal-tab-height) + var(--jp-border-width))}.jupyter-widgets.widget-tab>.p-TabBar .p-TabBar-tab,.jupyter-widgets.jupyter-widget-tab>.p-TabBar .p-TabBar-tab,.jupyter-widgets.jupyter-widget-tab>.lm-TabBar .lm-TabBar-tab{flex:0 1 var(--jp-widgets-horizontal-tab-width);min-width:35px;min-height:calc(var(--jp-widgets-horizontal-tab-height) + var(--jp-border-width));line-height:var(--jp-widgets-horizontal-tab-height);margin-left:calc(-1 * var(--jp-border-width));padding:0 10px;background:var(--jp-layout-color2);color:var(--jp-ui-font-color2);border:var(--jp-border-width) solid var(--jp-border-color1);border-bottom:none;position:relative}.jupyter-widgets.widget-tab>.p-TabBar .p-TabBar-tab.p-mod-current,.jupyter-widgets.jupyter-widget-tab>.p-TabBar .p-TabBar-tab.p-mod-current,.jupyter-widgets.jupyter-widget-tab>.lm-TabBar .lm-TabBar-tab.lm-mod-current{color:var(--jp-ui-font-color0);background:var(--jp-layout-color1);min-height:calc(var(--jp-widgets-horizontal-tab-height) + 2 * var(--jp-border-width));transform:translateY(var(--jp-border-width));overflow:visible}.jupyter-widgets.widget-tab>.p-TabBar .p-TabBar-tab.p-mod-current:before,.jupyter-widgets.jupyter-widget-tab>.p-TabBar .p-TabBar-tab.p-mod-current:before,.jupyter-widgets.jupyter-widget-tab>.lm-TabBar .lm-TabBar-tab.lm-mod-current:before{position:absolute;top:calc(-1 * var(--jp-border-width));left:calc(-1 * var(--jp-border-width));content:"";height:var(--jp-widgets-horizontal-tab-top-border);width:calc(100% + 2 * var(--jp-border-width));background:var(--jp-brand-color1)}.jupyter-widgets.widget-tab>.p-TabBar .p-TabBar-tab:first-child,.jupyter-widgets.jupyter-widget-tab>.p-TabBar .p-TabBar-tab:first-child,.jupyter-widgets.jupyter-widget-tab>.lm-TabBar .lm-TabBar-tab:first-child{margin-left:0}.jupyter-widgets.widget-tab>.p-TabBar .p-TabBar-tab:hover:not(.p-mod-current),.jupyter-widgets.jupyter-widget-tab>.p-TabBar .p-TabBar-tab:hover:not(.p-mod-current),.jupyter-widgets.jupyter-widget-tab>.lm-TabBar .lm-TabBar-tab:hover:not(.lm-mod-current){background:var(--jp-layout-color1);color:var(--jp-ui-font-color1)}.jupyter-widgets.widget-tab>.p-TabBar .p-mod-closable>.p-TabBar-tabCloseIcon,.jupyter-widgets.jupyter-widget-tab>.p-TabBar .p-mod-closable>.p-TabBar-tabCloseIcon,.jupyter-widgets.jupyter-widget-tab>.lm-TabBar .lm-mod-closable>.lm-TabBar-tabCloseIcon{margin-left:4px}.jupyter-widgets.widget-tab>.p-TabBar .p-mod-closable>.p-TabBar-tabCloseIcon:before,.jupyter-widgets.jupyter-widget-widget-tab>.p-TabBar .p-mod-closable>.p-TabBar-tabCloseIcon:before,.jupyter-widgets.jupyter-widget-tab>.lm-TabBar .lm-mod-closable>.lm-TabBar-tabCloseIcon:before{font-family:FontAwesome;content:""}.jupyter-widgets.widget-tab>.p-TabBar .p-TabBar-tabIcon,.jupyter-widgets.widget-tab>.p-TabBar .p-TabBar-tabLabel,.jupyter-widgets.widget-tab>.p-TabBar .p-TabBar-tabCloseIcon,.jupyter-widgets.jupyter-widget-tab>.p-TabBar .p-TabBar-tabIcon,.jupyter-widgets.jupyter-widget-tab>.p-TabBar .p-TabBar-tabLabel,.jupyter-widgets.jupyter-widget-tab>.p-TabBar .p-TabBar-tabCloseIcon,.jupyter-widgets.jupyter-widget-tab>.lm-TabBar .lm-TabBar-tabIcon,.jupyter-widgets.jupyter-widget-tab>.lm-TabBar .lm-TabBar-tabLabel,.jupyter-widgets.jupyter-widget-tab>.lm-TabBar .lm-TabBar-tabCloseIcon{line-height:var(--jp-widgets-horizontal-tab-height)}.jupyter-widget-Collapse{display:flex;flex-direction:column;align-items:stretch}.jupyter-widget-Collapse-header{padding:var(--jp-widgets-input-padding);cursor:pointer;color:var(--jp-ui-font-color2);background-color:var(--jp-layout-color2);border:var(--jp-widgets-border-width) solid var(--jp-border-color1);padding:calc(var(--jp-widgets-container-padding) * 2 / 3) var(--jp-widgets-container-padding);font-weight:700}.jupyter-widget-Collapse-header:hover{background-color:var(--jp-layout-color1);color:var(--jp-ui-font-color1)}.jupyter-widget-Collapse-open>.jupyter-widget-Collapse-header{background-color:var(--jp-layout-color1);color:var(--jp-ui-font-color0);cursor:default;border-bottom:none}.jupyter-widget-Collapse-contents{padding:var(--jp-widgets-container-padding);background-color:var(--jp-layout-color1);color:var(--jp-ui-font-color1);border-left:var(--jp-widgets-border-width) solid var(--jp-border-color1);border-right:var(--jp-widgets-border-width) solid var(--jp-border-color1);border-bottom:var(--jp-widgets-border-width) solid var(--jp-border-color1);overflow:auto}.jupyter-widget-Accordion{display:flex;flex-direction:column;align-items:stretch}.jupyter-widget-Accordion .jupyter-widget-Collapse{margin-bottom:0}.jupyter-widget-Accordion .jupyter-widget-Collapse+.jupyter-widget-Collapse{margin-top:4px}.widget-html,.widget-htmlmath,.jupyter-widget-html,.jupyter-widget-htmlmath{font-size:var(--jp-widgets-font-size)}.widget-html>.widget-html-content,.widget-htmlmath>.widget-html-content,.jupyter-widget-html>.jupyter-widget-html-content,.jupyter-widget-htmlmath>.jupyter-widget-html-content{align-self:stretch;flex-grow:1;flex-shrink:1;line-height:var(--jp-widgets-inline-height);position:relative}.widget-image,.jupyter-widget-image{max-width:100%;height:auto}
package/dist/index.html CHANGED
@@ -64,8 +64,8 @@
64
64
  }
65
65
  }
66
66
  </script>
67
- <script type="module" crossorigin src="/assets/index-BvrHu37J.js"></script>
68
- <link rel="stylesheet" crossorigin href="/assets/index-Dfj_xsKU.css">
67
+ <script type="module" crossorigin src="/assets/index-DK4nS74r.js"></script>
68
+ <link rel="stylesheet" crossorigin href="/assets/index-DzjpmUak.css">
69
69
  </head>
70
70
  <body class="bg-slate-50 text-slate-900 antialiased">
71
71
  <div id="root"></div>
@@ -45,6 +45,7 @@ exports.persistSessionToken = persistSessionToken;
45
45
  exports.authMiddleware = authMiddleware;
46
46
  exports.authWebSocketMiddleware = authWebSocketMiddleware;
47
47
  const url_1 = require("url");
48
+ const crypto_1 = require("crypto");
48
49
  const fs = __importStar(require("fs"));
49
50
  const path = __importStar(require("path"));
50
51
  const os = __importStar(require("os"));
@@ -132,13 +133,28 @@ function extractClusterSecret(req) {
132
133
  }
133
134
  return value;
134
135
  }
136
+ function secretsEqual(a, b) {
137
+ const ab = Buffer.from(a);
138
+ const bb = Buffer.from(b);
139
+ if (ab.length !== bb.length)
140
+ return false;
141
+ return (0, crypto_1.timingSafeEqual)(ab, bb);
142
+ }
143
+ /** Loopback = same machine (or arrived through an ssh tunnel, which
144
+ * terminates on this machine) — the trust boundary for the on-disk
145
+ * session-token fallback used by MCP/CLI tools. */
146
+ function isLoopback(addr) {
147
+ if (!addr)
148
+ return false;
149
+ return addr === '127.0.0.1' || addr === '::1' || addr === '::ffff:127.0.0.1' || addr.startsWith('127.');
150
+ }
135
151
  function hasValidClusterSecret(req) {
136
152
  const clusterSecret = getClusterSecret();
137
153
  if (!clusterSecret) {
138
154
  return false;
139
155
  }
140
156
  const provided = extractClusterSecret(req);
141
- return Boolean(provided && provided === clusterSecret);
157
+ return Boolean(provided && secretsEqual(provided, clusterSecret));
142
158
  }
143
159
  function isClusterRoute(pathname) {
144
160
  return pathname.startsWith('/api/servers') || pathname.startsWith('/servers');
@@ -205,8 +221,10 @@ async function authMiddleware(request, reply) {
205
221
  let token = extractToken(request);
206
222
  // Fallback: if no token in request, check the persisted session token file.
207
223
  // This allows MCP servers and CLI tools to piggyback on the browser's auth
208
- // without needing their own TOTP flow.
209
- if (!token) {
224
+ // without needing their own TOTP flow. Loopback-only: those tools run on
225
+ // this machine (or reach it through an ssh tunnel, which lands on
226
+ // loopback); a token-less request from the network gets no free ride.
227
+ if (!token && isLoopback(request.ip)) {
210
228
  token = readSessionToken();
211
229
  }
212
230
  if (!token) {
@@ -245,7 +263,7 @@ function authWebSocketMiddleware(request) {
245
263
  return false;
246
264
  }
247
265
  const provided = extractClusterSecret(request);
248
- if (provided !== clusterSecret) {
266
+ if (!provided || !secretsEqual(provided, clusterSecret)) {
249
267
  console.log('[Auth] WebSocket rejected - invalid cluster secret');
250
268
  return false;
251
269
  }
@@ -48,8 +48,8 @@ const os = __importStar(require("os"));
48
48
  const otplib_1 = require("otplib");
49
49
  const jwt = __importStar(require("jsonwebtoken"));
50
50
  const qrcode = __importStar(require("qrcode-terminal"));
51
- // Config file location
52
- const NEBULA_DIR = path.join(os.homedir(), '.nebula');
51
+ // Config file location (env override is a test seam)
52
+ const NEBULA_DIR = process.env.NEBULA_AUTH_DIR || path.join(os.homedir(), '.nebula');
53
53
  const AUTH_CONFIG_FILE = path.join(NEBULA_DIR, 'auth.json');
54
54
  // JWT settings
55
55
  const JWT_SECRET_LENGTH = 64;
@@ -58,8 +58,10 @@ const LONG_SESSION_DAYS = 30;
58
58
  // Rate limiting: max 5 attempts per 30 seconds
59
59
  const MAX_ATTEMPTS = 5;
60
60
  const WINDOW_MS = 30000;
61
- // Temporary debugging setting: accept up to five TOTP steps before and after
62
- // current time while investigating clock skew and login flow issues.
61
+ // DELIBERATELY wide (±5 steps = ±2.5min): cluster login nodes have shipped
62
+ // with minutes of clock skew and a tight window locks the user out entirely.
63
+ // Availability beats the marginal brute-force surface here — do not "fix"
64
+ // this back to 1 without confirming server NTP is trustworthy everywhere.
63
65
  const TOTP_WINDOW_STEPS = 5;
64
66
  otplib_1.authenticator.options = {
65
67
  ...otplib_1.authenticator.options,
@@ -105,6 +107,15 @@ class AuthService {
105
107
  }
106
108
  catch (err) {
107
109
  console.error('[Auth] Failed to load auth config:', err);
110
+ // NEVER silently overwrite an existing-but-unreadable config: it may
111
+ // hold the user's only copy of their TOTP secret (e.g. truncated by a
112
+ // quota-full write). Preserve it for recovery before regenerating.
113
+ try {
114
+ const backup = `${AUTH_CONFIG_FILE}.corrupt-${Date.now()}`;
115
+ fs.copyFileSync(AUTH_CONFIG_FILE, backup);
116
+ console.error(`[Auth] Unreadable auth config backed up to ${backup} — regenerating fresh secrets.`);
117
+ }
118
+ catch { /* backup is best-effort */ }
108
119
  }
109
120
  }
110
121
  // No config exists - generate new secrets
@@ -137,9 +148,28 @@ class AuthService {
137
148
  saveConfig() {
138
149
  if (!this.config)
139
150
  return;
140
- fs.writeFileSync(AUTH_CONFIG_FILE, JSON.stringify(this.config, null, 2), {
141
- mode: 0o600, // Owner read/write only
142
- });
151
+ // Atomic write (tmp + rename): a plain writeFileSync truncates BEFORE
152
+ // writing, so a quota-full/crashed write leaves an empty auth.json and
153
+ // the TOTP secret then exists only in this process's memory — exactly the
154
+ // failure that forces users to re-enroll after the next restart. With
155
+ // rename, a failed write leaves the previous file untouched.
156
+ const tmp = `${AUTH_CONFIG_FILE}.${process.pid}.tmp`;
157
+ try {
158
+ fs.writeFileSync(tmp, JSON.stringify(this.config, null, 2), { mode: 0o600 });
159
+ fs.renameSync(tmp, AUTH_CONFIG_FILE);
160
+ }
161
+ catch (err) {
162
+ try {
163
+ fs.unlinkSync(tmp);
164
+ }
165
+ catch { /* nothing partial to clean */ }
166
+ // Loud, specific failure: the server keeps working from memory, but the
167
+ // secret will NOT survive a restart — the user must be able to see why.
168
+ console.error(`[Auth] FAILED to persist auth config to ${AUTH_CONFIG_FILE} — ` +
169
+ `2FA secrets exist ONLY in memory and will be lost on restart. ` +
170
+ `Check disk quota/permissions. Error:`, err);
171
+ throw err;
172
+ }
143
173
  }
144
174
  /**
145
175
  * Print QR code and manual key to terminal for initial setup
@@ -10,10 +10,21 @@ declare class ClientRegistration {
10
10
  private registrationRetryTimeout;
11
11
  private registered;
12
12
  private serverId;
13
+ /**
14
+ * The local IP on the interface that ROUTES TO the main server, learned by
15
+ * opening a TCP connection to it. Registering os.hostname() broke on
16
+ * clusters where compute nodes report an FQDN the controller cannot
17
+ * resolve (cri22cn416.cri.uchicago.edu vs the resolvable cri22cn416):
18
+ * every controller→node fetch died as 500 "fetch failed" while heartbeats
19
+ * (outbound) kept the allocation looking healthy. An IP needs no DNS at
20
+ * all, and the interface that reaches the controller is the one the
21
+ * controller can reach back on.
22
+ */
23
+ private routableLocalAddress;
13
24
  /**
14
25
  * Initialize client registration from environment variables
15
26
  */
16
- initFromEnv(localPort: number): void;
27
+ initFromEnv(localPort: number): Promise<void>;
17
28
  /**
18
29
  * Register with the main server
19
30
  */
@@ -43,6 +43,7 @@ exports.clientRegistration = void 0;
43
43
  const server_registry_1 = require("./server-registry");
44
44
  const resource_service_1 = require("../resources/resource-service");
45
45
  const os = __importStar(require("os"));
46
+ const net = __importStar(require("net"));
46
47
  const HEARTBEAT_INTERVAL_MS = 30_000; // 30 seconds
47
48
  const REGISTRATION_RETRY_MS = 10_000; // 10 seconds retry on failure
48
49
  class ClientRegistration {
@@ -51,24 +52,58 @@ class ClientRegistration {
51
52
  registrationRetryTimeout = null;
52
53
  registered = false;
53
54
  serverId = null;
55
+ /**
56
+ * The local IP on the interface that ROUTES TO the main server, learned by
57
+ * opening a TCP connection to it. Registering os.hostname() broke on
58
+ * clusters where compute nodes report an FQDN the controller cannot
59
+ * resolve (cri22cn416.cri.uchicago.edu vs the resolvable cri22cn416):
60
+ * every controller→node fetch died as 500 "fetch failed" while heartbeats
61
+ * (outbound) kept the allocation looking healthy. An IP needs no DNS at
62
+ * all, and the interface that reaches the controller is the one the
63
+ * controller can reach back on.
64
+ */
65
+ routableLocalAddress(mainServerUrl) {
66
+ return new Promise((resolve) => {
67
+ try {
68
+ const u = new URL(mainServerUrl);
69
+ const port = Number(u.port) || (u.protocol === 'https:' ? 443 : 80);
70
+ const sock = net.connect({ host: u.hostname, port, timeout: 3000 }, () => {
71
+ const addr = sock.localAddress || null;
72
+ sock.destroy();
73
+ resolve(addr);
74
+ });
75
+ sock.on('error', () => resolve(null));
76
+ sock.on('timeout', () => { sock.destroy(); resolve(null); });
77
+ }
78
+ catch {
79
+ resolve(null);
80
+ }
81
+ });
82
+ }
54
83
  /**
55
84
  * Initialize client registration from environment variables
56
85
  */
57
- initFromEnv(localPort) {
86
+ async initFromEnv(localPort) {
58
87
  const mainServerUrl = process.env.NEBULA_MAIN_SERVER;
59
88
  if (!mainServerUrl) {
60
89
  console.log('[ClientRegistration] No NEBULA_MAIN_SERVER set, running as standalone');
61
90
  return;
62
91
  }
63
- const localHost = process.env.NEBULA_HOST || os.hostname();
92
+ // Explicit override → routable IP → hostname (last resort; may be an
93
+ // FQDN the controller can't resolve).
94
+ const localHost = process.env.NEBULA_HOST
95
+ || (await this.routableLocalAddress(mainServerUrl))
96
+ || os.hostname();
64
97
  const serverName = process.env.NEBULA_SERVER_NAME || os.hostname();
65
98
  const secret = process.env.NEBULA_CLUSTER_SECRET;
99
+ const allocationToken = process.env.NEBULA_ALLOCATION_TOKEN;
66
100
  this.config = {
67
101
  mainServerUrl: mainServerUrl.replace(/\/$/, ''), // Remove trailing slash
68
102
  localHost,
69
103
  localPort,
70
104
  serverName,
71
105
  secret,
106
+ allocationToken,
72
107
  };
73
108
  // Set local server ID
74
109
  const serverId = `${localHost}:${localPort}`;
@@ -101,6 +136,7 @@ class ClientRegistration {
101
136
  name: this.config.serverName,
102
137
  secret: this.config.secret,
103
138
  resources, // Include resources in registration
139
+ allocationToken: this.config.allocationToken,
104
140
  }),
105
141
  });
106
142
  if (!response.ok) {
@@ -158,9 +158,17 @@ async function getRemoteKernelStatus(sessionId) {
158
158
  if (!server) {
159
159
  throw new Error(`Server not found: ${serverId}`);
160
160
  }
161
- const response = await fetch(`${server.url}/api/kernels/${remoteSessionId}/status`, {
162
- headers: { ...getClusterHeaders() },
163
- });
161
+ let response;
162
+ try {
163
+ response = await fetch(`${server.url}/api/kernels/${remoteSessionId}/status`, {
164
+ headers: { ...getClusterHeaders() },
165
+ });
166
+ }
167
+ catch (err) {
168
+ // Connection-level failure (refused/reset) — the node may be gone.
169
+ server_registry_1.serverRegistry.reportUnreachable(serverId);
170
+ throw err;
171
+ }
164
172
  if (!response.ok) {
165
173
  const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
166
174
  throw new Error(error.detail || 'Failed to get kernel status');
@@ -291,6 +299,28 @@ function createWebSocketProxy(sessionId, clientWs) {
291
299
  const remoteWs = new ws_1.WebSocket(wsUrl, {
292
300
  headers: getClusterHeaders(),
293
301
  });
302
+ // The client (UI) sends its messages as soon as *its* socket is open — which
303
+ // is before this remote socket finishes connecting. Those early messages
304
+ // (notably the one-time `sync_outputs` that subscribes to the kernel's output
305
+ // stream, and the first `execute`) would otherwise be silently dropped,
306
+ // leaving the cell running with no output. Buffer them and flush on open.
307
+ const pending = [];
308
+ const sendToRemote = (data, isBinary) => {
309
+ if (remoteWs.readyState === ws_1.WebSocket.OPEN) {
310
+ remoteWs.send(isBinary ? data : (typeof data === 'string' ? data : data.toString()));
311
+ }
312
+ else {
313
+ pending.push({ data, isBinary });
314
+ }
315
+ };
316
+ remoteWs.on('open', () => {
317
+ for (const { data, isBinary } of pending) {
318
+ if (remoteWs.readyState === ws_1.WebSocket.OPEN) {
319
+ remoteWs.send(isBinary ? data : (typeof data === 'string' ? data : data.toString()));
320
+ }
321
+ }
322
+ pending.length = 0;
323
+ });
294
324
  // Forward messages from remote to client
295
325
  remoteWs.on('message', (data, isBinary) => {
296
326
  if (clientWs.readyState !== ws_1.WebSocket.OPEN) {
@@ -302,20 +332,18 @@ function createWebSocketProxy(sessionId, clientWs) {
302
332
  }
303
333
  clientWs.send(typeof data === 'string' ? data : data.toString());
304
334
  });
305
- // Forward messages from client to remote
335
+ // Forward messages from client to remote (buffered until remote is open)
306
336
  clientWs.on('message', (data, isBinary) => {
307
- if (remoteWs.readyState !== ws_1.WebSocket.OPEN) {
308
- return;
309
- }
310
- if (isBinary) {
311
- remoteWs.send(data);
312
- return;
313
- }
314
- remoteWs.send(typeof data === 'string' ? data : data.toString());
337
+ sendToRemote(data, isBinary);
315
338
  });
316
339
  // Handle remote close
317
340
  remoteWs.on('close', () => {
318
341
  console.log(`[KernelProxy] Remote WebSocket closed for ${sessionId}`);
342
+ // The kernel channel dropping is the earliest signal a compute node
343
+ // died (walltime, scancel, crash) — let the allocation layer verify.
344
+ const { serverId: sid } = parseSessionId(sessionId);
345
+ if (sid)
346
+ server_registry_1.serverRegistry.reportUnreachable(sid);
319
347
  if (clientWs.readyState === ws_1.WebSocket.OPEN) {
320
348
  clientWs.close();
321
349
  }
@@ -330,6 +358,9 @@ function createWebSocketProxy(sessionId, clientWs) {
330
358
  // Handle errors
331
359
  remoteWs.on('error', (err) => {
332
360
  console.error(`[KernelProxy] Remote WebSocket error for ${sessionId}:`, err);
361
+ const { serverId: sid } = parseSessionId(sessionId);
362
+ if (sid)
363
+ server_registry_1.serverRegistry.reportUnreachable(sid);
333
364
  });
334
365
  // Track the proxy
335
366
  const session = proxiedSessions.get(sessionId);
@@ -17,6 +17,8 @@ export interface PeerServer {
17
17
  status: 'online' | 'offline' | 'unknown';
18
18
  kernelspecs?: string[];
19
19
  resources?: ServerResources;
20
+ allocationToken?: string;
21
+ allocationId?: string;
20
22
  }
21
23
  export interface RegisterRequest {
22
24
  host: string;
@@ -24,6 +26,7 @@ export interface RegisterRequest {
24
26
  name?: string;
25
27
  secret?: string;
26
28
  resources?: ServerResources;
29
+ allocationToken?: string;
27
30
  }
28
31
  declare class ServerRegistry {
29
32
  private servers;
@@ -72,10 +75,24 @@ declare class ServerRegistry {
72
75
  * Get all registered peer servers
73
76
  */
74
77
  getAllServers(): PeerServer[];
78
+ /**
79
+ * Find a registered server by its scheduler allocation token.
80
+ * Used by the allocation service to correlate a job's registration.
81
+ */
82
+ getServerByAllocationToken(token: string): PeerServer | undefined;
75
83
  /**
76
84
  * Get all online peer servers
77
85
  */
78
86
  getOnlineServers(): PeerServer[];
87
+ private lostListeners;
88
+ onServerLost(cb: (server: PeerServer) => void): void;
89
+ private notifyLost;
90
+ /**
91
+ * A request proxied to this server just failed at the connection level
92
+ * (refused/reset/timeout). Don't mark it offline on one failure — that's
93
+ * the heartbeat's job — but do tell listeners so they can verify fast.
94
+ */
95
+ reportUnreachable(serverId: string): void;
79
96
  /**
80
97
  * Check server health and update status
81
98
  */
@@ -9,7 +9,8 @@
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.serverRegistry = void 0;
11
11
  const cluster_secret_1 = require("./cluster-secret");
12
- const HEARTBEAT_TIMEOUT_MS = 90_000; // 90 seconds (3 missed heartbeats at 30s interval)
12
+ const HEARTBEAT_TIMEOUT_MS = 90_000; // 90s (3 missed heartbeats at 30s interval) -> mark offline
13
+ const OFFLINE_REAP_MS = 180_000; // 3 min without a heartbeat -> remove from the list entirely
13
14
  const CLEANUP_INTERVAL_MS = 30_000; // Check for stale servers every 30s
14
15
  class ServerRegistry {
15
16
  servers = new Map();
@@ -88,6 +89,9 @@ class ServerRegistry {
88
89
  if (request.resources) {
89
90
  existing.resources = request.resources;
90
91
  }
92
+ if (request.allocationToken) {
93
+ existing.allocationToken = request.allocationToken;
94
+ }
91
95
  console.log(`[ServerRegistry] Updated registration for ${serverId}`);
92
96
  return { success: true, serverId };
93
97
  }
@@ -102,6 +106,7 @@ class ServerRegistry {
102
106
  lastHeartbeat: now,
103
107
  status: 'online',
104
108
  resources: request.resources,
109
+ allocationToken: request.allocationToken,
105
110
  };
106
111
  this.servers.set(serverId, server);
107
112
  console.log(`[ServerRegistry] Registered new server: ${serverId} (${request.name || 'unnamed'})`);
@@ -144,23 +149,68 @@ class ServerRegistry {
144
149
  getAllServers() {
145
150
  return Array.from(this.servers.values());
146
151
  }
152
+ /**
153
+ * Find a registered server by its scheduler allocation token.
154
+ * Used by the allocation service to correlate a job's registration.
155
+ */
156
+ getServerByAllocationToken(token) {
157
+ if (!token)
158
+ return undefined;
159
+ for (const server of this.servers.values()) {
160
+ if (server.allocationToken === token)
161
+ return server;
162
+ }
163
+ return undefined;
164
+ }
147
165
  /**
148
166
  * Get all online peer servers
149
167
  */
150
168
  getOnlineServers() {
151
169
  return this.getAllServers().filter(s => s.status === 'online');
152
170
  }
171
+ // Listeners fired when a peer looks dead (heartbeat timeout, or a proxy
172
+ // request to it failed at the connection level). Lets the allocation
173
+ // service react instantly instead of waiting for its next squeue poll.
174
+ lostListeners = [];
175
+ onServerLost(cb) {
176
+ this.lostListeners.push(cb);
177
+ }
178
+ notifyLost(server) {
179
+ for (const cb of this.lostListeners) {
180
+ try {
181
+ cb(server);
182
+ }
183
+ catch { /* listener errors must not break the sweep */ }
184
+ }
185
+ }
186
+ /**
187
+ * A request proxied to this server just failed at the connection level
188
+ * (refused/reset/timeout). Don't mark it offline on one failure — that's
189
+ * the heartbeat's job — but do tell listeners so they can verify fast.
190
+ */
191
+ reportUnreachable(serverId) {
192
+ const server = this.servers.get(serverId);
193
+ if (server)
194
+ this.notifyLost(server);
195
+ }
153
196
  /**
154
197
  * Check server health and update status
155
198
  */
156
199
  checkServerHealth() {
157
200
  const now = Date.now();
158
- for (const server of this.servers.values()) {
159
- if (now - server.lastHeartbeat > HEARTBEAT_TIMEOUT_MS) {
160
- if (server.status === 'online') {
161
- console.log(`[ServerRegistry] Server ${server.id} marked offline (heartbeat timeout)`);
162
- server.status = 'offline';
163
- }
201
+ for (const [id, server] of this.servers) {
202
+ const stale = now - server.lastHeartbeat;
203
+ // Reap long-gone servers so dead compute allocations / peers don't linger
204
+ // in the list forever. A recovered server just re-registers and reappears.
205
+ if (stale > OFFLINE_REAP_MS) {
206
+ this.servers.delete(id);
207
+ console.log(`[ServerRegistry] Server ${id} removed (offline for ${Math.round(stale / 1000)}s)`);
208
+ continue;
209
+ }
210
+ if (stale > HEARTBEAT_TIMEOUT_MS && server.status === 'online') {
211
+ console.log(`[ServerRegistry] Server ${server.id} marked offline (heartbeat timeout)`);
212
+ server.status = 'offline';
213
+ this.notifyLost(server);
164
214
  }
165
215
  }
166
216
  }