#author("2025-10-28T09:48:15+00:00","default:iseki","iseki") # Markdown #author("2025-10-28T09:48:46+00:00","default:iseki","iseki") #notemd # JupyterLab Notice プラグイン導入メモ(jnotice) > **目的**: ユーザ HOME の `.jupyterhub_notice.txt` をポーリングし、JupyterLab 画面にトースト表示する。イメージ焼き直しで **Lab4 federated extension** を最小構成で組み込み。 --- ## 1) 成果物の配置 ``` /opt/conda/share/jupyter/labextensions/jnotice/ ├─ package.json └─ static/ └─ remoteEntry.jnotice.js /opt/conda/etc/jupyter/jupyter_server_config.d/ ├─ 90-allow-hidden.json └─ 90-jnotice.json ``` ### 90-allow-hidden.json ```json { "ContentsManager": { "allow_hidden": true }, "ServerApp": { "allow_hidden": true } } ``` ### 90-jnotice.json(**Server 側**の設定) ```json { "ServerApp": { "tornado_settings": { "page_config_data": { "federated_extensions": [ { "name": "jnotice", "extension": "./extension", "load": "extensions/jnotice/static/remoteEntry.jnotice.js" } ] } } } } ``` > **備考**: Lab4 環境では `jupyter_lab_config.d` ではなく **`jupyter_server_config.d`** を参照する。 ### package.json(labextension 側) ```json { "name": "jnotice", "version": "0.0.1", "private": true, "jupyterlab": { "extension": true, "outputDir": "static", "_build": { "name": "jnotice", "load": "static/remoteEntry.jnotice.js", "extension": "./extension" } }, "dependencies": { "@jupyterlab/application": "^4.0.0" }, "devDependencies": { "typescript": "^5.0.0", "ts-loader": "^9.0.0", "webpack": "^5.0.0", "webpack-cli": "^5.0.0" } } ``` ### webpack.config.cjs(assign + Banner で _JUPYTERLAB を保証) ```js const path = require('path'); const webpack = require('webpack'); const { container } = webpack; const { ModuleFederationPlugin } = container; module.exports = { output: { path: path.resolve(__dirname, 'static'), filename: 'remoteEntry.jnotice.js', publicPath: 'auto' }, resolve: { extensions: ['.ts', '.js'] }, module: { rules: [{ test: /\.ts$/, use: 'ts-loader', exclude: /node_modules/ }] }, plugins: [ new webpack.BannerPlugin({ banner: 'window._JUPYTERLAB = window._JUPYTERLAB || {};', raw: true }), new ModuleFederationPlugin({ name: 'jnotice', filename: 'remoteEntry.jnotice.js', shareScope: 'jupyterlab', library: { type: 'assign', name: 'window._JUPYTERLAB["jnotice"]' }, exposes: { './extension': './src/index.ts' }, shared: { '@jupyterlab/application': { singleton: true, requiredVersion: '^4.0.0', import: false, shareKey: '@jupyterlab/application', shareScope: 'jupyterlab' }, '@jupyterlab/ui-components': { singleton: true, requiredVersion: '^4.0.0', import: false, shareKey: '@jupyterlab/ui-components', shareScope: 'jupyterlab' }, '@lumino/widgets': { singleton: true, requiredVersion: '^2.0.0', import: false, shareKey: '@lumino/widgets', shareScope: 'jupyterlab' } } }) ], mode: 'production', optimization: { minimize: true } }; ``` ### src/index.ts(ポーリング & トースト) ```ts import { JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/application'; const plugin: JupyterFrontEndPlugin<void> = { id: 'jnotice:plugin', autoStart: true, activate: async (_app: JupyterFrontEnd) => { const cfgEl = document.getElementById('jupyter-config-data'); const cfg = cfgEl && cfgEl.textContent ? JSON.parse(cfgEl.textContent) : {}; const base: string = cfg.baseUrl || (location.pathname.match(/\/user\/[^/]+\/?/)?.[0] || '/'); const xsrf = (document.cookie.match(/(?:^|; )_xsrf=([^;]+)/)?.[1] || ''); const url = base + 'files/.jupyterhub_notice.txt'; let last = ''; async function poll() { try { const r = await fetch(url + '?_=' + Date.now(), { credentials: 'same-origin', headers: xsrf ? { 'X-XSRFToken': xsrf } : {} }); if (!r.ok) return; const t = (await r.text()).trim(); if (t && t !== last) { last = t; toast(t); } } catch {} } function toast(msg: string) { const el = document.createElement('div'); el.textContent = msg; el.style.cssText = 'position:fixed;right:20px;bottom:20px;background:#ffefc6;color:#222;padding:10px 16px;border-radius:10px;box-shadow:0 2px 8px rgba(0,0,0,.2);z-index:9999;max-width:360px;'; document.body.appendChild(el); setTimeout(() => el.remove(), 15000); } setTimeout(() => { poll(); setInterval(poll, 30000); }, 1000); console.log('[jnotice] plugin loaded'); } }; export default plugin; ``` --- ## 2) ビルド(イメージ内) ```bash # 例: /tmp/jnotice にソース一式を展開 npm install --no-audit --no-fund npx webpack --config webpack.config.cjs --mode=production # 生成物コピー LABEXT=/opt/conda/share/jupyter/labextensions/jnotice mkdir -p "$LABEXT/static" cp -a static/remoteEntry.jnotice.js "$LABEXT/static/" cp -a package.json "$LABEXT/" chmod -R a+rX "$LABEXT" ``` --- ## 3) 動作確認の手順 1. ブラウザで Lab を開く → **開発者ツール** を開く。 2. コンソールで以下を実行: ```js const cfg = JSON.parse(document.getElementById('jupyter-config-data')?.textContent||'{}'); cfg.federated_extensions?.find(x=>x.name==='jnotice'); // ← 存在すれば OK window._JUPYTERLAB?.jnotice; // ← {get, init} が見えれば OK ``` 3. `~/.jupyterhub_notice.txt` にメッセージを書き込み → 1分以内(初回1秒後・以降30秒間隔)にトースト。 --- ## 4) よくあるハマりどころ * **federated_extensions が undefined**: 設定ファイルは **`jupyter_server_config.d`** に置く。 * **「init/get を読めない」**: remoteEntry が `window._JUPYTERLAB['jnotice']` に **assign** されていない。`BannerPlugin` と `library: { type: 'assign', name: 'window._JUPYTERLAB["jnotice"]' }` を確認。 * **403 /files**: XSRF ヘッダ不足。実装では `_xsrf` を `X-XSRFToken` に付与済み。 * **キャッシュ**: remoteEntry を更新後は **ハードリロード** か、`load` に `?v=<timestamp>` を付与する運用。 --- ## 5) health_check と通知ファイル * health_check.sh が `.jupyterhub_notice.txt` に警告を書き出す(サンプル) ```bash NOTICE_FILE="$NB_DIR/.jupyterhub_notice.txt" mkdir -p "$(dirname "$NOTICE_FILE")" 2>/dev/null || true printf '%s\n' '⚠️ あと5分で自動停止します' > "$NOTICE_FILE" ``` * Jupyter 側は hidden 許可済みなので `/user/<name>/files/.jupyterhub_notice.txt` で取得可。 --- ## 6) 今後の改善 * メッセージにレベル(info/warn/error)や TTL、クリックで消す動作。 * Lab/Notebook 両対応の軽量バンドル(Notebook: `custom.js` 相当の仕組み検討)。 * i18n(日本語/英語切替)。 --- **最終確認フロー(ワンライナー)** ```js (()=>{ const cfg=JSON.parse(document.getElementById('jupyter-config-data')?.textContent||'{}'); console.log(!!cfg.federated_extensions?.find(x=>x.name==='jnotice'), window._JUPYTERLAB?.jnotice); })(); ```