#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);
})();
```

トップ   編集 差分 履歴 添付 複製 名前変更 リロード   新規 ページ一覧 検索 最終更新   ヘルプ   最終更新のRSS