feat: add create-app.sh, _template-minimal, update deploy-server.sh (passwordless), update CLAUDE.md

This commit is contained in:
posimai 2026-03-17 22:13:39 +09:00
parent 862fd00374
commit 148041ed07
8 changed files with 478 additions and 14 deletions

View File

@ -181,8 +181,8 @@ npm run deploy
## Synology バックエンドserver.js
- ファイル: `server.js`ルートに配置、git 管理外)
- **デプロイ方法(必須)**: `bash deploy-server.sh` を実行するSSH → docker cp → docker restart まで自動
- SSHパスワード → sudoパスワードの順に2回入力を求められる
- **デプロイ方法(必須)**: `bash deploy-server.sh` を実行するSSH鍵認証・完全無人
- 入力不要SSH鍵 `~/.ssh/id_ed25519` + docker グループで自動実行)
- スクリプト内の処理: `ssh` でファイル転送 → `docker cp``docker restart posimai_api` → ログ確認
- ⚠️ `vercel --prod` や File Station での手動アップロードは使わない
- API base: `https://posimai-lab.tail72e846.ts.net/brain/api`
@ -234,11 +234,26 @@ npm run deploy
## 新アプリ作成
`_template/` をコピーして使う。`APP_NAME` / `APP_ID` / `APP_DESCRIPTION` を置換するだけで動く。
### テンプレート選択
| テンプレート | 使うべきアプリ |
|------------|--------------|
| `_template/` | 複数ビュー・i18n・Magic Link 認証・サイドバーナビが必要なアプリbrain, daily, together, journal 相当) |
| `_template-minimal/` | 単機能ツール・サイドバー不要なアプリdiff, clean, timer, lens, ambient 相当) |
迷ったら `_template-minimal/` を選ぶ(後からフル機能に移行する方が簡単)。
`create-app.sh` で新規アプリを一括セットアップできるGit 初期化 + Gitea/GitHub リポジトリ作成 + Vercel 連携まで自動):
```bash
bash create-app.sh posimai-myapp "My App Name" "アプリの説明"
```
`create-app.sh``_template/` を使う。`_template-minimal/` を使いたい場合は手動でコピーしてから `create-app.sh` の Step 3 以降と同じ Git セットアップを行う(`_template-minimal/README.md` 参照)。
**完全な手順Git / Vercel セットアップ含む)は必ず `_template/README.md` を参照すること。**
特に以下の2点は詰まりやすいので注意
- Gitea リポジトリは **Web UI で先に作成**してから `git remote add gitea` する
- Gitea リポジトリは **Web UI で先に作成**してから `git remote add gitea` する`create-app.sh` は `GITEA_TOKEN` 環境変数があれば API で自動作成)
- `vercel git connect` 後、**空コミット push** で初回本番デプロイをトリガーする
### 新アプリ作成後の必須チェックリスト(抜け漏れ禁止)

View File

