Google Apps ScriptだけでGmail+Calendar統合ダッシュボード(NexusHub)を作った

「外部サービスに頼らず、Googleワークスペースだけで業務用ポータルを作れないか」

そう思い立ったのは、サブスクリプションツールの整理をしていたときだ。Notionを情報集約に使っていたが、Gmailの未読数・今日のカレンダー予定・タスクの状況を一覧で見るためだけに課金し続けるのは割に合わない気がしてきた。

Googleアカウント1つで全部賄えるなら、Google Apps Script(GAS)で自作した方が早いんじゃないか——そう判断して作ったのが「NexusHub」だ。

NexusHubとは

NexusHubはGoogle Apps Scriptで動くワークスペースダッシュボードで、以下の情報を1ページで確認できる。

  • Gmailの未読件数・重要メール一覧
  • 今日と明日のCalendar予定
  • Google Tasksの期限付きタスク
  • 任意のブックマークリンク集
  • スプレッドシートのサイドバーとして動作するため、インストール不要。Googleアカウントさえあれば動く。

    GASを選んだ理由

    通常なら「こういうダッシュボードを作るならNext.jsで」と考えるかもしれない。実際、最初はそう考えた。

    ただ今回はいくつかの理由でGASを選んだ。

    ホスティングが不要

    GASはGoogleのサーバーで動くため、VPSもVercelも必要ない。維持コストがゼロ。

    OAuth設定が不要

    GmailやCalendarへのアクセスは、実行者のGoogleアカウントで自動的に認証される。OAuth 2.0のフローを自前で実装する手間がかからない。

    スプレッドシートと統合できる

    ブックマークや設定値をスプレッドシートのセルに書くだけで管理できる。データベース不要。

    「外部依存を最小にして動くものを作る」という制約のもとでは、GASが最適解だった。

    GASのプロジェクト構成

    NexusHub/
    ├── Code.gs          # エントリーポイント・メニュー登録
    ├── Gmail.gs         # Gmail API連携
    ├── Calendar.gs      # Calendar API連携
    ├── Tasks.gs         # Tasks API連携
    ├── Sidebar.html     # ダッシュボードUI
    └── Styles.html      # CSSスタイル(HTMLに include)

    GASはファイル分割ができるので、機能ごとにファイルを分けた。

    スプレッドシートのサイドバーを開く

    まずエントリーポイントとなるCode.gsを書く。スプレッドシートを開いたときにカスタムメニューを追加し、そこからサイドバーを起動する。

    function onOpen() {
      SpreadsheetApp.getUi()
        .createMenu('NexusHub')
        .addItem('ダッシュボードを開く', 'openSidebar')
        .addToUi();
    }
    
    function openSidebar() {
      const html = HtmlService.createTemplateFromFile('Sidebar')
        .evaluate()
        .setTitle('NexusHub')
        .setWidth(400);
    
      SpreadsheetApp.getUi().showSidebar(html);
    }
    
    // HTMLファイルから別のHTMLファイルをincludeする仕組み
    function include(filename) {
      return HtmlService.createHtmlOutputFromFile(filename).getContent();
    }

    include()関数はGASのHTML分割に必須のパターンで、CSSを別ファイルに切り出すために使う。

    Gmail連携

    未読数と重要メールの一覧を取得する。

    // Gmail.gs
    function getGmailData() {
      const result = {
        unreadCount: 0,
        importantThreads: [],
      };
    
      // 未読件数
      result.unreadCount = GmailApp.getInboxUnreadCount();
    
      // 重要メール(スター付き or 重要マーク付き)
      const threads = GmailApp.search('is:important is:unread', 0, 10);
    
      threads.forEach(thread => {
        const lastMessage = thread.getMessages().pop();
        result.importantThreads.push({
          subject: thread.getFirstMessageSubject(),
          from: lastMessage.getFrom(),
          date: Utilities.formatDate(
            lastMessage.getDate(),
            Session.getScriptTimeZone(),
            'MM/dd HH:mm'
          ),
          threadId: thread.getId(),
        });
      });
    
      return result;
    }

    GmailApp.search()はGmail検索クエリをそのまま使えるため、フィルタリング条件を柔軟に設定できる。is:important is:unread以外にもfrom:boss@company.comlabel:要対応なども使える。

    Calendar連携

    今日と明日の予定を取得する。

    // Calendar.gs
    function getCalendarEvents() {
      const now = new Date();
      const tomorrow = new Date(now);
      tomorrow.setDate(tomorrow.getDate() + 1);
    
      const endOfTomorrow = new Date(tomorrow);
      endOfTomorrow.setHours(23, 59, 59, 999);
    
      const calendars = CalendarApp.getAllOwnedCalendars();
      const events = [];
    
      calendars.forEach(calendar => {
        // 非表示カレンダーはスキップ
        if (!calendar.isSelected()) return;
    
        const calEvents = calendar.getEvents(now, endOfTomorrow);
        calEvents.forEach(event => {
          events.push({
            title: event.getTitle(),
            start: Utilities.formatDate(
              event.getStartTime(),
              Session.getScriptTimeZone(),
              'MM/dd HH:mm'
            ),
            end: Utilities.formatDate(
              event.getEndTime(),
              Session.getScriptTimeZone(),
              'HH:mm'
            ),
            isToday: event.getStartTime().toDateString() === now.toDateString(),
            calendarName: calendar.getName(),
            color: calendar.getColor(),
          });
        });
      });
    
      // 開始時刻でソート
      events.sort((a, b) => new Date(a.start) - new Date(b.start));
    
      return events;
    }

    calendar.isSelected()で非表示にしているカレンダーを除外するのがポイントだ。Googleカレンダーの「表示/非表示」設定をGASでも尊重する。

    サイドバーのHTML

    取得したデータをサイドバーに表示するHTMLを書く。GASのサイドバーは通常のHTMLなので、CSSも普通に書ける。

    
    
    
    
      
      
    
    
      

    NexusHub

    Gmail

    読み込み中...

    今日・明日の予定

    読み込み中...

    google.script.runはGASのクライアントサイドJavaScriptからサーバーサイドGAS関数を呼び出すAPIだ。withSuccessHandlerでコールバックを指定する非同期の仕組みになっている。

    実際に使ってみた感想

    約2週間使い続けた感想を正直に書く。

    よかった点

    スプレッドシートを開くたびにサイドバーで今日の予定と未読状況を確認できるので、別タブでGmailとCalendarを開く必要がなくなった。特にCalendarの表示が便利で「今日あと何の予定があるか」が一目で分かる。

    不満点

    GASの実行には時間がかかる。サイドバーを開いてからデータが表示されるまで2〜3秒かかる。これはGASの制限で、劇的な改善は難しい。

    予期しなかった利点

    ブックマークセクションを作ったことで、よく使うURLをスプレッドシートのセルに書いておけるようになった。チームで使うときにURLの共有がスプレッドシートの編集だけで済む。

    まとめ

    Google Apps ScriptだけでGmail・Calendar統合ダッシュボードのNexusHubを作った。

  • 外部サービス不要・ホスティング不要
  • OAuth設定なしでGmail/Calendarにアクセス
  • スプレッドシートのサイドバーとして動作
  • `google.script.run`で非同期データ取得
  • GASは「プロトタイプを素早く作る」用途に特に向いていると感じた。本番運用に耐える大規模なシステムには向かないが、「自分一人が使うツール」なら完成度より速度を優先できるし、GASの制約の中でうまくやるパズル的な面白さもある。

    GASプロジェクトはGitHub Gistで公開予定。詳細が固まったらリンクを追加する。