Add Phase 8 features: analytics, reporting, health check

- Add pageviews table and /api/v1/analytics/pageview endpoint
- Abuse reporting API already existed at /api/v1/reports
- Add health_check.py script for monitoring
- ChatGPT additions: report modal UI, analytics tracking JS

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
rob
2025-12-31 22:49:58 -04:00
co-authored by Claude Opus 4.5
parent 096c0739d8
commit 1ac3054aaa
13 changed files with 453 additions and 42 deletions
+30
View File
@@ -1394,6 +1394,36 @@ def create_app() -> Flask:
}
})
@app.route("/api/v1/analytics/pageview", methods=["POST"])
def track_pageview() -> Response:
"""Track a page view (privacy-friendly, no cookies)."""
data = request.get_json() or {}
path = data.get("path", "").strip()
if not path or len(path) > 500:
return jsonify({"data": {"tracked": False}})
# Hash the IP for privacy (don't store raw IP)
ip_hash = hashlib.sha256(
(request.remote_addr or "unknown").encode()
).hexdigest()[:16]
referrer = request.headers.get("Referer", "")[:500] if request.headers.get("Referer") else None
user_agent = request.headers.get("User-Agent", "")[:500] if request.headers.get("User-Agent") else None
try:
g.db.execute(
"""
INSERT INTO pageviews (path, referrer, user_agent, ip_hash)
VALUES (?, ?, ?, ?)
""",
[path, referrer, user_agent, ip_hash],
)
g.db.commit()
return jsonify({"data": {"tracked": True}})
except Exception:
return jsonify({"data": {"tracked": False}})
@app.route("/api/v1/webhook/gitea", methods=["POST"])
def webhook_gitea() -> Response:
if request.content_length and request.content_length > MAX_BODY_BYTES:
+12
View File
@@ -214,6 +214,18 @@ CREATE INDEX IF NOT EXISTS idx_featured_tools_placement ON featured_tools(placem
CREATE INDEX IF NOT EXISTS idx_featured_contributors_placement ON featured_contributors(placement, status);
CREATE INDEX IF NOT EXISTS idx_reports_status ON reports(status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_content_pages_type ON content_pages(content_type, published);
CREATE TABLE IF NOT EXISTS pageviews (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT NOT NULL,
referrer TEXT,
user_agent TEXT,
ip_hash TEXT,
viewed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_pageviews_path ON pageviews(path, viewed_at DESC);
CREATE INDEX IF NOT EXISTS idx_pageviews_date ON pageviews(date(viewed_at), path);
"""
+69
View File
@@ -135,6 +135,73 @@ function closeSearch() {
closeSearchModal();
}
// Report modal helpers
function openReportModal() {
const modal = document.getElementById('report-modal');
if (modal) {
modal.classList.remove('hidden');
}
}
function closeReportModal() {
const modal = document.getElementById('report-modal');
if (modal) {
modal.classList.add('hidden');
}
}
function submitReport(owner, name) {
const form = document.getElementById('report-form');
if (!form) return;
const ownerField = document.getElementById('report-owner');
const nameField = document.getElementById('report-name');
if (ownerField) ownerField.value = owner;
if (nameField) nameField.value = name;
if (!form.dataset.bound) {
form.addEventListener('submit', async function (e) {
e.preventDefault();
const payload = {
tool_owner: ownerField ? ownerField.value : owner,
tool_name: nameField ? nameField.value : name,
reason: form.querySelector('[name="reason"]')?.value || '',
description: form.querySelector('[name="description"]')?.value || ''
};
try {
const response = await fetch('/api/v1/reports', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
});
if (response.ok) {
closeReportModal();
alert('Thank you for your report. We will review it shortly.');
} else {
const error = await response.json();
alert((error.error && error.error.message) || 'Failed to submit report. Please try again.');
}
} catch (err) {
alert('Failed to submit report. Please try again.');
}
});
form.dataset.bound = 'true';
}
openReportModal();
}
// Analytics
function trackPageView() {
fetch('/api/v1/analytics/pageview', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({path: window.location.pathname})
}).catch(() => {});
}
// Keyboard shortcuts
document.addEventListener('keydown', function(e) {
// ESC to close modals
@@ -157,6 +224,8 @@ document.addEventListener('DOMContentLoaded', function() {
// Initialize any dynamic components
initializeDropdowns();
trackPageView();
});
// Dropdown initialization
@@ -121,6 +121,11 @@
</svg>
</button>
</div>
<button type="button"
onclick="submitReport('{{ tool.owner }}', '{{ tool.name }}')"
class="mt-4 w-full inline-flex items-center justify-center px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-md hover:bg-red-700">
Report Tool
</button>
</div>
<!-- Versions -->
@@ -195,7 +200,7 @@
<!-- Report Tool -->
<div class="text-center">
<button type="button"
onclick="openReportModal()"
onclick="submitReport('{{ tool.owner }}', '{{ tool.name }}')"
class="text-sm text-gray-500 hover:text-red-600 transition-colors">
Report this tool
</button>
@@ -218,7 +223,8 @@
Help us maintain a safe registry by reporting tools that violate our guidelines.
</p>
<input type="hidden" name="tool_id" value="{{ tool.id }}">
<input type="hidden" name="tool_owner" id="report-owner" value="{{ tool.owner }}">
<input type="hidden" name="tool_name" id="report-name" value="{{ tool.name }}">
<div class="mt-6 space-y-4">
<div>
@@ -226,19 +232,18 @@
<select name="reason" id="reason" required
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500">
<option value="">Select a reason...</option>
<option value="malicious">Malicious code</option>
<option value="spam">Spam or advertising</option>
<option value="copyright">Copyright violation</option>
<option value="inappropriate">Inappropriate content</option>
<option value="broken">Broken or non-functional</option>
<option value="spam">Spam</option>
<option value="malicious">Malicious</option>
<option value="duplicate">Duplicate</option>
<option value="inappropriate">Inappropriate</option>
<option value="other">Other</option>
</select>
</div>
<div>
<label for="details" class="block text-sm font-medium text-gray-700 mb-1">Details (optional)</label>
<textarea name="details" id="details" rows="3"
placeholder="Provide additional context..."
<label for="description" class="block text-sm font-medium text-gray-700 mb-1">Description (optional)</label>
<textarea name="description" id="description" rows="3"
placeholder="Provide additional context"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 resize-none"></textarea>
</div>
</div>
@@ -275,37 +280,5 @@ function copyInstall() {
});
}
function openReportModal() {
document.getElementById('report-modal').classList.remove('hidden');
}
function closeReportModal() {
document.getElementById('report-modal').classList.add('hidden');
}
// Handle report form submission
document.getElementById('report-form').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
const data = Object.fromEntries(formData);
try {
const response = await fetch('/api/v1/reports', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
if (response.ok) {
closeReportModal();
alert('Thank you for your report. We will review it shortly.');
} else {
const error = await response.json();
alert(error.error || 'Failed to submit report. Please try again.');
}
} catch (err) {
alert('Failed to submit report. Please try again.');
}
});
</script>
{% endblock %}