@ -0,0 +1,64 @@
# Posimai Minimal App Template
シンプルなツール系 PWA アプリ用のボイラープレート。
サイドバーなし・i18n なし・Magic Link なし。ヘッダー + メインコンテンツのみ。
## _template vs _template-minimal の使い分け
| テンプレート | 向いているアプリ |
|------------|----------------|
| `_template/` | 複数ビュー・i18n・Magic Link・設定パネルが必要なアプリbrain, daily, together 等) |
| `_template-minimal/` | 単機能ツールdiff, clean, timer, lens, ambient 等) |
## 使い方
```bash
# 1. テンプレートをコピー
cp -r _template-minimal posimai-[new-app-name]
cd posimai-[new-app-name]
# 2. APP_NAME / APP_ID / APP_DESCRIPTION を置換
# APP_NAME → 表示名 (例: "Posimai Memo")
# APP_ID → 識別子 (例: "posimai-memo")
# APP_DESCRIPTION → 説明文
# 3. 以降は _template/README.md の Step 3〜8 と同じ手順
```
または `create-app.sh` を使う(`--minimal` フラグはまだない。手動コピーで対応):
```bash
# create-app.sh は _template/ を使う。
# minimal を使いたい場合は先に手動コピーしてから Git セットアップだけ行う:
cd posimai-[new-app-name]
git init -b main && git add . && git commit -m "init: APP_NAME"
gh repo create posimai/APP_ID --private
git remote add gitea http://100.76.7.3:3000/mai/APP_ID.git
git remote add github https://github.com/posimai/APP_ID.git
npm run deploy
echo "https://github.com/posimai/APP_ID.git" | vercel git connect
git commit --allow-empty -m "ci: trigger initial Vercel deployment"
npm run deploy
```
## 実装ガイド
`index.html` の中の以下のコメント箇所を編集するだけ:
1. `<!-- ── APP-SPECIFIC HTML ──` → コンテンツ HTML
2. `/* ── APP-SPECIFIC STYLES ──` → CSS
3. `// ── APP-SPECIFIC JS ──` → JavaScript ロジック
4. ヘッダーアイコン(`header-dot`)をアプリに合わせたものに変更可
## デザイントークン(変更禁止)
| トークン | 値 | 用途 |
|---------|-----|------|
| `--bg` | `#0D0D0D` | ページ背景 |
| `--surface` | `#1A1A1A` | カード背景 |
| `--surface2` | `#252525` | ネスト要素 |
| `--border` | `#2D2D2D` | ボーダー |
| `--text` | `#F3F4F6` | 主テキスト |
| `--text2` | `#9CA3AF` | 副テキスト |
| `--accent` | `#6EE7B7` | アクセント |
| `--radius` | `12px` | 角丸 |

View File

