# 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) よくあるハマりどころ
## 5) health_check と通知ファイル
```bash
NOTICE_FILE="$NB_DIR/.jupyterhub_notice.txt"
mkdir -p "$(dirname "$NOTICE_FILE")" 2>/dev/null || true
printf '%s\n' '⚠️ あと5分で自動停止します' > "$NOTICE_FILE"
```
## 6) 今後の改善
```js
(()=>{
const cfg=JSON.parse(document.getElementById('jupyter-config-data')?.textContent||'{}');
console.log(!!cfg.federated_extensions?.find(x=>x.name==='jnotice'), window._JUPYTERLAB?.jnotice);
})();
```