本番デプロイ設定: SSL, Kamal, API環境変数化, テスト修正

This commit is contained in:
2026-02-21 23:45:12 +09:00
parent e90ea88758
commit 9626db3dcf
21 changed files with 226 additions and 78 deletions

View File

@@ -18,3 +18,6 @@
# Improve security by using a password manager. Never check config/master.key into git! # Improve security by using a password manager. Never check config/master.key into git!
RAILS_MASTER_KEY=$(cat config/master.key) RAILS_MASTER_KEY=$(cat config/master.key)
# Local registry does not require auth, but Kamal needs a value
KAMAL_REGISTRY_PASSWORD=unused

View File

@@ -7,8 +7,8 @@ class BoardPostsController < ApplicationController
before_action :set_current_participant before_action :set_current_participant
def create def create
# 参加チェック # 参加チェック(国選択フェーズでも共通掲示板への投稿を許可)
unless @board.member?(@current_participant) && @game.status == "in_progress" unless @board.member?(@current_participant) && @game.status.in?(%w[power_selection in_progress])
return redirect_to game_board_path(@game, @board), alert: "投稿権限がありません" return redirect_to game_board_path(@game, @board), alert: "投稿権限がありません"
end end

View File

@@ -22,6 +22,10 @@ class BoardsController < ApplicationController
else else
return redirect_to game_path(@game), alert: "参加者ではないためアクセスできません" unless @current_participant return redirect_to game_path(@game), alert: "参加者ではないためアクセスできません" unless @current_participant
if @game.status == "power_selection"
# 国選択フェーズ:共通掲示板のみ表示
@boards = @game.boards.global.includes(:participants, :board_posts, :board_memberships).to_a
else
# 進行中参加中の掲示板のみeager load で N+1 防止) # 進行中参加中の掲示板のみeager load で N+1 防止)
@boards = @current_participant.boards.includes(:participants, :board_posts, :board_memberships).order(created_at: :desc) @boards = @current_participant.boards.includes(:participants, :board_posts, :board_memberships).order(created_at: :desc)
# 共通掲示板のフォールバック # 共通掲示板のフォールバック
@@ -32,6 +36,7 @@ class BoardsController < ApplicationController
@boards = @boards.sort_by { |b| b.global? ? 0 : 1 } @boards = @boards.sort_by { |b| b.global? ? 0 : 1 }
end end
end end
end
def show def show
@current_participant = @game.participants.find_by(user: current_user) @current_participant = @game.participants.find_by(user: current_user)
@@ -70,11 +75,11 @@ class BoardsController < ApplicationController
end end
end end
@posts = @board.board_posts.includes(:participant).order(created_at: :desc) @posts = @board.board_posts.includes(participant: :user).order(created_at: :desc)
@proposals = @board.board_proposals.includes(:proposer).order(created_at: :desc) @proposals = @board.board_proposals.includes(:proposer).order(created_at: :desc)
@new_post = BoardPost.new @new_post = BoardPost.new
@new_proposal = BoardProposal.new @new_proposal = BoardProposal.new
@active_members = @board.active_memberships.includes(:participant).map(&:participant) @active_members = @board.active_memberships.includes(participant: :user).map(&:participant)
# メンバー追加用:招待可能なプレイヤー一覧 # メンバー追加用:招待可能なプレイヤー一覧
if @board.negotiation? && !@board.history_mode? if @board.negotiation? && !@board.history_mode?

View File

@@ -75,8 +75,11 @@ class GamesController < ApplicationController
# 全7カ国固定 # 全7カ国固定
powers = %w[AUSTRIA ENGLAND FRANCE GERMANY ITALY RUSSIA TURKEY] powers = %w[AUSTRIA ENGLAND FRANCE GERMANY ITALY RUSSIA TURKEY]
# N+1 防止のためparticipantsをキャッシュ
participants_cache = @game.participants.includes(:user).index_by(&:power)
@country_statuses = powers.map do |power| @country_statuses = powers.map do |power|
participant = @game.participants.find_by(power: power) participant = participants_cache[power]
# 終了済みなら全員完了扱い、そうでなければターンごとの提出状況 # 終了済みなら全員完了扱い、そうでなければターンごとの提出状況
submitted = @game_finished ? true : @display_turn.orders_submitted_for?(power) submitted = @game_finished ? true : @display_turn.orders_submitted_for?(power)

