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:
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user