@ -0,0 +1,199 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex, nofollow">
<script>
(function () {
var t = localStorage.getItem('APP_ID-theme') || 'system';
var dark = t === 'dark' || (t === 'system' && matchMedia('(prefers-color-scheme:dark)').matches);
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme-pref', t);
})();
</script>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="description" content="APP_DESCRIPTION">
<meta name="color-scheme" content="dark light">
<meta name="theme-color" content="#0D0D0D" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#F9FAFB" media="(prefers-color-scheme: light)">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="APP_NAME">
<link rel="manifest" href="/manifest.json">
<link rel="icon" type="image/png" href="/logo.png">
<link rel="apple-touch-icon" href="/logo.png">
<title>APP_NAME</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@0.344.0/dist/umd/lucide.min.js"></script>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root, [data-theme="dark"] {
--bg:#0D0D0D; --surface:#1A1A1A; --surface2:#252525; --border:#2D2D2D;
--text:#F3F4F6; --text2:#9CA3AF; --text3:#6B7280;
--accent:#6EE7B7; --accent-dim:rgba(110,231,183,0.1); --accent-border:rgba(110,231,183,0.25);
--header-bg:rgba(13,13,13,0.85);
--overlay-bg:rgba(0,0,0,0.6); --shadow-lg:0 8px 32px rgba(0,0,0,0.5); --shadow-sm:0 2px 8px rgba(0,0,0,0.3);
color-scheme: dark;
}
[data-theme="light"] {
--bg:#F9FAFB; --surface:#FFFFFF; --surface2:#F3F4F6; --border:#E5E7EB;
--text:#111827; --text2:#4B5563; --text3:#9CA3AF;
--accent:#059669; --accent-dim:rgba(5,150,105,0.08); --accent-border:rgba(5,150,105,0.2);
--header-bg:rgba(249,250,251,0.85);
--overlay-bg:rgba(0,0,0,0.4); --shadow-lg:0 8px 32px rgba(0,0,0,0.15); --shadow-sm:0 2px 8px rgba(0,0,0,0.08);
color-scheme: light;
}
:root { --radius:12px; --radius-sm:8px; --header-h:52px; --ease:cubic-bezier(.4,0,.2,1); --dur:0.2s; }
body { font-family: 'Inter', -apple-system, sans-serif; background: var(--bg); color: var(--text); font-size: 14px; line-height: 1.6; -webkit-font-smoothing: antialiased; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
:focus:not(:focus-visible) { outline: none; }
/* Header */
.header { height: var(--header-h); display: flex; align-items: center; gap: 8px; padding: 0 16px; background: var(--header-bg); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border-bottom: 1px solid var(--border); position: sticky; top: 0; z-index: 100; }
.header-brand { display: flex; align-items: center; gap: 8px; flex: 1; }
.header-dot { width: 7px; height: 7px; background: var(--accent); border-radius: 50%; }
.header-title { font-size: 14px; font-weight: 600; letter-spacing: -0.01em; }
.icon-btn { background: none; border: none; cursor: pointer; color: var(--text2); min-width: 44px; min-height: 44px; display: flex; align-items: center; justify-content: center; border-radius: var(--radius-sm); transition: color var(--dur), background var(--dur); flex-shrink: 0; margin: 0 -8px; }
.icon-btn:hover { color: var(--text); background: var(--surface2); }
/* Main content */
main { padding: 24px 20px calc(40px + env(safe-area-inset-bottom)); max-width: 860px; width: 100%; margin: 0 auto; }
/* Settings panel */
.settings-panel { position: fixed; top: 0; right: 0; bottom: 0; width: 280px; background: var(--surface); border-left: 1px solid var(--border); display: flex; flex-direction: column; z-index: 300; transform: translateX(100%); transition: transform .25s var(--ease); }
.settings-panel.open { transform: translateX(0); box-shadow: var(--shadow-lg); }
.settings-panel-header { height: var(--header-h); display: flex; align-items: center; justify-content: space-between; padding: 0 12px 0 16px; flex-shrink: 0; border-bottom: 1px solid var(--border); }
.settings-panel-title { font-size: 14px; font-weight: 600; }
.settings-panel-body { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 24px; }
.settings-group-label { font-size: 10px; font-weight: 600; color: var(--text3); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 10px; }
.settings-item { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--border); }
.settings-item:last-child { border-bottom: none; }
.settings-item-label { font-size: 13px; color: var(--text); }
.theme-selector { display: flex; gap: 4px; background: var(--surface2); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 3px; }
.theme-btn { flex: 1; display: flex; align-items: center; justify-content: center; gap: 5px; padding: 6px 8px; border-radius: 6px; border: none; cursor: pointer; font-family: inherit; font-size: 11px; font-weight: 500; color: var(--text3); background: none; transition: all var(--dur) var(--ease); white-space: nowrap; }
.theme-btn:hover { color: var(--text2); }
.theme-btn.active { background: var(--surface); color: var(--text); box-shadow: var(--shadow-sm); }
.overlay { display: none; position: fixed; inset: 0; background: var(--overlay-bg); backdrop-filter: blur(2px); z-index: 150; }
.overlay.open { display: block; }
/* Common components */
.btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 14px; border-radius: var(--radius-sm); border: none; cursor: pointer; font-family: inherit; font-size: 13px; font-weight: 500; transition: all var(--dur) var(--ease); }
.btn-primary { background: var(--accent); color: #0D0D0D; }
.btn-primary:hover { opacity: .88; }
.btn-ghost { background: var(--surface2); color: var(--text2); border: 1px solid var(--border); }
.btn-ghost:hover { color: var(--text); background: var(--surface); }
.card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; }
.section-label { font-size: 11px; font-weight: 600; color: var(--text3); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 12px; }
#toast { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 10px 16px; font-size: 13px; font-weight: 500; z-index: 9999; box-shadow: var(--shadow-lg); transition: opacity .2s, transform .2s; opacity: 0; pointer-events: none; white-space: nowrap; }
#toast.show { opacity: 1; transform: translateX(-50%) translateY(-4px); }
/* ── APP-SPECIFIC STYLES: ここにアプリ固有のスタイルを追加 ── */
</style>
</head>
<body>
<a href="#main-content" class="skip-link" tabindex="0" style="position:absolute;top:-100%;left:8px;background:var(--accent);color:#0D0D0D;padding:8px 16px;border-radius:8px;font-weight:600;font-size:13px;z-index:10000;text-decoration:none">コンテンツへスキップ</a>
<!-- Settings panel -->
<aside class="settings-panel" id="settingsPanel" role="complementary">
<div class="settings-panel-header">
<span class="settings-panel-title">設定</span>
<button class="icon-btn" id="settingsCloseBtn" aria-label="設定を閉じる">
<i data-lucide="x" style="width:18px;height:18px;stroke-width:1.75"></i>
</button>
</div>
<div class="settings-panel-body">
<div>
<div class="settings-group-label">外観</div>
<div class="settings-item">
<div class="settings-item-label">テーマ</div>
<div class="theme-selector">
<button class="theme-btn" data-theme-val="dark"><i data-lucide="moon" style="width:12px;height:12px;stroke-width:1.75"></i>ダーク</button>
<button class="theme-btn" data-theme-val="light"><i data-lucide="sun" style="width:12px;height:12px;stroke-width:1.75"></i>ライト</button>
<button class="theme-btn" data-theme-val="system"><i data-lucide="monitor" style="width:12px;height:12px;stroke-width:1.75"></i>自動</button>
</div>
</div>
</div>
</div>
</aside>
<div class="overlay" id="overlay" aria-hidden="true"></div>
<header class="header">
<div class="header-brand">
<div class="header-dot" aria-hidden="true"></div>
<span class="header-title">APP_NAME</span>
</div>
<button class="icon-btn" id="settingsBtn" aria-label="設定" aria-expanded="false">
<i data-lucide="settings" style="width:18px;height:18px;stroke-width:1.5"></i>
</button>
</header>
<main id="main-content">
<!-- ── APP-SPECIFIC HTML: ここにアプリ固有のコンテンツを追加 ── -->
<!-- Empty state placeholder — アプリ実装時に削除 -->
<div style="text-align:center;padding:80px 24px;color:var(--text3);">
<i data-lucide="box" style="width:40px;height:40px;stroke-width:1.25;margin-bottom:12px;display:block;margin-inline:auto;opacity:.5"></i>
<div style="font-size:14px;font-weight:500;">APP_NAME</div>
<div style="font-size:13px;margin-top:4px;">APP_DESCRIPTION</div>
</div>
</main>
<div id="toast" role="status" aria-live="polite"></div>
<script>
// ── Theme ──
const STORE_KEY = 'APP_ID';
function applyTheme(val) {
const dark = val === 'dark' || (val === 'system' && matchMedia('(prefers-color-scheme:dark)').matches);
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme-pref', val);
document.querySelectorAll('[data-theme-val]').forEach(b => b.classList.toggle('active', b.dataset.themeVal === val));
}
document.querySelectorAll('[data-theme-val]').forEach(b => b.addEventListener('click', () => {
const val = b.dataset.themeVal;
localStorage.setItem(STORE_KEY + '-theme', val);
applyTheme(val);
}));
applyTheme(localStorage.getItem(STORE_KEY + '-theme') || 'system');
// ── Settings panel ──
const settingsPanel = document.getElementById('settingsPanel');
const overlay = document.getElementById('overlay');
document.getElementById('settingsBtn').addEventListener('click', () => {
settingsPanel.classList.add('open');
overlay.classList.add('open');
document.getElementById('settingsBtn').setAttribute('aria-expanded', 'true');
});
function closeSettings() {
settingsPanel.classList.remove('open');
overlay.classList.remove('open');
document.getElementById('settingsBtn').setAttribute('aria-expanded', 'false');
}
document.getElementById('settingsCloseBtn').addEventListener('click', closeSettings);
overlay.addEventListener('click', closeSettings);
// ── Toast ──
function showToast(msg, duration = 2500) {
const el = document.getElementById('toast');
el.textContent = msg;
el.classList.add('show');
setTimeout(() => el.classList.remove('show'), duration);
}
// ── PWA Service Worker ──
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').catch(() => {});
}
// ── Lucide icons ──
lucide.createIcons();
// ── APP-SPECIFIC JS: ここにアプリ固有のロジックを追加 ──
</script>
</body>
</html>