View File

@@ -38,15 +38,15 @@ export default class extends Controller {
<div class="flex ${isMe ? 'justify-end' : 'justify-start'} mb-4 message-item" id="post_${data.post_id}"> <div class="flex ${isMe ? 'justify-end' : 'justify-start'} mb-4 message-item" id="post_${data.post_id}">
${!isMe ? ` ${!isMe ? `
<div class="flex-shrink-0 mr-3"> <div class="flex-shrink-0 mr-3">
<span class="inline-flex items-center justify-center h-8 w-8 rounded-full text-xs font-bold border border-gray-300 bg-white text-gray-700 shadow-sm" title="${data.power}"> <span class="inline-flex items-center justify-center h-8 w-8 rounded-full text-xs font-bold border border-gray-300 bg-white text-gray-700 shadow-sm" title="${data.display_name || data.power || '?'}">
${data.power ? data.power.substring(0, 2) : '?'} ${(data.display_name || data.power || '?').substring(0, 2)}
</span> </span>
</div> </div>
` : ''} ` : ''}
<div class="max-w-lg ${isMe ? 'order-1' : 'order-2'}"> <div class="max-w-lg ${isMe ? 'order-1' : 'order-2'}">
<div class="flex items-baseline space-x-2 mb-1 ${isMe ? 'justify-end' : 'justify-start'}"> <div class="flex items-baseline space-x-2 mb-1 ${isMe ? 'justify-end' : 'justify-start'}">
${!isMe ? `<span class="text-xs font-bold text-gray-900">${data.power}</span>` : ''} ${!isMe ? `<span class="text-xs font-bold text-gray-900">${data.display_name || data.power || '?'}</span>` : ''}
<span class="text-xs text-gray-500">${data.created_at}</span> <span class="text-xs text-gray-500">${data.created_at}</span>
${data.phase ? `<span class="text-xs bg-gray-100 px-1 rounded text-gray-600 border border-gray-200">${data.phase}</span>` : ''} ${data.phase ? `<span class="text-xs bg-gray-100 px-1 rounded text-gray-600 border border-gray-200">${data.phase}</span>` : ''}
</div> </div>

View File

@@ -17,6 +17,7 @@ class BoardPost < ApplicationRecord
post_id: id, post_id: id,
participant_id: participant.id, participant_id: participant.id,
power: participant.power, # 国名 power: participant.power, # 国名
display_name: participant.display_name, # 国名 or ユーザー名
body: body, body: body,
phase: phase, # フェーズ情報 phase: phase, # フェーズ情報
created_at: created_at.in_time_zone("Asia/Tokyo").strftime("%Y-%m-%d %H:%M") created_at: created_at.in_time_zone("Asia/Tokyo").strftime("%Y-%m-%d %H:%M")

View File

@@ -21,4 +21,9 @@ class Participant < ApplicationRecord
in: %w[AUSTRIA ENGLAND FRANCE GERMANY ITALY RUSSIA TURKEY], in: %w[AUSTRIA ENGLAND FRANCE GERMANY ITALY RUSSIA TURKEY],
message: "無効な国です" message: "無効な国です"
}, allow_nil: true }, allow_nil: true
# 表示名:国名が設定済みなら国名、未設定ならユーザー名
def display_name
power.present? ? power : user.username
end
end end

View File

@@ -2,7 +2,7 @@ require "faraday"
require "json" require "json"
class GameApiClient class GameApiClient
BASE_URL = "http://0.0.0.0:8000" BASE_URL = ENV.fetch("DIPLOMACY_API_URL", "http://0.0.0.0:8000")
def initialize def initialize
@connection = Faraday.new(url: BASE_URL) do |f| @connection = Faraday.new(url: BASE_URL) do |f|

