Production usage reveals edge cases invisible during development. Certificate duplicate tracking through API created confusion when multiple certificates shared identical property, type, and expiry dates. Pagination broke navigating backwards through large certificate lists due to Alpine.js DOM key reuse. Certificate viewing required extra clicks through modals before accessing PDFs. These polish problems didn't prevent functionality yet degraded user experience sufficiently warranting fixes.
Week 46 refined certificate management addressing production-discovered edge cases: implementing rake tasks detecting and cleaning duplicate certificates, fixing Alpine.js pagination rendering bugs, replacing modal workflows with direct document links, enhancing API with certificate deletion endpoints, and unifying pagination patterns across certificate types. This article explores the duplicate detection algorithm, pagination key stability fixes, UX improvements eliminating unnecessary clicks, and database query optimization preventing N+1 problems.
What Your Team Will Notice
Certificate lists now sort by descending expiry date showing newest certificates first, matching natural expectation that recent certificates appear at top. Previously ascending sort showed oldest certificates first requiring scrolling to find current documents—unintuitive for staff primarily interested in unexpired certificates.
Viewing certificates requires one click instead of two: document links open PDFs directly in new browser windows rather than triggering modals showing document lists. For certificates with single documents (the common case), this eliminates unnecessary interaction. Multi-document certificates open first document automatically whilst providing access to additional documents through property detail pages.
Pagination works reliably across all certificate types. Previously EPC certificates used simple Previous/Next buttons whilst Gas Safety, EICR, and HMO License certificates showed numbered pagination creating inconsistent interfaces. Now all certificate types display identical numbered pagination: ← 1 ... 10 [11] 12 ... 25 → with highlighted current page and ellipsis for skipped ranges.
Navigating pagination backwards no longer causes rendering glitches. Previously clicking earlier page numbers sometimes showed two highlighted pages simultaneously or missing page numbers due to Alpine.js reusing DOM elements incorrectly. Fixed key generation ensures each page button maintains stable identity regardless of navigation direction.
Rake tasks provide database maintenance capabilities: rake certificates:find_duplicates identifies certificate groups sharing property, type, and expiry date; rake certificates:cleanup_duplicates removes duplicates automatically keeping newest certificates whilst deleting older duplicates. These tools handle data quality problems emerging through API usage or import processes.
Under the Bonnet: Duplicate Detection
The duplicate detection algorithm groups certificates by key fields:
# lib/tasks/find_duplicate_certificates.rake
namespace :certificates do
desc 'Find duplicate certificates (same property, type, and expiry date)'
task find_duplicates: :environment do
puts 'Searching for duplicate certificates...'
puts
# Group certificates by property_id, certificate_type_id, and expiry_date
# Count groups and filter those with multiple certificates
duplicates = Certificate
.select('property_id, certificate_type_id, expiry_date, COUNT(*) as count')
.group('property_id, certificate_type_id, expiry_date')
.having('COUNT(*) > 1')
.order('COUNT(*) DESC')
if duplicates.any?
puts "Found #{duplicates.count} duplicate certificate groups:"
puts
total_duplicates = 0
duplicates.each do |dup|
# Find all certificates in this duplicate group
certs = Certificate.where(
property_id: dup.property_id,
certificate_type_id: dup.certificate_type_id,
expiry_date: dup.expiry_date
).order(:id)
property = Property.find(dup.property_id)
cert_type = CertificateType.find(dup.certificate_type_id)
puts "Property: #{property.reference} (#{property.display_address})"
puts "Type: #{cert_type.name}"
puts "Expiry: #{dup.expiry_date}"
puts "Count: #{dup.count} certificates"
certs.each do |cert|
puts " - ID: #{cert.id}, Created: #{cert.created_at}, Documents: #{cert.documents.count}"
end
puts
total_duplicates += (dup.count - 1) # Subtract 1 to count extras
end
puts "=" * 60
puts "Total duplicate groups: #{duplicates.count}"
puts "Total extra certificates: #{total_duplicates}"
puts
puts "Run 'rake certificates:cleanup_duplicates' to remove them"
else
puts 'No duplicate certificates found ✓'
end
end
end
This task identifies duplicates systematically using SQL grouping and aggregation, providing detailed reporting before any destructive operations occur.
Automatic Duplicate Cleanup
The cleanup task removes duplicates safely:
# lib/tasks/cleanup_duplicate_certificates.rake
namespace :certificates do
desc 'Cleanup duplicate certificates (keeps newest, deletes older)'
task cleanup_duplicates: :environment do
puts 'Cleaning up duplicate certificates...'
puts 'Strategy: Keep certificate with highest ID (newest), delete others'
puts
deleted_count = 0
error_count = 0
# Find duplicate groups
duplicates = Certificate
.select('property_id, certificate_type_id, expiry_date')
.group('property_id, certificate_type_id, expiry_date')
.having('COUNT(*) > 1')
duplicates.each do |dup|
# Find all certificates in group, ordered by ID (oldest first)
certs = Certificate.where(
property_id: dup.property_id,
certificate_type_id: dup.certificate_type_id,
expiry_date: dup.expiry_date
).order(:id)
# Keep last (newest) certificate, delete others
certificates_to_delete = certs[0...-1]
certificates_to_delete.each do |cert|
begin
property = cert.property
cert_type = cert.certificate_type
puts "Deleting duplicate: ID #{cert.id}"
puts " Property: #{property.reference}"
puts " Type: #{cert_type.name}"
puts " Expiry: #{cert.expiry_date}"
puts " Keeping: ID #{certs.last.id}"
cert.destroy!
deleted_count += 1
puts " ✓ Deleted"
rescue StandardError => e
puts " ✗ Error: #{e.message}"
error_count += 1
end
puts
end
end
puts "=" * 60
puts "Deleted #{deleted_count} duplicate certificates"
puts "Errors: #{error_count}" if error_count > 0
puts
puts "Remaining duplicates: #{Certificate.select('property_id, certificate_type_id, expiry_date').group('property_id, certificate_type_id, expiry_date').having('COUNT(*) > 1').count.count}"
end
end
This cleanup preserves newest certificates (highest ID indicating most recent creation) whilst removing older duplicates, preventing accidental data loss whilst addressing duplicate problems.
Pagination Fixes: Stable Keys for Alpine.js
The pagination bug stemmed from unstable key generation:
<!-- BEFORE (buggy): -->
<template x-for="item in pageNumbers" :key="item">
<!-- Problem: Multiple ellipsis elements have same key "..."
Alpine.js reuses DOM elements incorrectly -->
<button>{{ item }}</button>
</template>
<!-- AFTER (fixed): -->
<template x-for="(item, index) in pageNumbers" :key="item === '...' ? `ellipsis-${index}` : `page-${item}`">
<!-- Solution: Unique stable keys for all elements
Ellipsis: "ellipsis-0", "ellipsis-1"
Pages: "page-1", "page-11", "page-25" -->
<button>{{ item }}</button>
</template>
The fix ensures each pagination element receives stable unique key preventing DOM reuse problems:
// app/views/certificates/index.html.erb (Alpine.js component)
{
// ... existing properties ...
get pageNumbers() {
const pages = []
const current = this.currentPage
const total = this.totalPages
// Always show first page
pages.push(1)
if (current > 3) {
pages.push('...') // Ellipsis before current range
}
// Show pages around current
for (let i = Math.max(2, current - 1); i <= Math.min(total - 1, current + 1); i++) {
pages.push(i)
}
if (current < total - 2) {
pages.push('...') // Ellipsis after current range
}
// Always show last page (if more than 1 page)
if (total > 1) {
pages.push(total)
}
return pages
},
goToPage(page) {
if (page >= 1 && page <= this.totalPages) {
this.currentPage = page
this.loadCertificates()
}
},
nextPage() {
if (this.currentPage < this.totalPages) {
this.currentPage++
this.loadCertificates()
}
},
prevPage() {
if (this.currentPage > 1) {
this.currentPage--
this.loadCertificates()
}
}
}
This pagination logic generates consistent number ranges with ellipsis, whilst stable keys prevent rendering glitches during navigation.
Unified Pagination Across Certificate Types
EPC certificates previously used different pagination:
<!-- BEFORE (EPC-specific simple pagination): -->
<div class="pagination-simple">
<button x-show="currentPage > 1" @click="prevPage">Previous</button>
<span>Page {{ currentPage }} of {{ totalPages }}</span>
<button x-show="currentPage < totalPages" @click="nextPage">Next</button>
</div>
<!-- AFTER (unified numbered pagination): -->
<nav class="pagination">
<button @click="prevPage" :disabled="currentPage === 1">←</button>
<template x-for="(item, index) in pageNumbers" :key="item === '...' ? `ellipsis-${index}` : `page-${item}`">
<button
x-show="item !== '...'"
@click="goToPage(item)"
:class="{ 'active': item === currentPage }"
x-text="item">
</button>
<span x-show="item === '...'" class="ellipsis">...</span>
</template>
<button @click="nextPage" :disabled="currentPage === totalPages">→</button>
</nav>
This unification provides consistent interface across all certificate types improving usability through familiar patterns.
UX Improvements: Direct Document Access
Certificate viewing streamlined removing unnecessary modals:
<!-- BEFORE (two-step process): -->
<a href="#" @click="openCertificateModal(certificate.id)">View</a>
<!-- Opens modal showing document list, then click document link -->
<!-- AFTER (direct access): -->
<% if certificate.documents.any? %>
<a href="<%= certificate.first_document_url %>"
target="_blank"
class="btn-view-certificate">
View Document
</a>
<% else %>
<span class="no-documents">No documents</span>
<% end %>
<% # For multi-document certificates, additional documents accessible on property page %>
The model provides convenient document URL access:
# app/models/certificate.rb
class Certificate < ApplicationRecord
belongs_to :property
belongs_to :certificate_type
has_many_attached :documents
# ... existing code ...
def first_document_url
return nil unless documents.attached?
Rails.application.routes.url_helpers.rails_blob_url(
documents.first,
only_path: false
)
end
def first_document_path
return nil unless documents.attached?
Rails.application.routes.url_helpers.rails_blob_path(
documents.first,
only_path: true
)
end
end
This direct access eliminates unnecessary interaction whilst maintaining access to all documents through property detail pages when needed.
API Enhancements
The certificates API gained deletion capability:
# config/routes.rb
namespace :api do
namespace :v1 do
resources :properties, param: :reference, only: [:index, :show] do
resources :certificates, only: [:index, :create, :destroy]
end
end
end
# app/controllers/api/v1/certificates_controller.rb
class Api::V1::CertificatesController < Api::V1::BaseController
before_action :set_property
before_action :set_certificate, only: [:destroy]
def index
# Fix N+1 query with eager loading
@certificates = @property.certificates
.includes(:certificate_type, documents_attachments: :blob)
.order(expiry_date: :desc)
render json: @certificates.as_json(
include: {
certificate_type: { only: [:id, :name] },
documents: { only: [:id, :filename] }
},
methods: [:first_document_url]
)
end
def create
@certificate = @property.certificates.build(certificate_params)
if @certificate.save
render json: @certificate, status: :created
else
render json: { errors: @certificate.errors.full_messages },
status: :unprocessable_entity
end
end
def destroy
if @certificate.destroy
render json: { message: 'Certificate deleted successfully' }, status: :ok
else
render json: { errors: @certificate.errors.full_messages },
status: :unprocessable_entity
end
end
private
def set_property
@property = Property.find_by!(reference: params[:property_reference])
end
def set_certificate
@certificate = @property.certificates.find(params[:id])
end
def certificate_params
params.require(:certificate).permit(
:certificate_type_id,
:issue_date,
:expiry_date,
:notes,
documents: []
)
end
end
This API provides complete CRUD operations whilst preventing N+1 queries through eager loading, improving performance when listing certificates.
What's Next
Certificate management could extend further: implementing expiry notifications sending reminders before certificates lapse, adding certificate renewal workflows tracking renewal progress, providing regulatory compliance dashboards showing missing required certificates, and enabling landlord portal access showing certificate status to property owners.
Pagination improvements might include: saving page preferences per user, infinite scroll as alternative to numbered pagination, keyboard navigation (arrow keys for next/previous), and URL parameter persistence maintaining page state through browser back/forward.
Duplicate prevention could be proactive: validating uniqueness during creation preventing duplicates at entry, providing merge functionality combining duplicate certificate documents, and implementing deduplication during API imports automatically handling duplicate submissions.
By addressing production-discovered edge cases through duplicate detection tools, pagination fixes, and UX improvements, LetAdmin demonstrates that polish and attention to detail matter—small improvements in usability, reliability, and data quality compound into significant value for daily users managing thousands of certificates across hundreds of properties.