View File

@ -0,0 +1,17 @@
{
"id": "/APP_ID/",
"name": "APP_NAME",
"short_name": "APP_NAME",
"description": "APP_DESCRIPTION",
"start_url": "/",
"display": "standalone",
"display_override": ["window-controls-overlay", "standalone"],
"background_color": "#0D0D0D",
"theme_color": "#0D0D0D",
"orientation": "portrait-primary",
"categories": ["productivity"],
"icons": [
{ "src": "/logo.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
{ "src": "/logo.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
]
}

View File

@ -0,0 +1,9 @@
{
"name": "APP_ID",
"version": "1.0.0",
"description": "APP_DESCRIPTION",
"private": true,
"scripts": {
"deploy": "git push gitea main && git push github main"
}
}

38
_template-minimal/sw.js Normal file
View File

@ -0,0 +1,38 @@
// Posimai SW — stale-while-revalidate + update notification
// バージョンは index.html に inline で管理(この文字列変更で旧キャッシュ削除)
const CACHE = 'APP_ID-v2';
const STATIC = ['/', '/index.html', '/manifest.json', '/logo.png'];
self.addEventListener('install', e => {
e.waitUntil(
caches.open(CACHE).then(c => c.addAll(STATIC))
// skipWaiting() は意図的に呼ばない
// → updatefound イベントで UI 側からユーザーに通知する方式を採用
);
});
self.addEventListener('activate', e => {
e.waitUntil(
caches.keys().then(keys =>
Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k)))
).then(() => self.clients.claim())
);
});
self.addEventListener('fetch', e => {
if (e.request.method !== 'GET') return;
if (!e.request.url.startsWith(self.location.origin)) return;
e.respondWith(
caches.open(CACHE).then(cache =>
cache.match(e.request).then(cached => {
const network = fetch(e.request).then(res => {
if (res.ok && res.type === 'basic') cache.put(e.request, res.clone());
return res;
}).catch(() => cached);
// stale-while-revalidate: キャッシュがあればすぐ返し、裏でネットワーク更新
return cached || network;
})
)
);
});