View File

@@ -3,8 +3,8 @@
<% unless is_me %> <% unless is_me %>
<div class="flex-shrink-0 mr-3"> <div class="flex-shrink-0 mr-3">
<!-- アバター代わりの国名バッジ --> <!-- アバター代わりの国名バッジ -->
<span class="inline-flex items-center justify-center h-8 w-8 rounded-full text-xs font-bold border border-gray-300 bg-white text-gray-700 shadow-sm" title="<%= post.participant.power %>"> <span class="inline-flex items-center justify-center h-8 w-8 rounded-full text-xs font-bold border border-gray-300 bg-white text-gray-700 shadow-sm" title="<%= post.participant.display_name %>">
<%= post.participant.power ? post.participant.power[0..1] : '?' %> <%= post.participant.display_name[0..1] %>
</span> </span>
</div> </div>
<% end %> <% end %>
@@ -12,7 +12,7 @@
<div class="max-w-lg <%= is_me ? 'order-1' : 'order-2' %>"> <div class="max-w-lg <%= is_me ? 'order-1' : 'order-2' %>">
<div class="flex items-baseline space-x-2 mb-1 <%= is_me ? 'justify-end' : 'justify-start' %>"> <div class="flex items-baseline space-x-2 mb-1 <%= is_me ? 'justify-end' : 'justify-start' %>">
<% unless is_me %> <% unless is_me %>
<span class="text-xs font-bold text-gray-900"><%= post.participant.power %></span> <span class="text-xs font-bold text-gray-900"><%= post.participant.display_name %></span>
<% end %> <% end %>
<span class="text-xs text-gray-500"><%= l post.created_at, format: :short %></span> <span class="text-xs text-gray-500"><%= l post.created_at, format: :short %></span>
<% if post.phase.present? %> <% if post.phase.present? %>

View File

@@ -95,7 +95,7 @@
<% if last_post %> <% if last_post %>
<i class="fa-solid fa-comment-dots flex-shrink-0 mr-1.5 text-gray-400"></i> <i class="fa-solid fa-comment-dots flex-shrink-0 mr-1.5 text-gray-400"></i>
<span class="truncate max-w-xs"><%= last_post.body.truncate(30) %></span> <span class="truncate max-w-xs"><%= last_post.body.truncate(30) %></span>
<span class="ml-2 text-xs text-gray-400">- <%= last_post.participant.power %></span> <span class="ml-2 text-xs text-gray-400">- <%= last_post.participant.display_name %></span>
<% else %> <% else %>
<span class="text-gray-400">投稿なし</span> <span class="text-gray-400">投稿なし</span>
<% end %> <% end %>

View File

@@ -36,7 +36,7 @@
メンバー: メンバー:
<% @active_members.each do |m| %> <% @active_members.each do |m| %>
<span class="ml-1 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium border <%= power_color_class(m.power) %>"> <span class="ml-1 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium border <%= power_color_class(m.power) %>">
<%= m.power %> <%= m.display_name %>
</span> </span>
<% end %> <% end %>
</div> </div>
@@ -69,7 +69,7 @@
<div class="flex-1 flex flex-col bg-white border border-gray-200 rounded-lg shadow-sm mr-4 overflow-hidden"> <div class="flex-1 flex flex-col bg-white border border-gray-200 rounded-lg shadow-sm mr-4 overflow-hidden">
<!-- 投稿フォーム (上部に移動) --> <!-- 投稿フォーム (上部に移動) -->
<% if @board.member?(@current_participant) && !@board.history_mode? %> <% if @board.member?(@current_participant) && !@board.history_mode? && @game.status.in?(%w[power_selection in_progress]) %>
<div class="p-4 bg-gray-50 border-b border-gray-200"> <div class="p-4 bg-gray-50 border-b border-gray-200">
<%= form_with model: [@game, @board, @new_post], local: true, class: "flex items-end space-x-2" do |f| %> <%= form_with model: [@game, @board, @new_post], local: true, class: "flex items-end space-x-2" do |f| %>
<div class="flex-1"> <div class="flex-1">
@@ -200,7 +200,7 @@
<li class="px-3 py-2 flex items-center justify-between text-sm"> <li class="px-3 py-2 flex items-center justify-between text-sm">
<div class="flex items-center"> <div class="flex items-center">
<span class="<%= 'line-through text-gray-400' if membership.left_at %>"> <span class="<%= 'line-through text-gray-400' if membership.left_at %>">
<%= membership.participant.power %> <%= membership.participant.display_name %>
</span> </span>
<% if membership.participant.user == current_user %> <% if membership.participant.user == current_user %>
<span class="ml-1 text-xs text-indigo-500">(YOU)</span> <span class="ml-1 text-xs text-indigo-500">(YOU)</span>

