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.
- package/README.md +110 -16
- package/dist/assets/errorwidget-Wy5ajPiM.js +5 -0
- package/dist/assets/fa-brands-400-CEJbCg16.woff +0 -0
- package/dist/assets/fa-brands-400-CSYNqBb_.ttf +0 -0
- package/dist/assets/fa-brands-400-DnkPfk3o.eot +0 -0
- package/dist/assets/fa-brands-400-UxlILjvJ.woff2 +0 -0
- package/dist/assets/fa-brands-400-cH1MgKbP.svg +3717 -0
- package/dist/assets/fa-regular-400-BhTwtT8w.eot +0 -0
- package/dist/assets/fa-regular-400-D1vz6WBx.ttf +0 -0
- package/dist/assets/fa-regular-400-DFnMcJPd.woff +0 -0
- package/dist/assets/fa-regular-400-DGzu1beS.woff2 +0 -0
- package/dist/assets/fa-regular-400-gwj8Pxq-.svg +801 -0
- package/dist/assets/fa-solid-900-B4ZZ7kfP.svg +5034 -0
- package/dist/assets/fa-solid-900-B6Axprfb.eot +0 -0
- package/dist/assets/fa-solid-900-BUswJgRo.woff2 +0 -0
- package/dist/assets/fa-solid-900-DOXgCApm.woff +0 -0
- package/dist/assets/fa-solid-900-mxuxnBEa.ttf +0 -0
- package/dist/assets/index-B_Bs5koo.js +1 -0
- package/dist/assets/index-BtWv4MIT.css +7 -0
- package/dist/assets/index-DK4nS74r.js +829 -0
- package/dist/assets/index-DeQN03nW.js +81 -0
- package/dist/assets/index-DzjpmUak.css +32 -0
- package/dist/assets/index-g6MjT-VG.js +1 -0
- package/dist/assets/services-shim-BirHVsih.js +33 -0
- package/dist/assets/viewlist-uomDf7I7.js +1 -0
- package/dist/assets/widgets-X7J3NxEn.css +1 -0
- package/dist/index.html +2 -2
- package/node-server/dist/auth/auth-middleware.js +22 -4
- package/node-server/dist/auth/auth-service.js +37 -7
- package/node-server/dist/cluster/client-registration.d.ts +12 -1
- package/node-server/dist/cluster/client-registration.js +38 -2
- package/node-server/dist/cluster/kernel-proxy.js +43 -12
- package/node-server/dist/cluster/server-registry.d.ts +17 -0
- package/node-server/dist/cluster/server-registry.js +57 -7
- package/node-server/dist/discovery/conda-locations.d.ts +73 -0
- package/node-server/dist/discovery/conda-locations.js +427 -0
- package/node-server/dist/discovery/discovery-service.d.ts +73 -4
- package/node-server/dist/discovery/discovery-service.js +211 -127
- package/node-server/dist/discovery/types.d.ts +0 -1
- package/node-server/dist/discovery/types.js +1 -2
- package/node-server/dist/environment.d.ts +12 -0
- package/node-server/dist/environment.js +104 -0
- package/node-server/dist/fs/fs-service.d.ts +86 -8
- package/node-server/dist/fs/fs-service.js +640 -99
- package/node-server/dist/fs/notebook-formats/percent.d.ts +25 -0
- package/node-server/dist/fs/notebook-formats/percent.js +286 -0
- package/node-server/dist/fs/notebook-formats/qmd.d.ts +29 -0
- package/node-server/dist/fs/notebook-formats/qmd.js +307 -0
- package/node-server/dist/fs/notebook-formats/registry.d.ts +12 -0
- package/node-server/dist/fs/notebook-formats/registry.js +77 -0
- package/node-server/dist/fs/notebook-formats/types.d.ts +37 -0
- package/node-server/dist/fs/notebook-formats/types.js +13 -0
- package/node-server/dist/fs/types.d.ts +9 -0
- package/node-server/dist/fs/types.js +8 -0
- package/node-server/dist/idle-exit.d.ts +52 -0
- package/node-server/dist/idle-exit.js +83 -0
- package/node-server/dist/index.js +198 -8
- package/node-server/dist/kernel/kernel-service.d.ts +130 -2
- package/node-server/dist/kernel/kernel-service.js +834 -64
- package/node-server/dist/kernel/kernelspec.d.ts +27 -0
- package/node-server/dist/kernel/kernelspec.js +75 -4
- package/node-server/dist/notebook/headless-handler.d.ts +12 -0
- package/node-server/dist/notebook/headless-handler.js +125 -29
- package/node-server/dist/notebook/operation-router.js +7 -2
- package/node-server/dist/notebook/undoRedoManager.d.ts +12 -1
- package/node-server/dist/notebook/undoRedoManager.js +49 -18
- package/node-server/dist/output/display-data.js +2 -0
- package/node-server/dist/private-tmp.d.ts +14 -0
- package/node-server/dist/private-tmp.js +70 -0
- package/node-server/dist/routes/autocomplete.d.ts +19 -0
- package/node-server/dist/routes/autocomplete.js +294 -0
- package/node-server/dist/routes/cluster.js +2 -2
- package/node-server/dist/routes/compute.d.ts +8 -0
- package/node-server/dist/routes/compute.js +136 -0
- package/node-server/dist/routes/fs.js +23 -6
- package/node-server/dist/routes/kernel.js +156 -3
- package/node-server/dist/routes/notebook.js +37 -3
- package/node-server/dist/routes/python.d.ts +1 -2
- package/node-server/dist/routes/python.js +139 -3
- package/node-server/dist/scheduler/allocation-service.d.ts +63 -0
- package/node-server/dist/scheduler/allocation-service.js +285 -0
- package/node-server/dist/scheduler/job-template.d.ts +30 -0
- package/node-server/dist/scheduler/job-template.js +85 -0
- package/node-server/dist/scheduler/mock-scheduler.d.ts +30 -0
- package/node-server/dist/scheduler/mock-scheduler.js +125 -0
- package/node-server/dist/scheduler/slurm-scheduler.d.ts +31 -0
- package/node-server/dist/scheduler/slurm-scheduler.js +406 -0
- package/node-server/dist/scheduler/types.d.ts +118 -0
- package/node-server/dist/scheduler/types.js +8 -0
- package/node-server/dist/scheduler/util.d.ts +7 -0
- package/node-server/dist/scheduler/util.js +20 -0
- package/node-server/dist/terminal/agent-registry.d.ts +87 -0
- package/node-server/dist/terminal/agent-registry.js +246 -0
- package/node-server/dist/terminal/binding-store.d.ts +57 -0
- package/node-server/dist/terminal/binding-store.js +0 -0
- package/node-server/dist/terminal/pty-manager.d.ts +23 -6
- package/node-server/dist/terminal/pty-manager.js +138 -19
- package/node-server/dist/terminal/server.js +155 -23
- package/node-server/dist/terminal/types.d.ts +2 -0
- package/node-server/dist/update-check.d.ts +20 -0
- package/node-server/dist/update-check.js +114 -0
- package/node-server/package.json +2 -0
- package/node_modules/nebula-autocomplete/README.md +145 -0
- package/node_modules/nebula-autocomplete/dist/backends/claude.d.ts +69 -0
- package/node_modules/nebula-autocomplete/dist/backends/claude.js +449 -0
- package/node_modules/nebula-autocomplete/dist/backends/codex.d.ts +45 -0
- package/node_modules/nebula-autocomplete/dist/backends/codex.js +108 -0
- package/node_modules/nebula-autocomplete/dist/client/fetcher.d.ts +16 -0
- package/node_modules/nebula-autocomplete/dist/client/fetcher.js +41 -0
- package/node_modules/nebula-autocomplete/dist/codemirror/ghostText.d.ts +43 -0
- package/node_modules/nebula-autocomplete/dist/codemirror/ghostText.js +175 -0
- package/node_modules/nebula-autocomplete/dist/core/engine.d.ts +17 -0
- package/node_modules/nebula-autocomplete/dist/core/engine.js +135 -0
- package/node_modules/nebula-autocomplete/dist/core/lru.d.ts +7 -0
- package/node_modules/nebula-autocomplete/dist/core/lru.js +26 -0
- package/node_modules/nebula-autocomplete/dist/core/prompt.d.ts +12 -0
- package/node_modules/nebula-autocomplete/dist/core/prompt.js +0 -0
- package/node_modules/nebula-autocomplete/dist/core/text.d.ts +53 -0
- package/node_modules/nebula-autocomplete/dist/core/text.js +169 -0
- package/node_modules/nebula-autocomplete/dist/index.d.ts +7 -0
- package/node_modules/nebula-autocomplete/dist/index.js +5 -0
- package/node_modules/nebula-autocomplete/dist/server/fastify.d.ts +40 -0
- package/node_modules/nebula-autocomplete/dist/server/fastify.js +52 -0
- package/node_modules/nebula-autocomplete/dist/transport.d.ts +55 -0
- package/node_modules/nebula-autocomplete/dist/transport.js +66 -0
- package/node_modules/nebula-autocomplete/dist/types.d.ts +103 -0
- package/node_modules/nebula-autocomplete/dist/types.js +1 -0
- package/node_modules/nebula-autocomplete/package.json +58 -0
- package/node_modules/nebula-autocomplete/server.d.ts +3 -0
- package/node_modules/nebula-autocomplete/server.js +3 -0
- package/package.json +18 -5
- package/scripts/postinstall.cjs +25 -5
- package/dist/assets/index-BvrHu37J.js +0 -648
- package/dist/assets/index-Dfj_xsKU.css +0 -32
- package/node-server/dist/llm/index.d.ts +0 -5
- package/node-server/dist/llm/index.js +0 -21
- package/node-server/dist/llm/llm-service.d.ts +0 -77
- package/node-server/dist/llm/llm-service.js +0 -454
- package/node-server/dist/llm/types.d.ts +0 -40
- package/node-server/dist/llm/types.js +0 -15
- package/node-server/dist/routes/llm.d.ts +0 -8
- 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-
|
|
68
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
62
|
-
//
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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; //
|
|
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
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
}
|