Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5,462 changes: 5,460 additions & 2 deletions app/assets/builds/application.css

Large diffs are not rendered by default.

13 changes: 12 additions & 1 deletion app/controllers/admin/airtag_logs_controller.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
class Admin::AirtagLogsController < Admin::BaseController
def index
@logs = AirtagApiLog.order(fetched_at: :desc).limit(500).to_a
@next_refresh_at = @logs.first&.fetched_at&.+ 5.minutes
last_full_fetch = AirtagApiLog.where(slug: nil).order(fetched_at: :desc).first
@next_refresh_at = last_full_fetch&.fetched_at&.+ 5.minutes
@airtag_ping_count = LocationPing.airtag.count

teams = Team.where.not(airtag_slug: [ nil, "" ]).includes(:house).index_by(&:id)
last_pinged_at_by_team = LocationPing.airtag
.where(team_id: teams.keys)
.group(:team_id)
.maximum(:pinged_at)

@last_detections = teams.map do |id, team|
{ team: team, last_ping: last_pinged_at_by_team[id] }
end.sort_by { |h| h[:last_ping] || Time.at(0) }.reverse
end

def refresh
Expand Down
15 changes: 14 additions & 1 deletion app/controllers/admin/teams_controller.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
class Admin::TeamsController < Admin::BaseController
before_action :set_team, only: [ :show, :edit, :update, :destroy ]
before_action :set_team, only: [ :show, :edit, :update, :destroy, :refresh_airtag ]
before_action :load_form_data, only: [ :new, :edit, :create, :update ]

def index
Expand Down Expand Up @@ -66,6 +66,19 @@ def update
end
end

def refresh_airtag
unless @team.airtag_slug.present?
return redirect_to admin_team_path(@team), alert: "This team has no AirTag slug configured."
end

result = MapDataService.new.fetch_device(@team.airtag_slug)
if result
redirect_to admin_team_path(@team), notice: "AirTag refreshed (#{result.length} ping(s) received)."
else
redirect_to admin_team_path(@team), alert: "AirTag refresh failed — check AirTag logs for details."
end
end

def destroy
@team.destroy
redirect_to admin_teams_path, notice: "Team deleted."
Expand Down
1 change: 1 addition & 0 deletions app/models/airtag_api_log.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
# fetched_at :datetime not null
# location_count :integer default(0), not null
# response_body :text
# slug :string
# success :boolean default(TRUE), not null
# created_at :datetime not null
# updated_at :datetime not null
Expand Down
16 changes: 16 additions & 0 deletions app/services/map_data_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,22 @@ def force_fetch
end
end

def fetch_device(slug)
fetched_at = Time.current
response = @conn.get("/devices/#{slug}")
location = response.body
locations = location.is_a?(Array) ? location : [ location ]
persist_pings(locations)
AirtagApiLog.create(fetched_at:, slug:, response_body: locations.to_json, location_count: locations.length, success: true)
locations
rescue Faraday::TimeoutError => e
AirtagApiLog.create(fetched_at: Time.current, slug:, response_body: { error: e.class, message: e.message }.to_json, location_count: 0, success: false)
nil
rescue Faraday::Error => e
AirtagApiLog.create(fetched_at: Time.current, slug:, response_body: { error: e.class, message: e.message }.to_json, location_count: 0, success: false)
nil
end

private

def fetch_from_api
Expand Down
41 changes: 41 additions & 0 deletions app/views/admin/airtag_logs/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,39 @@

<%= render 'admin/nav' %>