123
create-app.sh Normal file
View File

@ -0,0 +1,123 @@
#!/usr/bin/env bash
# ============================================
# Posimai — 新規アプリ作成スクリプト
# 使い方: bash create-app.sh posimai-myapp "My App Name" "アプリの説明"
# ============================================
set -e
APP_ID="${1}"
APP_NAME="${2}"
APP_DESC="${3:-${APP_NAME}}"
# --- 引数チェック ---
if [ -z "$APP_ID" ] || [ -z "$APP_NAME" ]; then
echo "使い方: bash create-app.sh <app-id> <app-name> [description]"
echo "例: bash create-app.sh posimai-memo \"Posimai Memo\" \"メモ帳\""
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
TARGET_DIR="$SCRIPT_DIR/$APP_ID"
if [ -d "$TARGET_DIR" ]; then
echo "[ERROR] ディレクトリが既に存在します: $TARGET_DIR"
exit 1
fi
GITEA_BASE="http://100.76.7.3:3000/mai"
GITHUB_ORG="posimai"
echo "========================================"
echo " Posimai App Creator"
echo "========================================"
echo " APP_ID : $APP_ID"
echo " APP_NAME: $APP_NAME"
echo " APP_DESC: $APP_DESC"
echo "========================================"
echo ""
# --- Step 1: テンプレートコピー & 置換 ---
echo "Step 1: テンプレートをコピーして置換..."
cp -r "$SCRIPT_DIR/_template" "$TARGET_DIR"
# sed で 3 変数を一括置換macOS / Linux / Git Bash 対応)
find "$TARGET_DIR" -type f \( -name "*.html" -o -name "*.json" -o -name "*.js" -o -name "*.md" \) | while IFS= read -r f; do
sed -i "s/APP_NAME/$APP_NAME/g; s/APP_ID/$APP_ID/g; s/APP_DESCRIPTION/$APP_DESC/g" "$f"
done
echo " コピー完了: $TARGET_DIR"
# --- Step 2: Git 初期化 ---
echo ""
echo "Step 2: Git 初期化..."
cd "$TARGET_DIR"
git init -b main
git add .
git commit -m "init: $APP_NAME"
echo " Git 初期化完了"
# --- Step 3: Gitea にリポジトリ作成 ---
echo ""
echo "Step 3: Gitea にリポジトリを作成..."
# Gitea の認証情報(~/.netrc または環境変数から取得)
GITEA_TOKEN="${GITEA_TOKEN:-}"
if [ -n "$GITEA_TOKEN" ]; then
GITEA_AUTH="-H \"Authorization: token $GITEA_TOKEN\""
curl -s -X POST "http://100.76.7.3:3000/api/v1/user/repos" \
-H "Content-Type: application/json" \
-H "Authorization: token $GITEA_TOKEN" \
-d "{\"name\":\"$APP_ID\",\"private\":false,\"auto_init\":false}" \
> /dev/null
echo " Gitea リポジトリ作成完了API"
else
echo " [WARN] GITEA_TOKEN 未設定。Gitea リポジトリは手動で作成してください:"
echo " → http://100.76.7.3:3000 で $APP_ID リポジトリを作成後、Enter を押してください"
read -r
fi
# --- Step 4: GitHub にリポジトリ作成 ---
echo ""
echo "Step 4: GitHub にリポジトリを作成..."
gh repo create "$GITHUB_ORG/$APP_ID" --private --description "$APP_DESC" 2>&1 || {
echo " [WARN] GitHub リポジトリ作成に失敗(既存の可能性)。続行します。"
}
echo " GitHub リポジトリ作成完了"
# --- Step 5: リモート追加 & push ---
echo ""
echo "Step 5: リモートを追加して push..."
git remote add gitea "$GITEA_BASE/$APP_ID.git"
git remote add github "https://github.com/$GITHUB_ORG/$APP_ID.git"
npm run deploy
echo " push 完了"
# --- Step 6: Vercel 連携 ---
echo ""
echo "Step 6: Vercel と GitHub を連携..."
echo "https://github.com/$GITHUB_ORG/$APP_ID.git" | vercel git connect 2>&1 || {
echo " [WARN] vercel git connect に失敗。手動で連携してください。"
}
# --- Step 7: 初回本番デプロイ ---
echo ""
echo "Step 7: 初回本番デプロイをトリガー..."
git commit --allow-empty -m "ci: trigger initial Vercel deployment"
npm run deploy
echo ""
echo "========================================"
echo " 完了! $APP_NAME"
echo "========================================"
echo ""
echo " ディレクトリ : $TARGET_DIR"
echo " Gitea : http://100.76.7.3:3000/mai/$APP_ID"
echo " GitHub : https://github.com/$GITHUB_ORG/$APP_ID"
echo ""
echo " 次の必須作業Dashboard 更新):"
echo " 1. posimai-dashboard/src/data/projects.json にカードを追加"
echo " 2. ecosystem/page.tsx の NODES / EDGES に追加"
echo " 3. timeline/page.tsx の EVENTS に追加"
echo " 4. access/page.tsx の APPS に追加"
echo " 5. Dashboard をデプロイ: cd posimai-dashboard && npm run deploy"
echo ""

