3cb-data

Unnamed repository; edit this file 'description' to name the repository.
git clone git://git.alexwennerberg.com/3cb-data.git
Log | Files | Refs | README | LICENSE

commit aa1bd88895719d5e06d0462c6185bc75ec34fa0f
parent 3ac5a213ea481d1291168520780e2f8becd54b8e
Author: alex wennerberg <alex@alexwennerberg.com>
Date:   Sun, 17 Aug 2025 16:16:59 -0400

Add round dates, player stats page, and banned card indicators

- Add date tracking to rounds with Google Drive API integration
- Create players page showing player statistics and activity
- Display banned card overlays on card images
- Improve UI with card search form and enhanced round listings
- Update deployment script for remote server sync
- Fix delete-last.sql to properly clean up latest round

Diffstat:
Mgetdata.py | 26++++++++++++++++++++++++--
Mmain.rb | 32++++++++++++++++++++++----------
Mrunround.sh | 9+++++----
Mschema.sql | 3++-
Msql/delete-last.sql | 6+++---
Mviews/index.erb | 19+++++++++++++++----
Mviews/matches.erb | 6+++++-
Aviews/players.erb | 23+++++++++++++++++++++++
8 files changed, 99 insertions(+), 25 deletions(-)

diff --git a/getdata.py b/getdata.py @@ -45,6 +45,23 @@ def file_to_wb(fileid): os.exit(1) return openpyxl.load_workbook(io.BytesIO(res.content), data_only=True) +def get_file_created_date(fileid): + headers = {"x-goog-api-key": os.environ.get("GOOG_KEY")} + params = {"fields": "createdTime"} + res = requests.get(f"https://www.googleapis.com/drive/v3/files/{fileid}", + params=params, + headers=headers) + if res.status_code != 200: + print(res.text) + return None + data = res.json() + if "createdTime" in data: + # Parse ISO format and return YYYY-MM-DD + from datetime import datetime + created_time = datetime.fromisoformat(data["createdTime"].replace('Z', '+00:00')) + return created_time.strftime('%Y-%m-%d') + return None + def update_bans(): curr_round = requests.get("https://sites.google.com/view/3cb-metashape/pairings-results") sheet = file_to_wb("1NZuROOCctbq4p4-CAHE-jOC0675QQjuqVnUqdXFCVD8")["banlist"] @@ -61,7 +78,8 @@ def update_bans(): schema = [""" create table if not exists round ( id integer primary key, - fileid text not null + fileid text not null, + date text ); """, """ @@ -131,12 +149,16 @@ def save_round(n, fileid): groups = wb["Groups"] except KeyError: groups = wb["Group"] + + # Get creation date from Google Drive API + date_str = get_file_created_date(fileid) + cur = con.cursor() save_sheet(cur, groups, n, False) if len(wb._sheets) > 1: final = wb["Final Group"] save_sheet(cur, final, n, True) - cur.execute("insert into round values (?, ?);", (n, fileid)); + cur.execute("insert into round values (?, ?, ?);", (n, fileid, date_str)); con.commit() # fixing some data issues def best_guess(card): diff --git a/main.rb b/main.rb @@ -7,17 +7,8 @@ db = SQLite3::Database.new "3cb.db" db.results_as_hash = true set :server_settings, :timeout => 5 -configure :production do - require 'prometheus/middleware/collector' - require 'prometheus/middleware/exporter' - use Rack::Deflater - use Prometheus::Middleware::Collector - use Prometheus::Middleware::Exporter -end - - get '/' do - @rounds = db.execute "select * from round order by id desc" + @rounds = db.execute "select r.id, r.fileid, r.date, count(distinct d.player) as player_count from round r left join deck d on r.id = d.round group by r.id, r.fileid, r.date order by r.id desc" @subtitle = "rounds" erb :index end @@ -25,6 +16,7 @@ end get '/round/:id' do |id| @round = db.execute("select * from round where id = ?", id)[0] @matches = db.execute "select * from round_score where round = ? order by final_score desc,prelim_score desc", id + @banned_cards = db.execute("select name from ban").map { |row| row["name"] } @subtitle = "round " + id erb :matches end @@ -35,6 +27,7 @@ get '/card' do card1 = ?1 or card2 = ?1 or card3 = ?1 order by round desc", name + @banned_cards = db.execute("select name from ban").map { |row| row["name"] } @subtitle = name erb :matches end @@ -42,6 +35,7 @@ end get '/player' do name = params[:name] @matches = db.execute "select * from round_score where player = ? order by round desc", name + @banned_cards = db.execute("select name from ban").map { |row| row["name"] } @subtitle = "player " + name erb :matches # rename round -> matches end @@ -58,6 +52,24 @@ get '/deck' do card1 = ?3 and card2 = ?1 and card3 = ?2 or card1 = ?3 and card2 = ?2 and card3 = ?1 order by round desc", [cards[0], cards[1], cards[2]]) + @banned_cards = db.execute("select name from ban").map { |row| row["name"] } @subtitle = cards.join("; ") erb :matches end + +get '/players' do + @players = db.execute " + select + d.player, + count(distinct d.round) as rounds_played, + sum(case when r.rank = 1 then 1 else 0 end) as rounds_won, + min(ro.date) as first_played + from deck d + left join rank r on d.round = r.round and d.player = r.player + left join round ro on d.round = ro.id + group by d.player + order by rounds_played desc + " + @subtitle = "players" + erb :players +end diff --git a/runround.sh b/runround.sh @@ -1,7 +1,8 @@ sqlite3 3cb.db < sql/delete-last.sql +source .venv/bin/activate python getdata.py sqlite3 3cb.db < sql/new.sql -sqlite3 3cb.db < sql/export.sql > _site/export.tsv -python3 buildpage.py > _site/index.html -rsync -r _site/ alpine@fishbb.org:/var/www/data.3cardmagic.org - +scp 3cb.db alpine@fishbb.org:~/ +scp main.rb alpine@fishbb.org:~ +scp -r views/ alpine@fishbb.org:~ +ssh alpine@fishbb.org "doas rc-service 3cm restart" diff --git a/schema.sql b/schema.sql @@ -1,6 +1,7 @@ CREATE TABLE round ( id integer primary key, - fileid text not null + fileid text not null, + date text ); CREATE TABLE deck ( round integer not null, diff --git a/sql/delete-last.sql b/sql/delete-last.sql @@ -1,3 +1,3 @@ -delete from round where id > 72; -delete from deck where round > 72; -delete from match where round > 72; +delete from deck where round = (select max(id) from round); +delete from match where round = (select max(id) from round); +delete from round where id = (select max(id) from round); diff --git a/views/index.erb b/views/index.erb @@ -1,9 +1,20 @@ <%= erb :header %> -<div class="flextable"> +<p><a href="/players">View all players</a></p> +<form action="/card" method="get" class="card-search"> + <input type="text" name="name" placeholder="Search for a card..." required> + <input type="submit" value="Search"> +</form> +<div class="rounds-list"> <% @rounds.each do |round| %> - <div class="flextable-item"> - <a href="/round/<%= round["id"] %>"><%= round["id"].to_s.rjust(2, "0") %></a>&nbsp; + <div class="round-item"> + <a href="/round/<%= round["id"] %>">Round <%= round["id"].to_s.rjust(2, "0") %></a> + <span class="round-meta"> + <% if round["date"] %> + <%= round["date"] %> + <% end %> +[<%= round["player_count"] %> players] + </span> </div> - <% end %> +<% end %> </div> <%= erb :footer %> diff --git a/views/matches.erb b/views/matches.erb @@ -1,6 +1,9 @@ <%= erb :header %> <% if @round %> <a href='https://docs.google.com/spreadsheets/d/<%= @round["fileid"] %>/'>Google Sheet</a><% end %> +<% if @matches.empty? %> + <p>This card has never been played, or may be spelled wrong!</p> +<% else %> <div class="flextable"> <% @matches.each do |match| %> <div class="deck-box" @@ -16,9 +19,10 @@ <a href="/deck?c1=<%= match["card1"] %>&c2=<%= match["card2"] %>&c3=<%= match["card3"]%>">deck</a><br> </div> <div> - <% [match["card1"], match["card2"], match["card3"]].each do |card| %><a href="/card?name=<%= card %>"><img alt='<%= card %>' width=146 height=204px loading=lazy src="https://api.scryfall.com/cards/named?exact=<%= url_encode(card) %>&format=image&version=medium" /></a><% end %> + <% [match["card1"], match["card2"], match["card3"]].each do |card| %><div style="position: relative; display: inline-block;"><a href="/card?name=<%= card %>"><img alt='<%= card %>' width=146 height=204px loading=lazy src="https://api.scryfall.com/cards/named?exact=<%= url_encode(card) %>&format=image&version=medium" /></a><% if @banned_cards.include?(card) %><div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(255, 0, 0, 0.7); color: white; font-weight: bold; font-size: 20px; padding: 5px 10px; border-radius: 5px;">BANNED</div><% end %></div><% end %> </div> </div> <% end %> </div> +<% end %> <%= erb :footer %> diff --git a/views/players.erb b/views/players.erb @@ -0,0 +1,22 @@ +<%= erb :header %> +<table class="players-table"> + <thead> + <tr> + <th>Player</th> + <th>Rounds</th> + <th>Wins</th> + <th>First Played</th> + </tr> + </thead> + <tbody> + <% @players.each do |player| %> + <tr> + <td><a href="/player?name=<%= url_encode(player["player"]) %>"><%= player["player"] %></a></td> + <td><%= player["rounds_played"] %></td> + <td><%= player["rounds_won"] %></td> + <td><%= player["first_played"] %></td> + </tr> + <% end %> + </tbody> +</table> +<%= erb :footer %> +\ No newline at end of file