Files
TimeTracker/app/templates/integrations/view.html
T
Dries Peeters 9449a46a42 feat(integrations): Linear connector and shared HTTP/sync helpers
- Add Linear import (GraphQL, personal API key, optional team key filter).
- Centralize integration HTTP via integration_session and session_request.
- Add integration_sync_context for project/task refs and custom_fields metadata.
- Refactor Asana, GitHub, GitLab, Jira, Trello, ActivityWatch, and QuickBooks to use helpers.
- Extend integration UI, settings, and scheduled sync behavior as needed.
2026-04-05 08:39:18 +02:00

174 lines
12 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ integration.name }} - {{ _('Integration') }} - {{ app_name }}{% endblock %}
{% block content %}
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6">
<div>
<h1 class="text-2xl font-bold">{{ integration.name }}</h1>
<p class="text-text-muted-light dark:text-text-muted-dark">{{ integration.provider|title }}</p>
</div>
<a href="{{ url_for('integrations.list_integrations') }}" class="bg-gray-200 dark:bg-gray-700 px-4 py-2 rounded-lg mt-4 md:mt-0">{{ _('Back to Integrations') }}</a>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="lg:col-span-2 space-y-6">
<div class="bg-card-light dark:bg-card-dark p-6 rounded-xl border border-border-light dark:border-border-dark shadow-sm">
<h2 class="text-lg font-semibold mb-4">{{ _('Integration Status') }}</h2>
<dl class="space-y-3">
<div>
<dt class="text-sm font-medium text-text-muted-light dark:text-text-muted-dark">{{ _('Status') }}</dt>
<dd>
{% if integration.is_active %}
<span class="px-2 py-1 text-xs rounded bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">{{ _('Active') }}</span>
{% else %}
<span class="px-2 py-1 text-xs rounded bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200">{{ _('Inactive') }}</span>
{% endif %}
</dd>
</div>
{% if credentials %}
<div>
<dt class="text-sm font-medium text-text-muted-light dark:text-text-muted-dark">{{ _('Connected') }}</dt>
<dd class="text-text-light dark:text-text-dark">{{ _('Yes') }}</dd>
</div>
{% if credentials.expires_at %}
<div>
<dt class="text-sm font-medium text-text-muted-light dark:text-text-muted-dark">{{ _('Token Expires') }}</dt>
<dd class="text-text-light dark:text-text-dark">{{ credentials.expires_at|user_datetime }}</dd>
</div>
{% endif %}
{% else %}
<div>
<dt class="text-sm font-medium text-text-muted-light dark:text-text-muted-dark">{{ _('Connected') }}</dt>
<dd class="text-text-light dark:text-text-dark">{{ _('No') }}</dd>
</div>
{% endif %}
{% if integration.last_sync_at %}
<div>
<dt class="text-sm font-medium text-text-muted-light dark:text-text-muted-dark">{{ _('Last Sync') }}</dt>
<dd class="text-text-light dark:text-text-dark">
{{ integration.last_sync_at|user_datetime }}
{% if integration.last_sync_status %}
{% if integration.last_sync_status == 'success' %}
<span class="ml-2 px-2 py-1 text-xs rounded bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">{{ _('Success') }}</span>
{% elif integration.last_sync_status == 'error' %}
<span class="ml-2 px-2 py-1 text-xs rounded bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200">{{ _('Error') }}</span>
{% else %}
<span class="ml-2 px-2 py-1 text-xs rounded bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200">{{ _('Pending') }}</span>
{% endif %}
{% endif %}
</dd>
</div>
{% endif %}
{% if integration.last_error %}
<div>
<dt class="text-sm font-medium text-text-muted-light dark:text-text-muted-dark">{{ _('Last Error') }}</dt>
<dd class="text-text-light dark:text-text-dark text-sm text-red-600 dark:text-red-400">{{ integration.last_error[:100] }}{% if integration.last_error|length > 100 %}...{% endif %}</dd>
</div>
{% endif %}
</dl>
</div>
<!-- Sync Events Log -->
<div class="bg-card-light dark:bg-card-dark p-6 rounded-xl border border-border-light dark:border-border-dark shadow-sm mt-6">
<h2 class="text-lg font-semibold mb-4">{{ _('Sync History') }}</h2>
{% if recent_events %}
<div class="space-y-2 max-h-[32rem] overflow-y-auto">
{% for event in recent_events %}
<div class="border-b border-border-light dark:border-border-dark pb-2 last:border-0">
<div class="flex items-start justify-between">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 flex-wrap">
<span class="text-sm font-medium text-text-light dark:text-text-dark">{{ event.event_type|replace('_', ' ')|title }}</span>
{% if event.status == 'success' %}
<span class="px-2 py-0.5 text-xs rounded bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">{{ _('Success') }}</span>
{% elif event.status == 'error' %}
<span class="px-2 py-0.5 text-xs rounded bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200">{{ _('Error') }}</span>
{% else %}
<span class="px-2 py-0.5 text-xs rounded bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200">{{ _('Pending') }}</span>
{% endif %}
</div>
{% if event.message %}
<p class="text-sm text-text-muted-light dark:text-text-muted-dark mt-1 break-words">{{ event.message }}</p>
{% endif %}
{% if event.event_metadata %}
<details class="mt-2">
<summary class="text-xs text-primary cursor-pointer hover:underline">{{ _('Details') }}</summary>
<pre class="mt-2 text-xs bg-gray-100 dark:bg-gray-900 p-2 rounded overflow-x-auto max-h-48 overflow-y-auto">{{ event.event_metadata | tojson }}</pre>
</details>
{% endif %}
<p class="text-xs text-text-muted-light dark:text-text-muted-dark mt-1">{{ event.created_at|user_datetime }}</p>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-text-muted-light dark:text-text-muted-dark">{{ _('No sync events yet') }}</p>
{% endif %}
</div>
</div>
<div>
<div class="bg-card-light dark:bg-card-dark p-6 rounded-xl border border-border-light dark:border-border-dark shadow-sm">
<h3 class="text-lg font-semibold mb-4">{{ _('Actions') }}</h3>
{% if connector_error %}
<div class="mb-4 p-3 bg-red-100 dark:bg-red-900/20 border border-red-300 dark:border-red-800 rounded-lg">
<p class="text-sm text-red-800 dark:text-red-200">
<i class="fas fa-exclamation-triangle mr-2"></i>
{{ _('Connector Error') }}: {{ connector_error }}
</p>
{% if integration.provider == 'caldav_calendar' %}
<a href="{{ url_for('integrations.caldav_setup') }}" class="mt-2 inline-block text-sm text-red-800 dark:text-red-200 hover:underline">
{{ _('Configure CalDAV Integration') }} →
</a>
{% elif integration.provider == 'activitywatch' %}
<a href="{{ url_for('integrations.activitywatch_setup') }}" class="mt-2 inline-block text-sm text-red-800 dark:text-red-200 hover:underline">
{{ _('Configure ActivityWatch') }} →
</a>
{% endif %}
</div>
{% endif %}
<div class="space-y-2">
{% if connector %}
<form method="POST" action="{{ url_for('integrations.test_integration', integration_id=integration.id) }}" class="mb-2">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="w-full bg-blue-600 text-white px-4 py-2 min-h-[44px] rounded-lg hover:bg-blue-700 transition-colors {% if not connector %}opacity-50 cursor-not-allowed{% endif %}" {% if not connector %}disabled{% endif %}>
<i class="fas fa-vial mr-2"></i>{{ _('Test Connection') }}
</button>
</form>
<form method="POST" action="{{ url_for('integrations.sync_integration', integration_id=integration.id) }}" class="mb-2">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="w-full bg-green-600 text-white px-4 py-2 min-h-[44px] rounded-lg hover:bg-green-700 transition-colors {% if not connector %}opacity-50 cursor-not-allowed{% endif %}" {% if not connector %}disabled{% endif %}>
<i class="fas fa-sync mr-2"></i>{{ _('Sync Now') }}
</button>
</form>
{% endif %}
<form method="POST" action="{{ url_for('integrations.reset_integration', integration_id=integration.id) }}" onsubmit="return confirm('{{ _('Are you sure you want to reset this integration? This will remove all credentials and configuration.') }}')" class="mb-2">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="w-full bg-orange-600 text-white px-4 py-2 min-h-[44px] rounded-lg hover:bg-orange-700 transition-colors">
<i class="fas fa-redo mr-2"></i>{{ _('Reset') }}
</button>
</form>
<form method="POST" action="{{ url_for('integrations.delete_integration', integration_id=integration.id) }}" onsubmit="return confirm('{{ _('Are you sure you want to delete this integration? This action cannot be undone.') }}')">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="w-full bg-red-600 text-white px-4 py-2 min-h-[44px] rounded-lg hover:bg-red-700 transition-colors">
<i class="fas fa-trash mr-2"></i>{{ _('Delete') }}
</button>
</form>
{% if integration.provider == 'caldav_calendar' %}
<a href="{{ url_for('integrations.caldav_setup') }}" class="block w-full bg-gray-600 text-white px-4 py-2 rounded-lg hover:bg-gray-700 transition-colors text-center">
<i class="fas fa-cog mr-2"></i>{{ _('Edit Configuration') }}
</a>
{% elif integration.provider == 'activitywatch' %}
<a href="{{ url_for('integrations.activitywatch_setup') }}" class="block w-full bg-gray-600 text-white px-4 py-2 rounded-lg hover:bg-gray-700 transition-colors text-center">
<i class="fas fa-cog mr-2"></i>{{ _('Edit Configuration') }}
</a>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}