Rename project from SmartTools to CmdForge
Major rename operation: - Rename Python package: smarttools -> cmdforge - Update CLI entry point: smarttools -> cmdforge - Update all imports and module references - Update pyproject.toml with new name, URLs, entry point - Update all HTML templates with new branding - Update documentation (CLAUDE.md, README.md, docs/*, wiki/*) - Update environment variables: - SMARTTOOLS_ENV -> CMDFORGE_ENV - SMARTTOOLS_REGISTRY_DB -> CMDFORGE_REGISTRY_DB - SMARTTOOLS_TOKEN -> CMDFORGE_TOKEN - SMARTTOOLS_REGISTRY_* -> CMDFORGE_REGISTRY_* - Update Dockerfile and docker-compose.yml - Update tests to use new package name - Update scripts and examples - Update package.json and tailwind.config.js All 158 unit tests pass. The CLI is working correctly with the new cmdforge command. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="bg-gray-50 min-h-screen">
|
||||
<!-- Dashboard Header -->
|
||||
<div class="bg-white border-b border-gray-200">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{% block dashboard_header %}
|
||||
<h1 class="text-2xl font-bold text-gray-900">Dashboard</h1>
|
||||
<p class="mt-1 text-gray-600">Welcome back, {{ user.display_name }}</p>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
||||
<!-- Sidebar Navigation -->
|
||||
<aside>
|
||||
<nav class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<a href="{{ url_for('web.dashboard') }}"
|
||||
class="flex items-center px-4 py-3 text-sm font-medium {{ 'bg-indigo-50 text-indigo-700 border-l-4 border-indigo-600' if active_page == 'overview' else 'text-gray-700 hover:bg-gray-50' }}">
|
||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
|
||||
</svg>
|
||||
Overview
|
||||
</a>
|
||||
<a href="{{ url_for('web.dashboard_tools') }}"
|
||||
class="flex items-center px-4 py-3 text-sm font-medium {{ 'bg-indigo-50 text-indigo-700 border-l-4 border-indigo-600' if active_page == 'tools' else 'text-gray-700 hover:bg-gray-50' }}">
|
||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
|
||||
</svg>
|
||||
My Tools
|
||||
</a>
|
||||
<a href="{{ url_for('web.dashboard_tokens') }}"
|
||||
class="flex items-center px-4 py-3 text-sm font-medium {{ 'bg-indigo-50 text-indigo-700 border-l-4 border-indigo-600' if active_page == 'tokens' else 'text-gray-700 hover:bg-gray-50' }}">
|
||||
<svg class="w-5 h-5 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
|
||||
</svg>
|
||||
API Tokens
|
||||
</a>
|
||||
<a href="{{ url_for('web.dashboard_settings') }}"
|
||||
class="flex items-center px-4 py-3 text-sm font-medium {{ 'bg-indigo-50 text-indigo-700 border-l-4 border-indigo-600' if active_page == 'settings' else 'text-gray-700 hover:bg-gray-50' }}">
|
||||
<svg class="w-5 h-5 mr-3" 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 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
Settings
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<!-- Quick Stats (mobile hidden) -->
|
||||
<div class="hidden lg:block mt-6 bg-white rounded-lg border border-gray-200 p-4">
|
||||
<h3 class="text-xs font-medium text-gray-500 uppercase tracking-wider mb-3">Quick Stats</h3>
|
||||
<dl class="space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-sm text-gray-600">Tools</dt>
|
||||
<dd class="text-sm font-medium text-gray-900">{{ stats.tools_count if stats else 0 }}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-sm text-gray-600">Downloads</dt>
|
||||
<dd class="text-sm font-medium text-gray-900">{{ (stats.total_downloads if stats else 0)|format_number }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="lg:col-span-3">
|
||||
{% block dashboard_content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,135 @@
|
||||
{% extends "dashboard/base.html" %}
|
||||
{% set active_page = 'overview' %}
|
||||
|
||||
{% block title %}Dashboard - CmdForge{% endblock %}
|
||||
|
||||
{% block dashboard_content %}
|
||||
<!-- Stats Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 bg-indigo-100 rounded-lg">
|
||||
<svg class="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm text-gray-500">Published Tools</p>
|
||||
<p class="text-2xl font-bold text-gray-900">{{ stats.tools_count }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 bg-green-100 rounded-lg">
|
||||
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm text-gray-500">Total Downloads</p>
|
||||
<p class="text-2xl font-bold text-gray-900">{{ stats.total_downloads|format_number }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 bg-cyan-100 rounded-lg">
|
||||
<svg class="w-6 h-6 text-cyan-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm text-gray-500">API Tokens</p>
|
||||
<p class="text-2xl font-bold text-gray-900">{{ stats.tokens_count }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Tools -->
|
||||
<div class="bg-white rounded-lg border border-gray-200">
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Your Tools</h2>
|
||||
<a href="{{ url_for('web.docs', path='publishing') }}"
|
||||
class="text-sm text-indigo-600 hover:text-indigo-800">
|
||||
+ Publish New Tool
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% if tools %}
|
||||
<ul class="divide-y divide-gray-200">
|
||||
{% for tool in tools[:5] %}
|
||||
<li class="px-6 py-4 flex items-center justify-between">
|
||||
<div>
|
||||
<a href="{{ url_for('web.tool_detail', owner=tool.owner, name=tool.name) }}"
|
||||
class="font-medium text-gray-900 hover:text-indigo-600">
|
||||
{{ tool.name }}
|
||||
</a>
|
||||
<p class="text-sm text-gray-500">v{{ tool.version }}</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-sm text-gray-900">{{ tool.downloads|format_number }} downloads</p>
|
||||
<p class="text-xs text-gray-500">{{ tool.published_at|timeago }}</p>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if tools|length > 5 %}
|
||||
<div class="px-6 py-4 border-t border-gray-200">
|
||||
<a href="{{ url_for('web.dashboard_tools') }}"
|
||||
class="text-sm text-indigo-600 hover:text-indigo-800">
|
||||
View all {{ tools|length }} tools
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="px-6 py-12 text-center">
|
||||
<svg class="mx-auto w-12 h-12 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
|
||||
</svg>
|
||||
<p class="mt-4 text-gray-600">You haven't published any tools yet.</p>
|
||||
<a href="{{ url_for('web.docs', path='publishing') }}"
|
||||
class="mt-4 inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
|
||||
Publish Your First Tool
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
{% if recent_activity %}
|
||||
<div class="mt-6 bg-white rounded-lg border border-gray-200">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Recent Activity</h2>
|
||||
</div>
|
||||
<ul class="divide-y divide-gray-200">
|
||||
{% for activity in recent_activity[:5] %}
|
||||
<li class="px-6 py-4 flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
{% if activity.type == 'download' %}
|
||||
<div class="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% elif activity.type == 'publish' %}
|
||||
<div class="w-8 h-8 bg-indigo-100 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm text-gray-900">{{ activity.description }}</p>
|
||||
<p class="text-xs text-gray-500">{{ activity.timestamp|timeago }}</p>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,254 @@
|
||||
{% extends "dashboard/base.html" %}
|
||||
{% from "components/forms.html" import text_input, textarea, button_primary, form_errors, success_alert %}
|
||||
{% set active_page = 'settings' %}
|
||||
|
||||
{% block title %}Settings - CmdForge Dashboard{% endblock %}
|
||||
|
||||
{% block dashboard_header %}
|
||||
<h1 class="text-2xl font-bold text-gray-900">Settings</h1>
|
||||
<p class="mt-1 text-gray-600">Manage your profile and account settings</p>
|
||||
{% endblock %}
|
||||
|
||||
{% block dashboard_content %}
|
||||
{{ success_alert(success_message) }}
|
||||
{{ form_errors(errors) }}
|
||||
|
||||
<!-- Profile Settings -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Profile</h2>
|
||||
<p class="text-sm text-gray-600">This information will be displayed on your public profile.</p>
|
||||
</div>
|
||||
|
||||
<form action="{{ url_for('web.dashboard_settings') }}" method="POST" class="p-6">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="form" value="profile">
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="display_name" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Display name
|
||||
</label>
|
||||
<input type="text"
|
||||
name="display_name"
|
||||
id="display_name"
|
||||
value="{{ user.display_name }}"
|
||||
required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="slug" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Username
|
||||
</label>
|
||||
<div class="flex">
|
||||
<span class="inline-flex items-center px-3 text-gray-500 bg-gray-100 border border-r-0 border-gray-300 rounded-l-lg">
|
||||
@
|
||||
</span>
|
||||
<input type="text"
|
||||
name="slug"
|
||||
id="slug"
|
||||
value="{{ user.slug }}"
|
||||
disabled
|
||||
class="flex-1 px-4 py-2 border border-gray-300 rounded-r-lg bg-gray-50 text-gray-500">
|
||||
</div>
|
||||
<p class="mt-1 text-xs text-gray-500">Username cannot be changed</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<label for="bio" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Bio
|
||||
</label>
|
||||
<textarea name="bio"
|
||||
id="bio"
|
||||
rows="3"
|
||||
placeholder="Tell others a bit about yourself..."
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 resize-y">{{ user.bio or '' }}</textarea>
|
||||
<p class="mt-1 text-xs text-gray-500">Brief description for your profile. Max 500 characters.</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<label for="website" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Website
|
||||
</label>
|
||||
<input type="url"
|
||||
name="website"
|
||||
id="website"
|
||||
value="{{ user.website or '' }}"
|
||||
placeholder="https://example.com"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<button type="submit"
|
||||
class="px-6 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
|
||||
Save Profile
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Email Settings -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Email</h2>
|
||||
<p class="text-sm text-gray-600">Your email address for account notifications.</p>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">{{ user.email }}</p>
|
||||
{% if user.verified %}
|
||||
<p class="text-xs text-green-600 flex items-center mt-1">
|
||||
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Verified
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="text-xs text-amber-600 flex items-center mt-1">
|
||||
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Not verified
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not user.verified %}
|
||||
<button type="button"
|
||||
onclick="resendVerification()"
|
||||
class="text-sm text-indigo-600 hover:text-indigo-800">
|
||||
Resend verification
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password Change -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Change Password</h2>
|
||||
<p class="text-sm text-gray-600">Update your password to keep your account secure.</p>
|
||||
</div>
|
||||
|
||||
<form action="{{ url_for('web.dashboard_settings') }}" method="POST" class="p-6">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="form" value="password">
|
||||
|
||||
<div class="space-y-4 max-w-md">
|
||||
<div>
|
||||
<label for="current_password" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Current password
|
||||
</label>
|
||||
<input type="password"
|
||||
name="current_password"
|
||||
id="current_password"
|
||||
required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="new_password" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
New password
|
||||
</label>
|
||||
<input type="password"
|
||||
name="new_password"
|
||||
id="new_password"
|
||||
required
|
||||
minlength="8"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<p class="mt-1 text-xs text-gray-500">Minimum 8 characters</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="confirm_password" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Confirm new password
|
||||
</label>
|
||||
<input type="password"
|
||||
name="confirm_password"
|
||||
id="confirm_password"
|
||||
required
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<button type="submit"
|
||||
class="px-6 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
|
||||
Update Password
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Danger Zone -->
|
||||
<div class="bg-white rounded-lg border border-red-200 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-red-200 bg-red-50">
|
||||
<h2 class="text-lg font-semibold text-red-800">Danger Zone</h2>
|
||||
<p class="text-sm text-red-600">Irreversible actions that affect your account.</p>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">Delete Account</p>
|
||||
<p class="text-sm text-gray-500">Permanently delete your account and all associated data.</p>
|
||||
</div>
|
||||
<button type="button"
|
||||
onclick="confirmDeleteAccount()"
|
||||
class="px-4 py-2 text-sm font-medium text-red-600 border border-red-300 rounded-md hover:bg-red-50">
|
||||
Delete Account
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function resendVerification() {
|
||||
try {
|
||||
const response = await fetch('/api/v1/me/resend-verification', {method: 'POST'});
|
||||
if (response.ok) {
|
||||
alert('Verification email sent! Please check your inbox.');
|
||||
} else {
|
||||
alert('Failed to send verification email. Please try again later.');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Failed to send verification email. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDeleteAccount() {
|
||||
const confirmed = confirm(
|
||||
'Are you absolutely sure you want to delete your account?\n\n' +
|
||||
'This will permanently delete:\n' +
|
||||
'- Your profile\n' +
|
||||
'- All your published tools\n' +
|
||||
'- All your API tokens\n\n' +
|
||||
'This action cannot be undone.'
|
||||
);
|
||||
|
||||
if (confirmed) {
|
||||
const doubleConfirm = prompt('Type "DELETE" to confirm account deletion:');
|
||||
if (doubleConfirm === 'DELETE') {
|
||||
deleteAccount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAccount() {
|
||||
try {
|
||||
const response = await fetch('/api/v1/me', {method: 'DELETE'});
|
||||
if (response.ok) {
|
||||
window.location.href = '/?deleted=1';
|
||||
} else {
|
||||
alert('Failed to delete account. Please try again or contact support.');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Failed to delete account. Please try again or contact support.');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,312 @@
|
||||
{% extends "dashboard/base.html" %}
|
||||
{% set active_page = 'tokens' %}
|
||||
|
||||
{% block title %}API Tokens - CmdForge Dashboard{% endblock %}
|
||||
|
||||
{% block dashboard_header %}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">API Tokens</h1>
|
||||
<p class="mt-1 text-gray-600">Manage tokens for CLI and API access</p>
|
||||
</div>
|
||||
<button type="button"
|
||||
onclick="openCreateTokenModal()"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
||||
</svg>
|
||||
Create Token
|
||||
</button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block dashboard_content %}
|
||||
<!-- Info Banner -->
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-5 h-5 text-blue-500 mt-0.5" 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>
|
||||
<div class="ml-3">
|
||||
<h4 class="text-sm font-medium text-blue-800">About API Tokens</h4>
|
||||
<p class="mt-1 text-sm text-blue-700">
|
||||
API tokens are used to authenticate with the CmdForge registry from the CLI.
|
||||
Use <code class="px-1 bg-blue-100 rounded">cmdforge auth login</code> to authenticate,
|
||||
or set the <code class="px-1 bg-blue-100 rounded">CMDFORGE_TOKEN</code> environment variable.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if tokens %}
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Name
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Created
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Last Used
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
{% for token in tokens %}
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="w-10 h-10 bg-cyan-100 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-5 h-5 text-cyan-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-900">{{ token.name }}</p>
|
||||
<p class="text-xs text-gray-500 font-mono">st_...{{ token.token_suffix }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ token.created_at|date_format }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ token.last_used_at|timeago if token.last_used_at else 'Never' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{% if token.revoked_at %}
|
||||
<span class="px-2 py-1 text-xs font-medium text-red-800 bg-red-100 rounded-full">
|
||||
Revoked
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="px-2 py-1 text-xs font-medium text-green-800 bg-green-100 rounded-full">
|
||||
Active
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
{% if not token.revoked_at %}
|
||||
<button type="button"
|
||||
onclick="revokeToken({{ token.id }}, '{{ token.name }}')"
|
||||
class="text-red-600 hover:text-red-900">
|
||||
Revoke
|
||||
</button>
|
||||
{% else %}
|
||||
<span class="text-gray-400">Revoked {{ token.revoked_at|timeago }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- Empty State -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 text-center py-16">
|
||||
<svg class="mx-auto w-16 h-16 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
|
||||
</svg>
|
||||
<h3 class="mt-4 text-lg font-medium text-gray-900">No API tokens</h3>
|
||||
<p class="mt-2 text-gray-600 max-w-sm mx-auto">
|
||||
Create an API token to authenticate with the registry from the command line.
|
||||
</p>
|
||||
<button type="button"
|
||||
onclick="openCreateTokenModal()"
|
||||
class="mt-6 inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
|
||||
Create Your First Token
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Create Token Modal -->
|
||||
<div id="create-token-modal" class="hidden fixed inset-0 z-50 overflow-y-auto" aria-modal="true" role="dialog">
|
||||
<div class="min-h-screen px-4 text-center">
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50" onclick="closeCreateTokenModal()"></div>
|
||||
<div class="inline-block w-full max-w-md my-8 text-left align-middle bg-white shadow-xl rounded-lg">
|
||||
<form id="create-token-form" onsubmit="createToken(event)">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
|
||||
<div class="p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Create API Token</h3>
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
Give your token a descriptive name so you can identify it later.
|
||||
</p>
|
||||
|
||||
<div class="mt-4">
|
||||
<label for="token-name" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Token name
|
||||
</label>
|
||||
<input type="text"
|
||||
name="name"
|
||||
id="token-name"
|
||||
required
|
||||
placeholder="e.g., Laptop CLI, CI/CD Pipeline"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 px-6 py-4 rounded-b-lg flex justify-end gap-3">
|
||||
<button type="button"
|
||||
onclick="closeCreateTokenModal()"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
|
||||
Create Token
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Token Created Modal (shows the token once) -->
|
||||
<div id="token-created-modal" class="hidden fixed inset-0 z-50 overflow-y-auto" aria-modal="true" role="dialog">
|
||||
<div class="min-h-screen px-4 text-center">
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50"></div>
|
||||
<div class="inline-block w-full max-w-lg my-8 text-left align-middle bg-white shadow-xl rounded-lg">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="ml-3 text-lg font-semibold text-gray-900">Token Created</h3>
|
||||
</div>
|
||||
|
||||
<div class="bg-amber-50 border border-amber-200 rounded-lg p-4 mb-4">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-5 h-5 text-amber-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||
</svg>
|
||||
<p class="ml-3 text-sm text-amber-800">
|
||||
<strong>Copy your token now.</strong> You won't be able to see it again!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-100 rounded-lg p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<code id="new-token-value" class="text-sm font-mono text-gray-800 break-all"></code>
|
||||
<button type="button"
|
||||
onclick="copyNewToken()"
|
||||
class="ml-4 p-2 text-gray-400 hover:text-gray-600 flex-shrink-0">
|
||||
<svg id="copy-token-icon" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<svg id="check-token-icon" class="w-5 h-5 hidden text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-4 text-sm text-gray-600">
|
||||
To use this token, run:
|
||||
</p>
|
||||
<pre class="mt-2 bg-gray-900 text-gray-100 text-sm p-3 rounded-lg overflow-x-auto"><code>export CMDFORGE_TOKEN="<span id="token-in-export"></span>"</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 px-6 py-4 rounded-b-lg flex justify-end">
|
||||
<button type="button"
|
||||
onclick="closeTokenCreatedModal()"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openCreateTokenModal() {
|
||||
document.getElementById('create-token-modal').classList.remove('hidden');
|
||||
document.getElementById('token-name').focus();
|
||||
}
|
||||
|
||||
function closeCreateTokenModal() {
|
||||
document.getElementById('create-token-modal').classList.add('hidden');
|
||||
document.getElementById('token-name').value = '';
|
||||
}
|
||||
|
||||
async function createToken(event) {
|
||||
event.preventDefault();
|
||||
const form = event.target;
|
||||
const name = form.querySelector('[name="name"]').value;
|
||||
|
||||
try {
|
||||
const response = await fetch('/dashboard/api/tokens', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({name: name})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
closeCreateTokenModal();
|
||||
showNewToken(result.data.token);
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(error.error || 'Failed to create token');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Failed to create token. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
function showNewToken(token) {
|
||||
document.getElementById('new-token-value').textContent = token;
|
||||
document.getElementById('token-in-export').textContent = token;
|
||||
document.getElementById('token-created-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeTokenCreatedModal() {
|
||||
document.getElementById('token-created-modal').classList.add('hidden');
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function copyNewToken() {
|
||||
const token = document.getElementById('new-token-value').textContent;
|
||||
navigator.clipboard.writeText(token).then(() => {
|
||||
document.getElementById('copy-token-icon').classList.add('hidden');
|
||||
document.getElementById('check-token-icon').classList.remove('hidden');
|
||||
setTimeout(() => {
|
||||
document.getElementById('copy-token-icon').classList.remove('hidden');
|
||||
document.getElementById('check-token-icon').classList.add('hidden');
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
async function revokeToken(tokenId, tokenName) {
|
||||
if (!confirm(`Are you sure you want to revoke "${tokenName}"? This cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/dashboard/api/tokens/${tokenId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(error.error || 'Failed to revoke token');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Failed to revoke token. Please try again.');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,259 @@
|
||||
{% extends "dashboard/base.html" %}
|
||||
{% set active_page = 'tools' %}
|
||||
|
||||
{% block title %}My Tools - CmdForge Dashboard{% endblock %}
|
||||
|
||||
{% block dashboard_header %}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900">My Tools</h1>
|
||||
<p class="mt-1 text-gray-600">Manage your published tools</p>
|
||||
</div>
|
||||
<a href="{{ url_for('web.docs', path='publishing') }}"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
||||
</svg>
|
||||
Publish New Tool
|
||||
</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block dashboard_content %}
|
||||
{% if tools %}
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Tool
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Version
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Downloads
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
{% for tool in tools %}
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="w-10 h-10 bg-indigo-100 rounded-lg flex items-center justify-center flex-shrink-0">
|
||||
<svg class="w-5 h-5 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<a href="{{ url_for('web.tool_detail', owner=tool.owner, name=tool.name) }}"
|
||||
class="text-sm font-medium text-gray-900 hover:text-indigo-600">
|
||||
{{ tool.name }}
|
||||
</a>
|
||||
<p class="text-sm text-gray-500 truncate max-w-xs">{{ tool.description or 'No description' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="text-sm text-gray-900">v{{ tool.version }}</span>
|
||||
<p class="text-xs text-gray-500">{{ tool.published_at|timeago }}</p>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{{ tool.downloads|format_number }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
{% if tool.deprecated %}
|
||||
<span class="px-2 py-1 text-xs font-medium text-amber-800 bg-amber-100 rounded-full">
|
||||
Deprecated
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="px-2 py-1 text-xs font-medium text-green-800 bg-green-100 rounded-full">
|
||||
Active
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<a href="{{ url_for('web.tool_detail', owner=tool.owner, name=tool.name) }}"
|
||||
class="text-indigo-600 hover:text-indigo-900">View</a>
|
||||
<button type="button"
|
||||
onclick="openDeprecateModal('{{ tool.owner }}', '{{ tool.name }}', {{ 'true' if tool.deprecated else 'false' }})"
|
||||
class="text-gray-400 hover:text-amber-600">
|
||||
{% if tool.deprecated %}Restore{% else %}Deprecate{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pending PRs Section -->
|
||||
{% if pending_prs %}
|
||||
<div class="mt-8">
|
||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">Pending Publications</h2>
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<ul class="divide-y divide-gray-200">
|
||||
{% for pr in pending_prs %}
|
||||
<li class="px-6 py-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900">{{ pr.owner }}/{{ pr.name }} v{{ pr.version }}</p>
|
||||
<p class="text-xs text-gray-500">Submitted {{ pr.created_at|timeago }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="px-2 py-1 text-xs font-medium text-blue-800 bg-blue-100 rounded-full">
|
||||
{{ pr.status }}
|
||||
</span>
|
||||
<a href="{{ pr.pr_url }}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sm text-indigo-600 hover:text-indigo-800">
|
||||
View PR
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<!-- Empty State -->
|
||||
<div class="bg-white rounded-lg border border-gray-200 text-center py-16">
|
||||
<svg class="mx-auto w-16 h-16 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4"/>
|
||||
</svg>
|
||||
<h3 class="mt-4 text-lg font-medium text-gray-900">No tools yet</h3>
|
||||
<p class="mt-2 text-gray-600 max-w-sm mx-auto">
|
||||
You haven't published any tools. Create your first tool and share it with the community.
|
||||
</p>
|
||||
<a href="{{ url_for('web.docs', path='publishing') }}"
|
||||
class="mt-6 inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700">
|
||||
Learn How to Publish
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Deprecate Modal -->
|
||||
<div id="deprecate-modal" class="hidden fixed inset-0 z-50 overflow-y-auto" aria-modal="true" role="dialog">
|
||||
<div class="min-h-screen px-4 text-center">
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50" onclick="closeDeprecateModal()"></div>
|
||||
<div class="inline-block w-full max-w-md my-8 text-left align-middle bg-white shadow-xl rounded-lg">
|
||||
<form id="deprecate-form" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input type="hidden" name="owner" id="deprecate-owner">
|
||||
<input type="hidden" name="name" id="deprecate-name">
|
||||
|
||||
<div class="p-6">
|
||||
<h3 id="deprecate-title" class="text-lg font-semibold text-gray-900">Deprecate Tool</h3>
|
||||
<p id="deprecate-desc" class="mt-2 text-sm text-gray-600">
|
||||
Mark this tool as deprecated. Users will see a warning when viewing or installing it.
|
||||
</p>
|
||||
|
||||
<div id="deprecate-fields" class="mt-4 space-y-4">
|
||||
<div>
|
||||
<label for="deprecated_message" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Deprecation message (optional)
|
||||
</label>
|
||||
<textarea name="deprecated_message"
|
||||
id="deprecated_message"
|
||||
rows="2"
|
||||
placeholder="e.g., This tool has been superseded by..."
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500"></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="replacement" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Replacement tool (optional)
|
||||
</label>
|
||||
<input type="text"
|
||||
name="replacement"
|
||||
id="replacement"
|
||||
placeholder="owner/tool-name"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 px-6 py-4 rounded-b-lg flex justify-end gap-3">
|
||||
<button type="button"
|
||||
onclick="closeDeprecateModal()"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit"
|
||||
id="deprecate-submit"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-amber-600 rounded-md hover:bg-amber-700">
|
||||
Deprecate
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let deprecateAction = '';
|
||||
|
||||
function openDeprecateModal(owner, name, isDeprecated) {
|
||||
document.getElementById('deprecate-owner').value = owner;
|
||||
document.getElementById('deprecate-name').value = name;
|
||||
|
||||
if (isDeprecated) {
|
||||
document.getElementById('deprecate-title').textContent = 'Restore Tool';
|
||||
document.getElementById('deprecate-desc').textContent = 'Remove the deprecation notice from this tool.';
|
||||
document.getElementById('deprecate-fields').classList.add('hidden');
|
||||
document.getElementById('deprecate-submit').textContent = 'Restore';
|
||||
document.getElementById('deprecate-submit').classList.remove('bg-amber-600', 'hover:bg-amber-700');
|
||||
document.getElementById('deprecate-submit').classList.add('bg-green-600', 'hover:bg-green-700');
|
||||
deprecateAction = `/dashboard/api/tools/${owner}/${name}/undeprecate`;
|
||||
} else {
|
||||
document.getElementById('deprecate-title').textContent = 'Deprecate Tool';
|
||||
document.getElementById('deprecate-desc').textContent = 'Mark this tool as deprecated. Users will see a warning.';
|
||||
document.getElementById('deprecate-fields').classList.remove('hidden');
|
||||
document.getElementById('deprecate-submit').textContent = 'Deprecate';
|
||||
document.getElementById('deprecate-submit').classList.remove('bg-green-600', 'hover:bg-green-700');
|
||||
document.getElementById('deprecate-submit').classList.add('bg-amber-600', 'hover:bg-amber-700');
|
||||
deprecateAction = `/dashboard/api/tools/${owner}/${name}/deprecate`;
|
||||
}
|
||||
|
||||
document.getElementById('deprecate-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeDeprecateModal() {
|
||||
document.getElementById('deprecate-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
document.getElementById('deprecate-form').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
const message = document.getElementById('deprecated_message')?.value || '';
|
||||
const replacement = document.getElementById('replacement')?.value || '';
|
||||
|
||||
try {
|
||||
const response = await fetch(deprecateAction, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({deprecated_message: message, replacement: replacement})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert((error.error && error.error.message) || 'Failed to update tool status');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Failed to update tool status. Please try again.');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user