Files
mcphub/core/templates/dashboard/service.html
airano-ir f203ca88de
Some checks failed
Release / Test before release (push) Has been cancelled
Release / Publish to PyPI (push) Has been cancelled
Release / Publish to Docker Hub (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
feat(v3.12.0): media pipeline, AI image generation, capability probe, companion v2.9.0
Three-month batch sync from internal repo (~80 commits) covering Tracks F.5a, F.7e, F.8, F.17, F.18, F.X.

WordPress media pipeline
- Pillow-based optimization, AI image generation (OpenAI / Stability / Replicate / Google Nano Banana / OpenRouter), chunked + resumable uploads, bulk delete/reassign, idempotent retries.

Capability discovery (F.7e)
- Per-site credential probe + adapters for WordPress / WooCommerce / Gitea, tier-fit unions granted ∪ roles, capability badge UI with HTMX partial re-check, install hint in every companion-unreachable error.

Companion plugin overhaul
- Renamed wordpress-plugin/airano-mcp-seo-bridge → wordpress-plugin/airano-mcp-bridge.
- Eight new endpoints: /capabilities, /bulk-meta, /export, /cache-purge, /transient-flush, /site-health, /audit-hook, /upload-and-attach.
- wp.org Plugin Check pass: i18n, WP_Filesystem, scheme allowlist on audit-hook URL.

Other
- Gitea ergonomics (F.17): batch files, tree, search, compare, releases, fork.
- Opportunistic bcrypt upgrade for legacy SHA-256 admin keys (F.8).
- n8n refactor: structured errors, capability probe, missing tools backfilled.
- Idempotency-Key dedup for AI media upload retries; WP client fast-fails on unreachable sites.

Docs
- README + CLAUDE.md drop the fixed "633 tools" claim. The total grows with each release; per-plugin approximations + dashboard-surfaced counts replace it.
- Tools/Tests badges removed in favour of "Plugins: 10".

Deployment
- PyPI mirror chain, optional BUILD_HTTP_PROXY, Alpine→Yandex apk mirror, Debian-slim Plan-B Dockerfile, mirror.gcr.io variant.

CI
- Black + Ruff clean on Python 3.12; pytest tests/ green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:25:58 +02:00

282 lines
18 KiB
HTML

{% extends "dashboard/base.html" %}
{% block title %}{{ service.display_name }} - MCP Hub{% endblock %}
{% block page_title %}{{ service.display_name }}{% endblock %}
{% block content %}
<div class="space-y-6">
<!-- Back button + Header -->
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<a href="/dashboard/services{% if lang and lang != 'en' %}?lang={{ lang }}{% endif %}"
class="text-gray-400 hover:text-gray-300 transition-colors">
<svg class="w-5 h-5{% if lang == 'fa' %} rotate-180{% endif %}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>
</a>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">{{ service.display_name }}</h2>
{% if service.is_public %}
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-300">
{% if lang == 'fa' %}عمومی{% else %}Public{% endif %}
</span>
{% else %}
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-500/20 text-gray-600 dark:text-gray-400">
{% if lang == 'fa' %}فقط مدیر{% else %}Admin Only{% endif %}
</span>
{% endif %}
</div>
<a href="/dashboard/sites/add?plugin_type={{ service.plugin_type }}{% if lang and lang != 'en' %}&lang={{ lang }}{% endif %}"
class="btn-primary px-4 py-2 rounded-lg text-white text-sm font-medium">
+ {{ t.get('add_site', 'Add Site') }}
</a>
</div>
<!-- Description & Notes -->
{% if service.description %}
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
{% if lang == 'fa' and service.description.fa %}
<p class="text-sm text-gray-600 dark:text-gray-300 leading-relaxed" dir="rtl">{{ service.description.fa }}</p>
{% elif service.description.en %}
<p class="text-sm text-gray-600 dark:text-gray-300 leading-relaxed">{{ service.description.en }}</p>
{% endif %}
{% set notes_key = 'notes_fa' if lang == 'fa' else 'notes_en' %}
{% if service.description[notes_key] %}
<ul class="mt-4 space-y-2{% if lang == 'fa' %} text-right{% endif %}" {% if lang == 'fa' %}dir="rtl"{% endif %}>
{% for note in service.description[notes_key] %}
<li class="flex items-start gap-2 text-sm text-gray-500 dark:text-gray-400">
<svg class="w-4 h-4 text-primary-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span>{{ note|safe }}</span>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endif %}
<!-- Overview Cards -->
{% set admin_tools = service.tools|selectattr('scope', 'equalto', 'admin')|list %}
<div class="grid grid-cols-1 md:grid-cols-{{ 4 if admin_tools|length > 0 else 3 }} gap-6">
<!-- Tools Count -->
<div class="bg-white dark:bg-gray-800 rounded-xl p-6 border border-gray-200 dark:border-gray-700">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">
{% if lang == 'fa' %}تعداد ابزار{% else %}Available Tools{% endif %}
</p>
<p class="text-3xl font-bold text-gray-900 dark:text-white mt-1">{{ service.tools_count }}</p>
</div>
<div class="w-12 h-12 bg-blue-500/20 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
</div>
</div>
</div>
<!-- Read Tools -->
{% set read_tools = service.tools|selectattr('scope', 'equalto', 'read')|list %}
<div class="bg-white dark:bg-gray-800 rounded-xl p-6 border border-gray-200 dark:border-gray-700">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">
{% if lang == 'fa' %}ابزار خواندن{% else %}Read Tools{% endif %}
</p>
<p class="text-3xl font-bold text-gray-900 dark:text-white mt-1">{{ read_tools|length }}</p>
</div>
<div class="w-12 h-12 bg-green-500/20 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg>
</div>
</div>
</div>
<!-- Write Tools -->
{% set write_tools = service.tools|selectattr('scope', 'equalto', 'write')|list %}
<div class="bg-white dark:bg-gray-800 rounded-xl p-6 border border-gray-200 dark:border-gray-700">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">
{% if lang == 'fa' %}ابزار نوشتن{% else %}Write Tools{% endif %}
</p>
<p class="text-3xl font-bold text-gray-900 dark:text-white mt-1">{{ write_tools|length }}</p>
</div>
<div class="w-12 h-12 bg-orange-500/20 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
</svg>
</div>
</div>
</div>
<!-- Admin Tools (F.7d) -->
{% if admin_tools|length > 0 %}
<div class="bg-white dark:bg-gray-800 rounded-xl p-6 border border-gray-200 dark:border-gray-700">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-500 dark:text-gray-400">
{% if lang == 'fa' %}ابزار مدیر{% else %}Admin Tools{% endif %}
</p>
<p class="text-3xl font-bold text-gray-900 dark:text-white mt-1">{{ admin_tools|length }}</p>
</div>
<div class="w-12 h-12 bg-red-500/20 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z"/>
</svg>
</div>
</div>
</div>
{% endif %}
</div>
<!-- F.20 prep: Companion plugin download hint (WP / WC only) -->
{% if companion_download_url %}
<div class="bg-indigo-50 dark:bg-indigo-500/10 border border-indigo-200 dark:border-indigo-500/30 rounded-xl p-5">
<div class="flex items-start gap-3">
<div class="w-10 h-10 bg-indigo-500/20 rounded-lg flex items-center justify-center flex-shrink-0">
<svg class="w-5 h-5 text-indigo-500 dark:text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
</div>
<div class="flex-1 min-w-0">
<h4 class="text-sm font-semibold text-indigo-900 dark:text-indigo-200">
{% if lang == 'fa' %}
افزونه همراه Airano MCP Bridge (اختیاری اما توصیه‌شده)
{% else %}
Airano MCP Bridge — companion plugin (optional but recommended)
{% endif %}
</h4>
<p class="mt-1 text-xs text-indigo-800/90 dark:text-indigo-300/90 leading-relaxed">
{% if lang == 'fa' %}
نصب افزونه همراه، این قابلیت‌ها را فعال می‌کند: آپلود فایل بزرگ‌تر از
<code class="font-mono">upload_max_filesize</code>، گزارش یکپارچه‌ی سلامت سایت،
purge کش، پاکسازی transient، متاهای bulk، خروجی ساختاریافته، probe قابلیت‌ها،
و webhook لاگ ممیزی. بدون آن، ابزارهای پایه کار می‌کنند اما موارد فوق غیرفعال خواهند بود.
{% else %}
Installing the companion plugin unlocks: uploads larger than
<code class="font-mono">upload_max_filesize</code>, unified site-health snapshot,
cache purge, transient flush, bulk meta writes, structured export, capability probe,
and audit-hook webhooks. Without it, basic tools still work but those features stay disabled.
{% endif %}
</p>
<div class="mt-3 flex flex-wrap items-center gap-3">
<a href="{{ companion_download_url }}"
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-medium transition-colors">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
</svg>
{% if lang == 'fa' %}دانلود airano-mcp-bridge.zip{% else %}Download airano-mcp-bridge.zip{% endif %}
</a>
<span class="text-xs text-indigo-700/70 dark:text-indigo-300/70">
{% if lang == 'fa' %}
نصب: WP Admin → افزونه‌ها → افزودن → Upload Plugin → انتخاب فایل → نصب و فعال‌سازی
{% else %}
Install via WP Admin → Plugins → Add New → Upload Plugin → select the zip → Activate
{% endif %}
</span>
</div>
</div>
</div>
</div>
{% endif %}
<!-- Setup Requirements -->
{% if service.credential_fields %}
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700">
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
{% if lang == 'fa' %}اطلاعات مورد نیاز{% else %}Setup Requirements{% endif %}
</h3>
</div>
<div class="p-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{% for field in service.credential_fields %}
<div class="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
<div class="w-8 h-8 bg-purple-500/20 rounded flex items-center justify-center flex-shrink-0 mt-0.5">
{% if field.type == 'password' %}
<svg class="w-4 h-4 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
</svg>
{% else %}
<svg class="w-4 h-4 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
</svg>
{% endif %}
</div>
<div>
<p class="text-sm font-medium text-gray-900 dark:text-white">{{ field.label }}</p>
{% if field.hint %}
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ field.hint }}</p>
{% endif %}
{% if field.required %}
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-red-100 dark:bg-red-500/20 text-red-600 dark:text-red-400 mt-1">
{% if lang == 'fa' %}الزامی{% else %}Required{% endif %}
</span>
{% else %}
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-600 text-gray-500 dark:text-gray-400 mt-1">
{% if lang == 'fa' %}اختیاری{% else %}Optional{% endif %}
</span>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endif %}
<!-- Tools List -->
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700">
<div class="p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
{% if lang == 'fa' %}لیست ابزارها{% else %}Tools{% endif %}
<span class="text-sm font-normal text-gray-400 dark:text-gray-500">({{ service.tools_count }})</span>
</h3>
<div class="relative">
<input type="text" id="tool-search" placeholder="{% if lang == 'fa' %}جستجو...{% else %}Search tools...{% endif %}"
class="w-48 px-3 py-1.5 text-sm bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:border-primary-500">
</div>
</div>
<div class="divide-y divide-gray-100 dark:divide-gray-700" id="tools-list">
{% for tool in service.tools %}
<div class="tool-item p-4 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors" data-name="{{ tool.name }}" data-desc="{{ tool.description|lower }}">
<div class="flex items-center justify-between">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<code class="text-sm font-mono text-primary-600 dark:text-primary-400">{{ tool.name }}</code>
{% if tool.scope == 'read' %}
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-300">read</span>
{% elif tool.scope == 'write' %}
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-orange-100 dark:bg-orange-500/20 text-orange-700 dark:text-orange-300">write</span>
{% else %}
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-300">admin</span>
{% endif %}
</div>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1 truncate">{{ tool.description }}</p>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.getElementById('tool-search').addEventListener('input', function(e) {
const q = e.target.value.toLowerCase();
document.querySelectorAll('.tool-item').forEach(function(item) {
const name = item.dataset.name || '';
const desc = item.dataset.desc || '';
item.style.display = (name.includes(q) || desc.includes(q)) ? '' : 'none';
});
});
</script>
{% endblock %}