View File

@@ -189,6 +189,22 @@
</div> </div>
</div> </div>
<!-- 掲示板リンク(国選択の相談用) -->
<div class="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-md">
<div class="flex items-center justify-between">
<div class="flex items-center">
<i class="fa-solid fa-comments text-blue-600 text-lg mr-3"></i>
<div>
<p class="text-sm font-medium text-blue-800">国選択を相談しませんか?</p>
<p class="text-xs text-blue-600">掲示板で他のプレイヤーと相談できます</p>
</div>
</div>
<%= link_to game_boards_path(@game), class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" do %>
<i class="fa-solid fa-comment-dots mr-2"></i> 掲示板へ
<% end %>
</div>
</div>
<!-- ゲーム管理者用コントロール --> <!-- ゲーム管理者用コントロール -->
<% if (current_user&.admin? || @game.administrator == current_user) && @game.all_powers_assigned? %> <% if (current_user&.admin? || @game.administrator == current_user) && @game.all_powers_assigned? %>
<div class="mt-6 pt-6 border-t border-gray-200"> <div class="mt-6 pt-6 border-t border-gray-200">
@@ -475,6 +491,11 @@
<span class="inline-flex items-center px-2 py-0.5 rounded text-sm font-bold border border-gray-200 shadow-sm <%= power_color_class(status[:power]) %>"> <span class="inline-flex items-center px-2 py-0.5 rounded text-sm font-bold border border-gray-200 shadow-sm <%= power_color_class(status[:power]) %>">
<%= status[:power] %> <%= status[:power] %>
</span> </span>
<% if status[:participant] %>
<span class="ml-2 text-xs text-gray-500">
(<%= status[:participant].user.username %>)
</span>
<% end %>
<% if status[:is_user] %> <% if status[:is_user] %>
<span class="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-indigo-100 text-indigo-800">YOU</span> <span class="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-indigo-100 text-indigo-800">YOU</span>
<% elsif status[:participant].nil? %> <% elsif status[:participant].nil? %>

View File

@@ -1 +1 @@
7mDalwYZdLi7w0m1MvGVhygx/O0YiNIiiVA+tj/LBukGJInb/cy6jHV7xg1oGIhzzQ1nl0sUbuMHtitmdkb6QWOUiSPAjkpiqfx5sbnE+5Q7U5CPjg0szp7CrNbBhL4ojibKYRPI5Js78x6eBSr9L1vmoWirFZS/ar3B2TwGQ0yNPxghxN7kQ0pEVoiFZUmuXmErih/nTUy5patG1zPPGsXUiAkMGYXztn6n+cIahX6lgFpV79HzI/c01VeMcOV/pZRs/RrQrzfTlnSX2UQlsHqLTQyXx4O6yDUNF8Mii7g6N57jyd26Osi5OIUAsBkrZOhfSSXmd3ceBlt+oSlMXtKaT+qfg+vywqI026eDlsKyEYbCyIaXp9kn+8VMaJcCI+qSGlewfxmxFKVgq7MyNDBrL1VM51UVO00s6cxXWUZ2W96t1SnLyVwcq/NIuhN530imvLvE6cAOAJJKgCfY1gEmUCZ3kuEMO64OSL5Ynm2wxyMF48cCJrxP--rN5KnyzLPaDv/sop--ouscdd1e738zVM6LolPIQA== NPIjsO0jVJUPWi47JkVirr7vs24Tgxo1+ZUhxCjTTtgGsZjXCHCP1agNcjl1MLlRzjy+2DZgbZuhLGbx33e7veoQ2YMz5kb+AZ7aMzkXla8+TZlqXWYBpQtx3yquV7c7SBHBRy+F+KkvLsyEinWDceoZ3O8kYeu3Fw3QsXBlKENHfDmNEnZ2csm9Yak+jpptNpe+kojuiT/r2F4cD5unOZu8VMluhcGLZ2n2dBev3wFQo/tUgK8CmkFhkd5vSjCFuBhbu6dqwq1jxTrtZdhTG/aia6/RuAnrm/S5MJdxNnXq3dHIa8Wrg0qKp0DBwXxy3okWrRPaPT5udUcZvRA+7DS+to4FnoJDTKI+VanWzOxIbBeqYB9W+kqjKJB1w2Ii2rUoTfSPK9GEcJSb0QI9UNa8Hdjdn/w9MSj9Bkm4LtZch5+HptwLtpZrrKmt4FEIeNhMsVulrbbCMcAANxTRqPxOSbL7Zr5OMMSB/9ReP+atU6u8miWMQkXg--T8JQCM/t7+czYn/u--J/FQEJ3U1dEPM67GCMxR9w==

View File

@@ -7,34 +7,25 @@ image: dip_front
# Deploy to these servers. # Deploy to these servers.
servers: servers:
web: web:
- 192.168.0.1 - 153.127.48.108
# job: # job:
# hosts: # hosts:
# - 192.168.0.1 # - 153.127.48.108
# cmd: bin/jobs # cmd: bin/jobs
# Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server. # Enable SSL auto certification via Let's Encrypt and allow for multiple apps on a single web server.
# If used with Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption. # Kamal Proxy will automatically obtain and renew SSL certificates.
# proxy:
# Using an SSL proxy like this requires turning on config.assume_ssl and config.force_ssl in production.rb! ssl: true
# host: diplo.kontei.net
# Don't use this when deploying to multiple web servers (then you have to terminate SSL at your load balancer).
#
# proxy:
# ssl: true
# host: app.example.com
# Where you keep your container images. # Where you keep your container images.
# Using a local registry on the VPS server.
registry: registry:
# Alternatives: hub.docker.com / registry.digitalocean.com / ghcr.io / ...
server: localhost:5555 server: localhost:5555
username: kamal
# Needed for authenticated registries. password:
# username: your-user - KAMAL_REGISTRY_PASSWORD
# Always use an access token rather than real password when possible.
# password:
# - KAMAL_REGISTRY_PASSWORD
# Inject ENV variables into containers (secrets come from .kamal/secrets). # Inject ENV variables into containers (secrets come from .kamal/secrets).
env: env:
@@ -51,9 +42,8 @@ env:
# Set number of cores available to the application on each server (default: 1). # Set number of cores available to the application on each server (default: 1).
# WEB_CONCURRENCY: 2 # WEB_CONCURRENCY: 2
# Match this to any external database server to configure Active Record correctly # Diplomacy API URL (dip_api container accessible via Docker host network)
# Use dip_front-db for a db accessory server on same machine via local kamal docker network. DIPLOMACY_API_URL: http://172.17.0.1:8000
# DB_HOST: 192.168.0.2
# Log everything from Rails # Log everything from Rails
# RAILS_LOG_LEVEL: debug # RAILS_LOG_LEVEL: debug
@@ -77,28 +67,20 @@ volumes:
asset_path: /rails/public/assets asset_path: /rails/public/assets
# Configure the image builder. # Configure the image builder.
# Build on the remote VPS server to avoid insecure registry issues.
builder: builder:
arch: amd64 arch: amd64
remote: ssh://kontei@153.127.48.108
# # Build image via remote server (useful for faster amd64 builds on arm64 computers) # Use a non-root ssh user
# remote: ssh://docker@docker-builder-server ssh:
# user: kontei
# # Pass arguments and secrets to the Docker build process
# args:
# RUBY_VERSION: 4.0.1
# secrets:
# - GITHUB_TOKEN
# - RAILS_MASTER_KEY
# Use a different ssh user than root
# ssh:
# user: app
# Use accessory services (secrets come from .kamal/secrets). # Use accessory services (secrets come from .kamal/secrets).
# accessories: # accessories:
# db: # db:
# image: mysql:8.0 # image: mysql:8.0
# host: 192.168.0.2 # host: 153.127.48.108
# # Change to 3306 to expose port to the world instead of just local network. # # Change to 3306 to expose port to the world instead of just local network.
# port: "127.0.0.1:3306:3306" # port: "127.0.0.1:3306:3306"
# env: # env:
@@ -113,7 +95,7 @@ builder:
# - data:/var/lib/mysql # - data:/var/lib/mysql
# redis: # redis:
# image: valkey/valkey:8 # image: valkey/valkey:8
# host: 192.168.0.2 # host: 153.127.48.108
# port: 6379 # port: 6379
# directories: # directories:
# - data:/data # - data:/data