<% if @last_detections.any? %>
<div class="neo-card-flush mb-6">
<div class="px-5 py-3 border-b-2 border-black bg-black flex items-center justify-between">
<p class="text-xs font-bold uppercase tracking-widest text-white/70">Last AirTag Detection per Team</p>
<span class="text-white/50 text-xs"><%= @last_detections.count %> tagged teams</span>
</div>
<div class="divide-y divide-gray-100">
<% @last_detections.each do |entry| %>
<% team = entry[:team] %>
<% last_ping = entry[:last_ping] %>
<div class="px-5 py-3 flex items-center gap-4">
<%= link_to team.name, admin_team_path(team), class: "font-bold text-black text-sm hover:underline flex-shrink-0 w-36 truncate" %>
<span class="font-mono text-xs text-gray-400 flex-shrink-0 w-24 truncate"><%= team.airtag_slug %></span>
<% if last_ping %>
<% age_seconds = (Time.current - last_ping).to_i %>
<span class="font-mono text-sm text-black"><%= last_ping.strftime("%b %-d, %H:%M:%S") %></span>
<span class="text-xs text-gray-400"><%= distance_of_time_in_words(last_ping, Time.current) %> ago</span>
<% if age_seconds > 30.minutes %>
<span class="px-2 py-0.5 text-[10px] font-black uppercase tracking-wide bg-red-100 text-red-700 border border-red-300 flex-shrink-0">Stale</span>
<% elsif age_seconds > 10.minutes %>
<span class="px-2 py-0.5 text-[10px] font-black uppercase tracking-wide bg-amber-100 text-amber-700 border border-amber-300 flex-shrink-0">Aging</span>
<% else %>
<span class="px-2 py-0.5 text-[10px] font-black uppercase tracking-wide bg-green-100 text-green-700 border border-green-300 flex-shrink-0">Fresh</span>
<% end %>
<% else %>
<span class="text-sm text-gray-400">No pings recorded</span>
<% end %>
</div>
<% end %>
</div>
</div>
<% end %>

<div class="grid grid-cols-2 gap-4 mb-6">
<div class="neo-card-flush px-6 py-4">
<p class="text-xs font-bold uppercase tracking-wide text-gray-400 mb-1">Next Refresh</p>
Expand Down Expand Up @@ -46,6 +79,7 @@
<thead class="bg-gray-100 border-b-2 border-gray-200">
<tr>
<th class="text-left px-6 py-4 font-bold text-gray-700">Fetched At</th>
<th class="text-left px-6 py-4 font-bold text-gray-700">Source</th>
<th class="text-left px-6 py-4 font-bold text-gray-700">Status</th>
<th class="text-left px-6 py-4 font-bold text-gray-700">Locations</th>
<th class="text-left px-6 py-4 font-bold text-gray-700">Last Detected</th>
Expand All @@ -58,6 +92,13 @@
<td class="px-6 py-4 text-sm font-mono text-gray-700 whitespace-nowrap">
<%= log.fetched_at.strftime("%b %-d, %Y %H:%M:%S") %>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<% if log.slug.present? %>
<span class="px-2 py-0.5 text-xs font-bold rounded bg-blue-100 text-blue-800 border border-blue-300 font-mono"><%= log.slug %></span>
<% else %>
<span class="px-2 py-0.5 text-xs font-bold rounded bg-gray-100 text-gray-600 border border-gray-300">scheduled</span>
<% end %>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<% if log.success? %>
<span class="px-2 py-0.5 text-xs font-bold rounded bg-green-100 text-green-800 border border-green-300">OK</span>
Expand Down
6 changes: 6 additions & 0 deletions app/views/admin/teams/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
<div class="flex gap-2">
<%= link_to "Public Profile", team_path(@team), class: "btn-game-ghost text-sm px-3 py-2" %>
<%= link_to "Edit", edit_admin_team_path(@team), class: "btn-game-ghost text-sm px-3 py-2" %>
<% if @team.airtag_slug.present? %>
<%= button_to "↻ Refresh AirTag", refresh_airtag_admin_team_path(@team),
method: :post,
class: "btn-game-ghost text-sm px-3 py-2",
form: { data: { turbo: false } } %>
<% end %>
<%= link_to "← Back to Teams", admin_teams_path, class: "btn-dark" %>
</div>
</div>
Expand Down
6 changes: 5 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@
end
resources :challenges
resources :houses
resources :teams
resources :teams do
member do
post :refresh_airtag
end
end
resources :zones, only: [ :index, :show, :edit, :update, :destroy, :create ] do
collection do
post :import
Expand Down
5 changes: 5 additions & 0 deletions db/migrate/20260522152552_add_slug_to_airtag_api_logs.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddSlugToAirtagApiLogs < ActiveRecord::Migration[8.1]
def change
add_column :airtag_api_logs, :slug, :string
end
end
3 changes: 2 additions & 1 deletion db/queue_schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[8.1].define(version: 2026_05_22_000001) do
ActiveRecord::Schema[8.1].define(version: 2026_05_22_152552) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"

Expand Down Expand Up @@ -57,6 +57,7 @@
t.datetime "fetched_at", null: false
t.integer "location_count", default: 0, null: false
t.text "response_body"
t.string "slug"
t.boolean "success", default: true, null: false
t.datetime "updated_at", null: false
t.index ["fetched_at"], name: "index_airtag_api_logs_on_fetched_at"
Expand Down
4 changes: 3 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading