Skip to content
Merged
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
33 changes: 16 additions & 17 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import logging
import json
from .models import initialize_database, close_database
from .schemas import BadgeParams, UrlStatsResponse, SystemStatsResponse
from .services import update_visit_count, get_url_visit_count, get_system_statistics, get_app_info, load_template
from .schemas import BadgeParams, TagStatsResponse, SystemStatsResponse
from .services import update_visit_count, get_tag_visit_count, get_system_statistics, get_app_info, load_template
from .utils import build_shields_url, get_security_headers

LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
Expand Down Expand Up @@ -71,48 +71,48 @@ def get_app_version():
async def badge(
request: Request,
response: Response,
url: str,
tag: str,
label: str = "visits",
color: str = "4ade80",
style: str = "flat",
logo: str = "",
):
try:
params = BadgeParams(url=url, label=label, color=color, style=style, logo=logo)
params = BadgeParams(tag=tag, label=label, color=color, style=style, logo=logo)
except Exception:
raise HTTPException(status_code=400, detail="Invalid parameters.")

cookie_id = request.cookies.get("visitor_id")

try:
count, was_incremented, new_cookie_id = update_visit_count(cookie_id, params.url)
count, was_incremented, new_cookie_id = update_visit_count(cookie_id, params.tag)
if new_cookie_id:
response.set_cookie(key="visitor_id", value=new_cookie_id, max_age=31536000, httponly=True, samesite="Lax")
except ValueError as e:
raise HTTPException(status_code=429, detail=str(e))
except Exception as e:
logger.error(f"Error updating visit count: {e}")
count = get_url_visit_count(params.url)
count = get_tag_visit_count(params.tag)

shields_url = build_shields_url(params.label, count, params.color, params.style, params.logo)

headers = get_security_headers()
return RedirectResponse(shields_url, headers=headers)

@app.get("/api/stats/{url}", response_model=UrlStatsResponse)
async def get_url_stats_endpoint(url: str):
@app.get("/api/stats/{tag}", response_model=TagStatsResponse)
async def get_tag_stats_endpoint(tag: str):
try:
if not url or len(url) > 200:
raise HTTPException(status_code=400, detail="Invalid URL parameter")
if not tag or len(tag) > 200:
raise HTTPException(status_code=400, detail="Invalid tag parameter")

count = get_url_visit_count(url)
return UrlStatsResponse(
url=url,
count = get_tag_visit_count(tag)
return TagStatsResponse(
tag=tag,
visit_count=count,
last_updated=int(time.time())
)
except Exception as e:
logger.error(f"Error getting URL stats: {e}")
logger.error(f"Error getting tag stats: {e}")
raise HTTPException(status_code=500, detail="Internal server error")

@app.get("/api/stats", response_model=SystemStatsResponse)
Expand All @@ -121,10 +121,9 @@ async def get_system_stats_endpoint():
stats = get_system_statistics()

return SystemStatsResponse(
total_tracked_urls=stats["total_tracked_urls"],
total_tracked_tags=stats["total_tracked_tags"],
total_visits=stats["total_visits"],
new_badges_today=stats["new_badges_today"],
rate_limit_window_hours=0
new_badges_today=stats["new_badges_today"]
)
except Exception as e:
logger.error(f"Error getting system stats: {e}")
Expand Down
4 changes: 2 additions & 2 deletions src/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ class Meta:
database = db

class Badge(BaseModel):
"""Badge URLs with their total visit counts"""
url = CharField(max_length=200, unique=True)
"""Badge tags with their total visit counts"""
tag = CharField(max_length=200, unique=True)
visits = IntegerField(default=0)
created = IntegerField() # When first created

Expand Down
9 changes: 4 additions & 5 deletions src/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,21 @@
from typing import Annotated

class BadgeParams(BaseModel):
url: Annotated[str, Field(strip_whitespace=True, min_length=1, max_length=200)]
tag: Annotated[str, Field(strip_whitespace=True, min_length=1, max_length=200)]
label: Annotated[str, Field(strip_whitespace=True, min_length=1, max_length=20)] = "visits"
color: Annotated[str, Field(strip_whitespace=True, min_length=3, max_length=10)] = "4ade80"
style: Annotated[str, Field(strip_whitespace=True, min_length=2, max_length=10)] = "flat"
logo: Annotated[str, Field(strip_whitespace=True, max_length=20)] = ""

class UrlStatsResponse(BaseModel):
url: str
class TagStatsResponse(BaseModel):
tag: str
visit_count: int
last_updated: int

class SystemStatsResponse(BaseModel):
total_tracked_urls: int
total_tracked_tags: int
total_visits: int
new_badges_today: int
rate_limit_window_hours: int

class AppInfoResponse(BaseModel):
version: str
Expand Down
20 changes: 10 additions & 10 deletions src/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@

logger = logging.getLogger(__name__)

def update_visit_count(cookie_id: str, url_str: str) -> Tuple[int, bool, str]:
"""Update visit count for a URL and cookie combination"""
def update_visit_count(cookie_id: str, tag_str: str) -> Tuple[int, bool, str]:
"""Update visit count for a tag and cookie combination"""
current_time = int(time.time())
new_cookie_id = None

try:
with db.atomic():
badge, badge_created = Badge.get_or_create(
url=url_str,
tag=tag_str,
defaults={'created': current_time}
)

Expand All @@ -41,23 +41,23 @@ def update_visit_count(cookie_id: str, url_str: str) -> Tuple[int, bool, str]:
except Exception as e:
logger.error(f"Error updating visit count: {e}")
try:
badge = Badge.get(Badge.url == url_str)
badge = Badge.get(Badge.tag == tag_str)
return badge.visits, False, new_cookie_id
except Badge.DoesNotExist:
return 0, False, new_cookie_id

def get_url_visit_count(url_str: str) -> int:
"""Get total visit count for a URL"""
def get_tag_visit_count(tag_str: str) -> int:
"""Get total visit count for a tag"""
try:
badge = Badge.get(Badge.url == url_str)
badge = Badge.get(Badge.tag == tag_str)
return badge.visits
except Badge.DoesNotExist:
return 0

def get_system_statistics() -> dict:
"""Get system-wide statistics"""
try:
total_urls = Badge.select().count()
total_tags = Badge.select().count()
total_visits = Badge.select(fn.SUM(Badge.visits)).scalar() or 0

# Count badges created in last 24 hours
Expand All @@ -67,14 +67,14 @@ def get_system_statistics() -> dict:
).count()

