Datenbank bereinigt / Gitea-Integration gefixt

Dieser Commit ist enthalten in:
hendrik_gebhardt@gmx.de
2026-01-04 00:24:11 +00:00
committet von Server Deploy
Ursprung 395598c2b0
Commit c21be47428
37 geänderte Dateien mit 30993 neuen und 809 gelöschten Zeilen

Datei anzeigen

@ -431,81 +431,163 @@
font-weight: var(--font-medium);
}
/* Upload Types */
.admin-upload-types {
/* =========================
Extension Settings (New)
========================= */
.admin-upload-extensions {
margin-bottom: 1.5rem;
}
.admin-upload-types h3 {
.admin-upload-extensions h3 {
font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--text-primary);
margin: 0 0 1rem 0;
}
/* Upload Category */
.upload-category {
/* Extension Tags Container */
.extension-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
padding: 1rem;
background: var(--bg-tertiary);
border: 1px solid var(--border-light);
border-radius: var(--radius-lg);
margin-bottom: 0.75rem;
overflow: hidden;
transition: all var(--transition-fast);
min-height: 50px;
margin-bottom: 1rem;
}
.upload-category:hover {
border-color: var(--border-default);
.extension-empty {
color: var(--text-muted);
font-size: var(--text-sm);
font-style: italic;
}
.upload-category.disabled {
opacity: 0.5;
/* Single Extension Tag */
.extension-tag {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 6px 10px;
background: var(--primary);
color: white;
border-radius: var(--radius-full);
font-size: var(--text-sm);
font-weight: var(--font-medium);
font-family: var(--font-mono, monospace);
}
.upload-category.disabled .upload-category-types {
display: none;
}
.upload-category-header {
padding: 0.75rem 1rem;
background: var(--bg-card);
}
.upload-category-toggle {
.extension-tag-remove {
display: flex;
align-items: center;
gap: 0.75rem;
cursor: pointer;
user-select: none;
}
.upload-category-toggle input[type="checkbox"] {
justify-content: center;
width: 18px;
height: 18px;
accent-color: var(--primary);
padding: 0;
background: rgba(255, 255, 255, 0.2);
border: none;
border-radius: 50%;
cursor: pointer;
transition: background var(--transition-fast);
}
.upload-category-title {
.extension-tag-remove:hover {
background: rgba(255, 255, 255, 0.4);
}
.extension-tag-remove svg {
stroke: white;
}
/* Add Extension Group */
.extension-add-group {
margin-bottom: 1rem;
}
.extension-add-group label {
display: block;
font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--text-primary);
margin-bottom: 0.5rem;
}
.upload-category-types {
padding: 0.75rem 1rem;
.extension-input-row {
display: flex;
gap: 0.5rem;
max-width: 400px;
}
.extension-input {
flex: 1;
padding: 8px 12px;
background: var(--bg-input);
border: 1px solid var(--border-default);
border-radius: var(--radius-lg);
color: var(--text-primary);
font-size: var(--text-sm);
font-family: var(--font-mono, monospace);
}
.extension-input:focus {
border-color: var(--primary);
outline: none;
box-shadow: var(--shadow-focus);
}
.extension-input::placeholder {
color: var(--text-muted);
font-family: var(--font-primary);
}
/* Suggestions */
.extension-suggestions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
padding: 1rem;
background: var(--bg-secondary);
border-radius: var(--radius-lg);
}
.extension-suggestions-label {
font-size: var(--text-sm);
color: var(--text-secondary);
font-weight: var(--font-medium);
margin-right: 0.5rem;
}
.extension-suggestions-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.upload-type-tag {
display: inline-block;
.extension-suggestion {
padding: 4px 10px;
background: var(--primary-light);
color: var(--primary);
background: var(--bg-tertiary);
border: 1px dashed var(--border-default);
border-radius: var(--radius-full);
color: var(--text-secondary);
font-size: var(--text-xs);
font-weight: var(--font-medium);
cursor: pointer;
transition: all var(--transition-fast);
}
.extension-suggestion:hover {
background: var(--primary-light);
border-color: var(--primary);
color: var(--primary);
}
.extension-no-suggestions {
font-size: var(--text-xs);
color: var(--text-muted);
font-style: italic;
}
/* Upload Actions */
@ -515,6 +597,30 @@
}
/* Responsive */
/* Passwort-Input Gruppe */
.password-input-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.password-input-group input {
flex: 1;
}
.password-input-group .btn {
padding: 0.5rem;
min-width: auto;
display: flex;
align-items: center;
justify-content: center;
}
.password-input-group .btn svg {
width: 16px;
height: 16px;
}
@media (max-width: 768px) {
.admin-header {
padding: 1rem;

555
frontend/css/coding.css Normale Datei
Datei anzeigen

@ -0,0 +1,555 @@
/**
* TASKMATE - Coding Tab Styles
* ============================
* Styling für die Coding-Verzeichnis-Verwaltung
*/
/* View Container */
.view-coding {
padding: 1.5rem;
max-width: 1600px;
margin: 0 auto;
}
/* Header */
.coding-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.coding-header-centered {
justify-content: center;
}
.coding-header h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
color: var(--text-primary);
}
/* Grid Layout */
.coding-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.5rem;
}
/* Kachel */
.coding-tile {
background: var(--bg-secondary);
border-radius: 12px;
overflow: hidden;
box-shadow: var(--shadow-sm);
transition: transform 0.2s, box-shadow 0.2s;
display: flex;
flex-direction: column;
cursor: pointer;
}
.coding-tile:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
/* Farbbalken oben */
.coding-tile-color {
height: 4px;
flex-shrink: 0;
}
/* Tile Header */
.coding-tile-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 1rem 1rem 0.5rem;
}
.coding-tile-icon {
font-size: 2rem;
line-height: 1;
}
.coding-tile-menu {
background: none;
border: none;
padding: 0.25rem;
cursor: pointer;
color: var(--text-muted);
border-radius: 4px;
transition: background 0.2s, color 0.2s;
}
.coding-tile-menu:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
/* Tile Content */
.coding-tile-content {
padding: 0 1rem;
flex-grow: 1;
}
.coding-tile-name {
font-size: 1.125rem;
font-weight: 600;
margin-bottom: 0.25rem;
color: var(--text-primary);
}
.coding-tile-path {
font-size: 0.75rem;
color: var(--text-muted);
font-family: var(--font-mono, monospace);
word-break: break-all;
line-height: 1.4;
}
.coding-tile-description {
font-size: 0.875rem;
color: var(--text-secondary);
margin-top: 0.5rem;
line-height: 1.4;
}
/* CLAUDE.md Badge */
.coding-tile-badge {
display: inline-block;
background: linear-gradient(135deg, #F59E0B, #D97706);
color: white;
font-size: 0.65rem;
padding: 0.2rem 0.5rem;
border-radius: 4px;
margin-top: 0.5rem;
font-weight: 600;
letter-spacing: 0.02em;
}
/* Git Status */
.coding-tile-status {
display: flex;
gap: 0.5rem;
padding: 0.75rem 1rem;
flex-wrap: wrap;
align-items: center;
}
.git-branch-badge {
background: var(--bg-tertiary);
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-family: var(--font-mono, monospace);
color: var(--text-secondary);
}
.git-status-badge {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
}
.git-status-badge.loading {
background: var(--bg-tertiary);
color: var(--text-muted);
}
.git-status-badge.clean {
background: rgba(16, 185, 129, 0.15);
color: #10B981;
}
.git-status-badge.dirty {
background: rgba(245, 158, 11, 0.15);
color: #F59E0B;
}
.git-status-badge.error {
background: rgba(239, 68, 68, 0.15);
color: #EF4444;
}
.git-status-badge.ahead {
background: rgba(59, 130, 246, 0.15);
color: #3B82F6;
}
.git-status-badge.behind {
background: rgba(139, 92, 246, 0.15);
color: #8B5CF6;
}
/* Action Buttons */
.coding-tile-actions {
display: flex;
gap: 0.75rem;
padding: 1rem;
}
.btn-claude {
flex: 1;
background: linear-gradient(135deg, #F59E0B, #D97706);
color: white;
border: none;
padding: 0.75rem;
border-radius: 8px;
font-weight: 600;
font-size: 0.875rem;
cursor: pointer;
transition: opacity 0.2s, transform 0.2s;
}
.btn-claude:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.btn-claude:active {
transform: translateY(0);
}
.btn-codex {
flex: 1;
background: linear-gradient(135deg, #10B981, #059669);
color: white;
border: none;
padding: 0.75rem;
border-radius: 8px;
font-weight: 600;
font-size: 0.875rem;
cursor: pointer;
transition: opacity 0.2s, transform 0.2s;
}
.btn-codex:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.btn-codex:active {
transform: translateY(0);
}
/* Git Actions */
.coding-tile-git {
display: flex;
gap: 0.5rem;
padding: 0 1rem 1rem;
border-top: 1px solid var(--border-color);
padding-top: 0.75rem;
}
.coding-tile-git .btn {
flex: 1;
font-size: 0.75rem;
padding: 0.5rem;
}
/* Empty State */
.coding-empty {
text-align: center;
padding: 4rem 2rem;
color: var(--text-muted);
}
.coding-empty .empty-icon {
margin-bottom: 1rem;
color: var(--text-muted);
opacity: 0.5;
}
.coding-empty .empty-icon svg {
width: 64px;
height: 64px;
}
.coding-empty h3 {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--text-secondary);
}
.coding-empty p {
font-size: 0.875rem;
max-width: 400px;
margin: 0 auto;
}
/* Command Modal */
.command-box {
background: var(--bg-tertiary);
padding: 1rem;
border-radius: 8px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
margin-top: 0.75rem;
}
.command-box code {
font-family: var(--font-mono, monospace);
font-size: 0.875rem;
word-break: break-all;
flex: 1;
color: var(--text-primary);
}
/* Color Presets */
.color-presets {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
align-items: center;
}
.color-preset {
width: 28px;
height: 28px;
border-radius: 50%;
border: 2px solid transparent;
cursor: pointer;
transition: transform 0.2s, border-color 0.2s;
}
.color-preset:hover {
transform: scale(1.1);
}
.color-preset.selected {
border-color: var(--text-primary);
box-shadow: 0 0 0 2px var(--bg-secondary);
}
.color-picker-custom {
width: 28px;
height: 28px;
border-radius: 50%;
border: none;
cursor: pointer;
padding: 0;
overflow: hidden;
}
.color-picker-custom::-webkit-color-swatch-wrapper {
padding: 0;
}
.color-picker-custom::-webkit-color-swatch {
border: none;
border-radius: 50%;
}
/* Gitea Section in Modal */
.coding-gitea-section {
margin-top: 1rem;
padding: 1rem;
background: var(--bg-tertiary);
border-radius: 8px;
}
.coding-gitea-section summary {
cursor: pointer;
font-weight: 500;
color: var(--text-secondary);
user-select: none;
}
.coding-gitea-section summary:hover {
color: var(--text-primary);
}
/* Responsive */
@media (max-width: 768px) {
.view-coding {
padding: 1rem;
}
.coding-grid {
grid-template-columns: 1fr;
}
.coding-header {
flex-direction: column;
gap: 1rem;
align-items: stretch;
}
.coding-header h2 {
font-size: 1.25rem;
}
.coding-header .btn {
width: 100%;
justify-content: center;
}
.coding-tile-actions {
flex-direction: column;
}
.coding-tile-git {
flex-wrap: wrap;
}
.coding-tile-git .btn {
flex: 1 1 calc(50% - 0.25rem);
}
.command-box {
flex-direction: column;
align-items: stretch;
}
.command-box code {
text-align: center;
}
}
@media (max-width: 480px) {
.coding-empty {
padding: 2rem 1rem;
}
.coding-tile-git .btn {
flex: 1 1 100%;
}
}
/* CLAUDE.md Textarea im Modal */
#coding-claude-instructions {
font-family: var(--font-mono, 'Consolas', 'Monaco', monospace);
font-size: 0.85rem;
line-height: 1.5;
resize: vertical;
min-height: 200px;
}
/* Hint unter Labels */
.label-hint {
font-weight: normal;
font-size: 0.75rem;
color: var(--text-muted);
margin-left: 0.5rem;
}
.form-hint {
display: block;
margin-top: 0.25rem;
font-size: 0.75rem;
color: var(--text-muted);
}
/* CLAUDE.md Tabs */
.claude-tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.claude-tab {
padding: 0.5rem 1rem;
border: 1px solid var(--border-color);
background: var(--bg-tertiary);
color: var(--text-secondary);
border-radius: 6px 6px 0 0;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
transition: background 0.2s, color 0.2s, border-color 0.2s;
}
.claude-tab:hover {
background: var(--bg-secondary);
color: var(--text-primary);
}
.claude-tab.active {
background: var(--bg-secondary);
color: var(--text-primary);
border-bottom-color: var(--bg-secondary);
}
.claude-content {
display: block;
}
.claude-content.active {
display: block;
}
/* CLAUDE.md Link */
.claude-link-container {
margin-top: 0.5rem;
}
.claude-link {
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 0.75rem 1rem;
width: 100%;
text-align: left;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
}
.claude-link:hover:not(:disabled) {
background: var(--bg-secondary);
border-color: var(--primary-color);
}
.claude-link:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.claude-icon {
font-size: 1.1rem;
}
.claude-text {
color: var(--text-primary);
}
.claude-link:disabled .claude-text {
color: var(--text-muted);
font-style: italic;
}
/* CLAUDE.md Modal */
.claude-md-viewer {
width: 100%;
height: 70vh;
border: 1px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
}
.claude-md-display {
width: 100%;
height: 100%;
background: var(--bg-tertiary);
padding: 1.5rem;
font-family: var(--font-mono, 'Consolas', 'Monaco', monospace);
font-size: 0.85rem;
line-height: 1.6;
white-space: pre-wrap;
word-wrap: break-word;
overflow-y: auto;
color: var(--text-primary);
margin: 0;
border: none;
outline: none;
resize: none;
}

Datei anzeigen

@ -413,6 +413,14 @@
gap: var(--spacing-2);
}
/* Avatar Container für mehrere Avatare */
.list-cell-assignee .avatar-container {
display: flex;
align-items: center;
gap: 2px;
flex-shrink: 0;
}
.list-cell-assignee .avatar {
width: 24px;
height: 24px;
@ -424,6 +432,12 @@
font-weight: var(--font-semibold);
color: white;
flex-shrink: 0;
cursor: pointer;
transition: transform 0.2s;
}
.list-cell-assignee .avatar:hover {
transform: scale(1.1);
}
.list-cell-assignee select {
@ -440,6 +454,28 @@
outline: none;
}
/* Hide assignee dropdown - show only avatars */
.list-cell-assignee .assignee-select {
display: none;
}
/* Empty avatar placeholder */
.list-cell-assignee .avatar-empty {
background: var(--border-color) !important;
color: var(--text-muted);
border: 1px solid var(--border-light);
}
/* Show dropdown when editing */
.list-cell-assignee.editing .assignee-select {
display: block;
flex: 1;
}
.list-cell-assignee.editing .avatar-container {
display: none;
}
/* Empty State */
.list-empty {
display: flex;

472
frontend/css/mobile.css Normale Datei
Datei anzeigen

@ -0,0 +1,472 @@
/**
* TASKMATE - Mobile Styles
* ========================
* Touch-optimierte Mobile-Erfahrung
* Nur auf mobilen Breakpoints angewendet
*/
/* ========================================
DESKTOP: Mobile-Elemente verstecken
======================================== */
@media (min-width: 769px) {
.hamburger-btn,
.mobile-menu,
.mobile-menu-overlay,
.swipe-indicator {
display: none !important;
}
}
/* ========================================
MOBILE STYLES (max-width: 768px)
======================================== */
@media (max-width: 768px) {
/* ========================================
HAMBURGER BUTTON
======================================== */
.hamburger-btn {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 44px;
height: 44px;
padding: 10px;
background: transparent;
border: none;
cursor: pointer;
z-index: calc(var(--z-modal) + 10);
position: relative;
flex-shrink: 0;
}
.hamburger-line {
display: block;
width: 24px;
height: 2px;
background: var(--text-primary);
border-radius: 2px;
transition: all 0.3s ease;
}
.hamburger-line + .hamburger-line {
margin-top: 6px;
}
/* Hamburger zu X Animation */
.hamburger-btn.active .hamburger-line:nth-child(1) {
transform: translateY(8px) rotate(45deg);
}
.hamburger-btn.active .hamburger-line:nth-child(2) {
opacity: 0;
transform: scaleX(0);
}
.hamburger-btn.active .hamburger-line:nth-child(3) {
transform: translateY(-8px) rotate(-45deg);
}
/* ========================================
MOBILE SLIDE-IN MENU
======================================== */
.mobile-menu {
position: fixed;
top: 0;
left: 0;
width: 280px;
max-width: 85vw;
height: 100vh;
height: 100dvh;
background: var(--bg-card);
box-shadow: var(--shadow-xl);
z-index: var(--z-modal);
transform: translateX(-100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow-y: auto;
display: flex;
flex-direction: column;
}
.mobile-menu.open {
transform: translateX(0);
}
/* Overlay */
.mobile-menu-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--overlay-bg);
opacity: 0;
visibility: hidden;
z-index: calc(var(--z-modal) - 1);
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.mobile-menu-overlay.visible {
opacity: 1;
visibility: visible;
}
/* Menu Header */
.mobile-menu-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-4);
border-bottom: 1px solid var(--border-light);
flex-shrink: 0;
}
.mobile-menu-title {
font-size: var(--text-lg);
font-weight: var(--font-bold);
color: var(--primary);
margin: 0;
}
.mobile-menu-close {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
color: var(--text-secondary);
cursor: pointer;
border-radius: var(--radius-md);
transition: background 0.2s;
font-size: 1.5rem;
}
.mobile-menu-close:hover,
.mobile-menu-close:active {
background: var(--bg-hover);
}
/* Menu Sections */
.mobile-menu-section {
padding: var(--spacing-4);
border-bottom: 1px solid var(--border-light);
}
.mobile-menu-label {
display: block;
font-size: var(--text-xs);
font-weight: var(--font-semibold);
color: var(--text-tertiary);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: var(--spacing-2);
}
/* Project Select */
.mobile-project-select {
width: 100%;
padding: var(--spacing-3);
font-size: var(--text-base);
border: 1px solid var(--border-default);
border-radius: var(--radius-md);
background: var(--bg-input);
color: var(--text-primary);
font-family: var(--font-primary);
}
/* Navigation */
.mobile-menu-nav {
display: flex;
flex-direction: column;
gap: var(--spacing-1);
}
.mobile-nav-item {
display: flex;
align-items: center;
gap: var(--spacing-3);
padding: var(--spacing-3) var(--spacing-4);
font-size: var(--text-base);
font-weight: var(--font-medium);
color: var(--text-secondary);
background: transparent;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s;
text-align: left;
width: 100%;
}
.mobile-nav-item:hover,
.mobile-nav-item:active {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.mobile-nav-item.active {
background: var(--primary-light);
color: var(--primary);
}
.mobile-nav-item svg {
flex-shrink: 0;
width: 20px;
height: 20px;
}
/* User Section */
.mobile-menu-user {
margin-top: auto;
padding: var(--spacing-4);
border-top: 1px solid var(--border-light);
}
.mobile-user-info {
display: flex;
align-items: center;
gap: var(--spacing-3);
margin-bottom: var(--spacing-4);
}
.mobile-user-avatar {
width: 40px;
height: 40px;
border-radius: var(--radius-full);
background: var(--primary);
color: var(--text-inverse);
display: flex;
align-items: center;
justify-content: center;
font-weight: var(--font-semibold);
flex-shrink: 0;
}
.mobile-user-details {
display: flex;
flex-direction: column;
min-width: 0;
}
.mobile-user-name {
font-weight: var(--font-semibold);
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mobile-user-role {
font-size: var(--text-sm);
color: var(--text-tertiary);
}
.mobile-menu-btn {
width: 100%;
padding: var(--spacing-3);
font-size: var(--text-sm);
font-weight: var(--font-medium);
background: var(--bg-tertiary);
border: none;
border-radius: var(--radius-md);
color: var(--text-secondary);
cursor: pointer;
margin-bottom: var(--spacing-2);
transition: background 0.2s;
}
.mobile-menu-btn:hover,
.mobile-menu-btn:active {
background: var(--bg-hover);
}
.mobile-menu-btn-danger {
color: var(--error);
}
.mobile-menu-btn-danger:hover,
.mobile-menu-btn-danger:active {
background: var(--error-bg);
}
/* ========================================
HEADER ANPASSUNGEN
======================================== */
/* Desktop-Navigation verstecken */
.header-center .view-tabs {
display: none !important;
}
.header-left .project-selector {
display: none !important;
}
/* Header Layout anpassen */
.header-left {
gap: var(--spacing-2);
}
/* ========================================
TOUCH DRAG & DROP FEEDBACK
======================================== */
.task-card.touch-dragging {
transform: scale(1.03);
box-shadow: var(--shadow-xl);
opacity: 0.95;
z-index: 1000;
transition: none;
pointer-events: none;
}
.task-card.touch-drag-placeholder {
opacity: 0.3;
border: 2px dashed var(--border-default);
}
.column-body.touch-drag-over {
background: var(--primary-light);
border: 2px dashed var(--primary);
border-radius: var(--radius-md);
}
/* ========================================
SWIPE INDIKATOREN
======================================== */
.swipe-indicator {
position: fixed;
top: 50%;
transform: translateY(-50%);
width: 40px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
background: var(--overlay-bg);
color: var(--text-inverse);
z-index: var(--z-tooltip);
opacity: 0;
transition: opacity 0.15s ease;
pointer-events: none;
}
.swipe-indicator.left {
left: 0;
border-radius: 0 var(--radius-lg) var(--radius-lg) 0;
}
.swipe-indicator.right {
right: 0;
border-radius: var(--radius-lg) 0 0 var(--radius-lg);
}
.swipe-indicator.visible {
opacity: 0.7;
}
.swipe-indicator svg {
width: 24px;
height: 24px;
}
/* ========================================
BOARD VIEW - HORIZONTAL SCROLLING
======================================== */
.view-board .board {
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
}
.view-board .column {
scroll-snap-align: start;
flex-shrink: 0;
}
/* ========================================
PREVENT TEXT SELECTION DURING GESTURES
======================================== */
.is-swiping,
.is-swiping *,
.is-touch-dragging,
.is-touch-dragging * {
user-select: none !important;
-webkit-user-select: none !important;
-webkit-touch-callout: none !important;
}
/* ========================================
BODY SCROLL LOCK
======================================== */
body.mobile-menu-open {
overflow: hidden;
position: fixed;
width: 100%;
height: 100%;
}
/* ========================================
TOUCH-FREUNDLICHE ELEMENTE
======================================== */
/* Groessere Touch-Targets */
.calendar-day {
min-height: 70px;
touch-action: manipulation;
}
/* Task-Karten */
.task-card {
touch-action: pan-y;
}
/* Buttons */
.btn {
min-height: 44px;
min-width: 44px;
}
}
/* ========================================
EXTRA SMALL MOBILE (max 480px)
======================================== */
@media (max-width: 480px) {
.mobile-menu {
width: 100%;
max-width: 100%;
}
.hamburger-btn {
width: 40px;
height: 40px;
padding: 8px;
}
.hamburger-line {
width: 20px;
}
.hamburger-line + .hamburger-line {
margin-top: 5px;
}
.hamburger-btn.active .hamburger-line:nth-child(1) {
transform: translateY(7px) rotate(45deg);
}
.hamburger-btn.active .hamburger-line:nth-child(3) {
transform: translateY(-7px) rotate(-45deg);
}
}

Datei anzeigen

@ -25,8 +25,10 @@
<link rel="stylesheet" href="css/proposals.css">
<link rel="stylesheet" href="css/notifications.css">
<link rel="stylesheet" href="css/gitea.css">
<link rel="stylesheet" href="css/coding.css">
<link rel="stylesheet" href="css/knowledge.css">
<link rel="stylesheet" href="css/responsive.css">
<link rel="stylesheet" href="css/mobile.css">
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="assets/icons/task.svg">
@ -102,98 +104,29 @@
</div>
</div>
<!-- Dateitypen nach Kategorien -->
<div class="admin-upload-types">
<h3>Erlaubte Dateiformate</h3>
<!-- Erlaubte Dateiendungen -->
<div class="admin-upload-extensions">
<h3>Erlaubte Dateiendungen</h3>
<!-- Bildformate -->
<div class="upload-category" data-category="images">
<div class="upload-category-header">
<label class="upload-category-toggle">
<input type="checkbox" id="upload-cat-images" checked>
<span class="upload-category-title">Bildformate</span>
</label>
</div>
<div class="upload-category-types">
<span class="upload-type-tag" data-type="image/jpeg">JPEG</span>
<span class="upload-type-tag" data-type="image/png">PNG</span>
<span class="upload-type-tag" data-type="image/gif">GIF</span>
<span class="upload-type-tag" data-type="image/webp">WebP</span>
<span class="upload-type-tag" data-type="image/svg+xml">SVG</span>
<!-- Aktive Endungen als Tags -->
<div id="extension-tags" class="extension-tags">
<!-- Tags werden dynamisch gerendert -->
</div>
<!-- Neue Endung hinzufügen -->
<div class="extension-add-group">
<label for="extension-input">Neue Endung hinzufügen</label>
<div class="extension-input-row">
<input type="text" id="extension-input" class="extension-input" placeholder="z.B. xlsx" maxlength="10">
<button type="button" id="btn-add-extension" class="btn btn-secondary">+ Hinzufügen</button>
</div>
</div>
<!-- Dokumentformate -->
<div class="upload-category" data-category="documents">
<div class="upload-category-header">
<label class="upload-category-toggle">
<input type="checkbox" id="upload-cat-documents" checked>
<span class="upload-category-title">Dokumentformate</span>
</label>
</div>
<div class="upload-category-types">
<span class="upload-type-tag" data-type="application/pdf">PDF</span>
</div>
</div>
<!-- Office-Formate -->
<div class="upload-category" data-category="office">
<div class="upload-category-header">
<label class="upload-category-toggle">
<input type="checkbox" id="upload-cat-office" checked>
<span class="upload-category-title">Office-Formate</span>
</label>
</div>
<div class="upload-category-types">
<span class="upload-type-tag" data-type="application/msword">DOC</span>
<span class="upload-type-tag" data-type="application/vnd.openxmlformats-officedocument.wordprocessingml.document">DOCX</span>
<span class="upload-type-tag" data-type="application/vnd.ms-excel">XLS</span>
<span class="upload-type-tag" data-type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet">XLSX</span>
<span class="upload-type-tag" data-type="application/vnd.ms-powerpoint">PPT</span>
<span class="upload-type-tag" data-type="application/vnd.openxmlformats-officedocument.presentationml.presentation">PPTX</span>
</div>
</div>
<!-- Textformate -->
<div class="upload-category" data-category="text">
<div class="upload-category-header">
<label class="upload-category-toggle">
<input type="checkbox" id="upload-cat-text" checked>
<span class="upload-category-title">Textformate</span>
</label>
</div>
<div class="upload-category-types">
<span class="upload-type-tag" data-type="text/plain">TXT</span>
<span class="upload-type-tag" data-type="text/csv">CSV</span>
<span class="upload-type-tag" data-type="text/markdown">Markdown</span>
</div>
</div>
<!-- Archivformate -->
<div class="upload-category" data-category="archives">
<div class="upload-category-header">
<label class="upload-category-toggle">
<input type="checkbox" id="upload-cat-archives" checked>
<span class="upload-category-title">Archivformate</span>
</label>
</div>
<div class="upload-category-types">
<span class="upload-type-tag" data-type="application/zip">ZIP</span>
<span class="upload-type-tag" data-type="application/x-rar-compressed">RAR</span>
<span class="upload-type-tag" data-type="application/x-7z-compressed">7Z</span>
</div>
</div>
<!-- Datenformate -->
<div class="upload-category" data-category="data">
<div class="upload-category-header">
<label class="upload-category-toggle">
<input type="checkbox" id="upload-cat-data" checked>
<span class="upload-category-title">Datenformate</span>
</label>
</div>
<div class="upload-category-types">
<span class="upload-type-tag" data-type="application/json">JSON</span>
<!-- Vorschläge -->
<div class="extension-suggestions">
<span class="extension-suggestions-label">Vorschläge:</span>
<div id="extension-suggestions-list" class="extension-suggestions-list">
<!-- Vorschläge werden dynamisch gerendert -->
</div>
</div>
</div>
@ -212,6 +145,13 @@
<!-- Header -->
<header class="header">
<div class="header-left">
<!-- Hamburger Menu Button (Mobile) -->
<button id="hamburger-btn" class="hamburger-btn" aria-label="Menu" aria-expanded="false">
<span class="hamburger-line"></span>
<span class="hamburger-line"></span>
<span class="hamburger-line"></span>
</button>
<h1 class="logo">TaskMate</h1>
<!-- Project Selector -->
@ -235,7 +175,7 @@
<button class="view-tab" data-view="list">Liste</button>
<button class="view-tab" data-view="calendar">Kalender</button>
<button class="view-tab" data-view="proposals">Genehmigung</button>
<button class="view-tab" data-view="gitea">Gitea</button>
<button class="view-tab" data-view="coding">Coding</button>
<button class="view-tab" data-view="knowledge">Wissen</button>
</nav>
</div>
@ -509,383 +449,29 @@
</div>
</div>
<!-- Gitea View -->
<div id="view-gitea" class="view view-gitea hidden">
<!-- Modus-Schalter -->
<div id="gitea-mode-switch" class="gitea-mode-switch">
<button class="gitea-mode-btn active" data-mode="server">
<svg viewBox="0 0 24 24" width="18" height="18"><rect x="2" y="2" width="20" height="8" rx="2" stroke="currentColor" stroke-width="2" fill="none"/><rect x="2" y="14" width="20" height="8" rx="2" stroke="currentColor" stroke-width="2" fill="none"/><circle cx="6" cy="6" r="1" fill="currentColor"/><circle cx="6" cy="18" r="1" fill="currentColor"/></svg>
Server-Anwendung
</button>
<button class="gitea-mode-btn" data-mode="project">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" stroke="currentColor" stroke-width="2" fill="none"/></svg>
Projekt-Repository
<!-- Coding View -->
<div id="view-coding" class="view view-coding hidden">
<!-- Header -->
<div class="coding-header coding-header-centered">
<button id="add-coding-directory-btn" class="btn btn-primary">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
Anwendung hinzufügen
</button>
</div>
<!-- Server-Modus Ansicht -->
<div id="gitea-server-mode" class="gitea-section">
<!-- Server Repository Info Header -->
<div class="gitea-repo-header">
<div class="repo-info">
<h2 id="server-repo-name">
<svg viewBox="0 0 24 24" width="24" height="24"><rect x="2" y="2" width="20" height="8" rx="2" stroke="currentColor" stroke-width="2" fill="none"/><rect x="2" y="14" width="20" height="8" rx="2" stroke="currentColor" stroke-width="2" fill="none"/><circle cx="6" cy="6" r="1" fill="currentColor"/><circle cx="6" cy="18" r="1" fill="currentColor"/></svg>
<span>TaskMate Server</span>
</h2>
<a id="server-repo-url" class="repo-url" href="#" target="_blank"></a>
</div>
</div>
<!-- Server-Pfad Anzeige -->
<div class="gitea-local-path">
<span class="path-label">Server-Pfad:</span>
<code id="server-local-path-display">/home/claude-dev/TaskMate</code>
</div>
<!-- Server Git-Status Panel -->
<div id="server-status-section" class="gitea-status-panel">
<div class="status-grid">
<div class="status-item">
<span class="status-label">Branch</span>
<div class="branch-select-group">
<select id="server-branch-select" class="branch-select">
<!-- Branches dynamisch -->
</select>
</div>
</div>
<div class="status-item">
<span class="status-label">Status</span>
<span id="server-status-indicator" class="status-badge">Prüfe...</span>
</div>
<div class="status-item">
<span class="status-label">Änderungen</span>
<span id="server-changes-count" class="changes-count">0</span>
</div>
</div>
</div>
<!-- Server Git-Operationen -->
<div id="server-operations-section" class="gitea-operations-panel">
<h3>Git-Operationen</h3>
<div class="operations-grid">
<button id="btn-server-fetch" class="btn btn-secondary operation-btn">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" stroke="currentColor" stroke-width="2" fill="none"/><path d="M3 3v5h5" stroke="currentColor" stroke-width="2" fill="none"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" stroke="currentColor" stroke-width="2" fill="none"/><path d="M21 21v-5h-5" stroke="currentColor" stroke-width="2" fill="none"/></svg>
Fetch
</button>
<button id="btn-server-pull" class="btn btn-secondary operation-btn">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M12 5v14M19 12l-7 7-7-7" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
Pull
</button>
<button id="btn-server-push" class="btn btn-primary operation-btn">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M12 19V5M5 12l7-7 7 7" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
Push
</button>
<button id="btn-server-commit" class="btn btn-secondary operation-btn">
<svg viewBox="0 0 24 24" width="18" height="18"><circle cx="12" cy="12" r="4" stroke="currentColor" stroke-width="2" fill="none"/><path d="M12 2v6M12 16v6M2 12h6M16 12h6" stroke="currentColor" stroke-width="2" fill="none"/></svg>
Commit
</button>
</div>
</div>
<!-- Server Änderungen-Liste -->
<div id="server-changes-section" class="gitea-changes-panel hidden">
<h3>Geänderte Dateien</h3>
<div id="server-changes-list" class="changes-list">
<!-- Dynamisch gefüllt -->
</div>
</div>
<!-- Server Commit-Historie -->
<div id="server-commits-section" class="gitea-commits-panel">
<div class="commits-header">
<h3>Letzte Commits</h3>
<button id="btn-server-clear-commits" class="btn btn-small btn-secondary" title="Alle aus Anzeige entfernen">
Alle ausblenden
</button>
</div>
<div id="server-commits-list" class="commits-list">
<!-- Dynamisch gefüllt -->
</div>
</div>
<!-- Grid für Kacheln -->
<div id="coding-grid" class="coding-grid">
<!-- Kacheln werden per JS gerendert -->
</div>
<!-- Projekt-Modus: Browser-Upload Ansicht -->
<div id="gitea-browser-upload" class="gitea-section hidden">
<!-- Header -->
<div class="gitea-config-header">
<h2>
<svg viewBox="0 0 24 24" width="24" height="24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
Lokales Verzeichnis hochladen
</h2>
<p>Wählen Sie ein Verzeichnis von Ihrem Computer und pushen Sie es direkt ins Gitea.</p>
<!-- Empty State -->
<div id="coding-empty" class="coding-empty hidden">
<div class="empty-icon">
<svg viewBox="0 0 24 24" width="64" height="64"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" stroke="currentColor" stroke-width="2" fill="none"/></svg>
</div>
<!-- Browser-Kompatibilität Hinweis -->
<div id="browser-upload-compat" class="browser-compat-notice hidden">
<svg viewBox="0 0 24 24" width="20" height="20"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none"/><path d="M12 8v4M12 16h.01" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
<span>Die Verzeichnis-Auswahl funktioniert nur in Chrome, Edge oder Opera. In anderen Browsern können Sie Dateien per Drag & Drop hochladen.</span>
</div>
<!-- Schritt 1: Repository auswählen -->
<div class="upload-step">
<div class="step-header">
<span class="step-number">1</span>
<span class="step-title">Ziel-Repository auswählen</span>
</div>
<div class="form-group">
<div class="gitea-repo-select-group">
<select id="browser-upload-repo-select" class="form-control">
<option value="">-- Repository wählen --</option>
</select>
<button type="button" id="btn-refresh-upload-repos" class="btn btn-icon" title="Repositories aktualisieren">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" stroke="currentColor" stroke-width="2" fill="none"/><path d="M3 3v5h5" stroke="currentColor" stroke-width="2" fill="none"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" stroke="currentColor" stroke-width="2" fill="none"/><path d="M21 21v-5h-5" stroke="currentColor" stroke-width="2" fill="none"/></svg>
</button>
</div>
</div>
<div class="form-group">
<label for="browser-upload-branch">Ziel-Branch</label>
<input type="text" id="browser-upload-branch" class="form-control" value="main" placeholder="main">
</div>
</div>
<!-- Schritt 2: Verzeichnis auswählen -->
<div class="upload-step">
<div class="step-header">
<span class="step-number">2</span>
<span class="step-title">Verzeichnis auswählen</span>
</div>
<div class="directory-picker">
<button type="button" id="btn-select-directory" class="btn btn-secondary btn-lg">
<svg viewBox="0 0 24 24" width="20" height="20"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" stroke="currentColor" stroke-width="2" fill="none"/></svg>
Verzeichnis auswählen
</button>
<div id="drop-zone" class="drop-zone hidden">
<svg viewBox="0 0 24 24" width="48" height="48"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
<p>Oder Verzeichnis hierher ziehen</p>
</div>
</div>
</div>
<!-- Schritt 3: Datei-Vorschau (erscheint nach Auswahl) -->
<div id="upload-preview-section" class="upload-step hidden">
<div class="step-header">
<span class="step-number">3</span>
<span class="step-title">Ausgewählte Dateien</span>
<span id="upload-file-count" class="file-count">0 Dateien</span>
</div>
<div id="upload-files-list" class="upload-files-list">
<!-- Dynamisch gefüllt -->
</div>
<div class="excluded-info">
<small>Automatisch ausgeschlossen: .git, node_modules, __pycache__, .env, *.log</small>
</div>
</div>
<!-- Schritt 4: Commit und Push -->
<div id="upload-commit-section" class="upload-step hidden">
<div class="step-header">
<span class="step-number">4</span>
<span class="step-title">Commit erstellen und pushen</span>
</div>
<div class="form-group">
<label for="browser-upload-commit-message">Commit-Nachricht</label>
<textarea id="browser-upload-commit-message" class="form-control" rows="2" placeholder="Beschreiben Sie Ihre Änderungen..."></textarea>
</div>
<div class="upload-actions">
<button type="button" id="btn-cancel-upload" class="btn btn-secondary">
Abbrechen
</button>
<button type="button" id="btn-execute-upload" class="btn btn-primary">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M12 19V5M5 12l7-7 7 7" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
Commit & Push
</button>
</div>
<!-- Progress Bar -->
<div id="upload-progress-container" class="upload-progress hidden">
<div class="progress-bar">
<div id="upload-progress-bar" class="progress-fill"></div>
</div>
<span id="upload-progress-text">0%</span>
</div>
</div>
<h3>Keine Anwendungen</h3>
<p>Füge deine erste Server-Anwendung hinzu, um mit Claude oder Codex zu arbeiten.</p>
</div>
<!-- Projekt-Modus: Kein Projekt (altes Element, jetzt versteckt) -->
<div id="gitea-no-project" class="gitea-empty-state hidden">
<svg viewBox="0 0 24 24" width="64" height="64"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
<h3>Kein Projekt ausgewählt</h3>
<p>Wählen Sie ein Projekt aus, um die Git-Integration zu konfigurieren.</p>
</div>
<!-- Konfiguration (wenn nicht verknüpft) -->
<div id="gitea-config-section" class="gitea-section hidden">
<div class="gitea-config-header">
<h2>Repository-Konfiguration</h2>
<p>Verknüpfen Sie dieses Projekt mit einem Git-Repository</p>
</div>
<!-- Gitea-Verbindungsstatus -->
<div id="gitea-connection-status" class="gitea-connection-status">
<span class="status-indicator"></span>
<span class="status-text">Prüfe Verbindung...</span>
</div>
<form id="gitea-config-form" class="gitea-config-form">
<!-- Repository-Auswahl -->
<div class="form-group">
<label for="gitea-repo-select">Bestehendes Repository auswählen</label>
<div class="gitea-repo-select-group">
<select id="gitea-repo-select" class="form-control">
<option value="">-- Repository wählen --</option>
</select>
<button type="button" id="btn-refresh-repos" class="btn btn-icon" title="Repositories aktualisieren">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" stroke="currentColor" stroke-width="2" fill="none"/><path d="M3 3v5h5" stroke="currentColor" stroke-width="2" fill="none"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" stroke="currentColor" stroke-width="2" fill="none"/><path d="M21 21v-5h-5" stroke="currentColor" stroke-width="2" fill="none"/></svg>
</button>
</div>
</div>
<div class="gitea-divider">
<span>oder</span>
</div>
<!-- Neues Repository erstellen -->
<div class="form-group">
<button type="button" id="btn-create-repo" class="btn btn-secondary btn-block">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
Neues Repository erstellen
</button>
</div>
<div class="gitea-divider"></div>
<!-- Lokaler Pfad -->
<div class="form-group">
<label for="local-path-input">Lokaler Pfad (wo Claude Code arbeitet)</label>
<input type="text" id="local-path-input" class="form-control"
placeholder="z.B. D:\Projekte\MeinProjekt">
<span id="path-validation-result" class="form-hint"></span>
</div>
<!-- Default Branch -->
<div class="form-group">
<label for="default-branch-input">Standard-Branch</label>
<input type="text" id="default-branch-input" class="form-control"
value="main" placeholder="main">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" id="btn-save-config">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" stroke="currentColor" stroke-width="2" fill="none"/><path d="M17 21v-8H7v8M7 3v5h8" stroke="currentColor" stroke-width="2" fill="none"/></svg>
Konfiguration speichern
</button>
</div>
</form>
</div>
<!-- Hauptansicht (wenn verknüpft) -->
<div id="gitea-main-section" class="gitea-section hidden">
<!-- Repository-Info-Header -->
<div class="gitea-repo-header">
<div class="repo-info">
<h2 id="gitea-repo-name">
<svg viewBox="0 0 24 24" width="24" height="24"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22" stroke="currentColor" stroke-width="2" fill="none"/></svg>
<span>Repository</span>
</h2>
<a id="gitea-repo-url" class="repo-url" href="#" target="_blank"></a>
</div>
<div class="repo-actions">
<button id="btn-edit-config" class="btn btn-icon" title="Konfiguration bearbeiten">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" fill="none"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" fill="none"/></svg>
</button>
<button id="btn-remove-config" class="btn btn-icon btn-danger-hover" title="Konfiguration entfernen">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" stroke="currentColor" stroke-width="2" fill="none"/></svg>
</button>
</div>
</div>
<!-- Lokaler Pfad Anzeige -->
<div class="gitea-local-path">
<span class="path-label">Lokaler Pfad:</span>
<code id="gitea-local-path-display"></code>
</div>
<!-- Git-Status Panel -->
<div id="gitea-status-section" class="gitea-status-panel">
<div class="status-grid">
<div class="status-item">
<span class="status-label">Branch</span>
<div class="branch-select-group">
<select id="branch-select" class="branch-select">
<!-- Branches dynamisch -->
</select>
<button id="btn-rename-branch" class="btn btn-small btn-icon" title="Branch umbenennen">
<svg viewBox="0 0 24 24" width="16" height="16"><path d="M17 3a2.85 2.85 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z" stroke="currentColor" stroke-width="2" fill="none"/></svg>
</button>
</div>
</div>
<div class="status-item">
<span class="status-label">Status</span>
<span id="git-status-indicator" class="status-badge">Prüfe...</span>
</div>
<div class="status-item">
<span class="status-label">Änderungen</span>
<span id="git-changes-count" class="changes-count">0</span>
</div>
</div>
</div>
<!-- Git-Operationen -->
<div id="gitea-operations-section" class="gitea-operations-panel">
<h3>Git-Operationen</h3>
<div class="operations-grid">
<button id="btn-git-fetch" class="btn btn-secondary operation-btn">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" stroke="currentColor" stroke-width="2" fill="none"/><path d="M3 3v5h5" stroke="currentColor" stroke-width="2" fill="none"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" stroke="currentColor" stroke-width="2" fill="none"/><path d="M21 21v-5h-5" stroke="currentColor" stroke-width="2" fill="none"/></svg>
Fetch
</button>
<button id="btn-git-pull" class="btn btn-secondary operation-btn">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M12 5v14M19 12l-7 7-7-7" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
Pull
</button>
<button id="btn-git-push" class="btn btn-primary operation-btn">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M12 19V5M5 12l7-7 7 7" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
Push
</button>
<button id="btn-git-commit" class="btn btn-secondary operation-btn">
<svg viewBox="0 0 24 24" width="18" height="18"><circle cx="12" cy="12" r="4" stroke="currentColor" stroke-width="2" fill="none"/><path d="M12 2v6M12 16v6M2 12h6M16 12h6" stroke="currentColor" stroke-width="2" fill="none"/></svg>
Commit
</button>
</div>
</div>
<!-- Änderungen-Liste -->
<div id="gitea-changes-section" class="gitea-changes-panel hidden">
<h3>Geänderte Dateien</h3>
<div id="git-changes-list" class="changes-list">
<!-- Dynamisch gefüllt -->
</div>
</div>
<!-- Commit-Historie -->
<div id="gitea-commits-section" class="gitea-commits-panel">
<div class="commits-header">
<h3>Letzte Commits</h3>
<button id="btn-clear-commits" class="btn btn-small btn-secondary" title="Alle aus Anzeige entfernen">
Alle ausblenden
</button>
</div>
<div id="git-commits-list" class="commits-list">
<!-- Dynamisch gefüllt -->
</div>
</div>
</div>
</div>
<!-- Knowledge View (Wissensmanagement) -->
@ -1389,7 +975,21 @@
</div>
<div class="form-group">
<label for="user-password">Passwort <span id="password-hint" class="form-hint">(automatisch generiert)</span></label>
<input type="text" id="user-password" minlength="8" readonly>
<div class="password-input-group">
<input type="text" id="user-password" minlength="8" readonly>
<button type="button" id="edit-password-btn" class="btn btn-secondary btn-sm" title="Passwort bearbeiten">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" fill="none"/>
<path d="m18.5 2.5 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" fill="none"/>
</svg>
</button>
<button type="button" id="generate-password-btn" class="btn btn-secondary btn-sm" title="Neues Passwort generieren">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M1 4v6h6" stroke="currentColor" stroke-width="2" fill="none"/>
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" stroke="currentColor" stroke-width="2" fill="none"/>
</svg>
</button>
</div>
</div>
<div class="form-group">
<label for="user-role">Rolle</label>
@ -1908,9 +1508,152 @@
</div>
</div>
<!-- Coding Directory Modal -->
<div id="coding-modal" class="modal hidden">
<div class="modal-content modal-medium">
<div class="modal-header">
<h3 id="coding-modal-title">Anwendung hinzufügen</h3>
<button class="modal-close" aria-label="Schliessen">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="coding-name">Anwendungsname *</label>
<input type="text" id="coding-name" class="form-control" placeholder="z.B. TaskMate" required>
<small class="form-hint" id="coding-path-hint">Ordner: /home/claude-dev/<span id="coding-path-preview">...</span></small>
</div>
<div class="form-group">
<label for="coding-description">Beschreibung</label>
<textarea id="coding-description" class="form-control" rows="2" placeholder="Optionale Beschreibung..."></textarea>
</div>
<div class="form-group">
<label>Farbe</label>
<div class="color-presets" id="coding-color-presets">
<!-- Farb-Buttons werden per JS gerendert -->
</div>
</div>
<div class="form-group">
<label>CLAUDE.md</label>
<div class="claude-link-container">
<button type="button" id="coding-claude-link" class="claude-link" disabled>
<span class="claude-icon">📄</span>
<span class="claude-text">Keine CLAUDE.md vorhanden</span>
</button>
</div>
</div>
<details class="coding-gitea-section">
<summary>Gitea-Repository verknüpfen (optional)</summary>
<div class="form-group" style="margin-top: 1rem;">
<label for="coding-gitea-repo">Repository</label>
<select id="coding-gitea-repo" class="form-control">
<option value="">-- Kein Repository --</option>
</select>
</div>
<div class="form-group">
<label for="coding-branch">Standard-Branch</label>
<input type="text" id="coding-branch" class="form-control" value="main" placeholder="main">
</div>
</details>
</div>
<div class="modal-footer">
<button id="coding-delete-btn" class="btn btn-danger hidden">Löschen</button>
<button class="btn btn-secondary modal-cancel">Abbrechen</button>
<button id="coding-save-btn" class="btn btn-primary">Speichern</button>
</div>
</div>
</div>
<!-- Coding Command Modal -->
<div id="coding-command-modal" class="modal hidden">
<div class="modal-content modal-small">
<div class="modal-header">
<h3>Befehl ausführen</h3>
<button class="modal-close" aria-label="Schliessen">&times;</button>
</div>
<div class="modal-body">
<p id="coding-command-hint">Führe diesen Befehl in WSL aus:</p>
<div class="command-box">
<code id="coding-command-text"></code>
<button id="coding-copy-command" class="btn btn-sm btn-secondary">Kopieren</button>
</div>
</div>
</div>
</div>
<!-- Toast Container -->
<div id="toast-container" class="toast-container"></div>
<!-- Mobile Navigation Menu -->
<nav id="mobile-menu" class="mobile-menu" aria-hidden="true">
<div class="mobile-menu-header">
<h2 class="mobile-menu-title">TaskMate</h2>
<button id="mobile-menu-close" class="mobile-menu-close" aria-label="Menu schliessen">
<svg viewBox="0 0 24 24" width="24" height="24">
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
</div>
<div class="mobile-menu-section">
<label class="mobile-menu-label">Projekt</label>
<select id="mobile-project-select" class="mobile-project-select">
<!-- Populated by JS -->
</select>
</div>
<div class="mobile-menu-section">
<label class="mobile-menu-label">Ansicht</label>
<div class="mobile-menu-nav">
<button class="mobile-nav-item active" data-view="board">
<svg viewBox="0 0 24 24" width="20" height="20"><rect x="3" y="3" width="7" height="9" rx="1" stroke="currentColor" stroke-width="2" fill="none"/><rect x="14" y="3" width="7" height="5" rx="1" stroke="currentColor" stroke-width="2" fill="none"/><rect x="14" y="12" width="7" height="9" rx="1" stroke="currentColor" stroke-width="2" fill="none"/><rect x="3" y="16" width="7" height="5" rx="1" stroke="currentColor" stroke-width="2" fill="none"/></svg>
<span>Board</span>
</button>
<button class="mobile-nav-item" data-view="list">
<svg viewBox="0 0 24 24" width="20" height="20"><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<span>Liste</span>
</button>
<button class="mobile-nav-item" data-view="calendar">
<svg viewBox="0 0 24 24" width="20" height="20"><rect x="3" y="4" width="18" height="18" rx="2" stroke="currentColor" stroke-width="2" fill="none"/><line x1="16" y1="2" x2="16" y2="6" stroke="currentColor" stroke-width="2"/><line x1="8" y1="2" x2="8" y2="6" stroke="currentColor" stroke-width="2"/><line x1="3" y1="10" x2="21" y2="10" stroke="currentColor" stroke-width="2"/></svg>
<span>Kalender</span>
</button>
<button class="mobile-nav-item" data-view="proposals">
<svg viewBox="0 0 24 24" width="20" height="20"><path d="M9 11l3 3L22 4" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" stroke="currentColor" stroke-width="2" fill="none"/></svg>
<span>Genehmigung</span>
</button>
<button class="mobile-nav-item" data-view="coding">
<svg viewBox="0 0 24 24" width="20" height="20"><polyline points="16 18 22 12 16 6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/><polyline points="8 6 2 12 8 18" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span>Coding</span>
</button>
<button class="mobile-nav-item" data-view="knowledge">
<svg viewBox="0 0 24 24" width="20" height="20"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" stroke="currentColor" stroke-width="2" fill="none"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" stroke="currentColor" stroke-width="2" fill="none"/></svg>
<span>Wissen</span>
</button>
</div>
</div>
<div class="mobile-menu-section mobile-menu-user">
<div class="mobile-user-info">
<span id="mobile-user-avatar" class="mobile-user-avatar">U</span>
<div class="mobile-user-details">
<span id="mobile-user-name" class="mobile-user-name">Benutzer</span>
<span id="mobile-user-role" class="mobile-user-role">Angemeldet</span>
</div>
</div>
<button id="mobile-admin-btn" class="mobile-menu-btn hidden">Admin-Bereich</button>
<button id="mobile-logout-btn" class="mobile-menu-btn mobile-menu-btn-danger">Abmelden</button>
</div>
</nav>
<!-- Mobile Menu Overlay -->
<div id="mobile-menu-overlay" class="mobile-menu-overlay"></div>
<!-- Swipe Indicators -->
<div id="swipe-indicator-left" class="swipe-indicator left">
<svg viewBox="0 0 24 24" width="24" height="24"><path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
</div>
<div id="swipe-indicator-right" class="swipe-indicator right">
<svg viewBox="0 0 24 24" width="24" height="24"><path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
</div>
<!-- Onboarding Tour -->
<div id="onboarding-overlay" class="onboarding-overlay hidden">
<div id="onboarding-tooltip" class="onboarding-tooltip">
@ -1934,6 +1677,21 @@
<img id="lightbox-image" src="" alt="">
</div>
<!-- CLAUDE.md Modal -->
<div id="claude-md-modal" class="modal hidden">
<div class="modal-content modal-large">
<div class="modal-header">
<h3>CLAUDE.md (Nur-Lesen)</h3>
<button class="modal-close" aria-label="Schließen">&times;</button>
</div>
<div class="modal-body">
<div class="claude-md-viewer">
<pre id="claude-md-content" class="claude-md-display"></pre>
</div>
</div>
</div>
</div>
<!-- Socket.io Client -->
<script src="/socket.io/socket.io.js"></script>

Datei anzeigen

@ -14,7 +14,17 @@ class AdminManager {
this.users = [];
this.currentEditUser = null;
this.uploadSettings = null;
this.allowedExtensions = ['pdf', 'docx', 'txt'];
this.initialized = false;
// Vorschläge für häufige Dateiendungen
this.extensionSuggestions = [
'xlsx', 'pptx', 'doc', 'xls', 'ppt', // Office
'png', 'jpg', 'gif', 'svg', 'webp', // Bilder
'csv', 'json', 'xml', 'md', // Daten
'zip', 'rar', '7z', // Archive
'odt', 'ods', 'rtf' // OpenDocument
];
}
async init() {
@ -52,7 +62,10 @@ class AdminManager {
// Upload Settings Elements
this.uploadMaxSizeInput = $('#upload-max-size');
this.saveUploadSettingsBtn = $('#btn-save-upload-settings');
this.uploadCategories = $$('.upload-category');
this.extensionTagsContainer = $('#extension-tags');
this.extensionInput = $('#extension-input');
this.addExtensionBtn = $('#btn-add-extension');
this.extensionSuggestionsList = $('#extension-suggestions-list');
this.bindEvents();
this.initialized = true;
@ -88,13 +101,20 @@ class AdminManager {
// Upload Settings - Save Button
this.saveUploadSettingsBtn?.addEventListener('click', () => this.saveUploadSettings());
// Upload Settings - Category Toggles
this.uploadCategories?.forEach(category => {
const checkbox = category.querySelector('input[type="checkbox"]');
checkbox?.addEventListener('change', () => {
this.toggleUploadCategory(category, checkbox.checked);
});
// Upload Settings - Add Extension
this.addExtensionBtn?.addEventListener('click', () => this.addExtensionFromInput());
// Enter-Taste im Input-Feld
this.extensionInput?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.addExtensionFromInput();
}
});
// Password-Buttons
$('#edit-password-btn')?.addEventListener('click', () => this.togglePasswordEdit());
$('#generate-password-btn')?.addEventListener('click', () => this.generatePassword());
}
async loadUsers() {
@ -222,8 +242,12 @@ class AdminManager {
this.emailInput.value = user.email || '';
this.emailInput.disabled = false;
// Passwort-Feld bei Bearbeitung ausblenden
this.passwordInput.closest('.form-group').style.display = 'none';
// Passwort-Feld für Bearbeitung vorbereiten
this.passwordInput.closest('.form-group').style.display = 'block';
this.passwordInput.value = '';
this.passwordInput.placeholder = 'Neues Passwort (leer lassen = unverändert)';
this.passwordInput.readOnly = true;
this.passwordHint.textContent = '(optional - leer lassen für unverändert)';
this.roleSelect.value = user.role || 'user';
@ -381,6 +405,7 @@ class AdminManager {
async loadUploadSettings() {
try {
this.uploadSettings = await api.getUploadSettings();
this.allowedExtensions = this.uploadSettings.allowedExtensions || ['pdf', 'docx', 'txt'];
this.renderUploadSettings();
} catch (error) {
console.error('Error loading upload settings:', error);
@ -395,36 +420,108 @@ class AdminManager {
this.uploadMaxSizeInput.value = this.uploadSettings.maxFileSizeMB || 15;
}
// Kategorien setzen
const categoryMap = {
'images': 'upload-cat-images',
'documents': 'upload-cat-documents',
'office': 'upload-cat-office',
'text': 'upload-cat-text',
'archives': 'upload-cat-archives',
'data': 'upload-cat-data'
};
// Extension-Tags rendern
this.renderExtensionTags();
Object.entries(categoryMap).forEach(([category, checkboxId]) => {
const checkbox = $(`#${checkboxId}`);
const categoryEl = $(`.upload-category[data-category="${category}"]`);
// Vorschläge rendern
this.renderExtensionSuggestions();
}
if (checkbox && this.uploadSettings.allowedTypes?.[category]) {
const isEnabled = this.uploadSettings.allowedTypes[category].enabled;
checkbox.checked = isEnabled;
this.toggleUploadCategory(categoryEl, isEnabled);
}
renderExtensionTags() {
if (!this.extensionTagsContainer) return;
if (this.allowedExtensions.length === 0) {
this.extensionTagsContainer.innerHTML = '<span class="extension-empty">Keine Endungen definiert</span>';
return;
}
this.extensionTagsContainer.innerHTML = this.allowedExtensions.map(ext => `
<span class="extension-tag" data-extension="${ext}">
.${ext}
<button type="button" class="extension-tag-remove" data-remove="${ext}" title="Entfernen">
<svg viewBox="0 0 24 24" width="12" height="12"><path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
</button>
</span>
`).join('');
// Remove-Buttons Event Listener
this.extensionTagsContainer.querySelectorAll('.extension-tag-remove').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const ext = btn.dataset.remove;
this.removeExtension(ext);
});
});
}
toggleUploadCategory(categoryEl, enabled) {
if (!categoryEl) return;
renderExtensionSuggestions() {
if (!this.extensionSuggestionsList) return;
if (enabled) {
categoryEl.classList.remove('disabled');
} else {
categoryEl.classList.add('disabled');
// Nur Vorschläge anzeigen, die noch nicht aktiv sind
const availableSuggestions = this.extensionSuggestions.filter(
ext => !this.allowedExtensions.includes(ext)
);
if (availableSuggestions.length === 0) {
this.extensionSuggestionsList.innerHTML = '<span class="extension-no-suggestions">Alle Vorschläge bereits hinzugefügt</span>';
return;
}
this.extensionSuggestionsList.innerHTML = availableSuggestions.map(ext => `
<button type="button" class="extension-suggestion" data-suggestion="${ext}">+ ${ext}</button>
`).join('');
// Suggestion-Buttons Event Listener
this.extensionSuggestionsList.querySelectorAll('.extension-suggestion').forEach(btn => {
btn.addEventListener('click', () => {
const ext = btn.dataset.suggestion;
this.addExtension(ext);
});
});
}
addExtensionFromInput() {
const input = this.extensionInput?.value?.trim().toLowerCase();
if (!input) return;
// Punkt am Anfang entfernen falls vorhanden
const ext = input.replace(/^\./, '');
if (this.addExtension(ext)) {
this.extensionInput.value = '';
}
}
addExtension(ext) {
// Validierung: nur alphanumerisch, 1-10 Zeichen
if (!/^[a-z0-9]{1,10}$/.test(ext)) {
this.showToast('Ungültige Dateiendung (nur Buchstaben/Zahlen, max. 10 Zeichen)', 'error');
return false;
}
// Prüfen ob bereits vorhanden
if (this.allowedExtensions.includes(ext)) {
this.showToast(`Endung .${ext} bereits vorhanden`, 'error');
return false;
}
// Hinzufügen
this.allowedExtensions.push(ext);
this.renderExtensionTags();
this.renderExtensionSuggestions();
return true;
}
removeExtension(ext) {
// Prüfen ob mindestens eine Endung übrig bleibt
if (this.allowedExtensions.length <= 1) {
this.showToast('Mindestens eine Dateiendung muss erlaubt sein', 'error');
return;
}
this.allowedExtensions = this.allowedExtensions.filter(e => e !== ext);
this.renderExtensionTags();
this.renderExtensionSuggestions();
}
async saveUploadSettings() {
@ -437,51 +534,17 @@ class AdminManager {
return;
}
// Kategorien sammeln
const allowedTypes = {
images: {
enabled: $('#upload-cat-images')?.checked ?? true,
types: ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml']
},
documents: {
enabled: $('#upload-cat-documents')?.checked ?? true,
types: ['application/pdf']
},
office: {
enabled: $('#upload-cat-office')?.checked ?? true,
types: [
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml.presentation'
]
},
text: {
enabled: $('#upload-cat-text')?.checked ?? true,
types: ['text/plain', 'text/csv', 'text/markdown']
},
archives: {
enabled: $('#upload-cat-archives')?.checked ?? true,
types: ['application/zip', 'application/x-rar-compressed', 'application/x-7z-compressed']
},
data: {
enabled: $('#upload-cat-data')?.checked ?? true,
types: ['application/json']
}
};
// Prüfen ob mindestens eine Kategorie aktiviert ist
const hasEnabledCategory = Object.values(allowedTypes).some(cat => cat.enabled);
if (!hasEnabledCategory) {
this.showToast('Mindestens eine Dateikategorie muss aktiviert sein', 'error');
if (this.allowedExtensions.length === 0) {
this.showToast('Mindestens eine Dateiendung muss erlaubt sein', 'error');
return;
}
await api.updateUploadSettings({ maxFileSizeMB, allowedTypes });
await api.updateUploadSettings({
maxFileSizeMB,
allowedExtensions: this.allowedExtensions
});
this.uploadSettings = { maxFileSizeMB, allowedTypes };
this.uploadSettings = { maxFileSizeMB, allowedExtensions: this.allowedExtensions };
this.showToast('Upload-Einstellungen gespeichert', 'success');
} catch (error) {
console.error('Error saving upload settings:', error);
@ -489,6 +552,72 @@ class AdminManager {
}
}
/**
* Passwort-Bearbeitung umschalten
*/
togglePasswordEdit() {
const passwordInput = $('#user-password');
const editBtn = $('#edit-password-btn');
const hint = $('#password-hint');
if (!passwordInput || !editBtn) return;
if (passwordInput.readOnly) {
// Bearbeitung aktivieren
passwordInput.readOnly = false;
passwordInput.focus();
passwordInput.select();
editBtn.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M20 6L9 17l-5-5" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`;
editBtn.title = "Bearbeitung bestätigen";
hint.textContent = "(bearbeiten)";
} else {
// Bearbeitung beenden
passwordInput.readOnly = true;
editBtn.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" fill="none"/>
<path d="m18.5 2.5 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="currentColor" stroke-width="2" fill="none"/>
</svg>
`;
editBtn.title = "Passwort bearbeiten";
hint.textContent = this.currentEditUser ? "(geändert)" : "(automatisch generiert)";
}
}
/**
* Neues Passwort generieren
*/
generatePassword() {
const passwordInput = $('#user-password');
const hint = $('#password-hint');
if (!passwordInput) return;
// Starkes Passwort generieren (12 Zeichen)
const charset = 'ABCDEFGHJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789!@#$%&*';
let password = '';
for (let i = 0; i < 12; i++) {
password += charset.charAt(Math.floor(Math.random() * charset.length));
}
passwordInput.value = password;
passwordInput.readOnly = false;
if (hint) {
hint.textContent = "(neu generiert)";
}
// Passwort kurz markieren
passwordInput.focus();
passwordInput.select();
this.showToast('Neues Passwort generiert', 'success');
}
show() {
this.adminScreen?.classList.add('active');
}

Datei anzeigen

@ -7,9 +7,25 @@ class ApiClient {
constructor() {
this.baseUrl = '/api';
this.token = null;
this.refreshToken = null;
this.csrfToken = null;
this.refreshingToken = false;
this.requestQueue = [];
this.refreshTimer = null;
this.init();
}
init() {
// Token aus Storage laden
this.token = localStorage.getItem('auth_token');
this.refreshToken = localStorage.getItem('refresh_token');
this.csrfToken = sessionStorage.getItem('csrf_token');
console.log('[API] init() - Token loaded:', this.token ? this.token.substring(0, 20) + '...' : 'NULL');
// Starte Timer wenn Token und Refresh-Token vorhanden sind
if (this.token && this.refreshToken) {
this.startTokenRefreshTimer();
}
}
// Token Management
@ -18,10 +34,22 @@ class ApiClient {
this.token = token;
if (token) {
localStorage.setItem('auth_token', token);
// Starte proaktiven Token-Refresh Timer (nach 10 Minuten)
this.startTokenRefreshTimer();
} else {
this.token = null;
localStorage.removeItem('auth_token');
localStorage.removeItem('current_user');
this.clearTokenRefreshTimer();
}
}
setRefreshToken(token) {
this.refreshToken = token;
if (token) {
localStorage.setItem('refresh_token', token);
} else {
localStorage.removeItem('refresh_token');
}
}
@ -49,6 +77,94 @@ class ApiClient {
return token;
}
// Refresh Access Token using Refresh Token
async refreshAccessToken() {
if (this.refreshingToken) {
// Warte auf laufenden Refresh
return new Promise((resolve) => {
const checkRefresh = () => {
if (!this.refreshingToken) {
resolve();
} else {
setTimeout(checkRefresh, 100);
}
};
checkRefresh();
});
}
this.refreshingToken = true;
try {
const refreshToken = localStorage.getItem('refresh_token');
if (!refreshToken) {
throw new Error('Kein Refresh-Token vorhanden');
}
console.log('[API] Refreshing access token...');
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ refreshToken })
});
if (!response.ok) {
throw new Error(`Refresh failed: ${response.status}`);
}
const data = await response.json();
this.setToken(data.token);
if (data.csrfToken) {
this.setCsrfToken(data.csrfToken);
}
console.log('[API] Token refresh successful');
window.dispatchEvent(new CustomEvent('auth:token-refreshed', {
detail: { token: data.token }
}));
} catch (error) {
console.log('[API] Token refresh error:', error.message);
throw error;
} finally {
this.refreshingToken = false;
}
}
// Handle authentication failure
handleAuthFailure() {
console.log('[API] Authentication failed - clearing tokens');
this.setToken(null);
this.setRefreshToken(null);
this.setCsrfToken(null);
window.dispatchEvent(new CustomEvent('auth:logout'));
}
// Proaktiver Token-Refresh Timer
startTokenRefreshTimer() {
this.clearTokenRefreshTimer();
// Refresh nach 10 Minuten (Token läuft nach 15 Minuten ab)
this.refreshTimer = setTimeout(async () => {
if (this.refreshToken && !this.refreshingToken) {
try {
console.log('[API] Proactive token refresh...');
await this.refreshAccessToken();
} catch (error) {
console.log('[API] Proactive refresh failed:', error.message);
// Bei Fehler nicht automatisch ausloggen, warten bis Token wirklich abläuft
}
}
}, 10 * 60 * 1000); // 10 Minuten
}
clearTokenRefreshTimer() {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
this.refreshTimer = null;
}
}
// Base Request Method
async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
@ -58,6 +174,11 @@ class ApiClient {
...options.headers
};
// Sicherstellen, dass Token aktuell ist
if (!this.token && localStorage.getItem('auth_token')) {
this.init();
}
// Add auth token
const token = this.getToken();
console.log('[API] Request:', endpoint, 'Token:', token ? token.substring(0, 20) + '...' : 'NULL');
@ -103,22 +224,25 @@ class ApiClient {
// Handle 401 Unauthorized
if (response.status === 401) {
// Token der für diesen Request verwendet wurde
const requestToken = token;
const currentToken = localStorage.getItem('auth_token');
console.log('[API] 401 received for:', endpoint);
console.log('[API] Request token:', requestToken ? requestToken.substring(0, 20) + '...' : 'NULL');
console.log('[API] Current token:', currentToken ? currentToken.substring(0, 20) + '...' : 'NULL');
// Nur ausloggen wenn der Token der gleiche ist (kein neuer Login in der Zwischenzeit)
if (!currentToken || currentToken === requestToken) {
console.log('[API] Token invalid, triggering logout');
this.setToken(null);
window.dispatchEvent(new CustomEvent('auth:logout'));
} else {
console.log('[API] 401 ignored - new login occurred while request was in flight');
// Versuche Token mit Refresh-Token zu erneuern
if (this.refreshToken && !this.refreshingToken && !options._tokenRefreshAttempted) {
console.log('[API] Attempting token refresh...');
try {
await this.refreshAccessToken();
// Wiederhole original Request mit neuem Token
return this.request(endpoint, { ...options, _tokenRefreshAttempted: true });
} catch (refreshError) {
console.log('[API] Token refresh failed:', refreshError.message);
// Fallback zum Logout
this.handleAuthFailure();
throw new ApiError('Sitzung abgelaufen', 401);
}
}
// Kein Refresh-Token oder Refresh bereits versucht
this.handleAuthFailure();
throw new ApiError('Sitzung abgelaufen', 401);
}
@ -297,6 +421,12 @@ class ApiClient {
const response = await this.post('/auth/login', { username, password });
console.log('[API] login() response:', response ? 'OK' : 'NULL', 'token:', response?.token ? 'EXISTS' : 'MISSING');
this.setToken(response.token);
// Store refresh token if provided (new auth system)
if (response.refreshToken) {
this.setRefreshToken(response.refreshToken);
}
// Store CSRF token from login response
if (response.csrfToken) {
this.setCsrfToken(response.csrfToken);
@ -309,6 +439,7 @@ class ApiClient {
await this.post('/auth/logout', {});
} finally {
this.setToken(null);
this.setRefreshToken(null);
this.setCsrfToken(null);
}
}
@ -1071,6 +1202,62 @@ class ApiClient {
async searchKnowledge(query) {
return this.get(`/knowledge/search?q=${encodeURIComponent(query)}`);
}
// =====================
// CODING
// =====================
async getCodingDirectories() {
return this.get('/coding/directories');
}
async createCodingDirectory(data) {
return this.post('/coding/directories', data);
}
async updateCodingDirectory(id, data) {
return this.put(`/coding/directories/${id}`, data);
}
async deleteCodingDirectory(id) {
return this.delete(`/coding/directories/${id}`);
}
async getCodingDirectoryStatus(id) {
return this.get(`/coding/directories/${id}/status`);
}
async codingGitFetch(id) {
return this.post(`/coding/directories/${id}/fetch`);
}
async codingGitPull(id) {
return this.post(`/coding/directories/${id}/pull`);
}
async codingGitPush(id, force = false) {
return this.post(`/coding/directories/${id}/push`, { force });
}
async codingGitCommit(id, message) {
return this.post(`/coding/directories/${id}/commit`, { message });
}
async getCodingDirectoryBranches(id) {
return this.get(`/coding/directories/${id}/branches`);
}
async codingGitCheckout(id, branch) {
return this.post(`/coding/directories/${id}/checkout`, { branch });
}
async getCodingDirectoryCommits(id, limit = 20) {
return this.get(`/coding/directories/${id}/commits?limit=${limit}`);
}
async validateCodingPath(path) {
return this.post('/coding/validate-path', { path });
}
}
// Custom API Error Class

Datei anzeigen

@ -21,6 +21,8 @@ import proposalsManager from './proposals.js';
import notificationManager from './notifications.js';
import giteaManager from './gitea.js';
import knowledgeManager from './knowledge.js';
import codingManager from './coding.js';
import mobileManager from './mobile.js';
import { $, $$, debounce, getFromStorage, setToStorage } from './utils.js';
class App {
@ -80,11 +82,20 @@ class App {
// Initialize gitea manager
await giteaManager.init();
// Initialize coding manager
await codingManager.init();
// Initialize knowledge manager
await knowledgeManager.init();
// Initialize mobile features
mobileManager.init();
// Update UI
this.updateUserMenu();
// Dispatch event for mobile menu
document.dispatchEvent(new CustomEvent('projects:loaded'));
}
async initializeAdminApp() {
@ -321,6 +332,32 @@ class App {
window.addEventListener('online', () => this.handleOnline());
window.addEventListener('offline', () => this.handleOffline());
// Mobile events
document.addEventListener('project:selected', (e) => {
const projectId = e.detail?.projectId;
if (projectId) {
this.loadProject(projectId);
}
});
document.addEventListener('auth:logout', () => {
authManager.logout();
});
document.addEventListener('admin:open', () => {
// Redirect to admin screen for admins
if (authManager.isAdmin()) {
this.showAdminScreen();
}
});
document.addEventListener('task:move', async (e) => {
const { taskId, columnId, position } = e.detail;
if (taskId && columnId !== undefined) {
await boardManager.moveTask(taskId, columnId, position);
}
});
// Close modal on overlay click
$('.modal-overlay')?.addEventListener('click', () => {
// Check if task-modal is open - let it handle its own close (with auto-save)
@ -617,11 +654,11 @@ class App {
proposalsManager.resetToActiveView();
}
// Show/hide gitea manager
if (view === 'gitea') {
giteaManager.show();
// Show/hide coding manager
if (view === 'coding') {
codingManager.show();
} else {
giteaManager.hide();
codingManager.hide();
}
// Show/hide knowledge manager
@ -978,6 +1015,9 @@ class App {
const userRole = $('#user-role');
if (userRole) userRole.textContent = user.role === 'admin' ? 'Administrator' : 'Benutzer';
// Notify mobile menu
document.dispatchEvent(new CustomEvent('user:updated'));
}
toggleUserMenu() {

777
frontend/js/coding.js Normale Datei
Datei anzeigen

@ -0,0 +1,777 @@
/**
* TASKMATE - Coding Manager
* =========================
* Verwaltung von Server-Anwendungen mit Claude/Codex Integration
*/
import api from './api.js';
import { escapeHtml } from './utils.js';
// Toast-Funktion (verwendet das globale Toast-Event)
function showToast(message, type = 'info') {
window.dispatchEvent(new CustomEvent('toast:show', {
detail: { message, type }
}));
}
// Basis-Pfad für alle Anwendungen auf dem Server
const BASE_PATH = '/home/claude-dev';
// Farb-Presets für Anwendungen
const COLOR_PRESETS = [
'#4F46E5', // Indigo
'#7C3AED', // Violet
'#EC4899', // Pink
'#EF4444', // Red
'#F59E0B', // Amber
'#10B981', // Emerald
'#06B6D4', // Cyan
'#3B82F6', // Blue
'#8B5CF6', // Purple
'#6366F1' // Indigo Light
];
class CodingManager {
constructor() {
this.initialized = false;
this.directories = [];
this.refreshInterval = null;
this.editingDirectory = null;
this.giteaRepos = [];
}
/**
* Manager initialisieren
*/
async init() {
if (this.initialized) return;
this.bindEvents();
this.initialized = true;
console.log('[CodingManager] Initialisiert');
}
/**
* Event-Listener binden
*/
bindEvents() {
// Add-Button
const addBtn = document.getElementById('add-coding-directory-btn');
if (addBtn) {
addBtn.addEventListener('click', () => this.openModal());
}
// Modal Events
const modal = document.getElementById('coding-modal');
if (modal) {
// Close-Button
modal.querySelector('.modal-close')?.addEventListener('click', () => this.closeModal());
modal.querySelector('.modal-cancel')?.addEventListener('click', () => this.closeModal());
// Save-Button
document.getElementById('coding-save-btn')?.addEventListener('click', () => this.handleSave());
// Delete-Button
document.getElementById('coding-delete-btn')?.addEventListener('click', () => this.handleDelete());
// Backdrop-Click
modal.addEventListener('click', (e) => {
if (e.target === modal) this.closeModal();
});
// Farb-Presets
this.renderColorPresets();
// Name-Eingabe für Pfad-Preview
const nameInput = document.getElementById('coding-name');
if (nameInput) {
nameInput.addEventListener('input', () => this.updatePathPreview());
}
// CLAUDE.md Link Event
const claudeLink = document.getElementById('coding-claude-link');
if (claudeLink) {
claudeLink.addEventListener('click', () => this.openClaudeModal());
}
}
// Command-Modal Events
const cmdModal = document.getElementById('coding-command-modal');
if (cmdModal) {
cmdModal.querySelector('.modal-close')?.addEventListener('click', () => this.closeCommandModal());
cmdModal.addEventListener('click', (e) => {
if (e.target === cmdModal) this.closeCommandModal();
});
document.getElementById('coding-copy-command')?.addEventListener('click', () => this.copyCommand());
}
// Gitea-Repo Dropdown laden bei Details-Toggle
const giteaSection = document.querySelector('.coding-gitea-section');
if (giteaSection) {
giteaSection.addEventListener('toggle', (e) => {
if (e.target.open) {
this.loadGiteaRepos();
}
});
}
// CLAUDE.md Modal Events
const claudeModal = document.getElementById('claude-md-modal');
if (claudeModal) {
// Close-Button
claudeModal.querySelector('.modal-close')?.addEventListener('click', () => this.closeClaudeModal());
// Backdrop-Click
claudeModal.addEventListener('click', (e) => {
if (e.target === claudeModal) this.closeClaudeModal();
});
// ESC-Taste
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !claudeModal.classList.contains('hidden')) {
this.closeClaudeModal();
}
});
}
}
/**
* Farb-Presets rendern
*/
renderColorPresets() {
const container = document.getElementById('coding-color-presets');
if (!container) return;
container.innerHTML = COLOR_PRESETS.map(color => `
<button type="button" class="color-preset" data-color="${color}" style="background-color: ${color};" title="${color}"></button>
`).join('') + `
<input type="color" id="coding-color-custom" class="color-picker-custom" value="#4F46E5" title="Eigene Farbe">
`;
// Event-Listener für Presets
container.querySelectorAll('.color-preset').forEach(btn => {
btn.addEventListener('click', () => {
container.querySelectorAll('.color-preset').forEach(b => b.classList.remove('selected'));
btn.classList.add('selected');
document.getElementById('coding-color-custom').value = btn.dataset.color;
});
});
// Custom Color Input
document.getElementById('coding-color-custom')?.addEventListener('input', (e) => {
container.querySelectorAll('.color-preset').forEach(b => b.classList.remove('selected'));
});
}
/**
* Pfad-Preview aktualisieren
*/
updatePathPreview() {
const nameInput = document.getElementById('coding-name');
const preview = document.getElementById('coding-path-preview');
if (!nameInput || !preview) return;
const name = nameInput.value.trim();
preview.textContent = name || '...';
}
// switchClaudeTab entfernt - CLAUDE.md ist jetzt nur readonly
/**
* CLAUDE.md Link aktualisieren
*/
updateClaudeLink(content, projectName) {
const link = document.getElementById('coding-claude-link');
const textSpan = link?.querySelector('.claude-text');
// Debug entfernt
if (!link || !textSpan) {
console.error('CLAUDE.md link elements not found!');
return;
}
// Content für Modal speichern
this.currentClaudeContent = content;
this.currentProjectName = projectName;
if (content) {
link.disabled = false;
textSpan.textContent = `CLAUDE.md anzeigen (${Math.round(content.length / 1024)}KB)`;
} else {
link.disabled = true;
textSpan.textContent = 'Keine CLAUDE.md vorhanden';
}
}
/**
* CLAUDE.md Modal öffnen
*/
openClaudeModal() {
if (!this.currentClaudeContent) {
console.warn('No CLAUDE.md content to display');
return;
}
const modal = document.getElementById('claude-md-modal');
const overlay = document.querySelector('.modal-overlay');
const content = document.getElementById('claude-md-content');
const title = modal?.querySelector('.modal-header h3');
if (!modal || !content) {
console.error('CLAUDE.md modal elements not found!');
return;
}
// Titel setzen
if (title && this.currentProjectName) {
title.textContent = `CLAUDE.md - ${this.currentProjectName}`;
}
// Content setzen
content.textContent = this.currentClaudeContent;
// Modal anzeigen
modal.classList.remove('hidden');
modal.classList.add('visible');
if (overlay) {
overlay.classList.remove('hidden');
overlay.classList.add('visible');
}
// Modal opened
}
/**
* CLAUDE.md Modal schließen
*/
closeClaudeModal() {
const modal = document.getElementById('claude-md-modal');
const overlay = document.querySelector('.modal-overlay');
if (modal) {
modal.classList.remove('visible');
setTimeout(() => modal.classList.add('hidden'), 200);
}
if (overlay) {
overlay.classList.remove('visible');
setTimeout(() => overlay.classList.add('hidden'), 200);
}
// Modal closed
}
/**
* Gitea-Repositories laden
*/
async loadGiteaRepos() {
try {
const select = document.getElementById('coding-gitea-repo');
if (!select) return;
// Lade-Indikator
select.innerHTML = '<option value="">Laden...</option>';
const result = await api.getGiteaRepositories();
console.log('Gitea API Response:', result);
this.giteaRepos = result?.repositories || [];
select.innerHTML = '<option value="">-- Kein Repository --</option>' +
this.giteaRepos.map(repo => `
<option value="${repo.cloneUrl}" data-owner="${repo.owner || ''}" data-name="${repo.name}">
${escapeHtml(repo.fullName)}
</option>
`).join('');
// Wenn Editing, vorhandenen Wert setzen
if (this.editingDirectory?.giteaRepoUrl) {
select.value = this.editingDirectory.giteaRepoUrl;
}
} catch (error) {
console.error('Fehler beim Laden der Gitea-Repos:', error);
const select = document.getElementById('coding-gitea-repo');
if (select) {
select.innerHTML = '<option value="">Fehler beim Laden</option>';
}
}
}
/**
* Anwendungen laden
*/
async loadDirectories() {
try {
this.directories = await api.getCodingDirectories();
this.render();
} catch (error) {
console.error('Fehler beim Laden der Anwendungen:', error);
showToast('Fehler beim Laden der Anwendungen', 'error');
}
}
/**
* View rendern
*/
render() {
const grid = document.getElementById('coding-grid');
const empty = document.getElementById('coding-empty');
if (!grid) return;
if (this.directories.length === 0) {
grid.innerHTML = '';
grid.classList.add('hidden');
empty?.classList.remove('hidden');
return;
}
empty?.classList.add('hidden');
grid.classList.remove('hidden');
grid.innerHTML = this.directories.map(dir => this.renderTile(dir)).join('');
// Event-Listener für Tiles
this.bindTileEvents();
// Git-Status für jede Anwendung laden
this.directories.forEach(dir => this.updateTileStatus(dir.id));
}
/**
* Einzelne Kachel rendern
*/
renderTile(directory) {
const hasGitea = !!directory.giteaRepoUrl;
return `
<div class="coding-tile" data-id="${directory.id}">
<div class="coding-tile-color" style="background-color: ${directory.color || '#4F46E5'}"></div>
<div class="coding-tile-header">
<span class="coding-tile-icon">📁</span>
</div>
<div class="coding-tile-content">
<div class="coding-tile-name">${escapeHtml(directory.name)}</div>
<div class="coding-tile-path">${escapeHtml(directory.localPath)}</div>
${directory.description ? `<div class="coding-tile-description">${escapeHtml(directory.description)}</div>` : ''}
${directory.hasCLAUDEmd ? '<div class="coding-tile-badge">CLAUDE.md</div>' : ''}
</div>
<div class="coding-tile-status" id="coding-status-${directory.id}">
<span class="git-status-badge loading">Lade...</span>
</div>
<div class="coding-tile-actions">
<button class="btn-claude" data-id="${directory.id}" data-path="${escapeHtml(directory.localPath)}" title="SSH-Befehl für Claude kopieren">
Claude starten
</button>
</div>
${hasGitea ? `
<div class="coding-tile-git">
<button class="btn btn-sm btn-secondary coding-git-fetch" data-id="${directory.id}">Fetch</button>
<button class="btn btn-sm btn-secondary coding-git-pull" data-id="${directory.id}">Pull</button>
<button class="btn btn-sm btn-secondary coding-git-push" data-id="${directory.id}">Push</button>
<button class="btn btn-sm btn-secondary coding-git-commit" data-id="${directory.id}">Commit</button>
</div>
` : ''}
</div>
`;
}
/**
* Event-Listener für Tiles binden
*/
bindTileEvents() {
// Kachel-Klick für Modal
document.querySelectorAll('.coding-tile').forEach(tile => {
tile.addEventListener('click', (e) => {
// Nicht triggern wenn Button-Kind geklickt wird
if (e.target.closest('button')) return;
const id = parseInt(tile.dataset.id);
const dir = this.directories.find(d => d.id === id);
if (dir) this.openModal(dir);
});
});
// Claude-Buttons
document.querySelectorAll('.btn-claude').forEach(btn => {
btn.addEventListener('click', () => this.launchClaude(btn.dataset.path));
});
// Git-Buttons
document.querySelectorAll('.coding-git-fetch').forEach(btn => {
btn.addEventListener('click', () => this.gitFetch(parseInt(btn.dataset.id)));
});
document.querySelectorAll('.coding-git-pull').forEach(btn => {
btn.addEventListener('click', () => this.gitPull(parseInt(btn.dataset.id)));
});
document.querySelectorAll('.coding-git-push').forEach(btn => {
btn.addEventListener('click', () => this.gitPush(parseInt(btn.dataset.id)));
});
document.querySelectorAll('.coding-git-commit').forEach(btn => {
btn.addEventListener('click', () => this.promptCommit(parseInt(btn.dataset.id)));
});
}
/**
* Git-Status für eine Kachel aktualisieren
*/
async updateTileStatus(id) {
const statusEl = document.getElementById(`coding-status-${id}`);
if (!statusEl) return;
try {
const status = await api.getCodingDirectoryStatus(id);
if (!status.isGitRepo) {
statusEl.innerHTML = '<span class="git-status-badge">Kein Git-Repo</span>';
return;
}
const statusClass = status.isClean ? 'clean' : 'dirty';
const statusText = status.isClean ? 'Clean' : `${status.changes?.length || 0} Änderungen`;
statusEl.innerHTML = `
<span class="git-branch-badge">${escapeHtml(status.branch)}</span>
<span class="git-status-badge ${statusClass}">${statusText}</span>
${status.ahead > 0 ? `<span class="git-status-badge ahead">↑${status.ahead}</span>` : ''}
${status.behind > 0 ? `<span class="git-status-badge behind">↓${status.behind}</span>` : ''}
`;
} catch (error) {
statusEl.innerHTML = '<span class="git-status-badge error">Fehler</span>';
}
}
/**
* Claude Code starten - SSH-Befehl kopieren
*/
async launchClaude(path) {
const command = `ssh claude-dev@91.99.192.14 -t "cd ${path} && claude"`;
try {
await navigator.clipboard.writeText(command);
this.showCommandModal(
command,
'Befehl kopiert! Öffne Terminal/CMD und füge ein (Strg+V). Passwort: z0E1Al}q2H?Yqd!O'
);
showToast('SSH-Befehl kopiert!', 'success');
} catch (error) {
// Fallback wenn Clipboard nicht verfügbar
this.showCommandModal(
command,
'Kopiere diesen Befehl und füge ihn im Terminal ein. Passwort: z0E1Al}q2H?Yqd!O'
);
}
}
/**
* Command-Modal anzeigen
*/
showCommandModal(command, hint) {
const modal = document.getElementById('coding-command-modal');
const hintEl = document.getElementById('coding-command-hint');
const textEl = document.getElementById('coding-command-text');
if (!modal || !textEl) return;
hintEl.textContent = hint || 'Führe diesen Befehl aus:';
textEl.textContent = command;
this.currentCommand = command;
modal.classList.remove('hidden');
}
/**
* Command-Modal schließen
*/
closeCommandModal() {
const modal = document.getElementById('coding-command-modal');
if (modal) modal.classList.add('hidden');
}
/**
* Befehl in Zwischenablage kopieren
*/
async copyCommand() {
if (!this.currentCommand) return;
try {
await navigator.clipboard.writeText(this.currentCommand);
showToast('Befehl kopiert!', 'success');
} catch (error) {
console.error('Kopieren fehlgeschlagen:', error);
showToast('Kopieren fehlgeschlagen', 'error');
}
}
/**
* Git Fetch
*/
async gitFetch(id) {
try {
showToast('Fetch läuft...', 'info');
await api.codingGitFetch(id);
showToast('Fetch erfolgreich', 'success');
this.updateTileStatus(id);
} catch (error) {
showToast('Fetch fehlgeschlagen', 'error');
}
}
/**
* Git Pull
*/
async gitPull(id) {
try {
showToast('Pull läuft...', 'info');
await api.codingGitPull(id);
showToast('Pull erfolgreich', 'success');
this.updateTileStatus(id);
} catch (error) {
showToast('Pull fehlgeschlagen: ' + (error.message || 'Unbekannter Fehler'), 'error');
}
}
/**
* Git Push
*/
async gitPush(id) {
try {
showToast('Push läuft...', 'info');
await api.codingGitPush(id);
showToast('Push erfolgreich', 'success');
this.updateTileStatus(id);
} catch (error) {
showToast('Push fehlgeschlagen: ' + (error.message || 'Unbekannter Fehler'), 'error');
}
}
/**
* Commit-Dialog anzeigen
*/
promptCommit(id) {
const message = prompt('Commit-Nachricht eingeben:');
if (message && message.trim()) {
this.gitCommit(id, message.trim());
}
}
/**
* Git Commit
*/
async gitCommit(id, message) {
try {
showToast('Commit läuft...', 'info');
await api.codingGitCommit(id, message);
showToast('Commit erfolgreich', 'success');
this.updateTileStatus(id);
} catch (error) {
showToast('Commit fehlgeschlagen: ' + (error.message || 'Unbekannter Fehler'), 'error');
}
}
/**
* Modal öffnen
*/
openModal(directory = null) {
this.editingDirectory = directory;
const modal = document.getElementById('coding-modal');
const overlay = document.querySelector('.modal-overlay');
const title = document.getElementById('coding-modal-title');
const deleteBtn = document.getElementById('coding-delete-btn');
if (!modal) return;
// Titel setzen
title.textContent = directory ? 'Anwendung bearbeiten' : 'Anwendung hinzufügen';
// Delete-Button anzeigen/verstecken
if (deleteBtn) {
deleteBtn.classList.toggle('hidden', !directory);
}
// Felder füllen
document.getElementById('coding-name').value = directory?.name || '';
document.getElementById('coding-description').value = directory?.description || '';
document.getElementById('coding-branch').value = directory?.defaultBranch || 'main';
// CLAUDE.md: Nur aus Dateisystem anzeigen
const claudeContent = directory?.claudeMdFromDisk || '';
this.updateClaudeLink(claudeContent, directory?.name);
// Pfad-Preview aktualisieren
this.updatePathPreview();
// Farbe setzen
const color = directory?.color || '#4F46E5';
document.getElementById('coding-color-custom').value = color;
document.querySelectorAll('.color-preset').forEach(btn => {
btn.classList.toggle('selected', btn.dataset.color === color);
});
// Gitea-Sektion zurücksetzen
const giteaSection = document.querySelector('.coding-gitea-section');
if (giteaSection) {
giteaSection.open = !!directory?.giteaRepoUrl;
}
// Repos laden wenn nötig
if (directory?.giteaRepoUrl) {
this.loadGiteaRepos();
}
// Modal und Overlay anzeigen
modal.classList.remove('hidden');
modal.classList.add('visible');
if (overlay) {
overlay.classList.remove('hidden');
overlay.classList.add('visible');
}
}
/**
* Modal schließen
*/
closeModal() {
const modal = document.getElementById('coding-modal');
const overlay = document.querySelector('.modal-overlay');
if (modal) {
modal.classList.remove('visible');
setTimeout(() => modal.classList.add('hidden'), 200);
this.editingDirectory = null;
}
if (overlay) {
overlay.classList.remove('visible');
setTimeout(() => overlay.classList.add('hidden'), 200);
}
}
/**
* Speichern-Handler
*/
async handleSave() {
const name = document.getElementById('coding-name').value.trim();
const description = document.getElementById('coding-description').value.trim();
// CLAUDE.md wird nicht mehr gespeichert - nur readonly
const defaultBranch = document.getElementById('coding-branch').value.trim() || 'main';
// Pfad automatisch aus Name generieren
const localPath = `${BASE_PATH}/${name}`;
// Farbe ermitteln
const selectedPreset = document.querySelector('.color-preset.selected');
const color = selectedPreset?.dataset.color || document.getElementById('coding-color-custom').value;
// Gitea-Daten
const giteaSelect = document.getElementById('coding-gitea-repo');
const giteaRepoUrl = giteaSelect?.value || null;
const giteaRepoOwner = giteaSelect?.selectedOptions[0]?.dataset.owner || null;
const giteaRepoName = giteaSelect?.selectedOptions[0]?.dataset.name || null;
if (!name) {
showToast('Anwendungsname ist erforderlich', 'error');
return;
}
const data = {
name,
localPath,
description,
color,
// claudeInstructions entfernt - CLAUDE.md ist readonly
giteaRepoUrl,
giteaRepoOwner,
giteaRepoName,
defaultBranch
};
try {
if (this.editingDirectory) {
await api.updateCodingDirectory(this.editingDirectory.id, data);
showToast('Anwendung aktualisiert', 'success');
} else {
await api.createCodingDirectory(data);
showToast('Anwendung hinzugefügt', 'success');
}
this.closeModal();
await this.loadDirectories();
} catch (error) {
console.error('Fehler beim Speichern:', error);
showToast(error.message || 'Fehler beim Speichern', 'error');
}
}
/**
* Löschen-Handler
*/
async handleDelete() {
if (!this.editingDirectory) return;
if (!confirm(`Anwendung "${this.editingDirectory.name}" wirklich löschen?`)) {
return;
}
try {
await api.deleteCodingDirectory(this.editingDirectory.id);
showToast('Anwendung gelöscht', 'success');
this.closeModal();
await this.loadDirectories();
} catch (error) {
console.error('Fehler beim Löschen:', error);
showToast('Fehler beim Löschen', 'error');
}
}
/**
* Auto-Refresh starten
*/
startAutoRefresh() {
this.stopAutoRefresh();
// Alle 30 Sekunden aktualisieren
this.refreshInterval = setInterval(() => {
this.directories.forEach(dir => this.updateTileStatus(dir.id));
}, 30000);
}
/**
* Auto-Refresh stoppen
*/
stopAutoRefresh() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = null;
}
}
/**
* View anzeigen
*/
async show() {
await this.loadDirectories();
this.startAutoRefresh();
}
/**
* View verstecken
*/
hide() {
this.stopAutoRefresh();
}
}
// Singleton-Instanz erstellen und exportieren
const codingManager = new CodingManager();
export default codingManager;

Datei anzeigen

@ -86,6 +86,13 @@ class ListViewManager {
this.contentElement.addEventListener('click', (e) => this.handleContentClick(e));
this.contentElement.addEventListener('change', (e) => this.handleContentChange(e));
this.contentElement.addEventListener('dblclick', (e) => this.handleDoubleClick(e));
// Stop editing when clicking outside
document.addEventListener('click', (e) => {
if (this.editingCell && !this.editingCell.contains(e.target)) {
this.stopEditing();
}
});
}
}
@ -407,19 +414,53 @@ class ListViewManager {
const users = store.get('users');
const cell = createElement('div', { className: 'list-cell list-cell-assignee list-cell-editable' });
const assignedUser = users.find(u => u.id === task.assignedTo);
// Avatar
if (assignedUser) {
const avatar = createElement('div', {
className: 'avatar',
style: { backgroundColor: assignedUser.color || '#6366F1' }
}, [getInitials(assignedUser.displayName)]);
cell.appendChild(avatar);
// Sammle alle zugewiesenen Benutzer aus assignees Array
const assignedUserIds = new Set();
// Verwende das assignees Array vom Backend
if (task.assignees && Array.isArray(task.assignees)) {
task.assignees.forEach(assignee => {
if (assignee && assignee.id) {
assignedUserIds.add(assignee.id);
}
});
}
// Fallback: Füge assigned_to hinzu falls assignees leer ist
if (assignedUserIds.size === 0 && task.assignedTo) {
assignedUserIds.add(task.assignedTo);
}
// User dropdown
// Container für mehrere Avatare
const avatarContainer = createElement('div', { className: 'avatar-container' });
if (assignedUserIds.size > 0) {
// Erstelle Avatar für jeden zugewiesenen Benutzer
Array.from(assignedUserIds).forEach(userId => {
const user = users.find(u => u.id === userId);
if (user) {
const avatar = createElement('div', {
className: 'avatar',
style: { backgroundColor: user.color || '#6366F1' },
title: user.displayName // Tooltip zeigt Name beim Hover
}, [getInitials(user.displayName)]);
avatarContainer.appendChild(avatar);
}
});
} else {
// Placeholder für "nicht zugewiesen"
const placeholder = createElement('div', {
className: 'avatar avatar-empty',
title: 'Nicht zugewiesen'
}, ['?']);
avatarContainer.appendChild(placeholder);
}
cell.appendChild(avatarContainer);
// User dropdown (versteckt, nur für Bearbeitung)
const select = createElement('select', {
className: 'assignee-select hidden',
dataset: { field: 'assignedTo', taskId: task.id }
});
@ -445,6 +486,24 @@ class ListViewManager {
// =====================
handleContentClick(e) {
// Handle avatar click for assignee editing
if (e.target.classList.contains('avatar') || e.target.classList.contains('avatar-empty')) {
const cell = e.target.closest('.list-cell-assignee');
if (cell) {
this.startEditingAssignee(cell);
return;
}
}
// Handle click on avatar container (wenn man neben Avatar klickt)
if (e.target.classList.contains('avatar-container')) {
const cell = e.target.closest('.list-cell-assignee');
if (cell) {
this.startEditingAssignee(cell);
return;
}
}
const target = e.target.closest('[data-action]');
if (!target) return;
@ -456,6 +515,35 @@ class ListViewManager {
}
}
/**
* Start editing assignee
*/
startEditingAssignee(cell) {
// Stop any current editing
this.stopEditing();
// Add editing class to show dropdown and hide avatar
cell.classList.add('editing');
// Focus the select element
const select = cell.querySelector('.assignee-select');
if (select) {
select.focus();
}
this.editingCell = cell;
}
/**
* Stop editing
*/
stopEditing() {
if (this.editingCell) {
this.editingCell.classList.remove('editing');
this.editingCell = null;
}
}
handleContentChange(e) {
const target = e.target;
const field = target.dataset.field;
@ -463,6 +551,11 @@ class ListViewManager {
if (field && taskId) {
this.updateTaskField(parseInt(taskId), field, target.value);
// Stop editing after change for assignee field
if (field === 'assignedTo') {
this.stopEditing();
}
}
}

696
frontend/js/mobile.js Normale Datei
Datei anzeigen

@ -0,0 +1,696 @@
/**
* TASKMATE - Mobile Module
* ========================
* Touch-Gesten, Hamburger-Menu, Swipe-Navigation
*/
import { $, $$ } from './utils.js';
class MobileManager {
constructor() {
// State
this.isMenuOpen = false;
this.isMobile = false;
this.currentView = 'board';
// Swipe state
this.touchStartX = 0;
this.touchStartY = 0;
this.touchCurrentX = 0;
this.touchCurrentY = 0;
this.touchStartTime = 0;
this.isSwiping = false;
this.swipeDirection = null;
// Touch drag & drop state
this.touchDraggedElement = null;
this.touchDragPlaceholder = null;
this.touchDragStartX = 0;
this.touchDragStartY = 0;
this.touchDragOffsetX = 0;
this.touchDragOffsetY = 0;
this.touchDragScrollInterval = null;
this.longPressTimer = null;
// Constants
this.SWIPE_THRESHOLD = 50;
this.SWIPE_VELOCITY_THRESHOLD = 0.3;
this.MOBILE_BREAKPOINT = 768;
this.LONG_PRESS_DURATION = 300;
// View order for swipe navigation
this.viewOrder = ['board', 'list', 'calendar', 'proposals', 'gitea', 'knowledge'];
// DOM elements
this.hamburgerBtn = null;
this.mobileMenu = null;
this.mobileOverlay = null;
this.mainContent = null;
this.swipeIndicatorLeft = null;
this.swipeIndicatorRight = null;
}
/**
* Initialize mobile features
*/
init() {
// Check if mobile
this.checkMobile();
window.addEventListener('resize', () => this.checkMobile());
// Cache DOM elements
this.hamburgerBtn = $('#hamburger-btn');
this.mobileMenu = $('#mobile-menu');
this.mobileOverlay = $('#mobile-menu-overlay');
this.mainContent = $('.main-content');
this.swipeIndicatorLeft = $('#swipe-indicator-left');
this.swipeIndicatorRight = $('#swipe-indicator-right');
// Bind events
this.bindMenuEvents();
this.bindSwipeEvents();
this.bindTouchDragEvents();
// Listen for view changes
document.addEventListener('view:changed', (e) => {
this.currentView = e.detail?.view || 'board';
this.updateActiveNavItem(this.currentView);
});
// Listen for project changes
document.addEventListener('projects:loaded', () => {
this.populateMobileProjectSelect();
});
// Listen for user updates
document.addEventListener('user:updated', () => {
this.updateUserInfo();
});
console.log('[Mobile] Initialized');
}
/**
* Check if current viewport is mobile
*/
checkMobile() {
this.isMobile = window.innerWidth <= this.MOBILE_BREAKPOINT;
}
// =====================
// HAMBURGER MENU
// =====================
/**
* Bind menu events
*/
bindMenuEvents() {
// Hamburger button
this.hamburgerBtn?.addEventListener('click', () => this.toggleMenu());
// Close button
$('#mobile-menu-close')?.addEventListener('click', () => this.closeMenu());
// Overlay click
this.mobileOverlay?.addEventListener('click', () => this.closeMenu());
// Navigation items
$$('.mobile-nav-item').forEach(item => {
item.addEventListener('click', () => {
const view = item.dataset.view;
this.switchView(view);
this.closeMenu();
});
});
// Project selector
$('#mobile-project-select')?.addEventListener('change', (e) => {
const projectId = parseInt(e.target.value);
if (projectId) {
document.dispatchEvent(new CustomEvent('project:selected', {
detail: { projectId }
}));
this.closeMenu();
}
});
// Admin button
$('#mobile-admin-btn')?.addEventListener('click', () => {
this.closeMenu();
document.dispatchEvent(new CustomEvent('admin:open'));
});
// Logout button
$('#mobile-logout-btn')?.addEventListener('click', () => {
this.closeMenu();
document.dispatchEvent(new CustomEvent('auth:logout'));
});
// Escape key to close
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.isMenuOpen) {
this.closeMenu();
}
});
}
/**
* Toggle menu open/close
*/
toggleMenu() {
if (this.isMenuOpen) {
this.closeMenu();
} else {
this.openMenu();
}
}
/**
* Open mobile menu
*/
openMenu() {
this.isMenuOpen = true;
this.hamburgerBtn?.classList.add('active');
this.hamburgerBtn?.setAttribute('aria-expanded', 'true');
this.mobileMenu?.classList.add('open');
this.mobileMenu?.setAttribute('aria-hidden', 'false');
this.mobileOverlay?.classList.add('visible');
document.body.classList.add('mobile-menu-open');
// Update user info when menu opens
this.updateUserInfo();
this.populateMobileProjectSelect();
// Focus close button
setTimeout(() => {
$('#mobile-menu-close')?.focus();
}, 300);
}
/**
* Close mobile menu
*/
closeMenu() {
this.isMenuOpen = false;
this.hamburgerBtn?.classList.remove('active');
this.hamburgerBtn?.setAttribute('aria-expanded', 'false');
this.mobileMenu?.classList.remove('open');
this.mobileMenu?.setAttribute('aria-hidden', 'true');
this.mobileOverlay?.classList.remove('visible');
document.body.classList.remove('mobile-menu-open');
// Return focus
this.hamburgerBtn?.focus();
}
/**
* Switch to a different view
*/
switchView(view) {
if (!this.viewOrder.includes(view)) return;
this.currentView = view;
// Update desktop tabs
$$('.view-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.view === view);
});
// Show/hide views
$$('.view').forEach(v => {
const viewName = v.id.replace('view-', '');
const isActive = viewName === view;
v.classList.toggle('active', isActive);
v.classList.toggle('hidden', !isActive);
});
// Update mobile nav
this.updateActiveNavItem(view);
// Dispatch event for other modules
document.dispatchEvent(new CustomEvent('view:changed', {
detail: { view }
}));
}
/**
* Update active nav item in mobile menu
*/
updateActiveNavItem(view) {
$$('.mobile-nav-item').forEach(item => {
item.classList.toggle('active', item.dataset.view === view);
});
}
/**
* Populate project select dropdown
*/
populateMobileProjectSelect() {
const select = $('#mobile-project-select');
const desktopSelect = $('#project-select');
if (!select || !desktopSelect) return;
// Copy options from desktop select
select.innerHTML = desktopSelect.innerHTML;
select.value = desktopSelect.value;
}
/**
* Update user info in mobile menu
*/
updateUserInfo() {
const avatar = $('#mobile-user-avatar');
const name = $('#mobile-user-name');
const role = $('#mobile-user-role');
const adminBtn = $('#mobile-admin-btn');
// Get user info from desktop user dropdown
const desktopAvatar = $('#user-avatar');
const desktopDropdown = $('.user-dropdown');
if (avatar && desktopAvatar) {
avatar.textContent = desktopAvatar.textContent;
avatar.style.backgroundColor = desktopAvatar.style.backgroundColor || 'var(--primary)';
}
if (name) {
const usernameEl = desktopDropdown?.querySelector('.user-info strong');
name.textContent = usernameEl?.textContent || 'Benutzer';
}
if (role) {
const roleEl = desktopDropdown?.querySelector('.user-info span:not(strong)');
role.textContent = roleEl?.textContent || 'Angemeldet';
}
// Show admin button for admins
if (adminBtn) {
const isAdmin = role?.textContent?.toLowerCase().includes('admin');
adminBtn.classList.toggle('hidden', !isAdmin);
}
}
// =====================
// SWIPE NAVIGATION
// =====================
/**
* Bind swipe events
*/
bindSwipeEvents() {
if (!this.mainContent) return;
this.mainContent.addEventListener('touchstart', (e) => this.handleSwipeStart(e), { passive: true });
this.mainContent.addEventListener('touchmove', (e) => this.handleSwipeMove(e), { passive: false });
this.mainContent.addEventListener('touchend', (e) => this.handleSwipeEnd(e), { passive: true });
this.mainContent.addEventListener('touchcancel', () => this.resetSwipe(), { passive: true });
}
/**
* Handle swipe start
*/
handleSwipeStart(e) {
if (!this.isMobile) return;
// Don't swipe if menu is open
if (this.isMenuOpen) return;
// Don't swipe if modal is open
if ($('.modal-overlay:not(.hidden)')) return;
// Don't swipe on scrollable elements
const target = e.target;
if (target.closest('.column-body') ||
target.closest('.modal') ||
target.closest('.calendar-grid') ||
target.closest('.knowledge-entry-list') ||
target.closest('.list-table') ||
target.closest('input') ||
target.closest('textarea') ||
target.closest('select')) {
return;
}
// Only single touch
if (e.touches.length !== 1) return;
this.touchStartX = e.touches[0].clientX;
this.touchStartY = e.touches[0].clientY;
this.touchStartTime = Date.now();
this.isSwiping = false;
this.swipeDirection = null;
}
/**
* Handle swipe move
*/
handleSwipeMove(e) {
if (!this.isMobile || this.touchStartX === 0) return;
const touch = e.touches[0];
this.touchCurrentX = touch.clientX;
this.touchCurrentY = touch.clientY;
const deltaX = this.touchCurrentX - this.touchStartX;
const deltaY = this.touchCurrentY - this.touchStartY;
// Determine direction on first significant movement
if (!this.swipeDirection && (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10)) {
if (Math.abs(deltaX) > Math.abs(deltaY) * 1.5) {
this.swipeDirection = 'horizontal';
this.isSwiping = true;
document.body.classList.add('is-swiping');
} else {
this.swipeDirection = 'vertical';
this.resetSwipe();
return;
}
}
if (this.swipeDirection !== 'horizontal') return;
// Prevent scroll
e.preventDefault();
// Show indicators
const currentIndex = this.viewOrder.indexOf(this.currentView);
if (deltaX > this.SWIPE_THRESHOLD && currentIndex > 0) {
this.swipeIndicatorLeft?.classList.add('visible');
this.swipeIndicatorRight?.classList.remove('visible');
} else if (deltaX < -this.SWIPE_THRESHOLD && currentIndex < this.viewOrder.length - 1) {
this.swipeIndicatorRight?.classList.add('visible');
this.swipeIndicatorLeft?.classList.remove('visible');
} else {
this.swipeIndicatorLeft?.classList.remove('visible');
this.swipeIndicatorRight?.classList.remove('visible');
}
}
/**
* Handle swipe end
*/
handleSwipeEnd() {
if (!this.isSwiping || this.swipeDirection !== 'horizontal') {
this.resetSwipe();
return;
}
const deltaX = this.touchCurrentX - this.touchStartX;
const deltaTime = Date.now() - this.touchStartTime;
const velocity = Math.abs(deltaX) / deltaTime;
// Valid swipe?
const isValidSwipe = Math.abs(deltaX) > this.SWIPE_THRESHOLD || velocity > this.SWIPE_VELOCITY_THRESHOLD;
if (isValidSwipe) {
const currentIndex = this.viewOrder.indexOf(this.currentView);
if (deltaX > 0 && currentIndex > 0) {
// Swipe right - previous view
this.switchView(this.viewOrder[currentIndex - 1]);
} else if (deltaX < 0 && currentIndex < this.viewOrder.length - 1) {
// Swipe left - next view
this.switchView(this.viewOrder[currentIndex + 1]);
}
}
this.resetSwipe();
}
/**
* Reset swipe state
*/
resetSwipe() {
this.touchStartX = 0;
this.touchStartY = 0;
this.touchCurrentX = 0;
this.touchCurrentY = 0;
this.touchStartTime = 0;
this.isSwiping = false;
this.swipeDirection = null;
document.body.classList.remove('is-swiping');
this.swipeIndicatorLeft?.classList.remove('visible');
this.swipeIndicatorRight?.classList.remove('visible');
}
// =====================
// TOUCH DRAG & DROP
// =====================
/**
* Bind touch drag events
*/
bindTouchDragEvents() {
const board = $('#board');
if (!board) return;
board.addEventListener('touchstart', (e) => this.handleTouchDragStart(e), { passive: false });
board.addEventListener('touchmove', (e) => this.handleTouchDragMove(e), { passive: false });
board.addEventListener('touchend', (e) => this.handleTouchDragEnd(e), { passive: true });
board.addEventListener('touchcancel', () => this.cancelTouchDrag(), { passive: true });
}
/**
* Handle touch drag start
*/
handleTouchDragStart(e) {
if (!this.isMobile) return;
const taskCard = e.target.closest('.task-card');
if (!taskCard) return;
// Cancel if multi-touch
if (e.touches.length > 1) {
this.cancelTouchDrag();
return;
}
const touch = e.touches[0];
this.touchDragStartX = touch.clientX;
this.touchDragStartY = touch.clientY;
// Long press to start drag
this.longPressTimer = setTimeout(() => {
this.startTouchDrag(taskCard, touch);
}, this.LONG_PRESS_DURATION);
}
/**
* Start touch drag
*/
startTouchDrag(taskCard, touch) {
this.touchDraggedElement = taskCard;
const rect = taskCard.getBoundingClientRect();
// Calculate offset
this.touchDragOffsetX = touch.clientX - rect.left;
this.touchDragOffsetY = touch.clientY - rect.top;
// Create placeholder
this.touchDragPlaceholder = document.createElement('div');
this.touchDragPlaceholder.className = 'task-card touch-drag-placeholder';
this.touchDragPlaceholder.style.height = rect.height + 'px';
taskCard.parentNode.insertBefore(this.touchDragPlaceholder, taskCard);
// Style dragged element
taskCard.classList.add('touch-dragging');
taskCard.style.position = 'fixed';
taskCard.style.left = rect.left + 'px';
taskCard.style.top = rect.top + 'px';
taskCard.style.width = rect.width + 'px';
taskCard.style.zIndex = '1000';
document.body.classList.add('is-touch-dragging');
// Haptic feedback
if (navigator.vibrate) {
navigator.vibrate(50);
}
}
/**
* Handle touch drag move
*/
handleTouchDragMove(e) {
// Cancel long press if finger moved
if (this.longPressTimer && !this.touchDraggedElement) {
const touch = e.touches[0];
const deltaX = Math.abs(touch.clientX - this.touchDragStartX);
const deltaY = Math.abs(touch.clientY - this.touchDragStartY);
if (deltaX > 10 || deltaY > 10) {
clearTimeout(this.longPressTimer);
this.longPressTimer = null;
}
return;
}
if (!this.touchDraggedElement) return;
e.preventDefault();
const touch = e.touches[0];
const taskCard = this.touchDraggedElement;
// Move element
taskCard.style.left = (touch.clientX - this.touchDragOffsetX) + 'px';
taskCard.style.top = (touch.clientY - this.touchDragOffsetY) + 'px';
// Find drop target
taskCard.style.pointerEvents = 'none';
const elemBelow = document.elementFromPoint(touch.clientX, touch.clientY);
taskCard.style.pointerEvents = '';
const columnBody = elemBelow?.closest('.column-body');
// Remove previous indicators
$$('.column-body.touch-drag-over').forEach(el => el.classList.remove('touch-drag-over'));
if (columnBody) {
columnBody.classList.add('touch-drag-over');
}
// Auto-scroll
this.autoScrollWhileDragging(touch);
}
/**
* Auto-scroll while dragging near edges
*/
autoScrollWhileDragging(touch) {
const board = $('#board');
if (!board) return;
const boardRect = board.getBoundingClientRect();
const scrollThreshold = 50;
const scrollSpeed = 8;
// Clear existing interval
if (this.touchDragScrollInterval) {
clearInterval(this.touchDragScrollInterval);
this.touchDragScrollInterval = null;
}
// Scroll left
if (touch.clientX < boardRect.left + scrollThreshold) {
this.touchDragScrollInterval = setInterval(() => {
board.scrollLeft -= scrollSpeed;
}, 16);
}
// Scroll right
else if (touch.clientX > boardRect.right - scrollThreshold) {
this.touchDragScrollInterval = setInterval(() => {
board.scrollLeft += scrollSpeed;
}, 16);
}
}
/**
* Handle touch drag end
*/
handleTouchDragEnd(e) {
// Clear long press timer
if (this.longPressTimer) {
clearTimeout(this.longPressTimer);
this.longPressTimer = null;
}
if (!this.touchDraggedElement) return;
const touch = e.changedTouches[0];
// Find drop target
this.touchDraggedElement.style.pointerEvents = 'none';
const elemBelow = document.elementFromPoint(touch.clientX, touch.clientY);
this.touchDraggedElement.style.pointerEvents = '';
const columnBody = elemBelow?.closest('.column-body');
if (columnBody) {
const columnId = parseInt(columnBody.closest('.column').dataset.columnId);
const taskId = parseInt(this.touchDraggedElement.dataset.taskId);
const position = this.calculateDropPosition(columnBody, touch.clientY);
// Dispatch move event
document.dispatchEvent(new CustomEvent('task:move', {
detail: { taskId, columnId, position }
}));
}
this.cleanupTouchDrag();
}
/**
* Calculate drop position in column
*/
calculateDropPosition(columnBody, mouseY) {
const taskCards = Array.from(columnBody.querySelectorAll('.task-card:not(.touch-dragging):not(.touch-drag-placeholder)'));
let position = taskCards.length;
for (let i = 0; i < taskCards.length; i++) {
const rect = taskCards[i].getBoundingClientRect();
if (mouseY < rect.top + rect.height / 2) {
position = i;
break;
}
}
return position;
}
/**
* Cancel touch drag
*/
cancelTouchDrag() {
if (this.longPressTimer) {
clearTimeout(this.longPressTimer);
this.longPressTimer = null;
}
this.cleanupTouchDrag();
}
/**
* Cleanup after touch drag
*/
cleanupTouchDrag() {
// Clear scroll interval
if (this.touchDragScrollInterval) {
clearInterval(this.touchDragScrollInterval);
this.touchDragScrollInterval = null;
}
// Reset dragged element
if (this.touchDraggedElement) {
this.touchDraggedElement.classList.remove('touch-dragging');
this.touchDraggedElement.style.position = '';
this.touchDraggedElement.style.left = '';
this.touchDraggedElement.style.top = '';
this.touchDraggedElement.style.width = '';
this.touchDraggedElement.style.zIndex = '';
this.touchDraggedElement.style.transform = '';
}
// Remove placeholder
if (this.touchDragPlaceholder) {
this.touchDragPlaceholder.remove();
this.touchDragPlaceholder = null;
}
// Remove indicators
$$('.column-body.touch-drag-over').forEach(el => el.classList.remove('touch-drag-over'));
document.body.classList.remove('is-touch-dragging');
// Reset state
this.touchDraggedElement = null;
this.touchDragStartX = 0;
this.touchDragStartY = 0;
this.touchDragOffsetX = 0;
this.touchDragOffsetY = 0;
}
}
// Create and export singleton
const mobileManager = new MobileManager();
export default mobileManager;

Datei anzeigen

@ -122,6 +122,13 @@ class NotificationManager {
updateBadge(count) {
this.unreadCount = count;
// Sicherstellen, dass badge-Element existiert
if (!this.badge) {
this.badge = document.getElementById('notification-badge');
}
if (!this.badge) return; // Wenn immer noch nicht gefunden, abbrechen
if (count > 0) {
this.badge.textContent = count > 99 ? '99+' : count;
this.badge.classList.remove('hidden');
@ -441,6 +448,12 @@ class NotificationManager {
this.unreadCount = 0;
this.isDropdownOpen = false;
this.closeDropdown();
// Elements neu binden falls nötig
if (!this.badge || !this.bellContainer) {
this.bindElements();
}
this.render();
}
}

Datei anzeigen

@ -4,7 +4,7 @@
* Offline support and caching
*/
const CACHE_VERSION = '152';
const CACHE_VERSION = '189';
const CACHE_NAME = 'taskmate-v' + CACHE_VERSION;
const STATIC_CACHE_NAME = 'taskmate-static-v' + CACHE_VERSION;
const DYNAMIC_CACHE_NAME = 'taskmate-dynamic-v' + CACHE_VERSION;
@ -39,12 +39,16 @@ const STATIC_ASSETS = [
'/js/notifications.js',
'/js/gitea.js',
'/js/knowledge.js',
'/js/coding.js',
'/js/mobile.js',
'/css/list.css',
'/css/mobile.css',
'/css/admin.css',
'/css/proposals.css',
'/css/notifications.css',
'/css/gitea.css',
'/css/knowledge.css'
'/css/knowledge.css',
'/css/coding.css'
];
// API routes to cache