View File

@@ -25,13 +25,13 @@ Rails.application.configure do
config.active_storage.service = :local config.active_storage.service = :local
# Assume all access to the app is happening through a SSL-terminating reverse proxy. # Assume all access to the app is happening through a SSL-terminating reverse proxy.
# config.assume_ssl = true config.assume_ssl = true
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
# config.force_ssl = true config.force_ssl = true
# Skip http-to-https redirect for the default health check endpoint. # Skip http-to-https redirect for the default health check endpoint.
# config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } }
# Log to STDOUT with the current request id as a default log tag. # Log to STDOUT with the current request id as a default log tag.
config.log_tags = [ :request_id ] config.log_tags = [ :request_id ]
@@ -58,7 +58,7 @@ Rails.application.configure do
# config.action_mailer.raise_delivery_errors = false # config.action_mailer.raise_delivery_errors = false
# Set host to be used by links generated in mailer templates. # Set host to be used by links generated in mailer templates.
config.action_mailer.default_url_options = { host: "example.com" } config.action_mailer.default_url_options = { host: "diplo.kontei.net" }
# Specify outgoing SMTP server. Remember to add smtp/* credentials via bin/rails credentials:edit. # Specify outgoing SMTP server. Remember to add smtp/* credentials via bin/rails credentials:edit.
# config.action_mailer.smtp_settings = { # config.action_mailer.smtp_settings = {
@@ -80,11 +80,11 @@ Rails.application.configure do
config.active_record.attributes_for_inspect = [ :id ] config.active_record.attributes_for_inspect = [ :id ]
# Enable DNS rebinding protection and other `Host` header attacks. # Enable DNS rebinding protection and other `Host` header attacks.
# config.hosts = [ config.hosts = [
# "example.com", # Allow requests from example.com "diplo.kontei.net",
# /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` /.*\.kontei\.net/
# ] ]
#
# Skip DNS rebinding protection for the default health check endpoint. # Skip DNS rebinding protection for the default health check endpoint.
# config.host_authorization = { exclude: ->(request) { request.path == "/up" } } config.host_authorization = { exclude: ->(request) { request.path == "/up" } }
end end

37
script/deploy_dip_api.sh Normal file
View File

