diff --git a/registry-seeds/official/code-review/README.md b/registry-seeds/official/code-review/README.md new file mode 100644 index 0000000..4b91d16 --- /dev/null +++ b/registry-seeds/official/code-review/README.md @@ -0,0 +1,21 @@ +# code-review + +Review code for issues, risks, and improvements. + +## Usage + +```bash +code-review --language python --focus bugs,security --severity medium < app.py +``` + +## Arguments + +- `--language`: Programming language or `auto`. +- `--focus`: Comma-separated focus areas (e.g., `bugs,security,performance`). +- `--severity`: Minimum severity level (`low`, `medium`, `high`). + +## Examples + +```bash +cat main.go | code-review --language go --focus performance --severity high +``` diff --git a/registry-seeds/official/code-review/config.yaml b/registry-seeds/official/code-review/config.yaml new file mode 100644 index 0000000..d096250 --- /dev/null +++ b/registry-seeds/official/code-review/config.yaml @@ -0,0 +1,37 @@ +name: code-review +version: "1.0.0" +description: "Review code for issues and improvements" +category: code-analysis +tags: + - code + - review + - quality +arguments: + - name: language + flag: "--language" + default: "auto" + description: "Programming language (e.g., python, javascript, go)" + - name: focus + flag: "--focus" + default: "bugs,security,performance" + description: "Comma-separated focus areas" + - name: severity + flag: "--severity" + default: "medium" + description: "Minimum severity: low, medium, high" +steps: + - type: prompt + provider: "claude" + template: | + You are a senior code reviewer. + Language: {language} + Focus areas: {focus} + Minimum severity: {severity} + + Review the following code. Identify issues, risks, and improvements. + Provide a concise, prioritized list with suggestions. + + Code: + {input} + output_var: review +output: "{review}" diff --git a/registry-seeds/official/explain/README.md b/registry-seeds/official/explain/README.md new file mode 100644 index 0000000..22e0a00 --- /dev/null +++ b/registry-seeds/official/explain/README.md @@ -0,0 +1,20 @@ +# explain + +Explain code or concepts clearly with adjustable audience and format. + +## Usage + +```bash +explain --audience beginner --format step-by-step < snippet.txt +``` + +## Arguments + +- `--audience`: Audience level (`beginner`, `general`, `expert`). +- `--format`: Output format (`paragraph`, `bullet`, `step-by-step`). + +## Examples + +```bash +echo "Explain recursion" | explain --audience general --format bullet +``` diff --git a/registry-seeds/official/explain/config.yaml b/registry-seeds/official/explain/config.yaml new file mode 100644 index 0000000..18243bc --- /dev/null +++ b/registry-seeds/official/explain/config.yaml @@ -0,0 +1,29 @@ +name: explain +version: "1.0.0" +description: "Explain code or concepts clearly" +category: education +tags: + - explanation + - learning + - code +arguments: + - name: audience + flag: "--audience" + default: "general" + description: "Audience level: beginner, general, expert" + - name: format + flag: "--format" + default: "paragraph" + description: "Output format: paragraph, bullet, or step-by-step" +steps: + - type: prompt + provider: "claude" + template: | + You are a clear and concise explainer. + Audience: {audience} + Format: {format} + + Explain the following content: + {input} + output_var: explanation +output: "{explanation}" diff --git a/registry-seeds/official/summarize/README.md b/registry-seeds/official/summarize/README.md new file mode 100644 index 0000000..940285e --- /dev/null +++ b/registry-seeds/official/summarize/README.md @@ -0,0 +1,20 @@ +# summarize + +Summarize text using AI. Designed for quick overviews, briefs, or executive summaries. + +## Usage + +```bash +summarize --max-length 200 --style neutral < input.txt +``` + +## Arguments + +- `--max-length`: Maximum length of the summary in words. Default: `200`. +- `--style`: Summary style. Options: `neutral`, `bullet`, `executive`. + +## Examples + +```bash +cat report.txt | summarize --max-length 120 --style executive +``` diff --git a/registry-seeds/official/summarize/config.yaml b/registry-seeds/official/summarize/config.yaml new file mode 100644 index 0000000..7077a20 --- /dev/null +++ b/registry-seeds/official/summarize/config.yaml @@ -0,0 +1,29 @@ +name: summarize +version: "1.0.0" +description: "Summarize text using AI" +category: text-processing +tags: + - summarization + - text + - ai +arguments: + - name: max_length + flag: "--max-length" + default: "200" + description: "Maximum length of the summary in words" + - name: style + flag: "--style" + default: "neutral" + description: "Summary style: neutral, bullet, or executive" +steps: + - type: prompt + provider: "claude" + template: | + You are a helpful assistant that summarizes text. + Style: {style} + Max length: {max_length} words. + + Summarize the following text: + {input} + output_var: summary +output: "{summary}" diff --git a/registry-seeds/official/translate/README.md b/registry-seeds/official/translate/README.md new file mode 100644 index 0000000..26cbb95 --- /dev/null +++ b/registry-seeds/official/translate/README.md @@ -0,0 +1,21 @@ +# translate + +Translate text between languages using AI. + +## Usage + +```bash +translate --from auto --to en --tone neutral < input.txt +``` + +## Arguments + +- `--from`: Source language (e.g., `en`, `es`, `fr`) or `auto`. +- `--to`: Target language (e.g., `en`, `es`, `fr`). +- `--tone`: Translation tone: `neutral`, `formal`, `casual`. + +## Examples + +```bash +echo "Hola mundo" | translate --from es --to en --tone formal +``` diff --git a/registry-seeds/official/translate/config.yaml b/registry-seeds/official/translate/config.yaml new file mode 100644 index 0000000..f705e89 --- /dev/null +++ b/registry-seeds/official/translate/config.yaml @@ -0,0 +1,34 @@ +name: translate +version: "1.0.0" +description: "Translate text between languages using AI" +category: text-processing +tags: + - translation + - localization + - text +arguments: + - name: from_language + flag: "--from" + default: "auto" + description: "Source language (e.g., en, es, fr) or auto" + - name: to_language + flag: "--to" + default: "en" + description: "Target language (e.g., en, es, fr)" + - name: tone + flag: "--tone" + default: "neutral" + description: "Tone: neutral, formal, or casual" +steps: + - type: prompt + provider: "claude" + template: | + You are a translation assistant. + Translate from: {from_language} + Translate to: {to_language} + Tone: {tone} + + Translate the following text: + {input} + output_var: translation +output: "{translation}" diff --git a/scripts/health_check.py b/scripts/health_check.py new file mode 100644 index 0000000..0d73003 --- /dev/null +++ b/scripts/health_check.py @@ -0,0 +1,116 @@ +"""Basic health check for the SmartTools Registry API and DB.""" + +from __future__ import annotations + +import json +import sys +import sqlite3 +from datetime import datetime, timedelta +from pathlib import Path +from urllib.request import Request, urlopen +from urllib.error import URLError, HTTPError + + +def _add_src_to_path() -> None: + repo_root = Path(__file__).resolve().parents[1] + sys.path.insert(0, str(repo_root / "src")) + + +def _get_registry_url() -> str: + from smarttools.config import get_registry_url + + return get_registry_url().rstrip("/") + + +def _get_db_path() -> Path: + from smarttools.registry.db import get_db_path + + return get_db_path() + + +def check_api(url: str) -> str | None: + endpoint = f"{url}/tools?limit=1" + try: + req = Request(endpoint, headers={"Accept": "application/json"}) + with urlopen(req, timeout=5) as resp: + if resp.status != 200: + return f"API unhealthy: status {resp.status}" + payload = json.loads(resp.read().decode("utf-8")) + if "data" not in payload: + return "API unhealthy: missing data field" + except HTTPError as exc: + return f"API unhealthy: HTTP {exc.code}" + except URLError as exc: + return f"API unhealthy: {exc.reason}" + except Exception as exc: + return f"API unhealthy: {exc}" + return None + + +def check_db(db_path: Path) -> str | None: + if not db_path.exists(): + return f"DB missing: {db_path}" + if not db_path.is_file(): + return f"DB path is not a file: {db_path}" + try: + with db_path.open("rb"): + pass + except OSError as exc: + return f"DB not readable: {exc}" + return None + + +def check_last_sync(db_path: Path) -> str | None: + try: + conn = sqlite3.connect(db_path) + try: + row = conn.execute("SELECT MAX(processed_at) FROM webhook_log").fetchone() + finally: + conn.close() + except sqlite3.Error as exc: + return f"DB error: {exc}" + + if not row or not row[0]: + return "No webhook sync recorded in webhook_log" + + raw = str(row[0]).replace("Z", "+00:00") + try: + last_sync = datetime.fromisoformat(raw) + except ValueError: + return f"Invalid sync timestamp: {row[0]}" + + if datetime.utcnow() - last_sync > timedelta(hours=1): + return f"Last sync too old: {last_sync.isoformat()}" + return None + + +def main() -> int: + _add_src_to_path() + errors = [] + + registry_url = _get_registry_url() + db_path = _get_db_path() + + api_error = check_api(registry_url) + if api_error: + errors.append(api_error) + + db_error = check_db(db_path) + if db_error: + errors.append(db_error) + else: + sync_error = check_last_sync(db_path) + if sync_error: + errors.append(sync_error) + + if errors: + for err in errors: + print(err, file=sys.stderr) + return 1 + + print("Health check OK") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/smarttools/registry/app.py b/src/smarttools/registry/app.py index 3fccee0..21ea853 100644 --- a/src/smarttools/registry/app.py +++ b/src/smarttools/registry/app.py @@ -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: diff --git a/src/smarttools/registry/db.py b/src/smarttools/registry/db.py index d4b134f..abcbcb6 100644 --- a/src/smarttools/registry/db.py +++ b/src/smarttools/registry/db.py @@ -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); """ diff --git a/src/smarttools/web/static/js/main.js b/src/smarttools/web/static/js/main.js index 6203c50..e19fca6 100644 --- a/src/smarttools/web/static/js/main.js +++ b/src/smarttools/web/static/js/main.js @@ -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 diff --git a/src/smarttools/web/templates/pages/tool_detail.html b/src/smarttools/web/templates/pages/tool_detail.html index e63bbaf..1cd132b 100644 --- a/src/smarttools/web/templates/pages/tool_detail.html +++ b/src/smarttools/web/templates/pages/tool_detail.html @@ -121,6 +121,11 @@ + @@ -195,7 +200,7 @@