Compare commits
2 Commits
e90ea88758
...
5c6d18eae0
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c6d18eae0 | |||
| 9626db3dcf |
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -22,14 +22,19 @@ 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
|
||||||
|
|
||||||
# 進行中:参加中の掲示板のみ(eager load で N+1 防止)
|
if @game.status == "power_selection"
|
||||||
@boards = @current_participant.boards.includes(:participants, :board_posts, :board_memberships).order(created_at: :desc)
|
# 国選択フェーズ:共通掲示板のみ表示
|
||||||
# 共通掲示板のフォールバック
|
@boards = @game.boards.global.includes(:participants, :board_posts, :board_memberships).to_a
|
||||||
global_board = @game.boards.global.first
|
else
|
||||||
if global_board && !@boards.include?(global_board)
|
# 進行中:参加中の掲示板のみ(eager load で N+1 防止)
|
||||||
@boards = [ global_board ] + @boards
|
@boards = @current_participant.boards.includes(:participants, :board_posts, :board_memberships).order(created_at: :desc)
|
||||||
|
# 共通掲示板のフォールバック
|
||||||
|
global_board = @game.boards.global.first
|
||||||
|
if global_board && !@boards.include?(global_board)
|
||||||
|
@boards = [ global_board ] + @boards
|
||||||
|
end
|
||||||
|
@boards = @boards.sort_by { |b| b.global? ? 0 : 1 }
|
||||||
end
|
end
|
||||||
@boards = @boards.sort_by { |b| b.global? ? 0 : 1 }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -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?
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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|
|
||||||
|
|||||||
@@ -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? %>
|
||||||
|
|||||||
@@ -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 %>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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? %>
|
||||||
|
|||||||
@@ -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==
|
||||||
@@ -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.
|
# nginx がSSL終端を行うため、Kamal Proxy の SSL は無効化
|
||||||
# If used with Cloudflare, set encryption mode in SSL/TLS setting to "Full" to enable CF-to-app encryption.
|
proxy:
|
||||||
#
|
ssl: false
|
||||||
# Using an SSL proxy like this requires turning on config.assume_ssl and config.force_ssl in production.rb!
|
host: diplo.kontei.net
|
||||||
#
|
app_port: 80
|
||||||
# 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
|
||||||
|
|||||||
@@ -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
37
script/deploy_dip_api.sh
Normal 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
54
script/setup_vps.sh
Normal 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' を実行"
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
4
test/fixtures/games.yml
vendored
4
test/fixtures/games.yml
vendored
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user