2026年3月17日火曜日

AWS Certified Solutions Architect - Associate 03 合格メモ

AWS Certified Solutions Architect - Associate 03 合格メモ

概要

一応残しておきます
GCP の ACE に比べて難しい印象です

環境

  • AWS SAA (03) (2026/03/16時点)
  • テストセンターにて受験

料金

  • 150ドル (約20,000円)

対策

完全に過去問ゲーというわけにはいかない感じです
過去問からそのまま採用されるケースはほぼない印象です

教科書

kindle で 0 円だったのでこれにしましたが他のやつの方がいいかもです

模擬試験

全部で804問です

解き方のポイント

  • 時間がかなり無い印象
    • 自分は時間ギリギリまで使った
    • わからない問題は飛ばすか適当に回答もあり
  • 15問はダミー
    • これがよくわからない
    • 正解でも誤答でも無関係らしい
    • しかもどれかわからない
    • これがあるから先程の適当回答も時間がなければワンチャンありという感じ
  • 過去問がそのまま出るケースはほぼなかった
    • 模擬試験を暗記していれば回答先読みで即回答が使えるが SAA03 はほぼできなさそう
    • 模擬試験と全く同じ問題はなかったが同じようなやつで回答項目が違ったり問題を少しひねったやつは出てくる
    • 全体的に意地悪な問題が多かった印象
  • 問題の日本語がおかしい
    • 説明が少なすぎて頭で補完しなければならない
    • これを実現するためにこのコンポーネントが足りないがそれは使う前提になっていたりとか
    • このケースは実はコストも関係しているから暗黙的に安いものを選択するとか
  • 実務関係なく考えななければならない
    • これは GCP も同様だった
    • 資格モードにならないとダメ
  • 消去法
    • これは結構使えるかもしれない
    • 明らかに違うだろうというやつが何個かあったので2択まで絞れる問題が多かった印象

実際に出題された問題の記憶

  • NoETL
  • LakeFormation + 認証
  • NATゲートウェイの固定IP化
  • DR関連の問題 (これが結構多かった)
  • 最適な構成の問題 (これも結構多かった)
  • STS + DMS + コードの内のSQL変換方法
  • Global Accelerator
  • AI 関連 (Comprehend, Rekognition, SageMaker など)

全体的にいやらしい問題が多かった印象です
単純な EC2 や EBS の構成の問題は少なかったです

スコアレポートはでない

詳細なのは出ませんが点数などの結果は certmetrics で確認できます
ちなみに試験後画面ですぐに出たりすることはないので合否もスコアレポートも後で確認になります

暫定合格を確認する方法もない

ないです

合否通知はいつくるか

当日は20時50分くらいに certmetrics にログインしたら合格結果が出ていました
ちなみにバッジでおなじみの credly からのメールは 21時50分くらいに来ていました

最後に

GCP に比べると難しい印象でした
ただ GCP に比べて結果がその日に出るのは嬉しいです
GCP は結果が出るまでに数日かかるのでイライラします

模擬試験ゲーというわけにもいかないので実務 or 教科書による各種サービスの詳細な仕様までわかっていたほうがいいです
しかも基本的なサービスと言うよりかはわりとニッチなサービスも知っておく必要がありそうです

2026年2月26日木曜日

stable-diffusion で新しいモデルを追加し古いモデルを削除した場合はキャッシュも削除しなければいけないっぽい

stable-diffusion で新しいモデルを追加し古いモデルを削除した場合はキャッシュも削除しなければいけないっぽい

概要

そうしないと古いモデルを参照し続けてしまうっぽい

環境

  • macOS 26.3

現象

waiNSFWIllustrious_v140.safetensors を models/Stable-diffusion から削除したが API 経由でなぜか参照する事案が発生

ログ

Checkpoint waiNSFWIllustrious_v140.safetensors not found; loading fallback waiIllustriousSDXL_v160.safetensors [a5f58eb1c3]

対処方法

一旦 webui.sh を停止します
そして以下のキャッシュを削除します

  • rm cache/hashes/cache.db
  • rm cache/safetensors-metadata/cache.db

