Kagi Billing Progress Tracker latest version (currently v1.2.1)
← Back to User Scripts
Script Content
// ==UserScript==
// @name Kagi: Billing Progress Tracker
// @namespace https://github.com/tjhleeds/user-scripts/
// @version 1.2.1
// @description Add progress percentages to Kagi billing page
// @author tjhleeds
// @match https://kagi.com/settings/billing
// @grant none
// ==/UserScript==
(function() {
'use strict';
const SEARCHES_PER_MONTH = 300;
const SEARCHES_PER_YEAR = SEARCHES_PER_MONTH * 12;
const MAX_RETRIES = 3;
const RETRY_DELAY_MS = 1000;
function addProgressInfo(retryCount = 0) {
console.log('addProgressInfo called, attempt:', retryCount + 1);
// Find the renewal date element
const renewalElement = findElementByText("Next renewal is");
if (!renewalElement) {
if (retryCount < MAX_RETRIES) {
console.log('Renewal element not found, retrying...');
setTimeout(() => addProgressInfo(retryCount + 1), RETRY_DELAY_MS);
} else {
console.log(`Renewal element not found after ${MAX_RETRIES} attempts, giving up`);
}
return;
}
console.log('Found renewal element:', renewalElement);
// Extract renewal date
const renewalText = renewalElement.textContent;
const renewalMatch = renewalText.match(/Next renewal is (\d{4}-\d{2}-\d{2})/);
if (!renewalMatch) {
console.log('Could not parse renewal date from:', renewalText);
return;
}
console.log('Parsed renewal date:', renewalMatch[1]);
const renewalDate = new Date(renewalMatch[1]);
const currentDate = new Date();
const yearAgo = new Date(renewalDate);
yearAgo.setFullYear(yearAgo.getFullYear() - 1);
// Calculate time-based progress
const totalYearMs = renewalDate.getTime() - yearAgo.getTime();
const elapsedYearMs = currentDate.getTime() - yearAgo.getTime();
const yearProgress = Math.min(100, Math.max(0, (elapsedYearMs / totalYearMs) * 100));
const MS_PER_DAY = 1000 * 60 * 60 * 24;
const totalDaysInBillingYear = Math.round(totalYearMs / MS_PER_DAY);
const elapsedDays = Math.floor(elapsedYearMs / MS_PER_DAY);
console.log('Year progress calculated:', yearProgress.toFixed(1) + '%');
// Find search count
const searchCountElement = document.querySelector('.billing_box_count_num_1');
if (!searchCountElement) {
console.log('Search count element not found');
return;
}
console.log('Found search count element:', searchCountElement);
const searchCount = parseInt(searchCountElement.textContent.replace(/,/g, '').trim());
const searchProgress = Math.min(100, (searchCount / SEARCHES_PER_YEAR) * 100);
console.log('Search count:', searchCount, 'Search progress:', searchProgress.toFixed(1) + '%');
// Calculate search usage rate (searches per month vs expected)
const expectedSearches = (yearProgress / 100) * SEARCHES_PER_YEAR;
const searchRate = expectedSearches > 0 ? (searchCount / expectedSearches) * 100 : 0;
console.log('Search rate:', searchRate.toFixed(1) + '%');
const difference = searchCount - expectedSearches;
const differenceText = difference >= 0
? `${Math.round(difference)} above expected`
: `${Math.round(Math.abs(difference))} below expected`;
// Create progress info elements
const progressDiv = document.createElement('div');
progressDiv.style.cssText = `
border: 1px solid rgb(163 158 52);
background-color: rgb(57 58 17);
padding: 12px;
margin-top: 8px;
border-radius: 6px;
color: rgb(204 204 204);
font-size: 14px;
`;
progressDiv.innerHTML = `
Year progress: ${yearProgress.toFixed(1)}% (${elapsedDays}/${totalDaysInBillingYear})
Search progress: ${searchProgress.toFixed(1)}% (${searchCount}/${SEARCHES_PER_YEAR})
Search rate: ${searchRate.toFixed(1)}% of expected usage (${differenceText})
`;
// Insert after the renewal date element
renewalElement.parentNode.insertBefore(progressDiv, renewalElement.nextSibling);
console.log('Progress info added to page');
}
// Helper function since :contains() doesn't work in querySelector
function findElementByText(text) {
const elements = document.querySelectorAll('span, div');
console.log('Searching through', elements.length, 'span and div elements for text:', text);
for (let element of elements) {
if (element.textContent.includes(text) && element.textContent.trim().startsWith(text)) {
console.log('Found exact match element:', element);
return element;
}
}
console.log('No element found containing text:', text);
return null;
}
// Wait for page to load and try to add progress info
console.log('Script loaded, document ready state:', document.readyState);
if (document.readyState === 'loading') {
console.log('Document still loading, waiting for DOMContentLoaded');
document.addEventListener('DOMContentLoaded', addProgressInfo);
} else {
console.log('Document already loaded, calling addProgressInfo immediately');
addProgressInfo();
}
})();