return {
"total_tracked_urls": total_urls,
"total_tracked_tags": total_tags,
"total_visits": total_visits,
"new_badges_today": recent_badges,
}
except Exception as e:
logger.error(f"Error getting system statistics: {e}")
return {
"total_tracked_urls": 0,
"total_tracked_tags": 0,
"total_visits": 0,
"new_badges_today": 0,
}
Expand Down
35 changes: 10 additions & 25 deletions static/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class BadgeGenerator {
}
initializeElements() {
this.form = document.getElementById('badge-form');
this.urlInput = document.getElementById('url');
this.tagInput = document.getElementById('tag');
this.labelInput = document.getElementById('label');
this.colorInput = document.getElementById('color');
this.colorPicker = document.getElementById('color-picker');
Expand All @@ -33,16 +33,9 @@ class BadgeGenerator {
option.addEventListener('click', () => {
this.selectColor(option);
});
});

// Copy button events
document.querySelectorAll('.copy-btn').forEach((btn) => {
btn.addEventListener('click', (e) => {
this.copyToClipboard(e.target);
});
}); // Real-time preview
[
this.urlInput,
this.tagInput,
this.labelInput,
this.colorInput,
this.styleSelect,
Expand Down Expand Up @@ -127,8 +120,8 @@ class BadgeGenerator {

updatePreview() {
if (!this.badgePreview) return;
const url = this.urlInput.value.trim();
if (!url) return;
const tag = this.tagInput.value.trim();
if (!tag) return;

const badgeUrl = this.buildBadgeUrl();
this.badgePreview.innerHTML = `<img src="${badgeUrl}" alt="Badge Preview" onerror="this.src='data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjIwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxyZWN0IHdpZHRoPSIxMDAiIGhlaWdodD0iMjAiIGZpbGw9IiNjY2MiLz48dGV4dCB4PSI1MCIgeT0iMTUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxMiIgZmlsbD0iIzMzMyI+UHJldmlldyBVbmF2YWlsYWJsZTwvdGV4dD48L3N2Zz4='" />`;
Expand All @@ -137,7 +130,7 @@ class BadgeGenerator {
buildBadgeUrl() {
const baseUrl = window.location.origin;
const params = new URLSearchParams({
url: this.urlInput.value.trim(),
tag: this.tagInput.value.trim(),
label: this.labelInput.value.trim() || 'visits',
color: this.colorInput.value.trim() || '247e62',
style: this.styleSelect.value || 'flat',
Expand All @@ -151,17 +144,9 @@ class BadgeGenerator {
}

generateBadge() {
const url = this.urlInput.value.trim();
if (!url) {
this.showToast('Please enter a URL');
return;
}

// Validate URL format
try {
new URL(url.startsWith('http') ? url : `https://${url}`);
} catch {
this.showToast('Please enter a valid URL');
const tag = this.tagInput.value.trim();
if (!tag) {
this.showToast('Please enter a tracking tag');
return;
}

Expand All @@ -173,11 +158,11 @@ class BadgeGenerator {
this.badgeUrl.textContent = badgeUrl;

// Generate markdown
const markdown = `[![${label}](${badgeUrl})](${url})`;
const markdown = `![${label}](${badgeUrl})`;
this.markdownCode.textContent = markdown;

// Generate HTML
const html = `<a href="${url}"><img src="${badgeUrl}" alt="${label}" /></a>`;
const html = `<img src="${badgeUrl}" alt="${label}" />`;
this.htmlCode.textContent = html;

// Show results
Expand Down
9 changes: 4 additions & 5 deletions templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ <h2 class="section-title">Badge Generator</h2>
<form id="badge-form" class="flex flex-col flex-grow">
<div class="flex-grow">
<div class="form-group">
<label class="form-label" for="url">Target URL *</label>
<input type="text" id="url" class="form-input"
placeholder="https://github.com/username/repo" required />
<label class="form-label" for="tag">Tracking Tag *</label>
<input type="text" id="tag" class="form-input"
placeholder="my-awesome-project" required />
</div>
<div class="form-group">
<label class="form-label" for="label">Badge Label</label>
Expand Down Expand Up @@ -96,8 +96,7 @@ <h2 class="section-title">Badge Generator</h2>
<h2 class="section-title">How to Use</h2>
<ol class="instructions">
<li>
Enter the URL you want to track visits for (your GitHub repo,
website, etc.)
Enter a unique tracking tag for your project
</li>
<li>
Customize the badge label, color, and style to match your project
Expand Down
Loading