@@ -0,0 +1,37 @@
#!/bin/bash
# ==================================================
# dip_api (FastAPI) のビルド・起動スクリプト
# VPS上で実行
# ==================================================
set -e
# dip_api のソースコードがある場所VPSにコピー後
DIP_API_DIR="${1:-/home/kontei/dip_api}"
echo "=== dip_api Docker イメージのビルド ==="
cd "$DIP_API_DIR"
docker build -t dip-api:latest .
echo ""
echo "=== 既存コンテナの停止・削除(ある場合) ==="
docker stop dip-api 2>/dev/null || true
docker rm dip-api 2>/dev/null || true
echo ""
echo "=== dip_api コンテナの起動 ==="
docker run -d \
--name dip-api \
--restart always \
-p 8000:8000 \
dip-api:latest
echo ""
echo "=== ヘルスチェック ==="
sleep 3
if curl -s http://localhost:8000/debug/heartbeat > /dev/null 2>&1; then
echo "✅ dip_api は正常に起動しています"
else
echo "⚠️ dip_api の起動を確認中... (数秒待ってから再確認してください)"
echo " docker logs dip-api で確認"
fi

54
script/setup_vps.sh Normal file
View File

@@ -0,0 +1,54 @@
#!/bin/bash
# ==================================================
# さくらVPS 初期セットアップスクリプト
# 対象: Ubuntu 24.04 LTS
# 実行: ssh kontei@153.127.48.108 でログイン後に実行
# ==================================================
set -e
echo "=== 1. Docker のインストール ==="
# Docker の公式リポジトリを追加
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# kontei ユーザーを docker グループに追加sudo なしで docker を使えるようにする)
sudo usermod -aG docker kontei
echo "※ docker グループの反映には再ログインが必要です"
echo ""
echo "=== 2. Docker レジストリの起動 ==="
# Kamal がイメージをプッシュするためのローカルレジストリ
sudo docker run -d \
-p 5555:5000 \
--restart always \
--name registry \
registry:2
echo ""
echo "=== 3. ファイアウォール設定 ==="
# HTTP/HTTPS ポートを開放
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Docker レジストリ(ローカルのみ)
sudo ufw allow from 127.0.0.1 to any port 5555
echo "※ UFW が有効でない場合は 'sudo ufw enable' で有効化してください"
echo ""
echo "=== セットアップ完了 ==="
echo "次のステップ:"
echo " 1. 再ログインして docker グループを反映"
echo " 2. 'docker ps' で registry コンテナが起動していることを確認"
echo " 3. DNS で diplo.kontei.net → 153.127.48.108 の A レコードを設定"
echo " 4. ローカルPCから 'bin/kamal setup' を実行"

View File

@@ -2,7 +2,11 @@ require "test_helper"
class GamesControllerTest < ActionDispatch::IntegrationTest class GamesControllerTest < ActionDispatch::IntegrationTest
setup do setup do
@user = users(:one)
login_as(@user)
@game = games(:one) @game = games(:one)
# edit/update/destroy には管理者権限が必要
@game.participants.create!(user: @user, is_administrator: true) unless @game.participants.exists?(user: @user)
end end
test "should get index" do test "should get index" do
@@ -17,13 +21,22 @@ class GamesControllerTest < ActionDispatch::IntegrationTest
test "should create game" do test "should create game" do
assert_difference("Game.count") do assert_difference("Game.count") do
post games_url, params: { game: { memo: @game.memo, participants_count: @game.participants_count, title: @game.title } } post games_url, params: { game: {
title: "New Test Game",
memo: @game.memo,
participants_count: @game.participants_count,
victory_sc_count: 18,
scoring_system: "none"
} }
end end
assert_redirected_to game_url(Game.last) assert_redirected_to game_url(Game.last)
end end
test "should show game" do test "should show game" do
# showアクションではgame_stateが必要なため、ターンにgame_stateを設定
turn = @game.turns.first
turn.update(game_state: { "centers" => {}, "units" => {} }) if turn
get game_url(@game) get game_url(@game)
assert_response :success assert_response :success
end end
@@ -34,7 +47,13 @@ class GamesControllerTest < ActionDispatch::IntegrationTest
end end
test "should update game" do test "should update game" do
patch game_url(@game), params: { game: { memo: @game.memo, participants_count: @game.participants_count, title: @game.title } } patch game_url(@game), params: { game: {
title: @game.title,
memo: @game.memo,
auto_order_mode: @game.auto_order_mode,
victory_sc_count: 18,
scoring_system: "none"
} }
assert_redirected_to game_url(@game) assert_redirected_to game_url(@game)
end end

