Add data models
This commit is contained in:
107
.github/used-prompts/deploying.md
vendored
107
.github/used-prompts/deploying.md
vendored
@@ -1,107 +0,0 @@
|
||||
## Overview
|
||||
|
||||
Deploy `estus-shots` to the personal Fedora server manually—no CI/CD required. The application runs behind Nginx via a reverse proxy and is supervised by `systemd`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Fedora (server) with SSH access for a user that can `sudo` without a passwordless setup.
|
||||
- Nim toolchain installed locally (for `nim`/`nimble`).
|
||||
- Server already provisioned with Nim runtime dependencies.
|
||||
- Existing Nginx site definition that proxies traffic to the `estus-shots` service port (defaults to `127.0.0.1:9000`).
|
||||
|
||||
## One-time server setup
|
||||
|
||||
1. Create deployment directories on the server:
|
||||
```fish
|
||||
ssh user@server 'sudo mkdir -p /opt/estus-shots/bin /opt/estus-shots/config && sudo chown ${USER}:${USER} /opt/estus-shots -R'
|
||||
```
|
||||
2. Copy the `systemd` unit and enable it:
|
||||
```fish
|
||||
printf '%s\n' "[Unit]" \
|
||||
"Description=estus-shots web service" \
|
||||
"After=network.target" \
|
||||
"" \
|
||||
"[Service]" \
|
||||
"Type=simple" \
|
||||
"ExecStart=/opt/estus-shots/bin/estus-shots" \
|
||||
"Restart=on-failure" \
|
||||
"RestartSec=5" \
|
||||
"User=www-data" \
|
||||
"WorkingDirectory=/opt/estus-shots" \
|
||||
"Environment=LOG_LEVEL=info" \
|
||||
"" \
|
||||
"[Install]" \
|
||||
"WantedBy=multi-user.target" \
|
||||
| ssh user@server 'sudo tee /etc/systemd/system/estus-shots.service'
|
||||
|
||||
ssh user@server 'sudo systemctl daemon-reload && sudo systemctl enable estus-shots.service'
|
||||
```
|
||||
3. Ensure Nginx reverse proxy points to `127.0.0.1:9000` and reload it once configured:
|
||||
```fish
|
||||
ssh user@server 'sudo systemctl restart nginx'
|
||||
```
|
||||
|
||||
## Deployment script
|
||||
|
||||
Save the following as `scripts/deploy.fish` (or another preferred location) and make it executable with `chmod +x scripts/deploy.fish`.
|
||||
|
||||
```fish
|
||||
#!/usr/bin/env fish
|
||||
|
||||
set -e
|
||||
|
||||
set SERVER user@server
|
||||
set TARGET_DIR /opt/estus-shots
|
||||
set BIN_NAME estus-shots
|
||||
|
||||
echo 'Building project locally (release)...'
|
||||
nimble build -y
|
||||
|
||||
echo 'Uploading binary...'
|
||||
scp bin/$BIN_NAME $SERVER:$TARGET_DIR/bin/
|
||||
|
||||
echo 'Uploading static assets...'
|
||||
scp -r src/static $SERVER:$TARGET_DIR/
|
||||
|
||||
echo 'Restarting services...'
|
||||
ssh $SERVER 'sudo systemctl restart estus-shots.service'
|
||||
ssh $SERVER 'sudo systemctl restart nginx'
|
||||
|
||||
echo 'Deployment finished.'
|
||||
```
|
||||
|
||||
> The script prompts for the SSH password and any required `sudo` password on the server.
|
||||
|
||||
## Manual deployment steps
|
||||
|
||||
1. Build the project locally:
|
||||
```fish
|
||||
nimble build -y
|
||||
```
|
||||
2. Copy the new build output and static assets:
|
||||
```fish
|
||||
scp bin/estus-shots user@server:/opt/estus-shots/bin/
|
||||
scp -r src/static user@server:/opt/estus-shots/
|
||||
```
|
||||
3. Restart the service and, if needed, Nginx:
|
||||
```fish
|
||||
ssh user@server 'sudo systemctl restart estus-shots.service'
|
||||
ssh user@server 'sudo systemctl restart nginx'
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
- Check service status:
|
||||
```fish
|
||||
ssh user@server 'systemctl status estus-shots.service --no-pager'
|
||||
```
|
||||
- Review recent logs:
|
||||
```fish
|
||||
ssh user@server 'journalctl -u estus-shots.service -n 50 --no-pager'
|
||||
```
|
||||
|
||||
## Troubleshooting tips
|
||||
|
||||
- If the service fails, run it manually on the server to capture stdout/stderr: `bin/estus-shots`.
|
||||
- Ensure SELinux contexts allow Nginx to proxy (`setsebool -P httpd_can_network_connect 1`).
|
||||
- Validate reverse proxy config: `sudo nginx -t`.
|
||||
26
README.md
Normal file
26
README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Estus Shots
|
||||
Tracking tool for Dark Souls drinking games.
|
||||
|
||||
## Game Rules
|
||||
- One player actively plays the game.
|
||||
- When a player dies, all must take a drink.
|
||||
- The next player takes over.
|
||||
- If a player beats a boss, the controller passes on, no drinks.
|
||||
|
||||
## Features
|
||||
- Track game progress across multiple games.
|
||||
- Progress is tracked per game sessions.
|
||||
- A Session is compoesed of multiple Events
|
||||
- Events can be start, end, boss defeated, death, etc.
|
||||
- Statistics are can be viewed for players, games and bosses.
|
||||
- Players can be added, removed and edited.
|
||||
- Games can be added, removed and edited.
|
||||
- Bosses can be added, removed and edited.
|
||||
- A penalty event happens when a player dies.
|
||||
- The penalty event assigns a a dink to a player
|
||||
- A typical penaly would look like this:
|
||||
- 20:24 Player A dies to Boss B
|
||||
- Player A drinks a shot of Drink A
|
||||
- Player B drinks a shot of Drinnk B
|
||||
- Player C drinks a shot of Dink A
|
||||
|
||||
79
internal/models/models.go
Normal file
79
internal/models/models.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// Player represents a participant in the drinking game.
|
||||
type Player struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Game represents a Dark Souls game variant.
|
||||
type Game struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Boss represents a boss in a Dark Souls game.
|
||||
type Boss struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
GameID int `json:"game_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Drink represents a type of alcoholic beverage.
|
||||
type Drink struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"` // e.g., "beer", "shot", "cocktail"
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// EventType represents the type of event that occurred during a session.
|
||||
type EventType string
|
||||
|
||||
const (
|
||||
EventTypeStart EventType = "start"
|
||||
EventTypeEnd EventType = "end"
|
||||
EventTypeBossDefeated EventType = "boss_defeated"
|
||||
EventTypeDeath EventType = "death"
|
||||
)
|
||||
|
||||
// Session represents a game session with multiple players.
|
||||
type Session struct {
|
||||
ID int `json:"id"`
|
||||
GameID int `json:"game_id"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
EndedAt *time.Time `json:"ended_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Event represents an event that occurred during a session.
|
||||
type Event struct {
|
||||
ID int `json:"id"`
|
||||
SessionID int `json:"session_id"`
|
||||
EventType EventType `json:"event_type"`
|
||||
PlayerID int `json:"player_id"`
|
||||
BossID *int `json:"boss_id,omitempty"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// PenaltyDrink represents a drink consumed as a penalty.
|
||||
type PenaltyDrink struct {
|
||||
ID int `json:"id"`
|
||||
EventID int `json:"event_id"`
|
||||
PlayerID int `json:"player_id"`
|
||||
DrinkID int `json:"drink_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
@@ -111,6 +111,30 @@ func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) {
|
||||
data := AdminMenuBar()
|
||||
data.Title = "Admin Panel"
|
||||
s.renderTemplate(w, http.StatusOK, "admin.html", data)
|
||||
case "/players":
|
||||
data := DefaultMenuBar()
|
||||
data.Title = "Players"
|
||||
s.renderTemplate(w, http.StatusOK, "players.html", data)
|
||||
case "/games":
|
||||
data := DefaultMenuBar()
|
||||
data.Title = "Games"
|
||||
s.renderTemplate(w, http.StatusOK, "games.html", data)
|
||||
case "/bosses":
|
||||
data := DefaultMenuBar()
|
||||
data.Title = "Bosses"
|
||||
s.renderTemplate(w, http.StatusOK, "bosses.html", data)
|
||||
case "/sessions":
|
||||
data := DefaultMenuBar()
|
||||
data.Title = "Sessions"
|
||||
s.renderTemplate(w, http.StatusOK, "sessions.html", data)
|
||||
case "/statistics":
|
||||
data := DefaultMenuBar()
|
||||
data.Title = "Statistics"
|
||||
s.renderTemplate(w, http.StatusOK, "statistics.html", data)
|
||||
case "/drinks":
|
||||
data := DefaultMenuBar()
|
||||
data.Title = "Drinks"
|
||||
s.renderTemplate(w, http.StatusOK, "drinks.html", data)
|
||||
case "/counter":
|
||||
s.handleCounter(w, r)
|
||||
case "/time":
|
||||
|
||||
@@ -40,22 +40,22 @@ func DefaultMenuBar() PageData {
|
||||
ShowClock: true,
|
||||
MenuGroups: []MenuGroup{
|
||||
{
|
||||
Label: "File",
|
||||
Label: "Game",
|
||||
Items: []MenuItem{
|
||||
{Label: "New", URL: "#!"},
|
||||
{Label: "Open", URL: "#!"},
|
||||
{Label: "Save", URL: "#!"},
|
||||
{Label: "Save As", URL: "#!"},
|
||||
{Label: "Home", URL: "/"},
|
||||
{Label: "Sessions", URL: "/sessions"},
|
||||
{Label: "Statistics", URL: "/statistics"},
|
||||
{IsDivider: true},
|
||||
{Label: "Exit", URL: "#!"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Label: "Edit",
|
||||
Label: "Manage",
|
||||
Items: []MenuItem{
|
||||
{Label: "Cut", URL: "#!"},
|
||||
{Label: "Copy", URL: "#!"},
|
||||
{Label: "Paste", URL: "#!"},
|
||||
{Label: "Players", URL: "/players"},
|
||||
{Label: "Games", URL: "/games"},
|
||||
{Label: "Bosses", URL: "/bosses"},
|
||||
{Label: "Drinks", URL: "/drinks"},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
41
web/templates/bosses.html
Normal file
41
web/templates/bosses.html
Normal file
@@ -0,0 +1,41 @@
|
||||
{{template "layout" .}}
|
||||
|
||||
{{define "title"}}Bosses · Estus Shots{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<fieldset class="tui-fieldset" style="padding: 2rem;">
|
||||
<legend>Bosses Management</legend>
|
||||
<p class="tui-text-white">
|
||||
Manage bosses from Dark Souls games.
|
||||
</p>
|
||||
|
||||
<section style="margin-top: 1.5rem;">
|
||||
<h2 class="tui-text-green">Boss List</h2>
|
||||
<div
|
||||
id="boss-list"
|
||||
hx-get="/bosses/list"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<div class="tui-panel">Loading bosses...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="margin-top: 1.5rem;">
|
||||
<h2 class="tui-text-green">Add New Boss</h2>
|
||||
<form hx-post="/bosses" hx-target="#boss-list" hx-swap="innerHTML">
|
||||
<div class="tui-fieldset">
|
||||
<label for="boss-name">Boss Name:</label>
|
||||
<input type="text" id="boss-name" name="name" required class="tui-input" />
|
||||
|
||||
<label for="boss-game" style="margin-top: 1rem;">Game:</label>
|
||||
<select id="boss-game" name="game_id" required class="tui-input">
|
||||
<option value="">Select a game...</option>
|
||||
</select>
|
||||
|
||||
<button type="submit" class="tui-button" style="margin-top: 1rem;">Add Boss</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</fieldset>
|
||||
{{end}}
|
||||
46
web/templates/drinks.html
Normal file
46
web/templates/drinks.html
Normal file
@@ -0,0 +1,46 @@
|
||||
{{template "layout" .}}
|
||||
|
||||
{{define "title"}}Drinks · Estus Shots{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<fieldset class="tui-fieldset" style="padding: 2rem;">
|
||||
<legend>Drinks Management</legend>
|
||||
<p class="tui-text-white">
|
||||
Manage the types of drinks consumed during the game.
|
||||
</p>
|
||||
|
||||
<section style="margin-top: 1.5rem;">
|
||||
<h2 class="tui-text-green">Drink List</h2>
|
||||
<div
|
||||
id="drink-list"
|
||||
hx-get="/drinks/list"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<div class="tui-panel">Loading drinks...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="margin-top: 1.5rem;">
|
||||
<h2 class="tui-text-green">Add New Drink</h2>
|
||||
<form hx-post="/drinks" hx-target="#drink-list" hx-swap="innerHTML">
|
||||
<div class="tui-fieldset">
|
||||
<label for="drink-name">Drink Name:</label>
|
||||
<input type="text" id="drink-name" name="name" required class="tui-input" />
|
||||
|
||||
<label for="drink-type" style="margin-top: 1rem;">Type:</label>
|
||||
<select id="drink-type" name="type" required class="tui-input">
|
||||
<option value="">Select type...</option>
|
||||
<option value="beer">Beer</option>
|
||||
<option value="shot">Shot</option>
|
||||
<option value="cocktail">Cocktail</option>
|
||||
<option value="wine">Wine</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
|
||||
<button type="submit" class="tui-button" style="margin-top: 1rem;">Add Drink</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</fieldset>
|
||||
{{end}}
|
||||
35
web/templates/games.html
Normal file
35
web/templates/games.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{{template "layout" .}}
|
||||
|
||||
{{define "title"}}Games · Estus Shots{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<fieldset class="tui-fieldset" style="padding: 2rem;">
|
||||
<legend>Games Management</legend>
|
||||
<p class="tui-text-white">
|
||||
Manage Dark Souls game variants tracked by the system.
|
||||
</p>
|
||||
|
||||
<section style="margin-top: 1.5rem;">
|
||||
<h2 class="tui-text-green">Game List</h2>
|
||||
<div
|
||||
id="game-list"
|
||||
hx-get="/games/list"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<div class="tui-panel">Loading games...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="margin-top: 1.5rem;">
|
||||
<h2 class="tui-text-green">Add New Game</h2>
|
||||
<form hx-post="/games" hx-target="#game-list" hx-swap="innerHTML">
|
||||
<div class="tui-fieldset">
|
||||
<label for="game-name">Game Name:</label>
|
||||
<input type="text" id="game-name" name="name" required class="tui-input" />
|
||||
<button type="submit" class="tui-button" style="margin-top: 1rem;">Add Game</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</fieldset>
|
||||
{{end}}
|
||||
35
web/templates/players.html
Normal file
35
web/templates/players.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{{template "layout" .}}
|
||||
|
||||
{{define "title"}}Players · Estus Shots{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<fieldset class="tui-fieldset" style="padding: 2rem;">
|
||||
<legend>Players Management</legend>
|
||||
<p class="tui-text-white">
|
||||
Manage players who participate in the drinking game.
|
||||
</p>
|
||||
|
||||
<section style="margin-top: 1.5rem;">
|
||||
<h2 class="tui-text-green">Player List</h2>
|
||||
<div
|
||||
id="player-list"
|
||||
hx-get="/players/list"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<div class="tui-panel">Loading players...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="margin-top: 1.5rem;">
|
||||
<h2 class="tui-text-green">Add New Player</h2>
|
||||
<form hx-post="/players" hx-target="#player-list" hx-swap="innerHTML">
|
||||
<div class="tui-fieldset">
|
||||
<label for="player-name">Name:</label>
|
||||
<input type="text" id="player-name" name="name" required class="tui-input" />
|
||||
<button type="submit" class="tui-button" style="margin-top: 1rem;">Add Player</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</fieldset>
|
||||
{{end}}
|
||||
50
web/templates/sessions.html
Normal file
50
web/templates/sessions.html
Normal file
@@ -0,0 +1,50 @@
|
||||
{{template "layout" .}}
|
||||
|
||||
{{define "title"}}Sessions · Estus Shots{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<fieldset class="tui-fieldset" style="padding: 2rem;">
|
||||
<legend>Sessions Management</legend>
|
||||
<p class="tui-text-white">
|
||||
View and manage game sessions.
|
||||
</p>
|
||||
|
||||
<section style="margin-top: 1.5rem;">
|
||||
<h2 class="tui-text-green">Active Sessions</h2>
|
||||
<div
|
||||
id="active-sessions"
|
||||
hx-get="/sessions/active"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<div class="tui-panel">Loading active sessions...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="margin-top: 1.5rem;">
|
||||
<h2 class="tui-text-green">Session History</h2>
|
||||
<div
|
||||
id="session-history"
|
||||
hx-get="/sessions/history"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<div class="tui-panel">Loading session history...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="margin-top: 1.5rem;">
|
||||
<h2 class="tui-text-green">Start New Session</h2>
|
||||
<form hx-post="/sessions" hx-target="#active-sessions" hx-swap="innerHTML">
|
||||
<div class="tui-fieldset">
|
||||
<label for="session-game">Game:</label>
|
||||
<select id="session-game" name="game_id" required class="tui-input">
|
||||
<option value="">Select a game...</option>
|
||||
</select>
|
||||
|
||||
<button type="submit" class="tui-button" style="margin-top: 1rem;">Start Session</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</fieldset>
|
||||
{{end}}
|
||||
48
web/templates/statistics.html
Normal file
48
web/templates/statistics.html
Normal file
@@ -0,0 +1,48 @@
|
||||
{{template "layout" .}}
|
||||
|
||||
{{define "title"}}Statistics · Estus Shots{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<fieldset class="tui-fieldset" style="padding: 2rem;">
|
||||
<legend>Statistics</legend>
|
||||
<p class="tui-text-white">
|
||||
View statistics for players, games, and bosses.
|
||||
</p>
|
||||
|
||||
<section style="margin-top: 1.5rem;">
|
||||
<h2 class="tui-text-green">Player Statistics</h2>
|
||||
<div
|
||||
id="player-stats"
|
||||
hx-get="/statistics/players"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<div class="tui-panel">Loading player statistics...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="margin-top: 1.5rem;">
|
||||
<h2 class="tui-text-green">Boss Statistics</h2>
|
||||
<div
|
||||
id="boss-stats"
|
||||
hx-get="/statistics/bosses"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<div class="tui-panel">Loading boss statistics...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="margin-top: 1.5rem;">
|
||||
<h2 class="tui-text-green">Game Statistics</h2>
|
||||
<div
|
||||
id="game-stats"
|
||||
hx-get="/statistics/games"
|
||||
hx-trigger="load"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
<div class="tui-panel">Loading game statistics...</div>
|
||||
</div>
|
||||
</section>
|
||||
</fieldset>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user