View File

@ -6,6 +6,7 @@
set -e
HOST="mai@100.76.7.3"
SSH_KEY="$HOME/.ssh/id_ed25519"
CONTAINER="posimai_api"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SERVER_FILE="$SCRIPT_DIR/server.js"
@ -23,20 +24,18 @@ fi
echo ""
echo "→ Step 1: server.js を Synology /tmp に転送..."
echo " (SSH パスワードを求められたら入力してください)"
ssh "$HOST" "cat > $REMOTE_TMP" < "$SERVER_FILE"
echo " ✓ 転送完了"
ssh -i "$SSH_KEY" -o BatchMode=yes "$HOST" "cat > $REMOTE_TMP" < "$SERVER_FILE"
echo " 転送完了"
echo ""
echo "→ Step 2: コンテナに適用 + 再起動..."
echo " (sudo パスワードを求められたら入力してください)"
ssh -t "$HOST" "
sudo $DOCKER cp $REMOTE_TMP $CONTAINER:/app/server.js && \
echo ' ✓ コピー完了' && \
sudo $DOCKER restart $CONTAINER && \
echo ' ✓ 再起動完了。ログを確認中...' && \
ssh -i "$SSH_KEY" -o BatchMode=yes "$HOST" "
$DOCKER cp $REMOTE_TMP $CONTAINER:/app/server.js && \
echo ' コピー完了' && \
$DOCKER restart $CONTAINER && \
echo ' 再起動完了。ログを確認中...' && \
sleep 5 && \
sudo $DOCKER logs $CONTAINER --tail 20
$DOCKER logs $CONTAINER --tail 20
"
echo ""