# Markdown

# 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`** に置く。[edit]

**「init/get を読めない」**: remoteEntry が `window._JUPYTERLAB['jnotice']` に **assign** されていない。`BannerPlugin` と `library: { type: 'assign', name: 'window._JUPYTERLAB["jnotice"]' }` を確認。[edit]

**403 /files**: XSRF ヘッダ不足。実装では `_xsrf` を `X-XSRFToken` に付与済み。[edit]

**キャッシュ**: remoteEntry を更新後は **ハードリロード** か、`load` に `?v=<timestamp>` を付与する運用。[edit]

## 5) health_check と通知ファイル

health_check.sh が `.jupyterhub_notice.txt` に警告を書き出す(サンプル)[edit]

```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` で取得可。[edit]

## 6) 今後の改善

メッセージにレベル(info/warn/error)や TTL、クリックで消す動作。[edit]

Lab/Notebook 両対応の軽量バンドル(Notebook: `custom.js` 相当の仕組み検討)。[edit]

i18n(日本語/英語切替)。[edit]

最終確認フロー(ワンライナー)**[edit]

```js
(()=>{

const cfg=JSON.parse(document.getElementById('jupyter-config-data')?.textContent||'{}');
console.log(!!cfg.federated_extensions?.find(x=>x.name==='jnotice'), window._JUPYTERLAB?.jnotice);

})();
```


トップ   新規 ページ一覧 検索 最終更新   ヘルプ   最終更新のRSS