new supervisor step2

This commit is contained in:
hayano
2024-10-28 20:25:05 +00:00
parent 2913a435c1
commit a6b816c9f2
12 changed files with 733 additions and 138 deletions

View File

@ -53,6 +53,7 @@ RUN pip install gunicorn
# xlsxwriterを追加 # xlsxwriterを追加
RUN pip install -r requirements.txt \ RUN pip install -r requirements.txt \
&& pip install django-cors-headers \
&& pip install xlsxwriter gunicorn && pip install xlsxwriter gunicorn
COPY . /app COPY . /app

View File

@ -53,10 +53,14 @@ INSTALLED_APPS = [
'leaflet', 'leaflet',
'leaflet_admin_list', 'leaflet_admin_list',
'rog.apps.RogConfig', 'rog.apps.RogConfig',
'corsheaders', # added
'django_filters' 'django_filters'
] ]
MIDDLEWARE = [ MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # できるだけ上部に
'django.middleware.common.CommonMiddleware',
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
@ -68,6 +72,43 @@ MIDDLEWARE = [
ROOT_URLCONF = 'config.urls' ROOT_URLCONF = 'config.urls'
CORS_ALLOW_ALL_ORIGINS = True # 開発環境のみ
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOWED_METHODS = [
'GET',
'POST',
'PUT',
'PATCH',
'DELETE',
'OPTIONS'
]
CORS_ALLOWED_HEADERS = [
'accept',
'accept-encoding',
'authorization',
'content-type',
'dnt',
'origin',
'user-agent',
'x-csrftoken',
'x-requested-with',
]
# 本番環境では以下のように制限する
CORS_ALLOWED_ORIGINS = [
"https://rogaining.sumasen.net",
"http://rogaining.sumasen.net",
]
# CSRFの設定
CSRF_TRUSTED_ORIGINS = [
"http://rogaining.sumasen.net",
"https://rogaining.sumasen.net",
]
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates',
@ -231,11 +272,15 @@ LOGGING = {
'level': 'DEBUG', 'level': 'DEBUG',
}, },
'loggers': { 'loggers': {
#'django': { 'django': {
# 'handlers': ['console'], 'handlers': ['console'],
# 'level': 'INFO', 'level': 'INFO',
# 'propagate': False, 'propagate': False,
#}, },
'django.request': {
'handlers': ['console'],
'level': 'DEBUG',
},
'rog': { 'rog': {
#'handlers': ['file','console'], #'handlers': ['file','console'],
'handlers': ['console'], 'handlers': ['console'],

View File

@ -18,6 +18,21 @@ from django.urls import path, include
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
# debug_urlsビューをrogアプリケーションのviewsからインポート
from rog import views as rog_views
DEBUG = True
ALLOWED_HOSTS = ['rogaining.sumasen.net', 'localhost', '127.0.0.1']
# CORSの設定
CORS_ALLOW_ALL_ORIGINS = True
CORS_ALLOWED_ORIGINS = [
"http://rogaining.sumasen.net",
"http://localhost",
"http://127.0.0.1",
]
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('auth/', include('knox.urls')), path('auth/', include('knox.urls')),
@ -27,3 +42,8 @@ urlpatterns = [
admin.site.site_header = "ROGANING" admin.site.site_header = "ROGANING"
admin.site.site_title = "Roganing Admin Portal" admin.site.site_title = "Roganing Admin Portal"
admin.site.index_title = "Welcome to Roganing Portal" admin.site.index_title = "Welcome to Roganing Portal"
# 開発環境での静的ファイル配信
if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@ -30,40 +30,37 @@ services:
env_file: env_file:
- .env - .env
restart: "on-failure" restart: "on-failure"
# depends_on:
# - postgres-db
networks: networks:
- rog-api - rog-api
#entrypoint: ["/app/wait-for.sh", "postgres-db:5432", "--", ""]
#command: python3 manage.py runserver 0.0.0.0:8100
supervisor-web: supervisor-web:
build: build:
context: . context: .
dockerfile: Dockerfile.supervisor dockerfile: Dockerfile.supervisor
volumes: volumes:
- type: bind - type: bind
source: ./supervisor/html source: ./supervisor/html
target: /usr/share/nginx/html target: /usr/share/nginx/html/supervisor
read_only: true read_only: true
- type: bind - type: bind
source: ./supervisor/nginx/default.conf source: ./supervisor/nginx/default.conf
target: /etc/nginx/conf.d/default.conf target: /etc/nginx/conf.d/default.conf
read_only: true read_only: true
- type: volume - type: volume
source: static_volume source: static_volume
target: /app/static target: /app/static
read_only: true read_only: true
- type: volume - type: volume
source: nginx_logs source: nginx_logs
target: /var/log/nginx target: /var/log/nginx
ports: ports:
- "80:80" - "80:80"
depends_on: depends_on:
- api - api
networks: networks:
- rog-api - rog-api
restart: always restart: always
networks: networks:

81
docker-compose.yaml.ssl Normal file
View File

@ -0,0 +1,81 @@
version: "3.9"
services:
# postgres-db:
# image: kartoza/postgis:12.0
# ports:
# - 5432:5432
# volumes:
# - postgres_data:/var/lib/postgresql
# - ./custom-postgresql.conf:/etc/postgresql/12/main/postgresql.conf
# environment:
# - POSTGRES_USER=${POSTGRES_USER}
# - POSTGRES_PASS=${POSTGRES_PASS}
# - POSTGRES_DBNAME=${POSTGRES_DBNAME}
# - POSTGRES_MAX_CONNECTIONS=600
# restart: "on-failure"
# networks:
# - rog-api
api:
build:
context: .
dockerfile: Dockerfile.gdal
command: python3 manage.py runserver 0.0.0.0:8100
volumes:
- .:/app
ports:
- 8100:8100
env_file:
- .env
restart: "on-failure"
# depends_on:
# - postgres-db
networks:
- rog-api
#entrypoint: ["/app/wait-for.sh", "postgres-db:5432", "--", ""]
#command: python3 manage.py runserver 0.0.0.0:8100
supervisor-web:
build:
context: .
dockerfile: Dockerfile.supervisor
volumes:
- type: bind
source: /etc/letsencrypt
target: /etc/nginx/ssl
read_only: true
- type: bind
source: ./supervisor/html
target: /usr/share/nginx/html
read_only: true
- type: bind
source: ./supervisor/nginx/default.conf
target: /etc/nginx/conf.d/default.conf
read_only: true
- type: volume
source: static_volume
target: /app/static
read_only: true
- type: volume
source: nginx_logs
target: /var/log/nginx
ports:
- "80:80"
depends_on:
- api
networks:
- rog-api
restart: always
networks:
rog-api:
driver: bridge
volumes:
postgres_data:
geoserver-data:
static_volume:
nginx_logs:

View File

@ -65,3 +65,4 @@ django-extra-fields==3.0.2
django-phonenumber-field==6.1.0 django-phonenumber-field==6.1.0
django-rest-knox==4.2.0 django-rest-knox==4.2.0
dj-database-url==2.0.0 dj-database-url==2.0.0
django-cors-headers==4.3.0

View File

@ -1,7 +1,8 @@
from sys import prefix from sys import prefix
from rest_framework import urlpatterns from rest_framework import urlpatterns
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from .views import LocationViewSet, Location_lineViewSet, Location_polygonViewSet, Jpn_Main_PerfViewSet, LocationsInPerf, ExtentForSubPerf, SubPerfInMainPerf, ExtentForMainPerf, LocationsInSubPerf, CatView, RegistrationAPI, LoginAPI, UserAPI, UserActionViewset, UserMakeActionViewset, UserDestinations, UpdateOrder, LocationInBound, DeleteDestination, CustomAreaLocations, GetAllGifuAreas, CustomAreaNames, userDetials, UserTracksViewSet, CatByCity, ChangePasswordView, GoalImageViewSet, CheckinImageViewSet, ExtentForLocations, DeleteAccount, PrivacyView, RegistrationView, TeamViewSet,MemberViewSet,EntryViewSet,RegisterView, VerifyEmailView, NewEventListView,NewEvent2ListView,NewCategoryListView,CategoryListView, MemberUserDetailView, TeamMembersWithUserView,MemberAddView,UserActivationView,RegistrationView,TempUserRegistrationView,ResendInvitationEmailView,update_user_info,update_user_detail,ActivateMemberView, ActivateNewMemberView, PasswordResetRequestView, PasswordResetConfirmView, NewCategoryViewSet,LocationInBound2,UserLastGoalTimeView,TeamEntriesView,update_entry_status,get_events,get_zekken_numbers,get_team_info,get_checkins,update_checkins,export_excel from .views import LocationViewSet, Location_lineViewSet, Location_polygonViewSet, Jpn_Main_PerfViewSet, LocationsInPerf, ExtentForSubPerf, SubPerfInMainPerf, ExtentForMainPerf, LocationsInSubPerf, CatView, RegistrationAPI, LoginAPI, UserAPI, UserActionViewset, UserMakeActionViewset, UserDestinations, UpdateOrder, LocationInBound, DeleteDestination, CustomAreaLocations, GetAllGifuAreas, CustomAreaNames, userDetials, UserTracksViewSet, CatByCity, ChangePasswordView, GoalImageViewSet, CheckinImageViewSet, ExtentForLocations, DeleteAccount, PrivacyView, RegistrationView, TeamViewSet,MemberViewSet,EntryViewSet,RegisterView, VerifyEmailView, NewEventListView,NewEvent2ListView,NewCategoryListView,CategoryListView, MemberUserDetailView, TeamMembersWithUserView,MemberAddView,UserActivationView,RegistrationView,TempUserRegistrationView,ResendInvitationEmailView,update_user_info,update_user_detail,ActivateMemberView, ActivateNewMemberView, PasswordResetRequestView, PasswordResetConfirmView, NewCategoryViewSet,LocationInBound2,UserLastGoalTimeView,TeamEntriesView,update_entry_status,get_events,get_zekken_numbers,get_team_info,get_checkins,update_checkins,export_excel,debug_urls
from django.urls import path, include from django.urls import path, include
from knox import views as knox_views from knox import views as knox_views
@ -51,7 +52,7 @@ router.register(r'newevent2', views.NewEvent2ViewSet)
# GET /api/teams/<team_id>/members-with-user/: 特定のチームの全メンバーとそのユーザー情報を取得 # GET /api/teams/<team_id>/members-with-user/: 特定のチームの全メンバーとそのユーザー情報を取得
app_name = 'rog' # 名前空間を追加
urlpatterns = router.urls urlpatterns = router.urls
@ -113,11 +114,13 @@ urlpatterns += [
# for Supervisor Web app # for Supervisor Web app
path('api/events/', get_events, name='get_events'), path('events/', views.get_events, name='get_events'),
path('api/zekken_numbers/<str:event_code>/', get_zekken_numbers, name='get_zekken_numbers'), path('debug/urls/', views.debug_urls, name='debug_urls'),
path('api/team_info/<int:zekken_number>/', get_team_info, name='get_team_info'), path('zekken_numbers/<str:event_code>/', views.get_zekken_numbers, name='get_zekken_numbers'),
path('api/checkins/<int:zekken_number>/', get_checkins, name='get_checkins'), path('team_info/<int:zekken_number>/', views.get_team_info, name='get_team_info'),
path('api/update_checkins/', update_checkins, name='update_checkins'), path('checkins/<int:zekken_number>/<str:event_code>/', views.get_checkins, name='get_checkins'),
path('api/export_excel/<int:zekken_number>/', export_excel, name='export_excel'), path('update_checkins/', views.update_checkins, name='update_checkins'),
path('export_excel/<int:zekken_number>/<str:event_code>/', views.export_excel, name='export_excel'),
# for Supervisor Web app # for Supervisor Web app
path('test/', views.test_api, name='test_api'),
] ]

View File

@ -30,7 +30,7 @@ from .permissions import IsTeamOwner,IsTeamOwnerOrMember
from curses.ascii import NUL from curses.ascii import NUL
from django.core.serializers import serialize from django.core.serializers import serialize
from .models import GoalImages, Location, Location_line, Location_polygon, JpnAdminMainPerf, Useractions, GifuAreas, RogUser, CustomUser, UserTracks, GoalImages, CheckinImages, NewEvent,NewEvent2, Team, Category, NewCategory,Entry, Member, TempUser,EntryMember from .models import GoalImages, Location, Location_line, Location_polygon, JpnAdminMainPerf, Useractions, GifuAreas, RogUser, CustomUser, UserTracks, GoalImages, CheckinImages, NewEvent,NewEvent2, Team, Category, NewCategory,Entry, Member, TempUser, EntryMember, GpsCheckin
from rest_framework import viewsets from rest_framework import viewsets
from .serializers import LocationSerializer, Location_lineSerializer, Location_polygonSerializer, JPN_main_perfSerializer, LocationCatSerializer, UserSerializer, LoginUserSerializer, UseractionsSerializer, UserDestinationSerializer, GifuAreaSerializer, LocationEventNameSerializer, RogUserSerializer, UserTracksSerializer, ChangePasswordSerializer, GolaImageSerializer, CheckinImageSerializer, RegistrationSerializer, MemberWithUserSerializer,TempUserRegistrationSerializer, PasswordResetRequestSerializer, PasswordResetConfirmSerializer from .serializers import LocationSerializer, Location_lineSerializer, Location_polygonSerializer, JPN_main_perfSerializer, LocationCatSerializer, UserSerializer, LoginUserSerializer, UseractionsSerializer, UserDestinationSerializer, GifuAreaSerializer, LocationEventNameSerializer, RogUserSerializer, UserTracksSerializer, ChangePasswordSerializer, GolaImageSerializer, CheckinImageSerializer, RegistrationSerializer, MemberWithUserSerializer,TempUserRegistrationSerializer, PasswordResetRequestSerializer, PasswordResetConfirmSerializer
from knox.models import AuthToken from knox.models import AuthToken
@ -52,7 +52,6 @@ from django.db.models import Q
from rest_framework import permissions from rest_framework import permissions
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.decorators import api_view
from rest_framework.decorators import api_view, permission_classes from rest_framework.decorators import api_view, permission_classes
from rest_framework.parsers import JSONParser, MultiPartParser from rest_framework.parsers import JSONParser, MultiPartParser
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
@ -63,7 +62,7 @@ from django.utils.decorators import method_decorator
from django.utils.encoding import force_str from django.utils.encoding import force_str
import logging import logging
from datetime import datetime from datetime import datetime,timedelta
from django.contrib.gis.measure import D from django.contrib.gis.measure import D
from django.contrib.gis.geos import Point from django.contrib.gis.geos import Point
@ -88,6 +87,8 @@ from django.core.exceptions import ValidationError
import xlsxwriter import xlsxwriter
from io import BytesIO from io import BytesIO
from django.urls import get_resolver
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@api_view(['PATCH']) @api_view(['PATCH'])
@ -2316,14 +2317,38 @@ class UserLastGoalTimeView(APIView):
# ----- for Supervisor ----- # ----- for Supervisor -----
@api_view(['GET']) @api_view(['GET'])
def get_events(request): def debug_urls(request):
events = NewEvent2.objects.filter(public=True) """デバッグ用利用可能なURLパターンを表示"""
return Response([{ resolver = get_resolver()
'code': event.event_name, urls = []
'name': event.event_name for pattern in resolver.url_patterns:
} for event in events]) try:
urls.append(str(pattern.pattern))
except:
urls.append(str(pattern))
return JsonResponse({'urls': urls})
@api_view(['GET']) @api_view(['GET'])
def get_events(request):
logger.debug(f"get_events was called. Path: {request.path}")
try:
events = NewEvent2.objects.filter(public=True)
logger.debug(f"Found {events.count()} events") # デバッグ用ログ
return Response([{
'code': event.event_name,
'name': event.event_name,
'start_datetime': event.start_datetime,
'end_datetime': event.end_datetime,
} for event in events])
logger.debug(f"Returning data: {data}") # デバッグ用ログ
return JsonResponse(data, safe=False)
except Exception as e:
print(f"Error in get_events: {str(e)}") # デバッグ用
return Response(
{"error": "Failed to retrieve events"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['GET'])
def get_zekken_numbers(request, event_code): def get_zekken_numbers(request, event_code):
entries = Entry.objects.filter( entries = Entry.objects.filter(
event__event_name=event_code, event__event_name=event_code,
@ -2333,31 +2358,101 @@ def get_zekken_numbers(request, event_code):
@api_view(['GET']) @api_view(['GET'])
def get_team_info(request, zekken_number): def get_team_info(request, zekken_number):
entry = Entry.objects.select_related('team').get(zekken_number=zekken_number) entry = Entry.objects.select_related('team','event').get(zekken_number=zekken_number)
members = Member.objects.filter(team=entry.team) members = Member.objects.filter(team=entry.team)
# チームのゴール時間を取得
goal_record = GoalImages.objects.filter(
team_name=entry.team.team_name,
event_code=entry.event.event_name
).order_by('-goaltime').first()
return Response({ return Response({
'team_name': entry.team.team_name, 'team_name': entry.team.team_name,
'members': ', '.join([f"{m.lastname} {m.firstname}" for m in members]), 'members': ', '.join([f"{m.lastname} {m.firstname}" for m in members]),
'start_time': entry.start_time.strftime('%Y-%m-%d %H:%M:%S') if entry.start_time else None, 'event_code': entry.event.event_name,
'goal_time': entry.goal_time.strftime('%Y-%m-%d %H:%M:%S') if entry.goal_time else None, 'start_datetime': entry.event.start_datetime,
'late_points': entry.late_point or 0 'end_datetime': goal_record.goaltime if goal_record else None
}) })
def create(self, request, *args, **kwargs):
logger.info(f"Attempting to create new member for team: {self.kwargs['team_id']}")
logger.debug(f"Received data: {request.data}")
team = Team.objects.get(id=self.kwargs['team_id'])
@api_view(['GET']) @api_view(['GET'])
def get_checkins(request, zekken_number): def get_checkins(request, *args, **kwargs):
checkins = GpsCheckin.objects.filter( #def get_checkins(request, zekken_number, event_code):
zekken_number=zekken_number try:
).order_by('path_order') # イベントコードをクエリパラメータから取得
return Response([{
'id': c.id, zekken_number = kwargs['zekken_number']
'path_order': c.path_order, if not zekken_number:
'cp_number': c.cp_number, logger.debug(f"=== Zekken_number is required.")
'create_at': c.create_at, return Response({"error": "zekken_number is required"}, status=400)
'validate_location': c.validate_location,
'points': c.points, event_code = kwargs['event_code']
'buy_flag': c.buy_flag if not event_code:
} for c in checkins]) logger.debug(f"=== event_code is required.")
return Response({"error": "event_code is required"}, status=400)
# チェックインデータの取得とCP情報の結合
checkins = GpsCheckin.objects.filter(
zekken_number=zekken_number,
event_code=event_code
).order_by('path_order')
# すべてのフィールドを確実に取得できるようにデバッグログを追加
logger.debug(f"Found {checkins.count()} checkins for zekken_number {zekken_number} and event_code {event_code}")
data = []
for c in checkins:
location = Location.objects.filter(cp=c.cp_number,group=event_code).first()
if location:
formatted_time = None
if c.create_at:
try:
# create_atが文字列の場合はdatetimeに変換
if isinstance(c.create_at, str):
c.create_at = datetime.strptime(c.create_at, '%Y-%m-%d %H:%M:%S')
# JST(+9時間)に変換して時刻フォーマット
jst_time = c.create_at + timedelta(hours=9)
formatted_time = jst_time.strftime('%H:%M:%S')
except (ValueError, TypeError, AttributeError) as e:
logger.error(f"Error formatting create_at for checkin {c.id}: {str(e)}")
logger.error(f"Raw create_at value: {c.create_at}")
formatted_time = None
data.append({
'id': c.id,
'path_order': c.path_order,
'cp_number': c.cp_number,
'sub_loc_id': location.sub_loc_id if location else f"#{c.cp_number}",
'location_name': location.location_name if location else None,
'create_at': formatted_time, #(c.create_at + timedelta(hours=9)).strftime('%H:%M:%S') if c.create_at else None,
'validate_location': c.validate_location,
'points': c.points or 0,
'buy_flag': c.buy_flag,
'photos': location.photos if location else None,
'image_address': c.image_address,
'receipt_address': c.image_receipt,
'location_name': location.location_name if location else None,
'checkin_point': location.checkin_point if location else None,
'buy_point': location.buy_point
})
logger.debug(f"data={data}")
return Response(data)
except Exception as e:
logger.error(f"Error in get_checkins: {str(e)}", exc_info=True)
return Response(
{"error": f"Failed to retrieve checkins: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
@api_view(['POST']) @api_view(['POST'])
def update_checkins(request): def update_checkins(request):
@ -2418,4 +2513,8 @@ def export_excel(request, zekken_number):
# ----- for Supervisor ----- # ----- for Supervisor -----
@api_view(['GET'])
def test_api(request):
logger.debug("Test API endpoint called")
return JsonResponse({"status": "API is working"})

View File

@ -13,7 +13,7 @@
<h1 class="text-2xl font-bold mb-6">スーパーバイザーパネル</h1> <h1 class="text-2xl font-bold mb-6">スーパーバイザーパネル</h1>
<!-- 選択フォーム --> <!-- 選択フォーム -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6"> <div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-2">イベントコード</label> <label class="block text-sm font-medium text-gray-700 mb-2">イベントコード</label>
<select id="eventCode" class="w-full border border-gray-300 rounded-md px-3 py-2"> <select id="eventCode" class="w-full border border-gray-300 rounded-md px-3 py-2">
@ -26,10 +26,6 @@
<option value="">ゼッケン番号を選択してください</option> <option value="">ゼッケン番号を選択してください</option>
</select> </select>
</div> </div>
</div>
<!-- チーム情報サマリー -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div class="bg-gray-50 p-4 rounded-lg"> <div class="bg-gray-50 p-4 rounded-lg">
<div class="text-sm text-gray-500">チーム名</div> <div class="text-sm text-gray-500">チーム名</div>
<div id="teamName" class="font-semibold"></div> <div id="teamName" class="font-semibold"></div>
@ -38,6 +34,10 @@
<div class="text-sm text-gray-500">メンバー</div> <div class="text-sm text-gray-500">メンバー</div>
<div id="members" class="font-semibold"></div> <div id="members" class="font-semibold"></div>
</div> </div>
</div>
<!-- チーム情報サマリー -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div class="bg-gray-50 p-4 rounded-lg"> <div class="bg-gray-50 p-4 rounded-lg">
<div class="text-sm text-gray-500">スタート時刻</div> <div class="text-sm text-gray-500">スタート時刻</div>
<div id="startTime" class="font-semibold"></div> <div id="startTime" class="font-semibold"></div>
@ -46,15 +46,23 @@
<div class="text-sm text-gray-500">ゴール時刻</div> <div class="text-sm text-gray-500">ゴール時刻</div>
<div id="goalTime" class="font-semibold"></div> <div id="goalTime" class="font-semibold"></div>
</div> </div>
</div> <div class="bg-gray-50 p-4 rounded-lg">
<div class="text-sm text-gray-500">ゴール時計</div>
<div id="goalTime" class="font-semibold"></div>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<div class="text-sm text-gray-500">判定</div>
<div id="validate" class="font-semibold text-blue-600"></div>
</div>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6"> <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div class="bg-gray-50 p-4 rounded-lg"> <div class="bg-gray-50 p-4 rounded-lg">
<div class="text-sm text-gray-500">チェックインポイント合計</div> <div class="text-sm text-gray-500">CP合計</div>
<div id="totalPoints" class="font-semibold"></div> <div id="totalPoints" class="font-semibold"></div>
</div> </div>
<div class="bg-gray-50 p-4 rounded-lg"> <div class="bg-gray-50 p-4 rounded-lg">
<div class="text-sm text-gray-500">い物ポイント合計</div> <div class="text-sm text-gray-500">合計</div>
<div id="buyPoints" class="font-semibold"></div> <div id="buyPoints" class="font-semibold"></div>
</div> </div>
<div class="bg-gray-50 p-4 rounded-lg"> <div class="bg-gray-50 p-4 rounded-lg">
@ -62,7 +70,7 @@
<div id="latePoints" class="font-semibold text-red-600"></div> <div id="latePoints" class="font-semibold text-red-600"></div>
</div> </div>
<div class="bg-gray-50 p-4 rounded-lg"> <div class="bg-gray-50 p-4 rounded-lg">
<div class="text-sm text-gray-500">総合計ポイント</div> <div class="text-sm text-gray-500">総合計</div>
<div id="finalPoints" class="font-semibold text-blue-600"></div> <div id="finalPoints" class="font-semibold text-blue-600"></div>
</div> </div>
</div> </div>
@ -72,11 +80,14 @@
<table class="min-w-full divide-y divide-gray-200"> <table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50"> <thead class="bg-gray-50">
<tr> <tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase"></th> <th class="px-2 py-3 text-left text-xs font-medium text-gray-500 uppercase">走行</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">CP番号</th> <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">規定写真</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">チェックイン時刻</th> <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">撮影写真</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">検証</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">CP名称</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">ポイント</th> <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">通過時刻</th>
<th class="px-2 py-3 text-left text-xs font-medium text-gray-500 uppercase">通過審査</th>
<th class="px-2 py-3 text-left text-xs font-medium text-gray-500 uppercase">買物審査</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">獲得点数</th>
</tr> </tr>
</thead> </thead>
<tbody id="checkinList" class="bg-white divide-y divide-gray-200"> <tbody id="checkinList" class="bg-white divide-y divide-gray-200">
@ -87,6 +98,9 @@
<!-- アクションボタン --> <!-- アクションボタン -->
<div class="mt-6 flex justify-end space-x-4"> <div class="mt-6 flex justify-end space-x-4">
<button onclick="showAddCPModal()" class="px-4 py-2 bg-blue-500 text-white rounded">
新規CP追加
</button>
<button id="saveButton" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"> <button id="saveButton" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
保存 保存
</button> </button>
@ -98,6 +112,10 @@
</div> </div>
<script> <script>
// APIのベースURLを環境に応じて設定
const API_BASE_URL = '/api';
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Sortable初期化 // Sortable初期化
const checkinList = document.getElementById('checkinList'); const checkinList = document.getElementById('checkinList');
@ -108,16 +126,25 @@
} }
}); });
// イベントコードのドロップダウン要素を取得
const eventCodeSelect = document.getElementById('eventCode');
const zekkenNumberSelect = document.getElementById('zekkenNumber');
// ゼッケン番号変更時の処理
zekkenNumberSelect.addEventListener('change', function(e) {
const eventCode = eventCodeSelect.value;
if (!eventCode) {
alert('イベントコードを選択してください');
return;
}
loadTeamData(e.target.value, eventCode);
});
// イベントコード変更時の処理 // イベントコード変更時の処理
document.getElementById('eventCode').addEventListener('change', function(e) { document.getElementById('eventCode').addEventListener('change', function(e) {
loadZekkenNumbers(e.target.value); loadZekkenNumbers(e.target.value);
}); });
// ゼッケン番号変更時の処理
document.getElementById('zekkenNumber').addEventListener('change', function(e) {
loadTeamData(e.target.value);
});
// チェックボックス変更時の処理 // チェックボックス変更時の処理
checkinList.addEventListener('change', function(e) { checkinList.addEventListener('change', function(e) {
if (e.target.type === 'checkbox') { if (e.target.type === 'checkbox') {
@ -131,28 +158,46 @@
// Excel出力ボタンの処理 // Excel出力ボタンの処理
document.getElementById('exportButton').addEventListener('click', exportExcel); document.getElementById('exportButton').addEventListener('click', exportExcel);
console.log('Page loaded, attempting to load events...');
// 初期データ読み込み // 初期データ読み込み
loadEventCodes(); loadEventCodes();
}); });
function loadEventCodes() { async function loadEventCodes() {
// APIからイベントコードを取得して選択肢を設定 try {
fetch('/api/events') const response = await fetch(`${API_BASE_URL}/events/`);
.then(response => response.json()) if (!response.ok) {
.then(data => { throw new Error(`HTTP error! status: ${response.status}`);
const select = document.getElementById('eventCode'); }
data.forEach(event => { const contentType = response.headers.get("content-type");
const option = document.createElement('option'); if (!contentType || !contentType.includes("application/json")) {
option.value = event.code; throw new TypeError("Response is not JSON");
option.textContent = event.name; }
select.appendChild(option);
}); const data = await response.json();
const select = document.getElementById('eventCode');
// 既存のオプションをクリア
select.innerHTML = '<option value="">イベントを選択してください</option>';
data.forEach(event => {
const option = document.createElement('option');
option.value = event.code;
option.textContent = event.name;
select.appendChild(option);
}); });
} catch (error) {
console.error('Error loading events:', error);
// ユーザーにエラーを表示
const select = document.getElementById('eventCode');
select.innerHTML = '<option value="">エラー: イベントの読み込みに失敗しました</option>';
}
} }
function loadZekkenNumbers(eventCode) { function loadZekkenNumbers(eventCode) {
// APIからゼッケン番号を取得して選択肢を設定 // APIからゼッケン番号を取得して選択肢を設定
fetch(`/api/zekken-numbers/${eventCode}`) fetch(`${API_BASE_URL}/zekken_numbers/${eventCode}`)
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
const select = document.getElementById('zekkenNumber'); const select = document.getElementById('zekkenNumber');
@ -166,11 +211,129 @@
}); });
} }
function loadTeamData(zekkenNumber) {
async function loadTeamData(zekkenNumber,event_code) {
try {
const [teamResponse, checkinsResponse] = await Promise.all([
fetch(`${API_BASE_URL}/team_info/${zekkenNumber}/`),
fetch(`${API_BASE_URL}/checkins/${zekkenNumber}/${event_code}/`),
]);
// 各レスポンスのステータスを個別にチェック
if (!teamResponse.ok)
throw new Error(`Team info fetch failed with status ${teamResponse.status}`);
if (!checkinsResponse.ok)
throw new Error(`Checkins fetch failed with status ${checkinsResponse.status}`);
const teamData = await teamResponse.json();
const checkinsData = await checkinsResponse.json();
// イベントコードに対応するイベントを検索
//const event = eventData.find(e => e.code === document.getElementById('eventCode').value);
document.getElementById('teamName').textContent = teamData.team_name || '-';
document.getElementById('members').textContent = teamData.members || '-';
document.getElementById('startTime').textContent =
teamData.start_datetime ? new Date(teamData.start_datetime).toLocaleString() : '-';
document.getElementById('goalTime').textContent =
teamData.end_datetime ? new Date(teamData.end_datetime).toLocaleString() : '-';
//'(未ゴール)';
// チェックインリストの更新
const tbody = document.getElementById('checkinList');
tbody.innerHTML = ''; // 既存のデータをクリア
let totalPoints = 0;
let buyPoints = 0;
checkinsData.forEach((checkin, index) => {
const tr = document.createElement('tr');
tr.dataset.id = checkin.id;
tr.dataset.cpNumber = checkin.cp_number;
const bgColor = checkin.buy_point > 0 ? 'bg-blue-100' : 'bg-red-100';
tr.innerHTML = `
<td class="px-6 py-4">${checkin.path_order}</td>
<td class="px-6 py-4">
${location.photos ?
`<img src="${checkin.photos}" class="h-20 w-20 object-cover rounded">` : ''}
<div class="text-sm">${checkin.photos}</div>
</td>
<td class="px-6 py-4">
${checkin.image_address ?
`<img src="${checkin.image_address}" class="h-20 w-20 object-cover rounded">` : ''}
${checkin.receipt_address && checkin.buy_flag ?
`<img src="${checkin.receipt_address}" class="h-20 w-20 object-cover rounded ml-2">` : ''}
<div class="text-sm">${checkin.image_address}</div>
<div class="text-sm">${checkin.receipt_address}</div>
</td>
<td class="px-6 py-4 ${bgColor}">
<div class="font-bold">${checkin.sub_loc_id}</div>
<div class="text-sm">${checkin.location_name}</div>
</td>
<td class="px-6 py-4">${checkin.create_at || '不明'}</td>
<td class="px-6 py-4">
<input type="checkbox"
${checkin.validate_location ? 'checked' : ''}
class="h-4 w-4 text-blue-600 rounded validate-checkbox"
onchange="updatePoints(this)">
</td>
<td class="px-6 py-4">
${checkin.buy_point > 0 ? `
<input type="checkbox"
${checkin.buy_flag ? 'checked' : ''}
class="h-4 w-4 text-green-600 rounded buy-checkbox"
onchange="updateBuyPoints(this)">
` : ''}
</td>
<td class="px-6 py-4 point-value">${checkin.points}</td>
`;
tbody.appendChild(tr);
if (checkin.points) {
if (checkin.buy_flag) {
buyPoints += checkin.points;
} else {
totalPoints += checkin.points;
}
}
});
// 合計ポイントの更新
document.getElementById('totalPoints').textContent = totalPoints;
document.getElementById('buyPoints').textContent = buyPoints;
document.getElementById('latePoints').textContent = teamData.late_points || 0;
document.getElementById('finalPoints').textContent =
totalPoints + buyPoints - (teamData.late_points || 0);
} catch (error) {
console.error('Error loading team info:', error);
// エラーメッセージをユーザーに表示
alert(`データの読み込みに失敗しました: ${error.message}`);
// UIをクリア
document.getElementById('teamName').textContent = '-';
document.getElementById('members').textContent = '-';
document.getElementById('startTime').textContent = '-';
document.getElementById('goalTime').textContent = '-';
document.getElementById('checkinList').innerHTML = '';
document.getElementById('totalPoints').textContent = '0';
document.getElementById('buyPoints').textContent = '0';
document.getElementById('latePoints').textContent = '0';
document.getElementById('finalPoints').textContent = '0';
}
}
function loadTeamData_old(zekkenNumber) {
// チーム情報とチェックインデータを取得 // チーム情報とチェックインデータを取得
Promise.all([ Promise.all([
fetch(`/api/team-info/${zekkenNumber}`), fetch(`${API_BASE_URL}/team_info/${zekkenNumber}`),
fetch(`/api/checkins/${zekkenNumber}`) fetch(`${API_BASE_URL}/checkins/${zekkenNumber}`)
]).then(responses => Promise.all(responses.map(r => r.json()))) ]).then(responses => Promise.all(responses.map(r => r.json())))
.then(([teamInfo, checkins]) => { .then(([teamInfo, checkins]) => {
updateTeamInfo(teamInfo); updateTeamInfo(teamInfo);
@ -194,20 +357,137 @@
checkins.forEach((checkin, index) => { checkins.forEach((checkin, index) => {
const tr = document.createElement('tr'); const tr = document.createElement('tr');
tr.dataset.id = checkin.id; tr.dataset.id = checkin.id;
tr.dataset.cpNumber = checkin.cp_number;
const bgColor = checkin.buy_point > 0 ? 'bg-blue-100' : 'bg-red-100';
tr.innerHTML = ` tr.innerHTML = `
<td class="px-6 py-4">${index + 1}</td> <td class="px-6 py-4">${checkin.path_order}</td>
<td class="px-6 py-4">${checkin.cp_number}</td> <td class="px-6 py-4 ${bgColor}">
<td class="px-6 py-4">${formatDateTime(checkin.create_at)}</td> <div class="font-bold">${checkin.sub_loc_id}</div>
<td class="px-6 py-4"> <div class="text-sm">${checkin.location_name}</div>
<input type="checkbox" ${checkin.validate_location ? 'checked' : ''}
class="h-4 w-4 text-blue-600 rounded validate-checkbox">
</td> </td>
<td class="px-6 py-4">${checkin.points}</td> <td class="px-6 py-4">
${checkin.image_address ?
`<img src="${checkin.image_address}" class="h-20 w-20 object-cover rounded">` : ''}
${checkin.receipt_address && checkin.buy_flag ?
`<img src="${checkin.receipt_address}" class="h-20 w-20 object-cover rounded ml-2">` : ''}
</td>
<td class="px-6 py-4">${checkin.create_at || '不明'}</td>
<td class="px-6 py-4">
<input type="checkbox"
${checkin.validate_location ? 'checked' : ''}
class="h-4 w-4 text-blue-600 rounded validate-checkbox"
onchange="updatePoints(this)">
</td>
<td class="px-6 py-4">
${checkin.buy_point > 0 ? `
<input type="checkbox"
${checkin.buy_flag ? 'checked' : ''}
class="h-4 w-4 text-green-600 rounded buy-checkbox"
onchange="updateBuyPoints(this)">
` : ''}
</td>
<td class="px-6 py-4 point-value">${checkin.points}</td>
`; `;
tbody.appendChild(tr); tbody.appendChild(tr);
}); });
calculateTotalPoints();
} }
// ポイント更新関数
function updatePoints(checkbox) {
const tr = checkbox.closest('tr');
const pointCell = tr.querySelector('.point-value');
const cpNumber = tr.dataset.cpNumber;
fetch(`${API_BASE_URL}/location/${cpNumber}/`)
.then(response => response.json())
.then(location => {
const points = checkbox.checked ? location.checkin_point : 0;
pointCell.textContent = points;
calculateTotalPoints();
});
}
// 買い物ポイント更新関数
function updateBuyPoints(checkbox) {
const tr = checkbox.closest('tr');
const pointCell = tr.querySelector('.point-value');
const cpNumber = tr.dataset.cpNumber;
fetch(`${API_BASE_URL}/location/${cpNumber}/`)
.then(response => response.json())
.then(location => {
const points = checkbox.checked ? location.buy_point : 0;
pointCell.textContent = points;
calculateTotalPoints();
});
}
// 新規CP追加用のモーダル
function showAddCPModal() {
const modal = document.createElement('div');
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center';
modal.innerHTML = `
<div class="bg-white p-6 rounded-lg w-96">
<h3 class="text-lg font-bold mb-4">新規CPを追加</h3>
<div class="space-y-4" id="cpInputs">
<div class="flex gap-2">
<input type="number" class="cp-input border rounded px-2 py-1 w-full" placeholder="CP番号">
</div>
</div>
<div class="flex justify-end mt-4 space-x-2">
<button onclick="addCPInput()" class="px-4 py-2 bg-blue-500 text-white rounded">
追加
</button>
<button onclick="saveCPs()" class="px-4 py-2 bg-green-500 text-white rounded">
保存
</button>
<button onclick="closeModal()" class="px-4 py-2 bg-gray-500 text-white rounded">
キャンセル
</button>
</div>
</div>
`;
document.body.appendChild(modal);
}
function saveCPs() {
const inputs = document.querySelectorAll('.cp-input');
const eventCode = eventCodeSelect.value;
const newCheckins = Array.from(inputs)
.map(input => ({
cp_number: input.value,
create_at: null,
validate_location: false,
buy_flag: false,
points: 0
}));
// APIを呼び出して保存
fetch(`${API_BASE_URL}/checkins/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
zekken_number: currentZekkenNumber,
checkins: newCheckins
})
})
.then(response => response.json())
.then(data => {
loadTeamData(currentZekkenNumber,eventCode);
closeModal();
});
}
function closeModal() {
document.querySelector('.fixed').remove();
}
function updatePathOrders() { function updatePathOrders() {
const rows = Array.from(document.getElementById('checkinList').children); const rows = Array.from(document.getElementById('checkinList').children);
rows.forEach((row, index) => { rows.forEach((row, index) => {
@ -250,7 +530,7 @@
validate_location: row.querySelector('.validate-checkbox').checked validate_location: row.querySelector('.validate-checkbox').checked
})); }));
fetch('/api/update-checkins', { fetch('${API_BASE_URL}/update_checkins', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@ -1,33 +1,38 @@
# HTTPS server # HTTPS server
server { server {
listen 80; listen 80;
listen [::]:80; server_name rogaining.sumasen.net localhost;
server_name rogaining.sumasen.net;
access_log /var/log/nginx/access.log; access_log /var/log/nginx/api_access.log;
error_log /var/log/nginx/error.log debug; error_log /var/log/nginx/api_error.log debug;
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html; index index.html;
location / { # Django admin
try_files $uri $uri/ /index.html; location ~ ^/(admin|api)/ {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
}
location /api/ {
proxy_pass http://api:8100; proxy_pass http://api:8100;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade; proxy_set_header X-CSRFToken $http_x_csrf_token;
proxy_set_header Connection 'upgrade';
# タイムアウト設定
proxy_connect_timeout 300;
proxy_send_timeout 300;
proxy_read_timeout 300;
send_timeout 300;
}
# Supervisor webapp
location /supervisor/ {
alias /usr/share/nginx/html/supervisor/;
try_files $uri $uri/ /supervisor/index.html;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
} }
location /static/ { location /static/ {
@ -36,6 +41,10 @@ server {
add_header Cache-Control "public, no-transform"; add_header Cache-Control "public, no-transform";
} }
location = / {
root /usr/share/nginx/html;
index index.html;
}
error_page 404 /404.html; error_page 404 /404.html;
error_page 500 502 503 504 /50x.html; error_page 500 502 503 504 /50x.html;

View File

@ -0,0 +1,45 @@
# HTTPS server
server {
listen 80;
listen [::]:80;
server_name rogaining.sumasen.net;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log debug;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
}
location /api/ {
proxy_pass http://api:8100;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
}
location /static/ {
alias /app/static/;
expires 1h;
add_header Cache-Control "public, no-transform";
}
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

View File

@ -7,29 +7,38 @@ server {
# HTTPS server # HTTPS server
server { server {
listen 443 ssl; listen 443 ssl http2;
listen [::]:80; listen [::]:443 ssl http2;
server_name rogaining.sumasen.net; server_name rogaining.sumasen.net;
ssl_certificate /etc/letsencrypt/live/rogaining.sumasen.net/fullchain.pem; ssl_certificate /etc/letsencrypt/live/rogaining.sumasen.net/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/rogaining.sumasen.net/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/rogaining.sumasen.net/privkey.pem;
# SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384;
access_log /var/log/nginx/access.log; access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log debug; error_log /var/log/nginx/error.log debug;
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html; index index.html;
location / { # Django admin
try_files $uri $uri/ /index.html; location /admin/ {
proxy_pass http://api:8100;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
} }
# Supervisor webapp
location /supervisor/ {
alias /usr/share/nginx/html/supervisor/;
try_files $uri $uri/ /supervisor/index.html;
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
}
# API endpoints
location /api/ { location /api/ {
proxy_pass http://api:8100; proxy_pass http://api:8100;
proxy_http_version 1.1; proxy_http_version 1.1;
@ -37,10 +46,15 @@ server {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
} }
location /static/ { location /static/ {
alias /app/static/; alias /app/static/;
expires 1h;
add_header Cache-Control "public, no-transform";
} }
error_page 404 /404.html; error_page 404 /404.html;