画像にもモデル情報が含まれているケースがあるので削除します

  • rm -rf outputs/txt2img-images/2025-09-30/*

これで再度 webui.sh を起動しましょう
ログが出ていなければ OK です

最後に

モデルを削除するだけではダメらしい

2026年2月22日日曜日

npm でパッケージをアップグレードする方法

npm でパッケージをアップグレードする方法

概要

いちいち package.json を編集するのは面倒です

環境

  • macOS 26.3
  • nodejs 22.21.0

方法

  • npx npm-check-updates -u
  • npm install

最後に

outdated とか使う方法があるらしいですがややこしいのでそういうのをまとめてやってくれる npm-check-updates を使うのが簡単です

参考サイト

2026年2月16日月曜日

Github actions で kitchen test をする場合は buildkit を無効にしなければ動かなくなった

Github actions で kitchen test をする場合は buildkit を無効にしなければ動かなくなった

概要

おそらく本当に最近仕様変更があった模様

環境

  • Github actions (2026/02/13時点)
  • Ruby 4.0.1
    • test-kitchen 3.9.1
    • kitchen-docker 3.0.0

Github actions の設定

name: Kitchen test

on:
  push:
    branches:
      - "master"

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3
        with:
          platforms: linux/arm64

      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true

      - name: Install dependencies
        run: bundle install

      - name: Run Test Kitchen
        run: |
          bundle exec kitchen test
        env:
          CHEF_LICENSE: accept
          DOCKER_BUILDKIT: 0

デバッグログを表示する場合

name: Kitchen test

on:
  push:
    branches:
      - "master"

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3
        with:
          platforms: linux/arm64

      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true

      - name: Install dependencies
        run: bundle install

      - name: Run Test Kitchen
        run: |
          bundle exec kitchen test --log-level debug || true
        env:
          CHEF_LICENSE: accept
          DOCKER_BUILDKIT: 0

      - name: Show Kitchen logs
        if: always()
        run: |
          echo "===== kitchen.log ====="
          cat .kitchen/logs/kitchen.log || true
          echo "===== instance log ====="
          cat .kitchen/logs/default-ubuntu-2204.log || true

最後に

内部的に buildkit を使うようになっており docker の出力形式が変更されたために起きていたエラーでした

2026年2月14日土曜日

スマホ版の Chrome ではちゃんと DOM を定義してから値を設定したりするほうがいい

スマホ版の Chrome ではちゃんと DOM を定義してから値を設定したりするほうがいい

概要

動的に appendChild とかするとリソースを消費しすぎと判断して描画を強制的に停止するようです

環境

  • iOS 26.2.1
  • Chrome 144.07559.95

ダメなコード

function addMessage(msg) {
  // avoid duplicates; update indicator if already present
  const existing = document.querySelector(`#log li[data-msg-id="${msg.id}"]`);
  if (existing) {
    const ind = existing.querySelector(".read-indicator");
    if (ind) {
      updateIndicator(ind, existing, msg.reads || []);
    }
    return;
  }
  // 新着メッセージ(相手からのもの)に対する通知・未読処理
  if (msg.message && msg.client_id !== CLIENT_ID) {
    // ブラウザ通知の呼び出し
    showBrowserNotification(msg);
    // タブが裏側にある場合、未読カウントを増やす
    if (document.visibilityState !== "visible") {
      unreadCount++;
      updateTabTitle();
    }
  }
  // new message
  const li = document.createElement("li");
  li.className = msg.client_id === CLIENT_ID ? "me" : "other";
  li.dataset.msgId = msg.id;

  const textSpan = document.createElement("span");
  textSpan.className = "msg-text";
  textSpan.textContent = msg.message;

  const ind = document.createElement("span");
  ind.className = "read-indicator";

  if (msg.client_id === CLIENT_ID) {
    // 自分のメッセージ: サーバーの reads 情報を使って表示
    updateIndicator(ind, li, msg.reads || []);
    li.appendChild(textSpan);
    li.appendChild(ind);
  } else {
    // 相手のメッセージ
    updateIndicator(ind, li, msg.reads || []);

    li.addEventListener("click", () => {
      if (isRead(msg.id)) return;
      setRead(msg.id);
      ind.classList.remove("unread");
      ind.classList.add("read");
      ind.title = "既読";
      // notify server
      markRead(msg.id);
    });

    li.appendChild(textSpan);
    li.appendChild(ind);
  }

  document.getElementById("log").appendChild(li);
}

良いコード

function addMessage(msg) {
  // 1. 基本チェック
  if (!msg || !msg.id) return;
  // read_event は applyReadEvent で処理されるため、ここではメッセージ本体がないものを除外
  if (msg.type === "read_event" || !msg.message) return;

  // 2. 重複チェック
  const log = document.getElementById("log");
  if (!log) return;
  const existing = document.querySelector(`#log li[data-msg-id="${msg.id}"]`);
  if (existing) return;

  // 3. 要素の作成
  const li = document.createElement("li");

  // 安全な ID チェック
  const currentId =
    typeof CLIENT_ID !== "undefined" ? CLIENT_ID : getClientId();

  li.className = msg.client_id === currentId ? "me" : "other";
  li.setAttribute("data-msg-id", msg.id); // dataset ではなく setAttribute を使用(古いブラウザ対策)

  const textSpan = document.createElement("span");
  textSpan.className = "msg-text";
  textSpan.textContent = String(msg.message); // 明示的に文字列変換

  const ind = document.createElement("span");
  ind.className = "read-indicator unread"; // デフォルトは unread

  // 4. 要素の組み立て(先に DOM に追加してから細部を調整するのがスマホでは安定します)
  li.appendChild(textSpan);
  li.appendChild(ind);
  log.appendChild(li);

  // 5. 既読インジケーターの更新(エラーが起きても表示自体は維持する)
  try {
    updateIndicator(ind, li, msg.reads || []);
  } catch (e) {
    console.warn("Indicator error ignored for safety", e);
  }

  // 6. イベントリスナー(相手のメッセージのみ)
  if (msg.client_id !== currentId) {
    li.addEventListener("click", function () {
      try {
        if (isRead(msg.id)) return;
        setRead(msg.id);
        ind.classList.remove("unread");
        ind.classList.add("read");
        markRead(msg.id);
      } catch (err) {
        console.error(err);
      }
    });

    // 通知処理(エラー回避のため try-catch)
    try {
      showBrowserNotification(msg);
    } catch (e) {}
  }

  // 7. スクロール
  log.scrollTop = log.scrollHeight;
}

比較

堅牢性とエラーハンドリング

「良いコード」は、外部要因でプログラムが止まらないよう工夫されています。

項目 ダメなコード 良いコード
ガード句 データの存在チェックが甘く、msg.idが欠落しているとエラーになる可能性がある。 冒頭で `!msg
例外処理 updateIndicator 等でエラーが起きると、後続の処理(DOM追加等)が止まる。 try-catch を活用し、一部の表示エラーが起きてもアプリ全体が止まらない。
変数の安全性 CLIENT_ID がグローバルにある前提。 typeof チェックやフォールバック(getClientId())を用意している。

責務の明確化と可読性

コードが整理されているため、後からの修正が容易です。

  • 単一責任の原則:
    • ダメ: addMessage 内で通知、未読カウント、DOM操作、重複チェックが混ざり、条件分岐(if (msg.client_id === CLIENT_ID) … else …)で同じような appendChild が重複している。
    • 良い: メッセージの型判定(read_event かどうか)を fetchMessages 側で仕分け、addMessage は「表示」に専念している。
  • DRY (Don’t Repeat Yourself):
    • ダメ: 自分のメッセージと相手のメッセージで、インジケーターの追加処理を2回書いている。
    • 良い: 共通の組み立て(li への追加)を先に行い、差分(イベントリスナー)だけを条件分岐で書いている。

ユーザー体験 (UX) と実用性

ブラウザやネットワークの特性を考慮した実装になっています。

  • スクロール処理: 「良いコード」では追加後に log.scrollTop = log.scrollHeight を行い、常に最新メッセージが見えるように配慮されています。
  • 通信の最適化: fetchMessages でキャッシュ回避のためのタイムスタンプを追加しており、古いデータを掴まされるリスクを減らしています。
  • 暗黙の型変換への対処: String(msg.message) とすることで、数値や null が送られてきても textContent で安全に表示できます。

改善のポイント(まとめ)

  • ❌ ダメな点
    • 重複コード: li.appendChild(textSpan) などが if/else 両方にあり、修正漏れの原因になる。
    • 不親切なエラー: ネットワークエラーやデータ不備で JS が停止し、画面が真っ白になるリスクがある。
    • 状態管理の欠如: スクロール位置の調整がないため、ユーザーが手動でスクロールする必要がある。
  • ✅ 良い点
    • 徹底したバリデーション: 「データはあるか?」「型は正しいか?」を常に疑っている。
    • 保守性の高い構造: コメントで工程(1〜7)が区切られており、どこで何をしているか一目でわかる。
    • API設計への配慮: lastId の更新により、重複してメッセージを取得しない仕組みが整っている。

最後に

スマホ版の Chrome も意識して JavaScript を書かないとダメなようです

2026年2月13日金曜日

サーバから Chrome ブラウザプッシュする実装の流れ

サーバから Chrome ブラウザプッシュする実装の流れ

概要

実装の流れを紹介します

環境

  • macOS 26.2
  • Python 3.12.11

鍵の作成

こちらを参考に作成します
鍵ファイルと JavaScript 用の鍵文字列を作成すれば OK です

サブスクリプション情報の保存 (Python)

JavaScrpt 側で通知の許可をした場合に通知先の情報をサーバ側に保存しておきます
今回は Redis で紹介しますが永続化できるものであれば何でも OK です

@app.route("/subscribe", methods=["POST"])
def subscribe():
    """フロントから送られてくる Subscription 情報を Redis に保存"""
    data = request.get_json()
    client_id = data.get("client_id")
    subscription_info = data.get("subscription")

    redis_conn = get_kv()
    # client_id ごとに購読情報を保存
    redis_conn.set(f"push_sub:{client_id}", json.dumps(subscription_info))
    return jsonify({"ok": True})

プッシュ送信処理 (Python)

あとで使いますが実際にプッシュを送信する処理です
メールアドレス部分はダミーでも問題ないですが一応ちゃんと設定した方がいいです (公開アプリの場合)

VAPID_PRIVATE_KEY_PATH = os.getenv("VAPID_PRIVATE_KEY_PATH", "./private_key.pem")
VAPID_CLAIMS = {"sub": "mailto:user@mail"}

def send_web_push(subscription_info, message_body):
    try:
        webpush(
            subscription_info=json.loads(subscription_info),
            data=json.dumps(message_body),
            vapid_private_key=VAPID_PRIVATE_KEY_PATH,
            vapid_claims=VAPID_CLAIMS,  # type: ignore
        )
    except WebPushException as ex:
        print("Push failed:", repr(ex))

ServiceWorker (JavaScript)

リモートプッシュはこの ServiceWorker がないと動きません
Python からのプッシュをこの ServiceWorker が受け取り通知を表示する感じです

通知のスタイルなどはシンプルにしているので必要であればアイコンなど設定してください

self.addEventListener("push", function (event) {
  console.log("Push received");
  let data = { title: "新着", body: "メッセージがあります" };

  try {
    if (event.data) {
      data = event.data.json(); // ここでエラーが起きると通知が出ない
    }
  } catch (e) {
    console.error("JSON parse error:", e);
  }

  const options = {
    body: data.body,
  };

  event.waitUntil(self.registration.showNotification(data.title, options));
});

ブラウザ側のサブスクリプション登録 (JavaScript)

ブラウザで特定のサイトなどにアクセスすると通知を許可しますかなど出るサイトがありますがそれを実装します
許可するをクリックしたら必要な情報を Python 側に送ります

urlBase64ToUint8Array に鍵文字列は冒頭で作成したものを使ってください

function urlBase64ToUint8Array(base64String) {
  const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
  const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
  const rawData = window.atob(base64);
  const outputArray = new Uint8Array(rawData.length);
  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
}

async function setupPush() {
  const register = await navigator.serviceWorker.register("/static/js/sw.js");

  // すでに購読済みかチェック
  let subscription = await register.pushManager.getSubscription();

  // 未購読なら新規作成
  if (!subscription) {
    subscription = await register.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array(
        "xxx",
      ),
    });
  }

  // サーバーへ送信(常に送ることで client_id との紐付けを確実に更新する)
  await fetch("/subscribe", {
    method: "POST",
    body: JSON.stringify({
      client_id: CLIENT_ID,
      subscription: subscription,
    }),
    headers: { "Content-Type": "application/json" },
  });
}

通知許可ダイアログ表示 (JavaScript)

ページが読み込まれた場合に自動で出してもいいですが嫌がられるケースもあるので特定のボタンを押したら許可する流れにします

document.addEventListener("DOMContentLoaded", () => {
  // Click title to set client_id and request notification permission
  const title = document.getElementById("notification");
  if (title) {
    title.addEventListener("click", async () => {
      // 通知の許可を求める
      if ("Notification" in window) {
        const permission = await Notification.requestPermission();
        if (permission === "granted") {
          // ここで実行!
          await setupPush();
        }
      }
    });
  }
  // 追加: すでに許可済みなら、ページ読み込み時に購読情報を同期する
  if ("Notification" in window && Notification.permission === "granted") {
    setupPush().catch((err) => console.error("Auto-setup failed:", err));
  }
});

プッシュ通知送信タイミング (Python)

あとは任意のタイミングでサーバ側で送信すれば OK です
当然ですが送信しまくると大変なことになるので条件など設けて適切なタイミングで送るようにしましょう

@app.route("/send", methods=["POST"])
def redis_chat_send():
    # 1... 既存の処理
    # 2. プッシュ通知の送信 (自分以外の購読者へ)
    try:
        # push_sub: で始まるキーをすべて取得
        # ※ ユーザー数が多い場合はメンバーリストを別途管理するのが理想ですが、
        # 現状のチャットルーム運用であれば keys で十分です
        sub_keys = redis_conn.keys("push_sub:*")

        for sub_key in sub_keys:  # type: ignore
            target_id = sub_key.decode().split(":")[-1]

            # 送信者(自分)以外にのみ送る
            if target_id != client_id:
                sub_info = redis_conn.get(sub_key)
                if sub_info:
                    logger.info(f"Sending push to {target_id}")
                    # 非同期処理にするのが理想ですが、まずは直列で実装
                    send_web_push(
                        sub_info,
                        {"title": "新着メッセージ", "body": f"{client_id}: {msg}"},
                    )
    except Exception as e:
        logger.error(f"Push notification error: {e}")

    return jsonify({"ok": True})

サブスクリプション確認

  • Redis 側にキーが登録されていること
  • 開発者コンソールで ServiceWorker が登録されていること

プッシュが届かない場合

  • Mac であれば通知の設定を確認しましょう
    • Mac の Chrome はデフォルトでオフになっている
    • Chrome で許可していても Mac 側で拒否していることが多い
  • サイトが https 化されていない
    • localhost ならテストできるようです
    • IP ベースのサイトや http のサイトでは動作しません

スマホ or タブレット版でプッシュ通知が届かない場合

  • ラップトップ版と違いかなり作りが違う
  • 単純に通知を許可することができない
  • iOS 側で当然許可する必要があるがそれでもサブスクリプションできないことがある
  • サイトを PWA 化して上げる必要などがあるかも

最後に

参考サイト

2026年2月12日木曜日

Windows のルーティング情報を可視化する Powershell

Windows のルーティング情報を可視化する Powershell

概要

タイトル通り

環境

  • Windows 11

スクリプト

# Visualize-Routes.ps1 (vis.jsを使用したHTMLベースの可視化)

param (
    [string]$OutputPath = "$($PSScriptRoot)\network_routes.html",
    [switch]$IncludeDefaultRoutes = $false,
    [string[]]$ExcludeNextHops = @()
)

function Visualize-NetRouteGraph {
    [CmdletBinding()]
    param (
        [string]$OutputPath = "$($PSScriptRoot)\network_routes.html",
        [switch]$IncludeDefaultRoutes = $false,
        [string[]]$ExcludeNextHops = @()
    )

    # ルーティング情報を取得
    $routes = Get-NetRoute
    
    # デフォルトルートを除外する場合
    if (-not $IncludeDefaultRoutes) {
        $routes = $routes | Where-Object { $_.NextHop -ne "::" -and $_.NextHop -ne "0.0.0.0" }
    }
    
    # 指定されたNextHopを除外する場合
    if ($ExcludeNextHops.Count -gt 0) {
        foreach ($excludeHop in $ExcludeNextHops) {
            $routes = $routes | Where-Object { $_.NextHop -ne $excludeHop }
        }
    }

    if (-not $routes) {
        Write-Warning "ルーティング情報が見つかりませんでした。"
        return
    }

    # ノードとエッジの定義
    $nodes = @{}
    $edges = @()
    $nodeId = 0
    $nodeMap = @{}
    $edgeMap = @{}  # エッジの重複を管理

    foreach ($route in $routes) {
        $destination = $route.DestinationPrefix
        $nextHop = $route.NextHop
        $ifIndex = $route.IfIndex
        $interface = $null
        
        # ネットワークアダプタ名を取得(エラーハンドリング付き)
        try {
            $interface = (Get-NetAdapter -IfIndex $ifIndex -ErrorAction Stop).Name
        }
        catch {
            $interface = $null
        }

        # ノードの作成(重複なし)
        if (-not $nodeMap.ContainsKey($destination)) {
            $nodeMap[$destination] = @{
                id    = $nodeId
                label = $destination
                title = "Destination: $destination"
                color = "lightblue"
                shape = "box"
            }
            $nodeId++
        }

        if (-not $nodeMap.ContainsKey($nextHop)) {
            $nodeMap[$nextHop] = @{
                id    = $nodeId
                label = $nextHop
                title = "NextHop: $nextHop"
                color = "lightgreen"
                shape = "ellipse"
            }
            $nodeId++
        }

        # エッジのキーを作成(重複チェック用)
        # NextHop → Destination の向きにすることで、トラフィックフローを表現
        $edgeKey = "$($nodeMap[$nextHop].id)->$($nodeMap[$destination].id)"
        
        # エッジラベルの作成
        $edgeInfo = "Metric: $($route.RouteMetric)"
        if ($interface) {
            $edgeInfo += " (via $interface)"
        }
        
        # 既存のエッジがあれば統合、なければ新規作成
        if ($edgeMap.ContainsKey($edgeKey)) {
            # 既存のエッジにラベルを追加
            $edgeMap[$edgeKey].labels += $edgeInfo
        }
        else {
            # 新規エッジを作成
            $edgeMap[$edgeKey] = @{
                from   = $nodeMap[$nextHop].id
                to     = $nodeMap[$destination].id
                labels = @($edgeInfo)
            }
        }
    }
    
    # エッジマップからエッジ配列を作成(ラベルを結合)
    foreach ($edgeKey in $edgeMap.Keys) {
        $edge = $edgeMap[$edgeKey]
        $edges += @{
            from  = $edge.from
            to    = $edge.to
            label = $edge.labels -join "`n"
        }
    }

    # JSON形式でノードとエッジを作成
    $nodesJson = $nodeMap.Values | ConvertTo-Json
    $edgesJson = $edges | ConvertTo-Json

    # HTMLテンプレートを生成
    $htmlContent = @"
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ネットワークルーティンググラフ</title>
    <script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        html, body {
            width: 100%;
            height: 100%;
            font-family: Arial, sans-serif;
        }
        #network {
            width: 100%;
            height: 100%;
            border: 1px solid lightgray;
        }
        .controls {
            position: absolute;
            top: 10px;
            left: 10px;
            background: white;
            padding: 10px;
            border: 1px solid #ccc;
            border-radius: 5px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            z-index: 100;
        }
        button {
            padding: 5px 10px;
            margin: 5px 2px;
            cursor: pointer;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
        }
        button:hover {
            background-color: #45a049;
        }
    </style>
</head>
<body>
    <div class="controls">
        <button onclick="resetView()">リセット</button>
        <button onclick="fitToScreen()">画面に合わせる</button>
    </div>
    <div id="network"></div>

    <script type="text/javascript">
        var nodes = new vis.DataSet($nodesJson);
        var edges = new vis.DataSet($edgesJson);

        var container = document.getElementById('network');
        var data = {
            nodes: nodes,
            edges: edges
        };

        var options = {
            physics: {
                enabled: true,
                stabilization: {
                    iterations: 200
                },
                barnesHut: {
                    gravitationalConstant: -26000,
                    centralGravity: 0.3,
                    springLength: 300,
                    springConstant: 0.04,
                    damping: 0.3
                }
            },
            nodes: {
                font: {
                    size: 14,
                    color: 'black'
                },
                borderWidth: 2,
                shadow: true
            },
            edges: {
                arrows: 'to',
                font: {
                    size: 12,
                    align: 'middle'
                },
                shadow: true,
                smooth: {
                    type: 'continuous'
                }
            },
            interaction: {
                navigationButtons: true,
                keyboard: true,
                zoomView: true,
                dragView: true
            }
        };

        var network = new vis.Network(container, data, options);

        function resetView() {
            network.fit();
        }

        function fitToScreen() {
            network.fit({
                animation: {
                    duration: 1000,
                    easingFunction: 'easeInOutQuad'
                }
            });
        }

        window.addEventListener('resize', function() {
            network.redraw();
        });

        network.fit();
    </script>
</body>
</html>
"@

    # HTMLファイルを保存
    $htmlContent | Set-Content $OutputPath -Encoding UTF8

    Write-Host "ネットワークルーティンググラフをHTMLで生成しました: $OutputPath"
    Write-Host "ブラウザで表示しています..."

    # ブラウザで開く
    Invoke-Item $OutputPath
}

# 関数を実行(スクリプトの引数を渡す)
$params = @{
    OutputPath = $OutputPath
}
if ($IncludeDefaultRoutes) {
    $params['IncludeDefaultRoutes'] = $IncludeDefaultRoutes
}
if ($ExcludeNextHops.Count -gt 0) {
    $params['ExcludeNextHops'] = $ExcludeNextHops
}
Visualize-NetRouteGraph @params

最後に

作成された HTML をブラウザ開けば OK です