View File

@@ -2,6 +2,8 @@ require "test_helper"
class TurnsControllerTest < ActionDispatch::IntegrationTest class TurnsControllerTest < ActionDispatch::IntegrationTest
setup do setup do
@user = users(:one)
login_as(@user)
@turn = turns(:one) @turn = turns(:one)
end end
@@ -17,7 +19,7 @@ class TurnsControllerTest < ActionDispatch::IntegrationTest
test "should create turn" do test "should create turn" do
assert_difference("Turn.count") do assert_difference("Turn.count") do
post turns_url, params: { turn: { game_id: @turn.game_id, game_stat: @turn.game_stat, number: @turn.number, phase: @turn.phase, svg_date: @turn.svg_date } } post turns_url, params: { turn: { game_id: @turn.game_id, game_state: @turn.game_state, number: @turn.number + 10, phase: @turn.phase, svg_date: @turn.svg_date } }
end end
assert_redirected_to turn_url(Turn.last) assert_redirected_to turn_url(Turn.last)
@@ -34,7 +36,7 @@ class TurnsControllerTest < ActionDispatch::IntegrationTest
end end
test "should update turn" do test "should update turn" do
patch turn_url(@turn), params: { turn: { game_id: @turn.game_id, game_stat: @turn.game_stat, number: @turn.number, phase: @turn.phase, svg_date: @turn.svg_date } } patch turn_url(@turn), params: { turn: { game_id: @turn.game_id, game_state: @turn.game_state, number: @turn.number, phase: @turn.phase, svg_date: @turn.svg_date } }
assert_redirected_to turn_url(@turn) assert_redirected_to turn_url(@turn)
end end

View File

@@ -5,9 +5,13 @@ one:
status: in_progress status: in_progress
turn_schedule: "0,12" turn_schedule: "0,12"
participants_count: 7 participants_count: 7
victory_sc_count: 18
scoring_system: none
two: two:
title: GameTwo title: GameTwo
status: recruiting status: recruiting
turn_schedule: "0,12" turn_schedule: "0,12"
participants_count: 1 participants_count: 1
victory_sc_count: 18
scoring_system: none

View File

@@ -1,19 +1,27 @@
ENV["RAILS_ENV"] ||= "test" ENV["RAILS_ENV"] ||= "test"
require_relative "../test/test_helper" require_relative "../test_helper"
class RefactoringVerificationTest < ActiveSupport::TestCase class RefactoringVerificationTest < ActiveSupport::TestCase
def setup def setup
@game = Game.create!(title: "Refactor Test #{Time.now.to_r}", status: "recruiting", participants_count: 7, is_solo_mode: true) @game = Game.create!(
title: "Refactor Test #{Time.now.to_r}",
status: "recruiting",
participants_count: 7,
is_solo_mode: true,
victory_sc_count: 18,
scoring_system: "none"
)
end end
# Mock Client Class # Mock Client Class
class MockClient class MockClient
def initialize(initial_state: {}, possible_orders: {}, render_result: "<svg>...</svg>", process_result: nil, auto_orders: nil) def initialize(initial_state: {}, possible_orders: {}, render_result: "<svg>...</svg>", process_result: nil, auto_orders: nil, validate_result: {})
@initial_state = initial_state @initial_state = initial_state
@possible_orders = possible_orders @possible_orders = possible_orders
@render_result = render_result @render_result = render_result
@process_result = process_result @process_result = process_result
@auto_orders = auto_orders @auto_orders = auto_orders
@validate_result = validate_result
end end
def api_game_initial_state(map_name = "standard") def api_game_initial_state(map_name = "standard")
@@ -32,6 +40,10 @@ class RefactoringVerificationTest < ActiveSupport::TestCase
@process_result @process_result
end end
def api_calculate_validate(game_state, orders)
@validate_result
end
def api_calculate_auto_orders(game_state, power_name) def api_calculate_auto_orders(game_state, power_name)
@auto_orders @auto_orders
end end