fixed SPA
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
FROM node:22-alpine
|
FROM node:24-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV CXXFLAGS="-std=c++20"
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
RUN apk add --no-cache python3 make g++ \
|
RUN apk add --no-cache python3 make g++ \
|
||||||
|
|||||||
1752
backend/package-lock.json
generated
Normal file
1752
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,9 +10,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"better-sqlite3": "^9.2.2",
|
"better-sqlite3": "^12.5.0",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"nodemailer": "^6.9.14"
|
"nodemailer": "^7.0.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.0.2"
|
"nodemon": "^3.0.2"
|
||||||
|
|||||||
@@ -747,6 +747,24 @@ function normalizeDailyBookmarkMarker(value) {
|
|||||||
return marker;
|
return marker;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeDailyBookmarkActive(value) {
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const trimmed = value.trim().toLowerCase();
|
||||||
|
if (trimmed === 'false' || trimmed === '0' || trimmed === 'off') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (trimmed === 'true' || trimmed === '1' || trimmed === 'on') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
function serializeDailyBookmark(row, dayKey) {
|
function serializeDailyBookmark(row, dayKey) {
|
||||||
if (!row) {
|
if (!row) {
|
||||||
return null;
|
return null;
|
||||||
@@ -760,6 +778,7 @@ function serializeDailyBookmark(row, dayKey) {
|
|||||||
title: row.title,
|
title: row.title,
|
||||||
url_template: row.url_template,
|
url_template: row.url_template,
|
||||||
marker: row.marker || '',
|
marker: row.marker || '',
|
||||||
|
is_active: Number(row.is_active ?? 1) !== 0,
|
||||||
resolved_url: resolvedUrl,
|
resolved_url: resolvedUrl,
|
||||||
notes: row.notes || '',
|
notes: row.notes || '',
|
||||||
created_at: sqliteTimestampToUTC(row.created_at),
|
created_at: sqliteTimestampToUTC(row.created_at),
|
||||||
@@ -1218,6 +1237,7 @@ db.exec(`
|
|||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
url_template TEXT NOT NULL,
|
url_template TEXT NOT NULL,
|
||||||
notes TEXT,
|
notes TEXT,
|
||||||
|
is_active INTEGER NOT NULL DEFAULT 1,
|
||||||
marker TEXT DEFAULT '',
|
marker TEXT DEFAULT '',
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
@@ -1246,6 +1266,7 @@ db.exec(`
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
ensureColumn('daily_bookmarks', 'marker', 'marker TEXT DEFAULT \'\'');
|
ensureColumn('daily_bookmarks', 'marker', 'marker TEXT DEFAULT \'\'');
|
||||||
|
ensureColumn('daily_bookmarks', 'is_active', 'is_active INTEGER NOT NULL DEFAULT 1');
|
||||||
|
|
||||||
const listDailyBookmarksStmt = db.prepare(`
|
const listDailyBookmarksStmt = db.prepare(`
|
||||||
SELECT
|
SELECT
|
||||||
@@ -1253,6 +1274,7 @@ const listDailyBookmarksStmt = db.prepare(`
|
|||||||
b.title,
|
b.title,
|
||||||
b.url_template,
|
b.url_template,
|
||||||
b.notes,
|
b.notes,
|
||||||
|
b.is_active,
|
||||||
b.marker,
|
b.marker,
|
||||||
b.created_at,
|
b.created_at,
|
||||||
b.updated_at,
|
b.updated_at,
|
||||||
@@ -1277,6 +1299,7 @@ const getDailyBookmarkStmt = db.prepare(`
|
|||||||
b.title,
|
b.title,
|
||||||
b.url_template,
|
b.url_template,
|
||||||
b.notes,
|
b.notes,
|
||||||
|
b.is_active,
|
||||||
b.marker,
|
b.marker,
|
||||||
b.created_at,
|
b.created_at,
|
||||||
b.updated_at,
|
b.updated_at,
|
||||||
@@ -1296,8 +1319,8 @@ const getDailyBookmarkStmt = db.prepare(`
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
const insertDailyBookmarkStmt = db.prepare(`
|
const insertDailyBookmarkStmt = db.prepare(`
|
||||||
INSERT INTO daily_bookmarks (id, title, url_template, notes, marker)
|
INSERT INTO daily_bookmarks (id, title, url_template, notes, marker, is_active)
|
||||||
VALUES (@id, @title, @url_template, @notes, @marker)
|
VALUES (@id, @title, @url_template, @notes, @marker, @is_active)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const findDailyBookmarkByUrlStmt = db.prepare(`
|
const findDailyBookmarkByUrlStmt = db.prepare(`
|
||||||
@@ -1321,6 +1344,7 @@ const updateDailyBookmarkStmt = db.prepare(`
|
|||||||
url_template = @url_template,
|
url_template = @url_template,
|
||||||
notes = @notes,
|
notes = @notes,
|
||||||
marker = @marker,
|
marker = @marker,
|
||||||
|
is_active = @is_active,
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = @id
|
WHERE id = @id
|
||||||
`);
|
`);
|
||||||
@@ -3942,6 +3966,9 @@ app.post('/api/daily-bookmarks', (req, res) => {
|
|||||||
const normalizedTitle = normalizeDailyBookmarkTitle(payload.title || payload.label, normalizedUrl);
|
const normalizedTitle = normalizeDailyBookmarkTitle(payload.title || payload.label, normalizedUrl);
|
||||||
const normalizedNotes = normalizeDailyBookmarkNotes(payload.notes);
|
const normalizedNotes = normalizeDailyBookmarkNotes(payload.notes);
|
||||||
const normalizedMarker = normalizeDailyBookmarkMarker(payload.marker || payload.tag);
|
const normalizedMarker = normalizeDailyBookmarkMarker(payload.marker || payload.tag);
|
||||||
|
const normalizedActive = normalizeDailyBookmarkActive(
|
||||||
|
payload.is_active ?? payload.active ?? true
|
||||||
|
);
|
||||||
|
|
||||||
if (!normalizedUrl) {
|
if (!normalizedUrl) {
|
||||||
return res.status(400).json({ error: 'URL-Template ist erforderlich' });
|
return res.status(400).json({ error: 'URL-Template ist erforderlich' });
|
||||||
@@ -3958,7 +3985,8 @@ app.post('/api/daily-bookmarks', (req, res) => {
|
|||||||
title: normalizedTitle,
|
title: normalizedTitle,
|
||||||
url_template: normalizedUrl,
|
url_template: normalizedUrl,
|
||||||
notes: normalizedNotes,
|
notes: normalizedNotes,
|
||||||
marker: normalizedMarker
|
marker: normalizedMarker,
|
||||||
|
is_active: normalizedActive
|
||||||
});
|
});
|
||||||
upsertDailyBookmarkCheckStmt.run({ bookmarkId: id, dayKey });
|
upsertDailyBookmarkCheckStmt.run({ bookmarkId: id, dayKey });
|
||||||
const saved = getDailyBookmarkStmt.get({ bookmarkId: id, dayKey });
|
const saved = getDailyBookmarkStmt.get({ bookmarkId: id, dayKey });
|
||||||
@@ -3978,6 +4006,9 @@ app.post('/api/daily-bookmarks/import', (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const normalizedMarker = normalizeDailyBookmarkMarker(payload.marker || payload.tag);
|
const normalizedMarker = normalizeDailyBookmarkMarker(payload.marker || payload.tag);
|
||||||
|
const normalizedActive = normalizeDailyBookmarkActive(
|
||||||
|
payload.is_active ?? payload.active ?? true
|
||||||
|
);
|
||||||
const collected = [];
|
const collected = [];
|
||||||
const addValue = (value) => {
|
const addValue = (value) => {
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== 'string') {
|
||||||
@@ -4030,7 +4061,8 @@ app.post('/api/daily-bookmarks/import', (req, res) => {
|
|||||||
title,
|
title,
|
||||||
url_template: template,
|
url_template: template,
|
||||||
notes: '',
|
notes: '',
|
||||||
marker: normalizedMarker
|
marker: normalizedMarker,
|
||||||
|
is_active: normalizedActive
|
||||||
});
|
});
|
||||||
const saved = getDailyBookmarkStmt.get({ bookmarkId: id, dayKey });
|
const saved = getDailyBookmarkStmt.get({ bookmarkId: id, dayKey });
|
||||||
if (saved) {
|
if (saved) {
|
||||||
@@ -4087,6 +4119,9 @@ app.put('/api/daily-bookmarks/:bookmarkId', (req, res) => {
|
|||||||
const normalizedMarker = normalizeDailyBookmarkMarker(
|
const normalizedMarker = normalizeDailyBookmarkMarker(
|
||||||
payload.marker ?? existing.marker ?? ''
|
payload.marker ?? existing.marker ?? ''
|
||||||
);
|
);
|
||||||
|
const normalizedActive = normalizeDailyBookmarkActive(
|
||||||
|
payload.is_active ?? payload.active ?? Number(existing.is_active ?? 1)
|
||||||
|
);
|
||||||
|
|
||||||
if (!normalizedUrl) {
|
if (!normalizedUrl) {
|
||||||
return res.status(400).json({ error: 'URL-Template ist erforderlich' });
|
return res.status(400).json({ error: 'URL-Template ist erforderlich' });
|
||||||
@@ -4106,7 +4141,8 @@ app.put('/api/daily-bookmarks/:bookmarkId', (req, res) => {
|
|||||||
title: normalizedTitle,
|
title: normalizedTitle,
|
||||||
url_template: normalizedUrl,
|
url_template: normalizedUrl,
|
||||||
notes: normalizedNotes,
|
notes: normalizedNotes,
|
||||||
marker: normalizedMarker
|
marker: normalizedMarker,
|
||||||
|
is_active: normalizedActive
|
||||||
});
|
});
|
||||||
const updated = getDailyBookmarkStmt.get({ bookmarkId, dayKey });
|
const updated = getDailyBookmarkStmt.get({ bookmarkId, dayKey });
|
||||||
res.json(serializeDailyBookmark(updated, dayKey));
|
res.json(serializeDailyBookmark(updated, dayKey));
|
||||||
@@ -4151,6 +4187,9 @@ app.post('/api/daily-bookmarks/:bookmarkId/check', (req, res) => {
|
|||||||
if (!existing) {
|
if (!existing) {
|
||||||
return res.status(404).json({ error: 'Bookmark nicht gefunden' });
|
return res.status(404).json({ error: 'Bookmark nicht gefunden' });
|
||||||
}
|
}
|
||||||
|
if (Number(existing.is_active ?? 1) === 0) {
|
||||||
|
return res.status(400).json({ error: 'Bookmark ist deaktiviert' });
|
||||||
|
}
|
||||||
|
|
||||||
upsertDailyBookmarkCheckStmt.run({ bookmarkId, dayKey });
|
upsertDailyBookmarkCheckStmt.run({ bookmarkId, dayKey });
|
||||||
const updated = getDailyBookmarkStmt.get({ bookmarkId, dayKey });
|
const updated = getDailyBookmarkStmt.get({ bookmarkId, dayKey });
|
||||||
@@ -4178,6 +4217,9 @@ app.delete('/api/daily-bookmarks/:bookmarkId/check', (req, res) => {
|
|||||||
if (!existing) {
|
if (!existing) {
|
||||||
return res.status(404).json({ error: 'Bookmark nicht gefunden' });
|
return res.status(404).json({ error: 'Bookmark nicht gefunden' });
|
||||||
}
|
}
|
||||||
|
if (Number(existing.is_active ?? 1) === 0) {
|
||||||
|
return res.status(400).json({ error: 'Bookmark ist deaktiviert' });
|
||||||
|
}
|
||||||
|
|
||||||
deleteDailyBookmarkCheckStmt.run({ bookmarkId, dayKey });
|
deleteDailyBookmarkCheckStmt.run({ bookmarkId, dayKey });
|
||||||
const updated = getDailyBookmarkStmt.get({ bookmarkId, dayKey });
|
const updated = getDailyBookmarkStmt.get({ bookmarkId, dayKey });
|
||||||
|
|||||||
@@ -1,50 +1,58 @@
|
|||||||
:root {
|
:root {
|
||||||
--bg: #0b1020;
|
--automation-bg: #f0f2f5;
|
||||||
--card: #0f172a;
|
--automation-card: #ffffff;
|
||||||
--card-soft: #131c35;
|
--automation-card-soft: #f7f8fa;
|
||||||
--border: rgba(255, 255, 255, 0.08);
|
--automation-border: #e4e6eb;
|
||||||
--muted: #94a3b8;
|
--automation-muted: #6b7280;
|
||||||
--text: #e2e8f0;
|
--automation-text: #0f172a;
|
||||||
--accent: #10b981;
|
--automation-accent: #1877f2;
|
||||||
--accent-2: #0ea5e9;
|
--automation-accent-2: #2563eb;
|
||||||
--danger: #ef4444;
|
--automation-danger: #dc2626;
|
||||||
--success: #22c55e;
|
--automation-success: #059669;
|
||||||
--shadow: 0 20px 60px rgba(0, 0, 0, 0.25);
|
--automation-shadow-sm: 0 1px 3px rgba(15, 23, 42, 0.08);
|
||||||
|
--automation-shadow-md: 0 14px 45px rgba(15, 23, 42, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
.automation-view * {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
.automation-view {
|
||||||
margin: 0;
|
color: var(--automation-text);
|
||||||
font-family: 'Manrope', 'Inter', system-ui, -apple-system, sans-serif;
|
|
||||||
background: radial-gradient(120% 120% at 10% 20%, rgba(16, 185, 129, 0.12), transparent 40%),
|
|
||||||
radial-gradient(120% 120% at 80% 0%, rgba(14, 165, 233, 0.12), transparent 40%),
|
|
||||||
var(--bg);
|
|
||||||
color: var(--text);
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 32px 18px 48px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.auto-shell {
|
.automation-view .auto-shell {
|
||||||
max-width: 1500px;
|
|
||||||
margin: 0 auto;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 18px;
|
gap: 16px;
|
||||||
|
padding-bottom: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auto-hero {
|
.automation-view .auto-hero {
|
||||||
background: linear-gradient(135deg, rgba(16, 185, 129, 0.12), rgba(14, 165, 233, 0.08)),
|
position: relative;
|
||||||
var(--card);
|
overflow: hidden;
|
||||||
border: 1px solid var(--border);
|
background: var(--automation-card);
|
||||||
border-radius: 18px;
|
border: 1px solid var(--automation-border);
|
||||||
padding: 22px 22px 18px;
|
border-radius: 14px;
|
||||||
box-shadow: var(--shadow);
|
padding: 18px 20px;
|
||||||
|
box-shadow: var(--automation-shadow-sm);
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-head {
|
.automation-view .auto-hero::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(120deg, rgba(24, 119, 242, 0.08), rgba(37, 99, 235, 0.05));
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.automation-view .auto-hero > * {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.automation-view .hero-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -52,91 +60,78 @@ body {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-text h1 {
|
.automation-view .hero-text h1 {
|
||||||
margin: 4px 0;
|
margin: 4px 0;
|
||||||
font-size: 28px;
|
font-size: 24px;
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.01em;
|
||||||
|
color: var(--automation-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.subline {
|
.automation-view .eyebrow {
|
||||||
margin: 6px 0 10px;
|
font-size: 12px;
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.eyebrow {
|
|
||||||
font-size: 13px;
|
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--accent-2);
|
color: var(--automation-accent-2);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-pills {
|
.automation-view .back-link {
|
||||||
|
color: var(--automation-accent);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.automation-view .back-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.automation-view .hero-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pill {
|
.automation-view .hero-stats {
|
||||||
background: rgba(255, 255, 255, 0.04);
|
margin-top: 12px;
|
||||||
color: var(--text);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 999px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-link {
|
|
||||||
color: var(--muted);
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-stats {
|
|
||||||
margin-top: 14px;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat {
|
.automation-view .stat {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: var(--automation-card-soft);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--automation-border);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
box-shadow: var(--automation-shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-label {
|
.automation-view .stat-label {
|
||||||
color: var(--muted);
|
color: var(--automation-muted);
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
margin: 0 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-value {
|
.automation-view .stat-value {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auto-grid {
|
.automation-view .panel {
|
||||||
display: grid;
|
background: var(--automation-card);
|
||||||
grid-template-columns: 1fr;
|
border: 1px solid var(--automation-border);
|
||||||
gap: 18px;
|
border-radius: 12px;
|
||||||
|
padding: 18px 18px 16px;
|
||||||
|
box-shadow: var(--automation-shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel {
|
.automation-view .panel-header {
|
||||||
background: var(--card);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 18px;
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-header {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -145,137 +140,147 @@ body {
|
|||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-eyebrow {
|
.automation-view .panel-eyebrow {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--muted);
|
color: var(--automation-muted);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-actions {
|
.automation-view .panel-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-grid {
|
.automation-view .form-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field {
|
.automation-view .field {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field.full {
|
.automation-view .field.full {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field.inline {
|
.automation-view .field.inline {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.field[data-section] {
|
|
||||||
|
.automation-view .field[data-section] {
|
||||||
display: grid;
|
display: grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
.automation-view label {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text);
|
color: var(--automation-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
.automation-view input,
|
||||||
textarea,
|
.automation-view textarea,
|
||||||
select {
|
.automation-view select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--automation-border);
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
background: #0c1427;
|
background: #fff;
|
||||||
color: var(--text);
|
color: var(--automation-text);
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
input:focus,
|
.automation-view input:focus,
|
||||||
textarea:focus,
|
.automation-view textarea:focus,
|
||||||
select:focus {
|
.automation-view select:focus {
|
||||||
border-color: var(--accent-2);
|
border-color: var(--automation-accent-2);
|
||||||
box-shadow: 0 0 0 2px rgba(14, 165, 233, 0.15);
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
.automation-view textarea {
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
small {
|
.automation-view small {
|
||||||
color: var(--muted);
|
color: var(--automation-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-hint {
|
.automation-view .template-hint {
|
||||||
border: 1px dashed var(--border);
|
border: 1px dashed var(--automation-border);
|
||||||
background: rgba(255, 255, 255, 0.02);
|
background: #f8fafc;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||||
color: var(--muted);
|
color: var(--automation-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-title {
|
.automation-view .template-title {
|
||||||
margin: 0 0 6px;
|
margin: 0 0 6px;
|
||||||
color: var(--text);
|
color: var(--automation-text);
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-copy {
|
.automation-view .template-copy {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-status {
|
.automation-view .form-status {
|
||||||
min-height: 22px;
|
min-height: 22px;
|
||||||
color: var(--muted);
|
color: var(--automation-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-status.error {
|
.automation-view .form-status.error {
|
||||||
color: var(--danger);
|
color: var(--automation-danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-status.success {
|
.automation-view .form-status.success {
|
||||||
color: var(--success);
|
color: var(--automation-success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.auto-table {
|
.automation-view .table-wrap {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.automation-view .auto-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
min-width: 720px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auto-table th,
|
.automation-view .auto-table th,
|
||||||
.auto-table td {
|
.automation-view .auto-table td {
|
||||||
padding: 10px 8px;
|
padding: 10px 8px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--automation-border);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auto-table th {
|
.automation-view .auto-table th {
|
||||||
color: var(--muted);
|
color: var(--automation-muted);
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
|
background: var(--automation-card-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.auto-table th[data-sort-column="runs"],
|
.automation-view .auto-table th[data-sort-column="runs"],
|
||||||
.auto-table td.runs-count {
|
.automation-view .auto-table td.runs-count {
|
||||||
width: 1%;
|
width: 1%;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auto-table .sort-indicator {
|
.automation-view .auto-table .sort-indicator {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
width: 10px;
|
width: 10px;
|
||||||
@@ -284,108 +289,112 @@ small {
|
|||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
border-left: 0;
|
border-left: 0;
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
opacity: 0.35;
|
opacity: 0.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auto-table th.sort-asc .sort-indicator {
|
.automation-view .auto-table th.sort-asc .sort-indicator {
|
||||||
border-top: 6px solid var(--accent-2);
|
border-top: 6px solid var(--automation-accent-2);
|
||||||
transform: rotate(225deg);
|
transform: rotate(225deg);
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auto-table th.sort-desc .sort-indicator {
|
.automation-view .auto-table th.sort-desc .sort-indicator {
|
||||||
border-top: 6px solid var(--accent-2);
|
border-top: 6px solid var(--automation-accent-2);
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-filter-row input,
|
.automation-view .table-filter-row th {
|
||||||
.table-filter-row select {
|
background: var(--automation-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.automation-view .table-filter-row input,
|
||||||
|
.automation-view .table-filter-row select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--automation-border);
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
background: #0c1427;
|
background: #fff;
|
||||||
color: var(--text);
|
color: var(--automation-text);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auto-table tr.is-selected {
|
.automation-view .auto-table tr.is-selected td {
|
||||||
background: rgba(14, 165, 233, 0.08);
|
background: #eef2ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-title {
|
.automation-view .row-title {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text);
|
color: var(--automation-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-sub {
|
.automation-view .row-sub {
|
||||||
color: var(--muted);
|
color: var(--automation-muted);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-wrap {
|
.automation-view .badge {
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
background: rgba(255, 255, 255, 0.04);
|
background: #eef2ff;
|
||||||
border: 1px solid var(--border);
|
color: var(--automation-accent-2);
|
||||||
|
border: 1px solid rgba(37, 99, 235, 0.28);
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge.success {
|
.automation-view .badge.success {
|
||||||
color: var(--success);
|
background: rgba(5, 150, 105, 0.12);
|
||||||
border-color: rgba(34, 197, 94, 0.35);
|
color: var(--automation-success);
|
||||||
|
border-color: rgba(5, 150, 105, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge.error {
|
.automation-view .badge.error {
|
||||||
color: var(--danger);
|
background: rgba(220, 38, 38, 0.12);
|
||||||
border-color: rgba(239, 68, 68, 0.35);
|
color: var(--automation-danger);
|
||||||
|
border-color: rgba(220, 38, 38, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-actions {
|
.automation-view .row-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hidden-value {
|
.automation-view .hidden-value {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-btn {
|
.automation-view .icon-btn {
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--automation-border);
|
||||||
background: rgba(255, 255, 255, 0.04);
|
background: var(--automation-card-soft);
|
||||||
color: var(--text);
|
color: var(--automation-text);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
transition: transform 0.15s ease, opacity 0.15s ease, border-color 0.15s ease;
|
transition: transform 0.15s ease, opacity 0.15s ease, border-color 0.15s ease, box-shadow 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-btn:hover {
|
.automation-view .icon-btn:hover {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
opacity: 0.95;
|
opacity: 0.95;
|
||||||
border-color: rgba(255, 255, 255, 0.2);
|
border-color: #d1d5db;
|
||||||
|
box-shadow: var(--automation-shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-btn.danger {
|
.automation-view .icon-btn.danger {
|
||||||
color: #ef4444;
|
color: var(--automation-danger);
|
||||||
border-color: rgba(239, 68, 68, 0.35);
|
border-color: rgba(220, 38, 38, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-btn,
|
.automation-view .primary-btn,
|
||||||
.secondary-btn,
|
.automation-view .secondary-btn,
|
||||||
.ghost-btn {
|
.automation-view .ghost-btn {
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
@@ -395,49 +404,50 @@ small {
|
|||||||
transition: transform 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;
|
transition: transform 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-btn {
|
.automation-view .primary-btn {
|
||||||
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
background: linear-gradient(135deg, var(--automation-accent-2), var(--automation-accent));
|
||||||
color: #0b1020;
|
color: #fff;
|
||||||
box-shadow: 0 10px 30px rgba(14, 165, 233, 0.35);
|
box-shadow: 0 10px 30px rgba(24, 119, 242, 0.25);
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary-btn {
|
.automation-view .secondary-btn {
|
||||||
background: rgba(255, 255, 255, 0.06);
|
background: #e4e6eb;
|
||||||
color: var(--text);
|
color: var(--automation-text);
|
||||||
border-color: var(--border);
|
border-color: #d1d5db;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ghost-btn {
|
.automation-view .ghost-btn {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text);
|
color: var(--automation-text);
|
||||||
border-color: var(--border);
|
border-color: var(--automation-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-btn:hover,
|
.automation-view .primary-btn:hover,
|
||||||
.secondary-btn:hover,
|
.automation-view .secondary-btn:hover,
|
||||||
.ghost-btn:hover {
|
.automation-view .ghost-btn:hover {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
opacity: 0.95;
|
opacity: 0.95;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-status,
|
.automation-view .list-status,
|
||||||
.runs-status,
|
.automation-view .runs-status,
|
||||||
.import-status {
|
.automation-view .import-status {
|
||||||
min-height: 20px;
|
min-height: 20px;
|
||||||
color: var(--muted);
|
color: var(--automation-muted);
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-input {
|
.automation-view .filter-input {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--automation-border);
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
background: #0c1427;
|
background: #fff;
|
||||||
color: var(--text);
|
color: var(--automation-text);
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.runs-list {
|
.automation-view .runs-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -445,14 +455,15 @@ small {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.run-item {
|
.automation-view .run-item {
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--automation-border);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background: #0c1427;
|
background: var(--automation-card-soft);
|
||||||
|
box-shadow: var(--automation-shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.run-top {
|
.automation-view .run-top {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -460,58 +471,57 @@ small {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.run-meta {
|
.automation-view .run-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--muted);
|
color: var(--automation-muted);
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.run-body {
|
.automation-view .run-body {
|
||||||
margin: 8px 0 0;
|
margin: 8px 0 0;
|
||||||
color: var(--text);
|
color: var(--automation-text);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.runs-hint {
|
.automation-view .runs-hint {
|
||||||
color: var(--muted);
|
color: var(--automation-muted);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.import-panel textarea {
|
.automation-view .import-hint {
|
||||||
width: 100%;
|
color: var(--automation-muted);
|
||||||
}
|
|
||||||
|
|
||||||
.import-hint {
|
|
||||||
color: var(--muted);
|
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch {
|
.automation-view .switch {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--automation-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch input {
|
.automation-view .switch input {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch-slider {
|
.automation-view .switch-slider {
|
||||||
width: 42px;
|
width: 42px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: #d1d5db;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: background 0.2s ease;
|
transition: background 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch-slider::after {
|
.automation-view .switch-slider::after {
|
||||||
content: '';
|
content: "";
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
@@ -519,51 +529,52 @@ small {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
left: 2px;
|
left: 2px;
|
||||||
|
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.25);
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch input:checked + .switch-slider {
|
.automation-view .switch input:checked + .switch-slider {
|
||||||
background: var(--accent);
|
background: var(--automation-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch input:checked + .switch-slider::after {
|
.automation-view .switch input:checked + .switch-slider::after {
|
||||||
transform: translateX(20px);
|
transform: translateX(20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.switch-label {
|
.automation-view .switch-label {
|
||||||
color: var(--muted);
|
color: var(--automation-muted);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal {
|
.automation-view .modal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: rgba(0, 0, 0, 0.45);
|
padding: 24px;
|
||||||
backdrop-filter: blur(2px);
|
background: rgba(15, 23, 42, 0.55);
|
||||||
z-index: 900;
|
z-index: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal__backdrop {
|
.automation-view .modal__backdrop {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal__content {
|
.automation-view .modal__content {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: var(--card);
|
background: var(--automation-card);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--automation-border);
|
||||||
border-radius: 16px;
|
border-radius: 14px;
|
||||||
padding: 18px;
|
padding: 20px 20px 16px;
|
||||||
width: min(1100px, 96vw);
|
width: min(1100px, 96vw);
|
||||||
max-height: 92vh;
|
max-height: 92vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--automation-shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal__header {
|
.automation-view .modal__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -571,25 +582,25 @@ small {
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-actions {
|
.automation-view .modal-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal[hidden] {
|
.automation-view .modal[hidden] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-panel {
|
.automation-view .preview-panel {
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--automation-border);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background: rgba(255, 255, 255, 0.02);
|
background: var(--automation-card-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-header {
|
.automation-view .preview-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -597,29 +608,29 @@ small {
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-grid {
|
.automation-view .preview-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-block {
|
.automation-view .preview-block {
|
||||||
border: 1px dashed var(--border);
|
border: 1px dashed var(--automation-border);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
background: #0c1427;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-label {
|
.automation-view .preview-label {
|
||||||
margin: 0 0 4px;
|
margin: 0 0 4px;
|
||||||
color: var(--muted);
|
color: var(--automation-muted);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-value {
|
.automation-view .preview-value {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--text);
|
color: var(--automation-text);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
@@ -627,65 +638,65 @@ small {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-hint {
|
.automation-view .preview-hint {
|
||||||
color: var(--muted);
|
color: var(--automation-muted);
|
||||||
margin: 8px 0 0;
|
margin: 8px 0 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder-table {
|
.automation-view .placeholder-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder-table td {
|
.automation-view .placeholder-table td {
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--automation-border);
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--text);
|
color: var(--automation-text);
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder-table td.placeholder-key {
|
.automation-view .placeholder-table td.placeholder-key {
|
||||||
width: 30%;
|
width: 30%;
|
||||||
color: var(--muted);
|
color: var(--automation-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder-hint {
|
.automation-view .placeholder-hint {
|
||||||
color: var(--muted);
|
color: var(--automation-muted);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin: 6px 0 0;
|
margin: 6px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1050px) {
|
@media (max-width: 1050px) {
|
||||||
.auto-grid {
|
.automation-view .form-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
body {
|
.automation-view .auto-shell {
|
||||||
padding: 16px 12px 32px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-header {
|
.automation-view .panel-header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-actions {
|
.automation-view .panel-actions {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-grid {
|
.automation-view .hero-head {
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-head {
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.automation-view .modal {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,299 +2,20 @@
|
|||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Automationen – Post Tracker</title>
|
<title>Automationen – Post Tracker</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="assets/app-icon-64.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="assets/app-icon-64.png">
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<script>
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
(function redirectToShell() {
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
const url = new URL(window.location.href);
|
||||||
<link rel="stylesheet" href="automation.css">
|
const params = new URLSearchParams(url.search);
|
||||||
|
params.set('view', 'automation');
|
||||||
|
const target = `index.html${params.toString() ? `?${params}` : ''}${url.hash}`;
|
||||||
|
window.location.replace(target);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="auto-shell">
|
<p>Weiterleitung zur Automations-Ansicht…</p>
|
||||||
<header class="auto-hero">
|
|
||||||
<div class="hero-head">
|
|
||||||
<div class="hero-text">
|
|
||||||
<a class="back-link" href="index.html">← Zurück zur App</a>
|
|
||||||
<p class="eyebrow">Automatisierte Requests</p>
|
|
||||||
<h1>Request Automationen</h1>
|
|
||||||
</div>
|
|
||||||
<div class="hero-actions">
|
|
||||||
<button class="ghost-btn" id="openImportBtn" type="button">📥 Vorlage importieren</button>
|
|
||||||
<button class="primary-btn" id="newAutomationBtn" type="button">+ Neue Automation</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="hero-stats" id="heroStats"></div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="auto-grid">
|
|
||||||
<section class="panel list-panel">
|
|
||||||
<div class="panel-header">
|
|
||||||
<div>
|
|
||||||
<p class="panel-eyebrow">Geplante Requests</p>
|
|
||||||
<h2>Automationen</h2>
|
|
||||||
</div>
|
|
||||||
<div class="panel-actions">
|
|
||||||
<input id="tableFilterInput" class="filter-input" type="search" placeholder="Filtern nach Name/Typ…" />
|
|
||||||
<button class="ghost-btn" id="refreshBtn" type="button">Aktualisieren</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="listStatus" class="list-status" aria-live="polite"></div>
|
|
||||||
<div class="table-wrap" id="automationTable">
|
|
||||||
<table class="auto-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th data-sort-column="name">Name<span class="sort-indicator"></span></th>
|
|
||||||
<th data-sort-column="next">Nächster Lauf<span class="sort-indicator"></span></th>
|
|
||||||
<th data-sort-column="last">Letzter Lauf<span class="sort-indicator"></span></th>
|
|
||||||
<th data-sort-column="status">Status<span class="sort-indicator"></span></th>
|
|
||||||
<th data-sort-column="runs">#Läufe<span class="sort-indicator"></span></th>
|
|
||||||
<th>Aktionen</th>
|
|
||||||
</tr>
|
|
||||||
<tr class="table-filter-row">
|
|
||||||
<th><input id="filterName" type="search" placeholder="Name/Typ/E-Mail/URL"></th>
|
|
||||||
<th><input id="filterNext" type="search" placeholder="z.B. heute"></th>
|
|
||||||
<th><input id="filterLast" type="search" placeholder="z.B. HTTP 200"></th>
|
|
||||||
<th>
|
|
||||||
<select id="filterStatus">
|
|
||||||
<option value="">Alle</option>
|
|
||||||
<option value="success">OK</option>
|
|
||||||
<option value="error">Fehler</option>
|
|
||||||
</select>
|
|
||||||
</th>
|
|
||||||
<th><input id="filterRuns" type="number" min="0" placeholder="≥"></th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="requestTableBody" class="list"></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<section class="panel runs-panel">
|
|
||||||
<div class="panel-header">
|
|
||||||
<div>
|
|
||||||
<p class="panel-eyebrow">Verlauf</p>
|
|
||||||
<h2>Run-Historie</h2>
|
|
||||||
</div>
|
|
||||||
<p class="runs-hint">Letzte Läufe der ausgewählten Automation.</p>
|
|
||||||
</div>
|
|
||||||
<div id="runsStatus" class="runs-status" aria-live="polite"></div>
|
|
||||||
<ul id="runsList" class="runs-list"></ul>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="formModal" class="modal" hidden>
|
|
||||||
<div class="modal__backdrop" id="formModalBackdrop"></div>
|
|
||||||
<div class="modal__content" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
|
|
||||||
<div class="modal__header">
|
|
||||||
<div>
|
|
||||||
<p class="panel-eyebrow" id="formModeLabel">Neue Automation</p>
|
|
||||||
<h2 id="modalTitle">Request & Zeitplan</h2>
|
|
||||||
</div>
|
|
||||||
<button class="ghost-btn" id="modalCloseBtn" type="button">×</button>
|
|
||||||
</div>
|
|
||||||
<form id="automationForm" class="form-grid" novalidate>
|
|
||||||
<div class="field">
|
|
||||||
<label for="typeSelect">Typ</label>
|
|
||||||
<select id="typeSelect">
|
|
||||||
<option value="request">HTTP Request</option>
|
|
||||||
<option value="email">E-Mail</option>
|
|
||||||
<option value="flow">Flow (bis 3 Schritte)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="nameInput">Name *</label>
|
|
||||||
<input id="nameInput" type="text" placeholder="API Ping (stündlich)" required maxlength="160">
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="descriptionInput">Notizen</label>
|
|
||||||
<textarea id="descriptionInput" rows="2" placeholder="Kurzbeschreibung oder Zweck"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="field" data-section="http">
|
|
||||||
<label for="urlInput">URL-Template *</label>
|
|
||||||
<input id="urlInput" type="url" placeholder="https://api.example.com/{{date}}/trigger" required>
|
|
||||||
</div>
|
|
||||||
<div class="field inline" data-section="http">
|
|
||||||
<label for="methodSelect">Methode</label>
|
|
||||||
<select id="methodSelect">
|
|
||||||
<option value="GET">GET</option>
|
|
||||||
<option value="POST">POST</option>
|
|
||||||
<option value="PUT">PUT</option>
|
|
||||||
<option value="PATCH">PATCH</option>
|
|
||||||
<option value="DELETE">DELETE</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field inline">
|
|
||||||
<label for="activeToggle">Aktiv</label>
|
|
||||||
<label class="switch">
|
|
||||||
<input type="checkbox" id="activeToggle" checked>
|
|
||||||
<span class="switch-slider"></span>
|
|
||||||
<span class="switch-label">Plan aktiv</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="field" data-section="http">
|
|
||||||
<label for="headersInput">Headers (Key: Value pro Zeile oder JSON)</label>
|
|
||||||
<textarea id="headersInput" rows="3" placeholder="Authorization: Bearer {{token}}\nContent-Type: application/json"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="field" data-section="http">
|
|
||||||
<label for="bodyInput">Body (optional, Templates möglich)</label>
|
|
||||||
<textarea id="bodyInput" rows="5" placeholder='{"date":"{{date}}","id":"{{uuid}}"}'></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="field full" data-section="email">
|
|
||||||
<label for="emailToInput">E-Mail Empfänger *</label>
|
|
||||||
<input id="emailToInput" type="text" placeholder="max@example.com, lisa@example.com">
|
|
||||||
</div>
|
|
||||||
<div class="field full" data-section="email">
|
|
||||||
<label for="emailSubjectInput">Betreff *</label>
|
|
||||||
<input id="emailSubjectInput" type="text" placeholder="Status Update {{date}}">
|
|
||||||
</div>
|
|
||||||
<div class="field full" data-section="email">
|
|
||||||
<label for="emailBodyInput">Body *</label>
|
|
||||||
<textarea id="emailBodyInput" rows="6" placeholder="Hallo,\nheutiger Status: {{uuid}}"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="field full" data-section="flow">
|
|
||||||
<div class="template-hint">
|
|
||||||
<p class="template-title">Flow Schritte</p>
|
|
||||||
<p class="template-copy">Max. 3 Schritte, Kontext steht als {{step1_json}}, {{step1_text}}, {{step1_status_code}} usw. im nächsten Schritt zur Verfügung.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field" data-section="flow">
|
|
||||||
<label for="flowStep1Url">Step 1 URL *</label>
|
|
||||||
<input id="flowStep1Url" type="url" placeholder="https://api.example.com/first">
|
|
||||||
</div>
|
|
||||||
<div class="field inline" data-section="flow">
|
|
||||||
<label for="flowStep1Method">Step 1 Methode</label>
|
|
||||||
<select id="flowStep1Method">
|
|
||||||
<option value="GET">GET</option>
|
|
||||||
<option value="POST">POST</option>
|
|
||||||
<option value="PUT">PUT</option>
|
|
||||||
<option value="PATCH">PATCH</option>
|
|
||||||
<option value="DELETE">DELETE</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field" data-section="flow">
|
|
||||||
<label for="flowStep1Headers">Step 1 Headers</label>
|
|
||||||
<textarea id="flowStep1Headers" rows="2" placeholder="Authorization: Bearer {{token}}"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="field" data-section="flow">
|
|
||||||
<label for="flowStep1Body">Step 1 Body</label>
|
|
||||||
<textarea id="flowStep1Body" rows="3" placeholder='{"id":"{{uuid}}"}'></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="field" data-section="flow">
|
|
||||||
<label for="flowStep2Url">Step 2 URL (optional)</label>
|
|
||||||
<input id="flowStep2Url" type="url" placeholder="https://api.example.com/second">
|
|
||||||
</div>
|
|
||||||
<div class="field inline" data-section="flow">
|
|
||||||
<label for="flowStep2Method">Step 2 Methode</label>
|
|
||||||
<select id="flowStep2Method">
|
|
||||||
<option value="GET">GET</option>
|
|
||||||
<option value="POST">POST</option>
|
|
||||||
<option value="PUT">PUT</option>
|
|
||||||
<option value="PATCH">PATCH</option>
|
|
||||||
<option value="DELETE">DELETE</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field" data-section="flow">
|
|
||||||
<label for="flowStep2Headers">Step 2 Headers</label>
|
|
||||||
<textarea id="flowStep2Headers" rows="2" placeholder="Authorization: Bearer {{token}}"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="field" data-section="flow">
|
|
||||||
<label for="flowStep2Body">Step 2 Body</label>
|
|
||||||
<textarea id="flowStep2Body" rows="3" placeholder='{"fromStep1":"{{step1_json.id}}"}'></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="field inline">
|
|
||||||
<label for="intervalPreset">Intervall</label>
|
|
||||||
<select id="intervalPreset">
|
|
||||||
<option value="hourly">Jede Stunde</option>
|
|
||||||
<option value="daily">Jeden Tag</option>
|
|
||||||
<option value="custom">Eigene Minuten</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field inline">
|
|
||||||
<label for="intervalMinutesInput">Intervall (Minuten)</label>
|
|
||||||
<input id="intervalMinutesInput" type="number" min="5" max="20160" step="5" value="60">
|
|
||||||
</div>
|
|
||||||
<div class="field inline">
|
|
||||||
<label for="jitterInput">Varianz (Minuten)</label>
|
|
||||||
<input id="jitterInput" type="number" min="0" max="120" step="5" value="10">
|
|
||||||
<small>Auslösung erfolgt zufällig +0…Varianz Min nach dem Intervall.</small>
|
|
||||||
</div>
|
|
||||||
<div class="field inline">
|
|
||||||
<label for="startAtInput">Start ab</label>
|
|
||||||
<input id="startAtInput" type="datetime-local">
|
|
||||||
</div>
|
|
||||||
<div class="field inline">
|
|
||||||
<label for="runUntilInput">Läuft bis</label>
|
|
||||||
<input id="runUntilInput" type="datetime-local">
|
|
||||||
</div>
|
|
||||||
<div class="field full">
|
|
||||||
<div class="template-hint">
|
|
||||||
<p class="template-title">Platzhalter</p>
|
|
||||||
<table class="placeholder-table">
|
|
||||||
<tbody id="placeholderTableBody"></tbody>
|
|
||||||
</table>
|
|
||||||
<p class="placeholder-hint">Beispiele beziehen sich auf den aktuellen Zeitpunkt.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field full">
|
|
||||||
<div class="preview-panel">
|
|
||||||
<div class="preview-header">
|
|
||||||
<div>
|
|
||||||
<p class="panel-eyebrow">Vorschau</p>
|
|
||||||
<h3>Aufgelöste Werte</h3>
|
|
||||||
</div>
|
|
||||||
<button class="secondary-btn" type="button" id="refreshPreviewBtn">Aktualisieren</button>
|
|
||||||
</div>
|
|
||||||
<div class="preview-grid">
|
|
||||||
<div class="preview-block">
|
|
||||||
<p class="preview-label">URL</p>
|
|
||||||
<pre id="previewUrl" class="preview-value">—</pre>
|
|
||||||
</div>
|
|
||||||
<div class="preview-block">
|
|
||||||
<p class="preview-label">Headers</p>
|
|
||||||
<pre id="previewHeaders" class="preview-value">—</pre>
|
|
||||||
</div>
|
|
||||||
<div class="preview-block">
|
|
||||||
<p class="preview-label">Body</p>
|
|
||||||
<pre id="previewBody" class="preview-value">—</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="preview-hint">Die Vorschau nutzt die aktuellen Formularwerte und füllt Platzhalter ({{date}}, {{uuid}}, …) mit Beispielwerten.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="field full modal-actions">
|
|
||||||
<div id="formStatus" class="form-status" aria-live="polite"></div>
|
|
||||||
<div class="panel-actions">
|
|
||||||
<button class="ghost-btn" id="resetFormBtn" type="button">Zurücksetzen</button>
|
|
||||||
<button class="primary-btn" id="saveBtn" type="submit">Speichern</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="importModal" class="modal" hidden>
|
|
||||||
<div class="modal__backdrop" id="importModalBackdrop"></div>
|
|
||||||
<div class="modal__content" role="dialog" aria-modal="true" aria-labelledby="importTitle">
|
|
||||||
<div class="modal__header">
|
|
||||||
<div>
|
|
||||||
<p class="panel-eyebrow">Import</p>
|
|
||||||
<h2 id="importTitle">Vorlage einfügen</h2>
|
|
||||||
</div>
|
|
||||||
<button class="ghost-btn" id="importCloseBtn" type="button">×</button>
|
|
||||||
</div>
|
|
||||||
<p class="import-hint">Füge hier "Copy as cURL", "Copy as fetch" oder Powershell ein. Header, Methode, Body und URL werden übernommen.</p>
|
|
||||||
<textarea id="importInput" rows="7" placeholder="curl https://api.example.com -X POST -H 'Authorization: Bearer token' --data '{\"hello\":\"world\"}'"></textarea>
|
|
||||||
<div class="modal-actions">
|
|
||||||
<div id="importStatus" class="import-status" aria-live="polite"></div>
|
|
||||||
<button class="secondary-btn" id="applyImportBtn" type="button">Vorlage übernehmen</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="vendor/list.min.js"></script>
|
|
||||||
<script src="automation.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
(function () {
|
(function () {
|
||||||
|
let active = false;
|
||||||
|
let initialized = false;
|
||||||
const API_URL = (() => {
|
const API_URL = (() => {
|
||||||
if (window.API_URL) return window.API_URL;
|
if (window.API_URL) return window.API_URL;
|
||||||
try {
|
try {
|
||||||
@@ -455,6 +457,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadRequests() {
|
async function loadRequests() {
|
||||||
|
if (!active) return;
|
||||||
if (!state.loading) {
|
if (!state.loading) {
|
||||||
setStatus(listStatus, 'Lade Automationen...');
|
setStatus(listStatus, 'Lade Automationen...');
|
||||||
}
|
}
|
||||||
@@ -511,6 +514,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateRelativeTimes() {
|
function updateRelativeTimes() {
|
||||||
|
if (!active) return;
|
||||||
renderHero();
|
renderHero();
|
||||||
if (!requestTableBody || !state.requests.length) return;
|
if (!requestTableBody || !state.requests.length) return;
|
||||||
const byId = new Map(state.requests.map((req) => [String(req.id), req]));
|
const byId = new Map(state.requests.map((req) => [String(req.id), req]));
|
||||||
@@ -1105,9 +1109,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleSseMessage(payload) {
|
function handleSseMessage(payload) {
|
||||||
|
if (!active) return;
|
||||||
if (!payload || !payload.type) return;
|
if (!payload || !payload.type) return;
|
||||||
|
const { type, request_id: requestId } = payload;
|
||||||
switch (payload.type) {
|
switch (payload.type) {
|
||||||
case 'automation-run':
|
case 'automation-run':
|
||||||
|
loadRequests();
|
||||||
|
if (requestId && state.selectedId && String(requestId) === String(state.selectedId)) {
|
||||||
|
loadRuns(state.selectedId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'automation-upsert':
|
case 'automation-upsert':
|
||||||
loadRequests();
|
loadRequests();
|
||||||
break;
|
break;
|
||||||
@@ -1117,6 +1128,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startSse() {
|
function startSse() {
|
||||||
|
if (!active) return;
|
||||||
if (sse || typeof EventSource === 'undefined') return;
|
if (sse || typeof EventSource === 'undefined') return;
|
||||||
try {
|
try {
|
||||||
sse = new EventSource(`${API_URL}/events`, { withCredentials: true });
|
sse = new EventSource(`${API_URL}/events`, { withCredentials: true });
|
||||||
@@ -1143,20 +1155,34 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
function ensureRelativeTimer() {
|
||||||
applyPresetDisabling();
|
|
||||||
resetForm();
|
|
||||||
loadRequests();
|
|
||||||
if (!relativeTimer) {
|
if (!relativeTimer) {
|
||||||
updateRelativeTimes();
|
updateRelativeTimes();
|
||||||
relativeTimer = setInterval(updateRelativeTimes, 60000);
|
relativeTimer = setInterval(updateRelativeTimes, 60000);
|
||||||
document.addEventListener('visibilitychange', () => {
|
|
||||||
if (!document.hidden) {
|
|
||||||
updateRelativeTimes();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cleanup(options = {}) {
|
||||||
|
const { keepActive = false } = options;
|
||||||
|
if (!keepActive) {
|
||||||
|
active = false;
|
||||||
|
}
|
||||||
|
if (relativeTimer) {
|
||||||
|
clearInterval(relativeTimer);
|
||||||
|
relativeTimer = null;
|
||||||
|
}
|
||||||
|
if (sse) {
|
||||||
|
sse.close();
|
||||||
|
sse = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
if (initialized) return;
|
||||||
|
applyPresetDisabling();
|
||||||
|
resetForm();
|
||||||
|
ensureRelativeTimer();
|
||||||
|
|
||||||
form.addEventListener('submit', handleSubmit);
|
form.addEventListener('submit', handleSubmit);
|
||||||
intervalPreset.addEventListener('change', applyPresetDisabling);
|
intervalPreset.addEventListener('change', applyPresetDisabling);
|
||||||
resetFormBtn.addEventListener('click', () => {
|
resetFormBtn.addEventListener('click', () => {
|
||||||
@@ -1217,8 +1243,32 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (document.hidden) {
|
||||||
|
cleanup({ keepActive: true });
|
||||||
|
} else {
|
||||||
|
if (active) {
|
||||||
|
ensureRelativeTimer();
|
||||||
startSse();
|
startSse();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.addEventListener('beforeunload', cleanup);
|
||||||
|
window.addEventListener('pagehide', cleanup);
|
||||||
|
window.addEventListener('unload', cleanup);
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function activate() {
|
||||||
init();
|
init();
|
||||||
|
active = true;
|
||||||
|
ensureRelativeTimer();
|
||||||
|
startSse();
|
||||||
|
loadRequests();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.AutomationPage = {
|
||||||
|
activate,
|
||||||
|
deactivate: cleanup
|
||||||
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -14,13 +14,13 @@
|
|||||||
--radius: 16px;
|
--radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
*,
|
.daily-bookmarks-view *,
|
||||||
*::before,
|
.daily-bookmarks-view *::before,
|
||||||
*::after {
|
.daily-bookmarks-view *::after {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
.daily-bookmarks-view {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: 'Space Grotesk', 'Inter', system-ui, -apple-system, sans-serif;
|
font-family: 'Space Grotesk', 'Inter', system-ui, -apple-system, sans-serif;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
@@ -29,12 +29,12 @@ body {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
.daily-bookmarks-view a {
|
||||||
color: var(--accent-2);
|
color: var(--accent-2);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
.daily-bookmarks-view a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,13 +49,13 @@ a:hover {
|
|||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.daily-shell {
|
.daily-bookmarks-view .daily-shell {
|
||||||
max-width: 1600px;
|
max-width: var(--content-max-width, 1600px);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 28px 18px 50px;
|
padding: 0 18px 36px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero {
|
.daily-bookmarks-view .hero {
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
@@ -366,6 +366,39 @@ a:hover {
|
|||||||
box-shadow: 0 0 0 3px rgba(34, 211, 238, 0.2);
|
box-shadow: 0 0 0 3px rgba(34, 211, 238, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.field__hint {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field--switch {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-control {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-control__label {
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field--switch input[type='checkbox'] {
|
||||||
|
width: auto;
|
||||||
|
min-width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
accent-color: var(--accent);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.form-preview {
|
.form-preview {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -446,6 +479,15 @@ a:hover {
|
|||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.primary-btn:disabled,
|
||||||
|
.secondary-btn:disabled,
|
||||||
|
.ghost-btn:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
.ghost-btn--tiny {
|
.ghost-btn--tiny {
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -588,11 +630,26 @@ a:hover {
|
|||||||
border-bottom-color: rgba(16, 185, 129, 0.3);
|
border-bottom-color: rgba(16, 185, 129, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bookmark-table td:last-child {
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
.bookmark-table tr.is-open td {
|
.bookmark-table tr.is-open td {
|
||||||
background: rgba(37, 99, 235, 0.08);
|
background: rgba(37, 99, 235, 0.08);
|
||||||
border-bottom-color: rgba(37, 99, 235, 0.2);
|
border-bottom-color: rgba(37, 99, 235, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bookmark-table tr.is-inactive td {
|
||||||
|
background: rgba(107, 114, 128, 0.16);
|
||||||
|
border-bottom-color: rgba(107, 114, 128, 0.28);
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bookmark-table tr.is-inactive a {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
.chip {
|
.chip {
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 6px 8px;
|
padding: 6px 8px;
|
||||||
@@ -611,6 +668,13 @@ a:hover {
|
|||||||
border-color: rgba(37, 99, 235, 0.2);
|
border-color: rgba(37, 99, 235, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chip--inactive {
|
||||||
|
background: rgba(107, 114, 128, 0.22);
|
||||||
|
color: #0f172a;
|
||||||
|
border-color: rgba(107, 114, 128, 0.32);
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.list-summary {
|
.list-summary {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -631,9 +695,10 @@ a:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.table-actions {
|
.table-actions {
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-actions .ghost-btn {
|
.table-actions .ghost-btn {
|
||||||
|
|||||||
@@ -2,208 +2,20 @@
|
|||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Daily Bookmarks</title>
|
<title>Daily Bookmarks</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="assets/app-icon-64.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="assets/app-icon-64.png">
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<script>
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
(function redirectToShell() {
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
|
const url = new URL(window.location.href);
|
||||||
<link rel="stylesheet" href="daily-bookmarks.css">
|
const params = new URLSearchParams(url.search);
|
||||||
|
params.set('view', 'daily-bookmarks');
|
||||||
|
const target = `index.html${params.toString() ? `?${params}` : ''}${url.hash}`;
|
||||||
|
window.location.replace(target);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="daily-shell">
|
<p>Weiterleitung zu Daily Bookmarks…</p>
|
||||||
<header class="hero">
|
|
||||||
<a class="back-link" href="index.html">← Zurück zur App</a>
|
|
||||||
<h1 class="hero__title">Daily Bookmarks</h1>
|
|
||||||
<div class="hero__controls">
|
|
||||||
<div class="day-switch">
|
|
||||||
<button class="ghost-btn" id="prevDayBtn" aria-label="Vorheriger Tag">◀</button>
|
|
||||||
<div class="day-switch__label">
|
|
||||||
<div id="dayLabel" class="day-switch__day">Heute</div>
|
|
||||||
<div id="daySubLabel" class="day-switch__sub"></div>
|
|
||||||
</div>
|
|
||||||
<button class="ghost-btn" id="nextDayBtn" aria-label="Nächster Tag">▶</button>
|
|
||||||
<button class="ghost-btn ghost-btn--today" id="todayBtn">Heute</button>
|
|
||||||
</div>
|
|
||||||
<div class="hero__actions">
|
|
||||||
<div class="bulk-actions">
|
|
||||||
<label for="bulkCountSelect">Anzahl</label>
|
|
||||||
<select id="bulkCountSelect">
|
|
||||||
<option value="1">1</option>
|
|
||||||
<option value="5">5</option>
|
|
||||||
<option value="10">10</option>
|
|
||||||
<option value="15">15</option>
|
|
||||||
<option value="20">20</option>
|
|
||||||
</select>
|
|
||||||
<label class="auto-open-toggle">
|
|
||||||
<input type="checkbox" id="autoOpenToggle">
|
|
||||||
<span>Auto öffnen</span>
|
|
||||||
</label>
|
|
||||||
<button class="secondary-btn" id="bulkOpenBtn" type="button">Öffnen & abhaken</button>
|
|
||||||
</div>
|
|
||||||
<button class="primary-btn" id="openCreateBtn" type="button">+ Bookmark</button>
|
|
||||||
<button class="secondary-btn" id="openImportBtn" type="button">Liste importieren</button>
|
|
||||||
<button class="ghost-btn" id="refreshBtn" type="button">Aktualisieren</button>
|
|
||||||
<span id="heroStats" class="hero__stats"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div id="autoOpenOverlay" class="auto-open-overlay" hidden>
|
|
||||||
<div class="auto-open-overlay__panel" id="autoOpenOverlayPanel">
|
|
||||||
<div class="auto-open-overlay__badge">Auto-Öffnen startet gleich</div>
|
|
||||||
<div class="auto-open-overlay__timer">
|
|
||||||
<span id="autoOpenCountdown" class="auto-open-overlay__count">0.0</span>
|
|
||||||
<span class="auto-open-overlay__unit">Sek.</span>
|
|
||||||
</div>
|
|
||||||
<p class="auto-open-overlay__text">
|
|
||||||
Die nächste Runde deiner gefilterten Bookmarks öffnet automatisch. Abbrechen, falls du noch warten willst.
|
|
||||||
</p>
|
|
||||||
<p class="auto-open-overlay__hint">Klicke irgendwo in dieses Panel, um abzubrechen.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<main class="panel list-panel">
|
|
||||||
<header class="panel__header panel__header--row">
|
|
||||||
<div>
|
|
||||||
<p class="panel__eyebrow">Tägliche Liste</p>
|
|
||||||
<h2 class="panel__title">Alle Bookmarks</h2>
|
|
||||||
</div>
|
|
||||||
<div class="list-summary" id="listSummary"></div>
|
|
||||||
</header>
|
|
||||||
<div id="listStatus" class="list-status" role="status" aria-live="polite"></div>
|
|
||||||
<div class="table-wrapper">
|
|
||||||
<table class="bookmark-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="col-url">
|
|
||||||
<button type="button" class="sort-btn" data-sort-key="url_template">URL (aufgelöst)</button>
|
|
||||||
</th>
|
|
||||||
<th class="col-marker">
|
|
||||||
<button type="button" class="sort-btn" data-sort-key="marker">Marker</button>
|
|
||||||
</th>
|
|
||||||
<th class="col-created">
|
|
||||||
<button type="button" class="sort-btn" data-sort-key="created_at">Erstelldatum</button>
|
|
||||||
</th>
|
|
||||||
<th class="col-last">
|
|
||||||
<button type="button" class="sort-btn" data-sort-key="last_completed_at">Letzte Erledigung</button>
|
|
||||||
</th>
|
|
||||||
<th>Aktionen</th>
|
|
||||||
</tr>
|
|
||||||
<tr class="table-filter-row">
|
|
||||||
<th>
|
|
||||||
<label class="visually-hidden" for="urlFilter">Nach URL filtern</label>
|
|
||||||
<input id="urlFilter" type="search" placeholder="URL filtern">
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
<label class="visually-hidden" for="markerFilter">Nach Marker filtern</label>
|
|
||||||
<select id="markerFilter">
|
|
||||||
<option value="">Alle Marker</option>
|
|
||||||
<option value="__none">Ohne Marker</option>
|
|
||||||
</select>
|
|
||||||
</th>
|
|
||||||
<th></th>
|
|
||||||
<th></th>
|
|
||||||
<th class="filter-hint">
|
|
||||||
<button type="button" class="ghost-btn ghost-btn--tiny" id="resetViewBtn" aria-label="Filter und Sortierung zurücksetzen">↺</button>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="tableBody"></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="bookmarkModal" class="modal" hidden>
|
|
||||||
<div class="modal__backdrop"></div>
|
|
||||||
<div class="modal__content" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
|
|
||||||
<header class="modal__header">
|
|
||||||
<div>
|
|
||||||
<p class="panel__eyebrow" id="formModeLabel">Neues Bookmark</p>
|
|
||||||
<h2 id="modalTitle" class="panel__title">Bookmark pflegen</h2>
|
|
||||||
<p class="panel__subtitle">Titel optional, URL-Template erforderlich. Platzhalter werden für den gewählten Tag aufgelöst.</p>
|
|
||||||
</div>
|
|
||||||
<button class="ghost-btn modal__close" type="button" id="modalCloseBtn" aria-label="Schließen">×</button>
|
|
||||||
</header>
|
|
||||||
<form id="bookmarkForm" class="bookmark-form" autocomplete="off">
|
|
||||||
<label class="field">
|
|
||||||
<span>Titel (optional)</span>
|
|
||||||
<input id="titleInput" type="text" name="title" maxlength="160" placeholder="z.B. Daily Gewinnspielrunde">
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span>URL-Template *</span>
|
|
||||||
<input id="urlInput" type="url" name="url_template" maxlength="800" placeholder="https://www.test.de/tag-{{day}}/" required>
|
|
||||||
</label>
|
|
||||||
<div id="urlSuggestionBox" class="suggestion-box" hidden></div>
|
|
||||||
<label class="field">
|
|
||||||
<span>Notiz (optional)</span>
|
|
||||||
<textarea id="notesInput" name="notes" maxlength="800" rows="3" placeholder="Kurze Hinweise oder To-do für diesen Link"></textarea>
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span>Marker (optional)</span>
|
|
||||||
<input id="markerInput" type="text" name="marker" maxlength="120" placeholder="z.B. März-Import oder Kampagne A">
|
|
||||||
</label>
|
|
||||||
<div class="form-preview">
|
|
||||||
<div>
|
|
||||||
<p class="form-preview__label">Aufgelöste URL für den gewählten Tag:</p>
|
|
||||||
<a id="previewLink" class="form-preview__link" href="#" target="_blank" rel="noopener">–</a>
|
|
||||||
</div>
|
|
||||||
<div class="form-preview__actions">
|
|
||||||
<button class="secondary-btn" type="button" id="resetBtn">Zurücksetzen</button>
|
|
||||||
<button class="primary-btn" type="submit" id="submitBtn">Speichern</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="placeholder-help">
|
|
||||||
<p class="placeholder-help__title">Dynamische Platzhalter</p>
|
|
||||||
<ul class="placeholder-help__list">
|
|
||||||
<li><code>{{day}}</code> Tag des Monats (1–31), <code>{{dd}}</code> zweistellig</li>
|
|
||||||
<li><code>{{date}}</code> liefert <code>YYYY-MM-DD</code></li>
|
|
||||||
<li><code>{{mm}}</code> Monat zweistellig, <code>{{yyyy}}</code> Jahr</li>
|
|
||||||
<li><code>{{day+1}}</code> oder <code>{{date-2}}</code> verschieben um Tage</li>
|
|
||||||
<li><code>{{counter:477}}</code> Basiswert + aktueller Tag, z.B. <code>https://www.test.de/sweepstakes/{{counter:477}}/</code></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div id="formStatus" class="form-status" role="status" aria-live="polite"></div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="importModal" class="modal" hidden>
|
|
||||||
<div class="modal__backdrop"></div>
|
|
||||||
<div class="modal__content" role="dialog" aria-modal="true" aria-labelledby="importModalTitle">
|
|
||||||
<header class="modal__header">
|
|
||||||
<div>
|
|
||||||
<p class="panel__eyebrow">Masseneingabe</p>
|
|
||||||
<h2 id="importModalTitle" class="panel__title">Viele Bookmarks importieren</h2>
|
|
||||||
<p class="panel__subtitle">Füge hunderte Links gleichzeitig hinzu und vergebe einen gemeinsamen Marker.</p>
|
|
||||||
</div>
|
|
||||||
<button class="ghost-btn modal__close" type="button" id="importCloseBtn" aria-label="Schließen">×</button>
|
|
||||||
</header>
|
|
||||||
<form id="importForm" class="bookmark-form" autocomplete="off">
|
|
||||||
<label class="field">
|
|
||||||
<span>Liste der URL-Templates *</span>
|
|
||||||
<textarea id="importInput" name="import_urls" maxlength="120000" rows="8" placeholder="Jeder Link in eine neue Zeile, z.B. https://example.com/{{date}}/"></textarea>
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span>Marker für alle Einträge (optional)</span>
|
|
||||||
<input id="importMarkerInput" type="text" name="import_marker" maxlength="120" placeholder="z.B. Batch März 2024">
|
|
||||||
</label>
|
|
||||||
<p class="import-hint">Doppelte oder ungültige Zeilen werden automatisch übersprungen.</p>
|
|
||||||
<div class="form-preview">
|
|
||||||
<div>
|
|
||||||
<p class="form-preview__label">Filter und Sortierung gelten auch beim Batch-Öffnen.</p>
|
|
||||||
</div>
|
|
||||||
<div class="form-preview__actions">
|
|
||||||
<button class="secondary-btn" type="button" id="importResetBtn">Zurücksetzen</button>
|
|
||||||
<button class="primary-btn" type="submit" id="importSubmitBtn">Import starten</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="importStatus" class="form-status" role="status" aria-live="polite"></div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="daily-bookmarks.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
(function () {
|
(function () {
|
||||||
|
let active = false;
|
||||||
|
let initialized = false;
|
||||||
const API_URL = window.API_URL || 'https://fb.srv.medeba-media.de/api';
|
const API_URL = window.API_URL || 'https://fb.srv.medeba-media.de/api';
|
||||||
const BULK_COUNT_STORAGE_KEY = 'dailyBookmarkBulkCount';
|
const BULK_COUNT_STORAGE_KEY = 'dailyBookmarkBulkCount';
|
||||||
const FILTER_STORAGE_KEY = 'dailyBookmarkFilters';
|
const FILTER_STORAGE_KEY = 'dailyBookmarkFilters';
|
||||||
@@ -27,55 +29,63 @@
|
|||||||
|
|
||||||
let editingId = null;
|
let editingId = null;
|
||||||
|
|
||||||
const dayLabel = document.getElementById('dayLabel');
|
const dailyDayLabel = document.getElementById('dailyDayLabel');
|
||||||
const daySubLabel = document.getElementById('daySubLabel');
|
const dailyDaySubLabel = document.getElementById('dailyDaySubLabel');
|
||||||
const prevDayBtn = document.getElementById('prevDayBtn');
|
const dailyPrevDayBtn = document.getElementById('dailyPrevDayBtn');
|
||||||
const nextDayBtn = document.getElementById('nextDayBtn');
|
const dailyNextDayBtn = document.getElementById('dailyNextDayBtn');
|
||||||
const todayBtn = document.getElementById('todayBtn');
|
const dailyTodayBtn = document.getElementById('dailyTodayBtn');
|
||||||
const refreshBtn = document.getElementById('refreshBtn');
|
const dailyRefreshBtn = document.getElementById('dailyRefreshBtn');
|
||||||
const heroStats = document.getElementById('heroStats');
|
const dailyHeroStats = document.getElementById('dailyHeroStats');
|
||||||
const listSummary = document.getElementById('listSummary');
|
const dailyListSummary = document.getElementById('dailyListSummary');
|
||||||
const listStatus = document.getElementById('listStatus');
|
const dailyListStatus = document.getElementById('dailyListStatus');
|
||||||
const tableBody = document.getElementById('tableBody');
|
const dailyTableBody = document.getElementById('dailyTableBody');
|
||||||
const bulkCountSelect = document.getElementById('bulkCountSelect');
|
const dailyBulkCountSelect = document.getElementById('dailyBulkCountSelect');
|
||||||
const bulkOpenBtn = document.getElementById('bulkOpenBtn');
|
const dailyBulkOpenBtn = document.getElementById('dailyBulkOpenBtn');
|
||||||
const autoOpenToggle = document.getElementById('autoOpenToggle');
|
const dailyAutoOpenToggle = document.getElementById('dailyAutoOpenToggle');
|
||||||
const autoOpenOverlay = document.getElementById('autoOpenOverlay');
|
const dailyAutoOpenOverlay = document.getElementById('dailyAutoOpenOverlay');
|
||||||
const autoOpenOverlayPanel = document.getElementById('autoOpenOverlayPanel');
|
const dailyAutoOpenOverlayPanel = document.getElementById('dailyAutoOpenOverlayPanel');
|
||||||
const autoOpenCountdown = document.getElementById('autoOpenCountdown');
|
const dailyAutoOpenCountdown = document.getElementById('dailyAutoOpenCountdown');
|
||||||
const openCreateBtn = document.getElementById('openCreateBtn');
|
const dailyOpenCreateBtn = document.getElementById('dailyOpenCreateBtn');
|
||||||
|
|
||||||
const modal = document.getElementById('bookmarkModal');
|
const modal = document.getElementById('dailyBookmarkModal');
|
||||||
const modalCloseBtn = document.getElementById('modalCloseBtn');
|
const dailyModalCloseBtn = document.getElementById('dailyModalCloseBtn');
|
||||||
const modalBackdrop = modal ? modal.querySelector('.modal__backdrop') : null;
|
const dailyModalBackdrop = modal ? modal.querySelector('.modal__backdrop') : null;
|
||||||
const formEl = document.getElementById('bookmarkForm');
|
const formEl = document.getElementById('dailyBookmarkForm');
|
||||||
const titleInput = document.getElementById('titleInput');
|
const dailyTitleInput = document.getElementById('dailyTitleInput');
|
||||||
const urlInput = document.getElementById('urlInput');
|
const dailyUrlInput = document.getElementById('dailyUrlInput');
|
||||||
const notesInput = document.getElementById('notesInput');
|
const dailyNotesInput = document.getElementById('dailyNotesInput');
|
||||||
const resetBtn = document.getElementById('resetBtn');
|
const dailyResetBtn = document.getElementById('dailyResetBtn');
|
||||||
const submitBtn = document.getElementById('submitBtn');
|
const dailySubmitBtn = document.getElementById('dailySubmitBtn');
|
||||||
const previewLink = document.getElementById('previewLink');
|
const dailyPreviewLink = document.getElementById('dailyPreviewLink');
|
||||||
const formStatus = document.getElementById('formStatus');
|
const dailyFormStatus = document.getElementById('dailyFormStatus');
|
||||||
const formModeLabel = document.getElementById('formModeLabel');
|
const dailyFormModeLabel = document.getElementById('dailyFormModeLabel');
|
||||||
const urlSuggestionBox = document.getElementById('urlSuggestionBox');
|
const dailyUrlSuggestionBox = document.getElementById('dailyUrlSuggestionBox');
|
||||||
const markerInput = document.getElementById('markerInput');
|
const dailyMarkerInput = document.getElementById('dailyMarkerInput');
|
||||||
const markerFilterSelect = document.getElementById('markerFilter');
|
const dailyActiveInput = document.getElementById('dailyActiveInput');
|
||||||
const urlFilterInput = document.getElementById('urlFilter');
|
const markerFilterSelect = document.getElementById('dailyMarkerFilter');
|
||||||
const resetViewBtn = document.getElementById('resetViewBtn');
|
const urlFilterInput = document.getElementById('dailyUrlFilter');
|
||||||
|
const dailyResetViewBtn = document.getElementById('dailyResetViewBtn');
|
||||||
const sortButtons = Array.from(document.querySelectorAll('[data-sort-key]'));
|
const sortButtons = Array.from(document.querySelectorAll('[data-sort-key]'));
|
||||||
const openImportBtn = document.getElementById('openImportBtn');
|
const dailyOpenImportBtn = document.getElementById('dailyOpenImportBtn');
|
||||||
const importModal = document.getElementById('importModal');
|
const dailyImportModal = document.getElementById('dailyImportModal');
|
||||||
const importCloseBtn = document.getElementById('importCloseBtn');
|
const dailyImportCloseBtn = document.getElementById('dailyImportCloseBtn');
|
||||||
const importBackdrop = importModal ? importModal.querySelector('.modal__backdrop') : null;
|
const dailyImportBackdrop = dailyImportModal ? dailyImportModal.querySelector('.modal__backdrop') : null;
|
||||||
const importForm = document.getElementById('importForm');
|
const dailyImportForm = document.getElementById('dailyImportForm');
|
||||||
const importInput = document.getElementById('importInput');
|
const dailyImportInput = document.getElementById('dailyImportInput');
|
||||||
const importMarkerInput = document.getElementById('importMarkerInput');
|
const dailyImportMarkerInput = document.getElementById('dailyImportMarkerInput');
|
||||||
const importResetBtn = document.getElementById('importResetBtn');
|
const dailyImportResetBtn = document.getElementById('dailyImportResetBtn');
|
||||||
const importSubmitBtn = document.getElementById('importSubmitBtn');
|
const dailyImportSubmitBtn = document.getElementById('dailyImportSubmitBtn');
|
||||||
const importStatus = document.getElementById('importStatus');
|
const dailyImportStatus = document.getElementById('dailyImportStatus');
|
||||||
|
|
||||||
const PLACEHOLDER_PATTERN = /\{\{\s*([^}]+)\s*\}\}/gi;
|
const PLACEHOLDER_PATTERN = /\{\{\s*([^}]+)\s*\}\}/gi;
|
||||||
|
|
||||||
|
function ensureStyles(enabled) {
|
||||||
|
const link = document.getElementById('dailyBookmarksCss');
|
||||||
|
if (link) {
|
||||||
|
link.disabled = !enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function formatDayKey(date) {
|
function formatDayKey(date) {
|
||||||
const d = date instanceof Date ? date : new Date();
|
const d = date instanceof Date ? date : new Date();
|
||||||
const year = d.getFullYear();
|
const year = d.getFullYear();
|
||||||
@@ -214,6 +224,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeItem(item) {
|
||||||
|
if (!item || typeof item !== 'object') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
is_active: Number(item.is_active ?? 1) !== 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function resolveTemplate(template, dayKey) {
|
function resolveTemplate(template, dayKey) {
|
||||||
if (typeof template !== 'string') {
|
if (typeof template !== 'string') {
|
||||||
return '';
|
return '';
|
||||||
@@ -343,32 +363,32 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderUrlSuggestions() {
|
function renderUrlSuggestions() {
|
||||||
if (!urlSuggestionBox) {
|
if (!dailyUrlSuggestionBox) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const suggestions = buildUrlSuggestions(urlInput ? urlInput.value : '');
|
const suggestions = buildUrlSuggestions(dailyUrlInput ? dailyUrlInput.value : '');
|
||||||
urlSuggestionBox.innerHTML = '';
|
dailyUrlSuggestionBox.innerHTML = '';
|
||||||
if (!suggestions.length) {
|
if (!suggestions.length) {
|
||||||
urlSuggestionBox.hidden = true;
|
dailyUrlSuggestionBox.hidden = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = document.createElement('span');
|
const text = document.createElement('span');
|
||||||
text.className = 'suggestion-box__text';
|
text.className = 'suggestion-box__text';
|
||||||
text.textContent = 'Mögliche Platzhalter:';
|
text.textContent = 'Mögliche Platzhalter:';
|
||||||
urlSuggestionBox.appendChild(text);
|
dailyUrlSuggestionBox.appendChild(text);
|
||||||
|
|
||||||
const applySuggestion = (value) => {
|
const applySuggestion = (value) => {
|
||||||
if (!urlInput) {
|
if (!dailyUrlInput) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
urlInput.value = value;
|
dailyUrlInput.value = value;
|
||||||
updatePreviewLink();
|
updatePreviewLink();
|
||||||
renderUrlSuggestions();
|
renderUrlSuggestions();
|
||||||
const end = urlInput.value.length;
|
const end = dailyUrlInput.value.length;
|
||||||
urlInput.focus();
|
dailyUrlInput.focus();
|
||||||
try {
|
try {
|
||||||
urlInput.setSelectionRange(end, end);
|
dailyUrlInput.setSelectionRange(end, end);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// ignore if not supported
|
// ignore if not supported
|
||||||
}
|
}
|
||||||
@@ -400,10 +420,10 @@
|
|||||||
});
|
});
|
||||||
item.appendChild(preview);
|
item.appendChild(preview);
|
||||||
|
|
||||||
urlSuggestionBox.appendChild(item);
|
dailyUrlSuggestionBox.appendChild(item);
|
||||||
});
|
});
|
||||||
|
|
||||||
urlSuggestionBox.hidden = false;
|
dailyUrlSuggestionBox.hidden = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function apiFetch(url, options = {}) {
|
async function apiFetch(url, options = {}) {
|
||||||
@@ -438,60 +458,64 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateDayUI() {
|
function updateDayUI() {
|
||||||
if (dayLabel) {
|
if (dailyDayLabel) {
|
||||||
dayLabel.textContent = formatDayLabel(state.dayKey);
|
dailyDayLabel.textContent = formatDayLabel(state.dayKey);
|
||||||
}
|
}
|
||||||
if (daySubLabel) {
|
if (dailyDaySubLabel) {
|
||||||
daySubLabel.textContent = formatRelativeDay(state.dayKey);
|
dailyDaySubLabel.textContent = formatRelativeDay(state.dayKey);
|
||||||
}
|
}
|
||||||
updatePreviewLink();
|
updatePreviewLink();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDayKey(dayKey) {
|
function setDayKey(dayKey) {
|
||||||
state.dayKey = formatDayKey(parseDayKey(dayKey));
|
state.dayKey = formatDayKey(parseDayKey(dayKey));
|
||||||
|
if (!active) return;
|
||||||
updateDayUI();
|
updateDayUI();
|
||||||
loadDailyBookmarks();
|
loadDailyBookmarks();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setFormStatus(message, isError = false) {
|
function setFormStatus(message, isError = false) {
|
||||||
if (!formStatus) {
|
if (!dailyFormStatus) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
formStatus.textContent = message || '';
|
dailyFormStatus.textContent = message || '';
|
||||||
formStatus.classList.toggle('form-status--error', !!isError);
|
dailyFormStatus.classList.toggle('form-status--error', !!isError);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setListStatus(message, isError = false) {
|
function setListStatus(message, isError = false) {
|
||||||
if (!listStatus) {
|
if (!dailyListStatus) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
listStatus.textContent = message || '';
|
dailyListStatus.textContent = message || '';
|
||||||
listStatus.classList.toggle('list-status--error', !!isError);
|
dailyListStatus.classList.toggle('list-status--error', !!isError);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePreviewLink() {
|
function updatePreviewLink() {
|
||||||
if (!previewLink || !urlInput) {
|
if (!dailyPreviewLink || !dailyUrlInput) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const resolved = resolveTemplate(urlInput.value || '', state.dayKey);
|
const resolved = resolveTemplate(dailyUrlInput.value || '', state.dayKey);
|
||||||
previewLink.textContent = resolved || '–';
|
dailyPreviewLink.textContent = resolved || '–';
|
||||||
if (resolved) {
|
if (resolved) {
|
||||||
previewLink.href = resolved;
|
dailyPreviewLink.href = resolved;
|
||||||
previewLink.target = '_blank';
|
dailyPreviewLink.target = '_blank';
|
||||||
previewLink.rel = 'noopener';
|
dailyPreviewLink.rel = 'noopener';
|
||||||
} else {
|
} else {
|
||||||
previewLink.removeAttribute('href');
|
dailyPreviewLink.removeAttribute('href');
|
||||||
}
|
}
|
||||||
renderUrlSuggestions();
|
renderUrlSuggestions();
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
editingId = null;
|
editingId = null;
|
||||||
formModeLabel.textContent = 'Neues Bookmark';
|
dailyFormModeLabel.textContent = 'Neues Bookmark';
|
||||||
submitBtn.textContent = 'Speichern';
|
dailySubmitBtn.textContent = 'Speichern';
|
||||||
formEl.reset();
|
formEl.reset();
|
||||||
if (markerInput) {
|
if (dailyMarkerInput) {
|
||||||
markerInput.value = '';
|
dailyMarkerInput.value = '';
|
||||||
|
}
|
||||||
|
if (dailyActiveInput) {
|
||||||
|
dailyActiveInput.checked = true;
|
||||||
}
|
}
|
||||||
setFormStatus('');
|
setFormStatus('');
|
||||||
updatePreviewLink();
|
updatePreviewLink();
|
||||||
@@ -499,18 +523,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openModal(mode, bookmark) {
|
function openModal(mode, bookmark) {
|
||||||
if (importModal && !importModal.hidden) {
|
if (dailyImportModal && !dailyImportModal.hidden) {
|
||||||
closeImportModal();
|
closeImportModal();
|
||||||
}
|
}
|
||||||
if (mode === 'edit' && bookmark) {
|
if (mode === 'edit' && bookmark) {
|
||||||
editingId = bookmark.id;
|
editingId = bookmark.id;
|
||||||
formModeLabel.textContent = 'Bookmark bearbeiten';
|
dailyFormModeLabel.textContent = 'Bookmark bearbeiten';
|
||||||
submitBtn.textContent = 'Aktualisieren';
|
dailySubmitBtn.textContent = 'Aktualisieren';
|
||||||
titleInput.value = bookmark.title || '';
|
dailyTitleInput.value = bookmark.title || '';
|
||||||
urlInput.value = bookmark.url_template || '';
|
dailyUrlInput.value = bookmark.url_template || '';
|
||||||
notesInput.value = bookmark.notes || '';
|
dailyNotesInput.value = bookmark.notes || '';
|
||||||
if (markerInput) {
|
if (dailyMarkerInput) {
|
||||||
markerInput.value = bookmark.marker || '';
|
dailyMarkerInput.value = bookmark.marker || '';
|
||||||
|
}
|
||||||
|
if (dailyActiveInput) {
|
||||||
|
dailyActiveInput.checked = bookmark.is_active !== false;
|
||||||
}
|
}
|
||||||
setFormStatus('Bearbeite vorhandenes Bookmark');
|
setFormStatus('Bearbeite vorhandenes Bookmark');
|
||||||
} else {
|
} else {
|
||||||
@@ -522,10 +549,10 @@
|
|||||||
modal.focus();
|
modal.focus();
|
||||||
}
|
}
|
||||||
updatePreviewLink();
|
updatePreviewLink();
|
||||||
if (mode === 'edit' && titleInput) {
|
if (mode === 'edit' && dailyTitleInput) {
|
||||||
titleInput.focus();
|
dailyTitleInput.focus();
|
||||||
} else if (urlInput) {
|
} else if (dailyUrlInput) {
|
||||||
urlInput.focus();
|
dailyUrlInput.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,22 +599,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getFilteredItems() {
|
function getFilteredItems() {
|
||||||
const markerFilter = state.filters.marker || '';
|
const dailyMarkerFilter = state.filters.marker || '';
|
||||||
const urlFilter = (state.filters.url || '').toLowerCase();
|
const dailyUrlFilter = (state.filters.url || '').toLowerCase();
|
||||||
return state.items.filter((item) => {
|
return state.items.filter((item) => {
|
||||||
const currentMarker = normalizeMarkerValue(item.marker).toLowerCase();
|
const currentMarker = normalizeMarkerValue(item.marker).toLowerCase();
|
||||||
if (markerFilter === '__none') {
|
if (dailyMarkerFilter === '__none') {
|
||||||
if (currentMarker) {
|
if (currentMarker) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (markerFilter) {
|
} else if (dailyMarkerFilter) {
|
||||||
if (currentMarker !== markerFilter.toLowerCase()) {
|
if (currentMarker !== dailyMarkerFilter.toLowerCase()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (urlFilter) {
|
if (dailyUrlFilter) {
|
||||||
const urlValue = (item.resolved_url || item.url_template || '').toLowerCase();
|
const urlValue = (item.resolved_url || item.url_template || '').toLowerCase();
|
||||||
if (!urlValue.includes(urlFilter)) {
|
if (!urlValue.includes(dailyUrlFilter)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -676,11 +703,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderTable() {
|
function renderTable() {
|
||||||
if (!tableBody) {
|
if (!dailyTableBody) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tableBody.innerHTML = '';
|
dailyTableBody.innerHTML = '';
|
||||||
updateSortIndicators();
|
updateSortIndicators();
|
||||||
|
|
||||||
if (state.loading) {
|
if (state.loading) {
|
||||||
@@ -706,7 +733,12 @@
|
|||||||
|
|
||||||
visibleItems.forEach((item) => {
|
visibleItems.forEach((item) => {
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
|
const isActive = item.is_active !== false;
|
||||||
|
if (isActive) {
|
||||||
tr.classList.add(item.completed_for_day ? 'is-done' : 'is-open');
|
tr.classList.add(item.completed_for_day ? 'is-done' : 'is-open');
|
||||||
|
} else {
|
||||||
|
tr.classList.add('is-inactive');
|
||||||
|
}
|
||||||
|
|
||||||
const urlTd = document.createElement('td');
|
const urlTd = document.createElement('td');
|
||||||
urlTd.className = 'url-cell';
|
urlTd.className = 'url-cell';
|
||||||
@@ -722,14 +754,23 @@
|
|||||||
|
|
||||||
const markerTd = document.createElement('td');
|
const markerTd = document.createElement('td');
|
||||||
markerTd.className = 'marker-cell';
|
markerTd.className = 'marker-cell';
|
||||||
|
markerTd.textContent = '';
|
||||||
if (normalizeMarkerValue(item.marker)) {
|
if (normalizeMarkerValue(item.marker)) {
|
||||||
const markerChip = document.createElement('span');
|
const markerChip = document.createElement('span');
|
||||||
markerChip.className = 'chip chip--marker';
|
markerChip.className = 'chip chip--marker';
|
||||||
markerChip.textContent = item.marker;
|
markerChip.textContent = item.marker;
|
||||||
markerTd.appendChild(markerChip);
|
markerTd.appendChild(markerChip);
|
||||||
} else {
|
} else {
|
||||||
markerTd.textContent = '–';
|
const placeholder = document.createElement('span');
|
||||||
markerTd.classList.add('muted');
|
placeholder.textContent = '–';
|
||||||
|
placeholder.classList.add('muted');
|
||||||
|
markerTd.appendChild(placeholder);
|
||||||
|
}
|
||||||
|
if (!isActive) {
|
||||||
|
const inactiveChip = document.createElement('span');
|
||||||
|
inactiveChip.className = 'chip chip--inactive';
|
||||||
|
inactiveChip.textContent = 'Deaktiviert';
|
||||||
|
markerTd.appendChild(inactiveChip);
|
||||||
}
|
}
|
||||||
tr.appendChild(markerTd);
|
tr.appendChild(markerTd);
|
||||||
|
|
||||||
@@ -748,9 +789,11 @@
|
|||||||
openBtn.className = 'ghost-btn';
|
openBtn.className = 'ghost-btn';
|
||||||
openBtn.type = 'button';
|
openBtn.type = 'button';
|
||||||
openBtn.textContent = '🔗';
|
openBtn.textContent = '🔗';
|
||||||
openBtn.title = 'Öffnen';
|
openBtn.title = isActive ? 'Öffnen' : 'Deaktiviert';
|
||||||
|
openBtn.disabled = !isActive;
|
||||||
openBtn.addEventListener('click', () => {
|
openBtn.addEventListener('click', () => {
|
||||||
const target = item.resolved_url || item.url_template;
|
const target = item.resolved_url || item.url_template;
|
||||||
|
if (!isActive) return;
|
||||||
if (target) {
|
if (target) {
|
||||||
window.open(target, '_blank', 'noopener');
|
window.open(target, '_blank', 'noopener');
|
||||||
}
|
}
|
||||||
@@ -761,7 +804,10 @@
|
|||||||
toggleBtn.className = 'ghost-btn';
|
toggleBtn.className = 'ghost-btn';
|
||||||
toggleBtn.type = 'button';
|
toggleBtn.type = 'button';
|
||||||
toggleBtn.textContent = item.completed_for_day ? '↩️' : '✅';
|
toggleBtn.textContent = item.completed_for_day ? '↩️' : '✅';
|
||||||
toggleBtn.title = item.completed_for_day ? 'Zurücksetzen' : 'Heute erledigt';
|
toggleBtn.title = isActive
|
||||||
|
? (item.completed_for_day ? 'Zurücksetzen' : 'Heute erledigt')
|
||||||
|
: 'Deaktiviert';
|
||||||
|
toggleBtn.disabled = !isActive;
|
||||||
toggleBtn.addEventListener('click', () => {
|
toggleBtn.addEventListener('click', () => {
|
||||||
if (item.completed_for_day) {
|
if (item.completed_for_day) {
|
||||||
undoDailyBookmark(item.id);
|
undoDailyBookmark(item.id);
|
||||||
@@ -792,7 +838,7 @@
|
|||||||
actionsTd.appendChild(deleteBtn);
|
actionsTd.appendChild(deleteBtn);
|
||||||
|
|
||||||
tr.appendChild(actionsTd);
|
tr.appendChild(actionsTd);
|
||||||
tableBody.appendChild(tr);
|
dailyTableBody.appendChild(tr);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -803,16 +849,20 @@
|
|||||||
td.className = className;
|
td.className = className;
|
||||||
td.textContent = text;
|
td.textContent = text;
|
||||||
tr.appendChild(td);
|
tr.appendChild(td);
|
||||||
tableBody.appendChild(tr);
|
dailyTableBody.appendChild(tr);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateHeroStats() {
|
function updateHeroStats() {
|
||||||
const total = state.items.length;
|
const activeItems = state.items.filter((item) => item.is_active !== false);
|
||||||
const done = state.items.filter((item) => item.completed_for_day).length;
|
const totalActive = activeItems.length;
|
||||||
const visibleItems = getFilteredItems();
|
const inactiveCount = state.items.length - totalActive;
|
||||||
|
const done = activeItems.filter((item) => item.completed_for_day).length;
|
||||||
|
const visibleItems = getFilteredItems().filter((item) => item.is_active !== false);
|
||||||
const visibleDone = visibleItems.filter((item) => item.completed_for_day).length;
|
const visibleDone = visibleItems.filter((item) => item.completed_for_day).length;
|
||||||
const filterSuffix = visibleItems.length !== total ? ` · Gefiltert: ${visibleDone}/${visibleItems.length}` : '';
|
const filterSuffix = visibleItems.length !== totalActive ? ` · Gefiltert: ${visibleDone}/${visibleItems.length}` : '';
|
||||||
const text = total ? `${done}/${total} erledigt${filterSuffix}` : 'Keine Bookmarks vorhanden';
|
const inactiveSuffix = inactiveCount ? ` · ${inactiveCount} deaktiviert` : '';
|
||||||
|
const baseText = totalActive ? `${done}/${totalActive} erledigt${filterSuffix}` : 'Keine aktiven Bookmarks';
|
||||||
|
const text = `${baseText}${inactiveSuffix}`;
|
||||||
const filterParts = [];
|
const filterParts = [];
|
||||||
if (state.filters.marker) {
|
if (state.filters.marker) {
|
||||||
filterParts.push(state.filters.marker === '__none' ? 'ohne Marker' : `Marker: ${state.filters.marker}`);
|
filterParts.push(state.filters.marker === '__none' ? 'ohne Marker' : `Marker: ${state.filters.marker}`);
|
||||||
@@ -821,11 +871,11 @@
|
|||||||
filterParts.push(`URL enthält „${state.filters.url}“`);
|
filterParts.push(`URL enthält „${state.filters.url}“`);
|
||||||
}
|
}
|
||||||
const filterText = filterParts.length ? ` · Filter: ${filterParts.join(' · ')}` : '';
|
const filterText = filterParts.length ? ` · Filter: ${filterParts.join(' · ')}` : '';
|
||||||
if (heroStats) {
|
if (dailyHeroStats) {
|
||||||
heroStats.textContent = `${text}${filterText}`;
|
dailyHeroStats.textContent = `${text}${filterText}`;
|
||||||
}
|
}
|
||||||
if (listSummary) {
|
if (dailyListSummary) {
|
||||||
listSummary.textContent = `${text} – Tag ${state.dayKey}${filterText}`;
|
dailyListSummary.textContent = `${text} – Tag ${state.dayKey}${filterText}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -837,27 +887,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateAutoOpenCountdownLabel(remainingMs) {
|
function updateAutoOpenCountdownLabel(remainingMs) {
|
||||||
if (!autoOpenCountdown) return;
|
if (!dailyAutoOpenCountdown) return;
|
||||||
const safeMs = Math.max(0, remainingMs);
|
const safeMs = Math.max(0, remainingMs);
|
||||||
const seconds = safeMs / 1000;
|
const seconds = safeMs / 1000;
|
||||||
const formatted = seconds >= 10 ? seconds.toFixed(0) : seconds.toFixed(1);
|
const formatted = seconds >= 10 ? seconds.toFixed(0) : seconds.toFixed(1);
|
||||||
autoOpenCountdown.textContent = formatted;
|
dailyAutoOpenCountdown.textContent = formatted;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideAutoOpenOverlay() {
|
function hideAutoOpenOverlay() {
|
||||||
clearAutoOpenCountdown();
|
clearAutoOpenCountdown();
|
||||||
if (autoOpenOverlay) {
|
if (dailyAutoOpenOverlay) {
|
||||||
autoOpenOverlay.classList.remove('visible');
|
dailyAutoOpenOverlay.classList.remove('visible');
|
||||||
autoOpenOverlay.hidden = true;
|
dailyAutoOpenOverlay.hidden = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAutoOpenOverlay(delayMs) {
|
function showAutoOpenOverlay(delayMs) {
|
||||||
if (!autoOpenOverlay) return;
|
if (!dailyAutoOpenOverlay) return;
|
||||||
const duration = Math.max(0, delayMs);
|
const duration = Math.max(0, delayMs);
|
||||||
hideAutoOpenOverlay();
|
hideAutoOpenOverlay();
|
||||||
autoOpenOverlay.hidden = false;
|
dailyAutoOpenOverlay.hidden = false;
|
||||||
requestAnimationFrame(() => autoOpenOverlay.classList.add('visible'));
|
requestAnimationFrame(() => dailyAutoOpenOverlay.classList.add('visible'));
|
||||||
updateAutoOpenCountdownLabel(duration);
|
updateAutoOpenCountdownLabel(duration);
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
state.autoOpenCountdownIntervalId = setInterval(() => {
|
state.autoOpenCountdownIntervalId = setInterval(() => {
|
||||||
@@ -882,13 +932,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function maybeAutoOpen(reason = '', delayMs = AUTO_OPEN_DELAY_MS) {
|
function maybeAutoOpen(reason = '', delayMs = AUTO_OPEN_DELAY_MS) {
|
||||||
|
if (!active) {
|
||||||
|
hideAutoOpenOverlay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!state.autoOpenEnabled) {
|
if (!state.autoOpenEnabled) {
|
||||||
hideAutoOpenOverlay();
|
hideAutoOpenOverlay();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state.processingBatch) return;
|
if (state.processingBatch) return;
|
||||||
if (state.autoOpenTriggered) return;
|
if (state.autoOpenTriggered) return;
|
||||||
const undone = getVisibleItems().filter((item) => !item.completed_for_day);
|
const undone = getVisibleItems().filter(
|
||||||
|
(item) => item.is_active !== false && !item.completed_for_day
|
||||||
|
);
|
||||||
if (!undone.length) {
|
if (!undone.length) {
|
||||||
hideAutoOpenOverlay();
|
hideAutoOpenOverlay();
|
||||||
return;
|
return;
|
||||||
@@ -921,6 +977,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadDailyBookmarks() {
|
async function loadDailyBookmarks() {
|
||||||
|
if (!active) return;
|
||||||
state.loading = true;
|
state.loading = true;
|
||||||
if (state.autoOpenTimerId) {
|
if (state.autoOpenTimerId) {
|
||||||
clearTimeout(state.autoOpenTimerId);
|
clearTimeout(state.autoOpenTimerId);
|
||||||
@@ -933,7 +990,8 @@
|
|||||||
renderTable();
|
renderTable();
|
||||||
try {
|
try {
|
||||||
const data = await apiFetch(`${API_URL}/daily-bookmarks?day=${encodeURIComponent(state.dayKey)}`);
|
const data = await apiFetch(`${API_URL}/daily-bookmarks?day=${encodeURIComponent(state.dayKey)}`);
|
||||||
state.items = Array.isArray(data) ? data : [];
|
const rows = Array.isArray(data) ? data : [];
|
||||||
|
state.items = rows.map((item) => normalizeItem(item)).filter(Boolean);
|
||||||
state.loading = false;
|
state.loading = false;
|
||||||
updateHeroStats();
|
updateHeroStats();
|
||||||
renderMarkerFilterOptions();
|
renderMarkerFilterOptions();
|
||||||
@@ -949,13 +1007,19 @@
|
|||||||
|
|
||||||
async function completeDailyBookmark(id) {
|
async function completeDailyBookmark(id) {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
const target = state.items.find((item) => item.id === id);
|
||||||
|
if (target && target.is_active === false) {
|
||||||
|
setListStatus('Bookmark ist deaktiviert.', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
state.error = '';
|
state.error = '';
|
||||||
try {
|
try {
|
||||||
const updated = await apiFetch(`${API_URL}/daily-bookmarks/${encodeURIComponent(id)}/check`, {
|
const updated = await apiFetch(`${API_URL}/daily-bookmarks/${encodeURIComponent(id)}/check`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ day: state.dayKey })
|
body: JSON.stringify({ day: state.dayKey })
|
||||||
});
|
});
|
||||||
state.items = state.items.map((item) => (item.id === id ? updated : item));
|
const normalized = normalizeItem(updated);
|
||||||
|
state.items = state.items.map((item) => (item.id === id ? normalized || item : item));
|
||||||
updateHeroStats();
|
updateHeroStats();
|
||||||
renderTable();
|
renderTable();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -967,12 +1031,18 @@
|
|||||||
|
|
||||||
async function undoDailyBookmark(id) {
|
async function undoDailyBookmark(id) {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
const target = state.items.find((item) => item.id === id);
|
||||||
|
if (target && target.is_active === false) {
|
||||||
|
setListStatus('Bookmark ist deaktiviert.', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
state.error = '';
|
state.error = '';
|
||||||
try {
|
try {
|
||||||
const updated = await apiFetch(`${API_URL}/daily-bookmarks/${encodeURIComponent(id)}/check?day=${encodeURIComponent(state.dayKey)}`, {
|
const updated = await apiFetch(`${API_URL}/daily-bookmarks/${encodeURIComponent(id)}/check?day=${encodeURIComponent(state.dayKey)}`, {
|
||||||
method: 'DELETE'
|
method: 'DELETE'
|
||||||
});
|
});
|
||||||
state.items = state.items.map((item) => (item.id === id ? updated : item));
|
const normalized = normalizeItem(updated);
|
||||||
|
state.items = state.items.map((item) => (item.id === id ? normalized || item : item));
|
||||||
updateHeroStats();
|
updateHeroStats();
|
||||||
renderTable();
|
renderTable();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1007,19 +1077,19 @@
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (state.saving) return;
|
if (state.saving) return;
|
||||||
|
|
||||||
const title = titleInput.value.trim();
|
const title = dailyTitleInput.value.trim();
|
||||||
const url = urlInput.value.trim();
|
const url = dailyUrlInput.value.trim();
|
||||||
const notes = notesInput.value.trim();
|
const notes = dailyNotesInput.value.trim();
|
||||||
const marker = markerInput ? markerInput.value.trim() : '';
|
const marker = dailyMarkerInput ? dailyMarkerInput.value.trim() : '';
|
||||||
|
|
||||||
if (!url) {
|
if (!url) {
|
||||||
setFormStatus('URL-Template ist Pflicht.', true);
|
setFormStatus('URL-Template ist Pflicht.', true);
|
||||||
urlInput.focus();
|
dailyUrlInput.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.saving = true;
|
state.saving = true;
|
||||||
submitBtn.disabled = true;
|
dailySubmitBtn.disabled = true;
|
||||||
setFormStatus('');
|
setFormStatus('');
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -1027,6 +1097,7 @@
|
|||||||
url_template: url,
|
url_template: url,
|
||||||
notes,
|
notes,
|
||||||
marker,
|
marker,
|
||||||
|
is_active: dailyActiveInput ? dailyActiveInput.checked : true,
|
||||||
day: state.dayKey
|
day: state.dayKey
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1040,10 +1111,11 @@
|
|||||||
method,
|
method,
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
|
const normalized = normalizeItem(saved);
|
||||||
if (editingId) {
|
if (editingId) {
|
||||||
state.items = state.items.map((item) => (item.id === editingId ? saved : item));
|
state.items = state.items.map((item) => (item.id === editingId ? normalized || item : item));
|
||||||
} else {
|
} else if (normalized) {
|
||||||
state.items = [saved, ...state.items];
|
state.items = [normalized, ...state.items];
|
||||||
}
|
}
|
||||||
updateHeroStats();
|
updateHeroStats();
|
||||||
renderMarkerFilterOptions();
|
renderMarkerFilterOptions();
|
||||||
@@ -1053,7 +1125,7 @@
|
|||||||
setFormStatus('Speichern fehlgeschlagen.', true);
|
setFormStatus('Speichern fehlgeschlagen.', true);
|
||||||
} finally {
|
} finally {
|
||||||
state.saving = false;
|
state.saving = false;
|
||||||
submitBtn.disabled = false;
|
dailySubmitBtn.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1062,7 +1134,9 @@
|
|||||||
if (!auto) {
|
if (!auto) {
|
||||||
cancelAutoOpen(false);
|
cancelAutoOpen(false);
|
||||||
}
|
}
|
||||||
const undone = getVisibleItems().filter((item) => !item.completed_for_day);
|
const undone = getVisibleItems().filter(
|
||||||
|
(item) => item.is_active !== false && !item.completed_for_day
|
||||||
|
);
|
||||||
if (!undone.length) {
|
if (!undone.length) {
|
||||||
if (!auto) {
|
if (!auto) {
|
||||||
setListStatus('Keine offenen Bookmarks für den gewählten Tag.', true);
|
setListStatus('Keine offenen Bookmarks für den gewählten Tag.', true);
|
||||||
@@ -1074,8 +1148,8 @@
|
|||||||
const selection = undone.slice(0, count);
|
const selection = undone.slice(0, count);
|
||||||
|
|
||||||
state.processingBatch = true;
|
state.processingBatch = true;
|
||||||
if (bulkOpenBtn) {
|
if (dailyBulkOpenBtn) {
|
||||||
bulkOpenBtn.disabled = true;
|
dailyBulkOpenBtn.disabled = true;
|
||||||
}
|
}
|
||||||
if (!auto) {
|
if (!auto) {
|
||||||
setListStatus('');
|
setListStatus('');
|
||||||
@@ -1093,15 +1167,16 @@
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ day: state.dayKey })
|
body: JSON.stringify({ day: state.dayKey })
|
||||||
});
|
});
|
||||||
state.items = state.items.map((entry) => (entry.id === item.id ? updated : entry));
|
const normalized = normalizeItem(updated);
|
||||||
|
state.items = state.items.map((entry) => (entry.id === item.id ? normalized || entry : entry));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setListStatus('Einige Bookmarks konnten nicht abgehakt werden.', true);
|
setListStatus('Einige Bookmarks konnten nicht abgehakt werden.', true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.processingBatch = false;
|
state.processingBatch = false;
|
||||||
if (bulkOpenBtn) {
|
if (dailyBulkOpenBtn) {
|
||||||
bulkOpenBtn.disabled = false;
|
dailyBulkOpenBtn.disabled = false;
|
||||||
}
|
}
|
||||||
if (auto) {
|
if (auto) {
|
||||||
setListStatus('');
|
setListStatus('');
|
||||||
@@ -1111,20 +1186,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setImportStatus(message, isError = false) {
|
function setImportStatus(message, isError = false) {
|
||||||
if (!importStatus) {
|
if (!dailyImportStatus) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
importStatus.textContent = message || '';
|
dailyImportStatus.textContent = message || '';
|
||||||
importStatus.classList.toggle('form-status--error', !!isError);
|
dailyImportStatus.classList.toggle('form-status--error', !!isError);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetImportForm() {
|
function resetImportForm() {
|
||||||
if (importForm) {
|
if (dailyImportForm) {
|
||||||
importForm.reset();
|
dailyImportForm.reset();
|
||||||
}
|
}
|
||||||
const suggestedMarker = state.filters.marker && state.filters.marker !== '__none' ? state.filters.marker : '';
|
const suggestedMarker = state.filters.marker && state.filters.marker !== '__none' ? state.filters.marker : '';
|
||||||
if (importMarkerInput) {
|
if (dailyImportMarkerInput) {
|
||||||
importMarkerInput.value = suggestedMarker;
|
dailyImportMarkerInput.value = suggestedMarker;
|
||||||
}
|
}
|
||||||
setImportStatus('');
|
setImportStatus('');
|
||||||
}
|
}
|
||||||
@@ -1134,18 +1209,18 @@
|
|||||||
closeModal();
|
closeModal();
|
||||||
}
|
}
|
||||||
resetImportForm();
|
resetImportForm();
|
||||||
if (importModal) {
|
if (dailyImportModal) {
|
||||||
importModal.hidden = false;
|
dailyImportModal.hidden = false;
|
||||||
importModal.focus();
|
dailyImportModal.focus();
|
||||||
}
|
}
|
||||||
if (importInput) {
|
if (dailyImportInput) {
|
||||||
importInput.focus();
|
dailyImportInput.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeImportModal() {
|
function closeImportModal() {
|
||||||
if (importModal) {
|
if (dailyImportModal) {
|
||||||
importModal.hidden = true;
|
dailyImportModal.hidden = true;
|
||||||
}
|
}
|
||||||
resetImportForm();
|
resetImportForm();
|
||||||
}
|
}
|
||||||
@@ -1154,12 +1229,12 @@
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (state.importing) return;
|
if (state.importing) return;
|
||||||
|
|
||||||
const rawText = importInput ? importInput.value.trim() : '';
|
const rawText = dailyImportInput ? dailyImportInput.value.trim() : '';
|
||||||
const marker = importMarkerInput ? importMarkerInput.value.trim() : '';
|
const marker = dailyImportMarkerInput ? dailyImportMarkerInput.value.trim() : '';
|
||||||
if (!rawText) {
|
if (!rawText) {
|
||||||
setImportStatus('Bitte füge mindestens eine URL ein.', true);
|
setImportStatus('Bitte füge mindestens eine URL ein.', true);
|
||||||
if (importInput) {
|
if (dailyImportInput) {
|
||||||
importInput.focus();
|
dailyImportInput.focus();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1175,8 +1250,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.importing = true;
|
state.importing = true;
|
||||||
if (importSubmitBtn) {
|
if (dailyImportSubmitBtn) {
|
||||||
importSubmitBtn.disabled = true;
|
dailyImportSubmitBtn.disabled = true;
|
||||||
}
|
}
|
||||||
setImportStatus('Import läuft...');
|
setImportStatus('Import läuft...');
|
||||||
|
|
||||||
@@ -1186,7 +1261,8 @@
|
|||||||
body: JSON.stringify({ urls, marker, day: state.dayKey })
|
body: JSON.stringify({ urls, marker, day: state.dayKey })
|
||||||
});
|
});
|
||||||
|
|
||||||
const importedItems = Array.isArray(result && result.items) ? result.items : [];
|
const importedItemsRaw = Array.isArray(result && result.items) ? result.items : [];
|
||||||
|
const importedItems = importedItemsRaw.map((item) => normalizeItem(item)).filter(Boolean);
|
||||||
const importedIds = new Set(importedItems.map((entry) => entry.id));
|
const importedIds = new Set(importedItems.map((entry) => entry.id));
|
||||||
const remaining = state.items.filter((item) => !importedIds.has(item.id));
|
const remaining = state.items.filter((item) => !importedIds.has(item.id));
|
||||||
state.items = [...importedItems, ...remaining];
|
state.items = [...importedItems, ...remaining];
|
||||||
@@ -1207,30 +1283,30 @@
|
|||||||
setImportStatus(error && error.message ? error.message : 'Import fehlgeschlagen.', true);
|
setImportStatus(error && error.message ? error.message : 'Import fehlgeschlagen.', true);
|
||||||
} finally {
|
} finally {
|
||||||
state.importing = false;
|
state.importing = false;
|
||||||
if (importSubmitBtn) {
|
if (dailyImportSubmitBtn) {
|
||||||
importSubmitBtn.disabled = false;
|
dailyImportSubmitBtn.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupEvents() {
|
function setupEvents() {
|
||||||
if (prevDayBtn) {
|
if (dailyPrevDayBtn) {
|
||||||
prevDayBtn.addEventListener('click', () => setDayKey(formatDayKey(addDays(parseDayKey(state.dayKey), -1))));
|
dailyPrevDayBtn.addEventListener('click', () => setDayKey(formatDayKey(addDays(parseDayKey(state.dayKey), -1))));
|
||||||
}
|
}
|
||||||
if (nextDayBtn) {
|
if (dailyNextDayBtn) {
|
||||||
nextDayBtn.addEventListener('click', () => setDayKey(formatDayKey(addDays(parseDayKey(state.dayKey), 1))));
|
dailyNextDayBtn.addEventListener('click', () => setDayKey(formatDayKey(addDays(parseDayKey(state.dayKey), 1))));
|
||||||
}
|
}
|
||||||
if (todayBtn) {
|
if (dailyTodayBtn) {
|
||||||
todayBtn.addEventListener('click', () => setDayKey(formatDayKey(new Date())));
|
dailyTodayBtn.addEventListener('click', () => setDayKey(formatDayKey(new Date())));
|
||||||
}
|
}
|
||||||
if (refreshBtn) {
|
if (dailyRefreshBtn) {
|
||||||
refreshBtn.addEventListener('click', () => loadDailyBookmarks());
|
dailyRefreshBtn.addEventListener('click', () => loadDailyBookmarks());
|
||||||
}
|
}
|
||||||
if (openCreateBtn) {
|
if (dailyOpenCreateBtn) {
|
||||||
openCreateBtn.addEventListener('click', () => openModal('create'));
|
dailyOpenCreateBtn.addEventListener('click', () => openModal('create'));
|
||||||
}
|
}
|
||||||
if (modalCloseBtn) {
|
if (dailyModalCloseBtn) {
|
||||||
modalCloseBtn.addEventListener('click', closeModal);
|
dailyModalCloseBtn.addEventListener('click', closeModal);
|
||||||
}
|
}
|
||||||
if (modal) {
|
if (modal) {
|
||||||
modal.addEventListener('click', (event) => {
|
modal.addEventListener('click', (event) => {
|
||||||
@@ -1239,49 +1315,50 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (modalBackdrop) {
|
if (dailyModalBackdrop) {
|
||||||
modalBackdrop.addEventListener('click', closeModal);
|
dailyModalBackdrop.addEventListener('click', closeModal);
|
||||||
}
|
}
|
||||||
document.addEventListener('keydown', (event) => {
|
document.addEventListener('keydown', (event) => {
|
||||||
|
if (!active) return;
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
if (modal && !modal.hidden) {
|
if (modal && !modal.hidden) {
|
||||||
closeModal();
|
closeModal();
|
||||||
}
|
}
|
||||||
if (importModal && !importModal.hidden) {
|
if (dailyImportModal && !dailyImportModal.hidden) {
|
||||||
closeImportModal();
|
closeImportModal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (urlInput) {
|
if (dailyUrlInput) {
|
||||||
urlInput.addEventListener('input', updatePreviewLink);
|
dailyUrlInput.addEventListener('input', updatePreviewLink);
|
||||||
urlInput.addEventListener('blur', renderUrlSuggestions);
|
dailyUrlInput.addEventListener('blur', renderUrlSuggestions);
|
||||||
}
|
}
|
||||||
if (formEl) {
|
if (formEl) {
|
||||||
formEl.addEventListener('submit', submitForm);
|
formEl.addEventListener('submit', submitForm);
|
||||||
}
|
}
|
||||||
if (resetBtn) {
|
if (dailyResetBtn) {
|
||||||
resetBtn.addEventListener('click', resetForm);
|
dailyResetBtn.addEventListener('click', resetForm);
|
||||||
}
|
}
|
||||||
if (bulkCountSelect) {
|
if (dailyBulkCountSelect) {
|
||||||
bulkCountSelect.value = String(state.bulkCount);
|
dailyBulkCountSelect.value = String(state.bulkCount);
|
||||||
bulkCountSelect.addEventListener('change', () => {
|
dailyBulkCountSelect.addEventListener('change', () => {
|
||||||
const value = parseInt(bulkCountSelect.value, 10);
|
const value = parseInt(dailyBulkCountSelect.value, 10);
|
||||||
if (!Number.isNaN(value)) {
|
if (!Number.isNaN(value)) {
|
||||||
state.bulkCount = value;
|
state.bulkCount = value;
|
||||||
persistBulkCount(value);
|
persistBulkCount(value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (bulkOpenBtn) {
|
if (dailyBulkOpenBtn) {
|
||||||
bulkOpenBtn.addEventListener('click', () => openBatch());
|
dailyBulkOpenBtn.addEventListener('click', () => openBatch());
|
||||||
}
|
}
|
||||||
if (autoOpenOverlayPanel) {
|
if (dailyAutoOpenOverlayPanel) {
|
||||||
autoOpenOverlayPanel.addEventListener('click', () => cancelAutoOpen(true));
|
dailyAutoOpenOverlayPanel.addEventListener('click', () => cancelAutoOpen(true));
|
||||||
}
|
}
|
||||||
if (autoOpenToggle) {
|
if (dailyAutoOpenToggle) {
|
||||||
autoOpenToggle.checked = !!state.autoOpenEnabled;
|
dailyAutoOpenToggle.checked = !!state.autoOpenEnabled;
|
||||||
autoOpenToggle.addEventListener('change', () => {
|
dailyAutoOpenToggle.addEventListener('change', () => {
|
||||||
state.autoOpenEnabled = autoOpenToggle.checked;
|
state.autoOpenEnabled = dailyAutoOpenToggle.checked;
|
||||||
persistAutoOpenEnabled(state.autoOpenEnabled);
|
persistAutoOpenEnabled(state.autoOpenEnabled);
|
||||||
state.autoOpenTriggered = false;
|
state.autoOpenTriggered = false;
|
||||||
if (!state.autoOpenEnabled && state.autoOpenTimerId) {
|
if (!state.autoOpenEnabled && state.autoOpenTimerId) {
|
||||||
@@ -1322,8 +1399,8 @@
|
|||||||
renderTable();
|
renderTable();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (resetViewBtn) {
|
if (dailyResetViewBtn) {
|
||||||
resetViewBtn.addEventListener('click', () => {
|
dailyResetViewBtn.addEventListener('click', () => {
|
||||||
state.filters = { marker: '', url: '' };
|
state.filters = { marker: '', url: '' };
|
||||||
state.sort = { ...DEFAULT_SORT };
|
state.sort = { ...DEFAULT_SORT };
|
||||||
persistFilters(state.filters);
|
persistFilters(state.filters);
|
||||||
@@ -1352,36 +1429,54 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (openImportBtn) {
|
if (dailyOpenImportBtn) {
|
||||||
openImportBtn.addEventListener('click', openImportModal);
|
dailyOpenImportBtn.addEventListener('click', openImportModal);
|
||||||
}
|
}
|
||||||
if (importCloseBtn) {
|
if (dailyImportCloseBtn) {
|
||||||
importCloseBtn.addEventListener('click', closeImportModal);
|
dailyImportCloseBtn.addEventListener('click', closeImportModal);
|
||||||
}
|
}
|
||||||
if (importModal) {
|
if (dailyImportModal) {
|
||||||
importModal.addEventListener('click', (event) => {
|
dailyImportModal.addEventListener('click', (event) => {
|
||||||
if (event.target === importModal) {
|
if (event.target === dailyImportModal) {
|
||||||
closeImportModal();
|
closeImportModal();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (importBackdrop) {
|
if (dailyImportBackdrop) {
|
||||||
importBackdrop.addEventListener('click', closeImportModal);
|
dailyImportBackdrop.addEventListener('click', closeImportModal);
|
||||||
}
|
}
|
||||||
if (importForm) {
|
if (dailyImportForm) {
|
||||||
importForm.addEventListener('submit', submitImportForm);
|
dailyImportForm.addEventListener('submit', submitImportForm);
|
||||||
}
|
}
|
||||||
if (importResetBtn) {
|
if (dailyImportResetBtn) {
|
||||||
importResetBtn.addEventListener('click', resetImportForm);
|
dailyImportResetBtn.addEventListener('click', resetImportForm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
updateDayUI();
|
if (initialized) return;
|
||||||
setupEvents();
|
setupEvents();
|
||||||
loadDailyBookmarks();
|
initialized = true;
|
||||||
updatePreviewLink();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
active = false;
|
||||||
|
cancelAutoOpen(false);
|
||||||
|
ensureStyles(false);
|
||||||
|
hideAutoOpenOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
function activate() {
|
||||||
|
ensureStyles(true);
|
||||||
init();
|
init();
|
||||||
|
active = true;
|
||||||
|
updateDayUI();
|
||||||
|
updatePreviewLink();
|
||||||
|
loadDailyBookmarks();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.DailyBookmarksPage = {
|
||||||
|
activate,
|
||||||
|
deactivate: cleanup
|
||||||
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
523
web/index.html
523
web/index.html
@@ -10,6 +10,8 @@
|
|||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
<link rel="stylesheet" href="dashboard.css">
|
<link rel="stylesheet" href="dashboard.css">
|
||||||
<link rel="stylesheet" href="settings.css">
|
<link rel="stylesheet" href="settings.css">
|
||||||
|
<link rel="stylesheet" href="automation.css">
|
||||||
|
<link id="dailyBookmarksCss" rel="stylesheet" href="daily-bookmarks.css" disabled>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="shell">
|
<div class="shell">
|
||||||
@@ -24,8 +26,8 @@
|
|||||||
<a class="site-nav__btn" data-view-target="dashboard" href="dashboard.html">📊 Dashboard</a>
|
<a class="site-nav__btn" data-view-target="dashboard" href="dashboard.html">📊 Dashboard</a>
|
||||||
<a class="site-nav__btn" data-view-target="settings" href="settings.html">⚙️ Einstellungen</a>
|
<a class="site-nav__btn" data-view-target="settings" href="settings.html">⚙️ Einstellungen</a>
|
||||||
<a class="site-nav__btn" data-view-target="bookmarks" href="bookmarks.html">🔖 Bookmarks</a>
|
<a class="site-nav__btn" data-view-target="bookmarks" href="bookmarks.html">🔖 Bookmarks</a>
|
||||||
<a class="site-nav__btn" href="automation.html">⚡️ Automationen</a>
|
<a class="site-nav__btn" data-view-target="automation" href="index.html?view=automation">⚡️ Automationen</a>
|
||||||
<a class="site-nav__btn" href="daily-bookmarks.html">✅ Daily Bookmarks</a>
|
<a class="site-nav__btn" data-view-target="daily-bookmarks" href="index.html?view=daily-bookmarks">✅ Daily Bookmarks</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,8 +85,8 @@
|
|||||||
|
|
||||||
<div class="tabs-section">
|
<div class="tabs-section">
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<button class="tab-btn active" data-tab="pending">Offene Beiträge</button>
|
|
||||||
<button class="tab-btn" data-tab="all">Alle Beiträge</button>
|
<button class="tab-btn" data-tab="all">Alle Beiträge</button>
|
||||||
|
<button class="tab-btn active" data-tab="pending">Offene Beiträge</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="merge-controls" id="mergeControls" hidden>
|
<div class="merge-controls" id="mergeControls" hidden>
|
||||||
<div class="merge-actions">
|
<div class="merge-actions">
|
||||||
@@ -158,6 +160,486 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
<section id="view-automation" class="app-view automation-view" data-view="automation">
|
||||||
|
<div class="container">
|
||||||
|
<div class="auto-shell">
|
||||||
|
<header class="auto-hero">
|
||||||
|
<div class="hero-head">
|
||||||
|
<div class="hero-text">
|
||||||
|
<h1>Request Automationen</h1>
|
||||||
|
</div>
|
||||||
|
<div class="hero-actions">
|
||||||
|
<button class="ghost-btn" id="openImportBtn" type="button">📥 Vorlage importieren</button>
|
||||||
|
<button class="primary-btn" id="newAutomationBtn" type="button">+ Neue Automation</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hero-stats" id="heroStats"></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="panel list-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<div>
|
||||||
|
<p class="panel-eyebrow">Geplante Requests</p>
|
||||||
|
<h2>Automationen</h2>
|
||||||
|
</div>
|
||||||
|
<div class="panel-actions">
|
||||||
|
<input id="tableFilterInput" class="filter-input" type="search" placeholder="Filtern nach Name/Typ…" />
|
||||||
|
<button class="ghost-btn" id="refreshBtn" type="button">Aktualisieren</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="listStatus" class="list-status" aria-live="polite"></div>
|
||||||
|
<div class="table-wrap" id="automationTable">
|
||||||
|
<table class="auto-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-sort-column="name">Name<span class="sort-indicator"></span></th>
|
||||||
|
<th data-sort-column="next">Nächster Lauf<span class="sort-indicator"></span></th>
|
||||||
|
<th data-sort-column="last">Letzter Lauf<span class="sort-indicator"></span></th>
|
||||||
|
<th data-sort-column="status">Status<span class="sort-indicator"></span></th>
|
||||||
|
<th data-sort-column="runs">#Läufe<span class="sort-indicator"></span></th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
<tr class="table-filter-row">
|
||||||
|
<th><input id="filterName" type="search" placeholder="Name/Typ/E-Mail/URL"></th>
|
||||||
|
<th><input id="filterNext" type="search" placeholder="z.B. heute"></th>
|
||||||
|
<th><input id="filterLast" type="search" placeholder="z.B. HTTP 200"></th>
|
||||||
|
<th>
|
||||||
|
<select id="filterStatus">
|
||||||
|
<option value="">Alle</option>
|
||||||
|
<option value="success">OK</option>
|
||||||
|
<option value="error">Fehler</option>
|
||||||
|
</select>
|
||||||
|
</th>
|
||||||
|
<th><input id="filterRuns" type="number" min="0" placeholder="≥"></th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="requestTableBody" class="list"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel runs-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<div>
|
||||||
|
<p class="panel-eyebrow">Verlauf</p>
|
||||||
|
<h2>Run-Historie</h2>
|
||||||
|
</div>
|
||||||
|
<p class="runs-hint">Letzte Läufe der ausgewählten Automation.</p>
|
||||||
|
</div>
|
||||||
|
<div id="runsStatus" class="runs-status" aria-live="polite"></div>
|
||||||
|
<ul id="runsList" class="runs-list"></ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="formModal" class="modal" hidden>
|
||||||
|
<div class="modal__backdrop" id="formModalBackdrop"></div>
|
||||||
|
<div class="modal__content" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
|
||||||
|
<div class="modal__header">
|
||||||
|
<div>
|
||||||
|
<p class="panel-eyebrow" id="formModeLabel">Neue Automation</p>
|
||||||
|
<h2 id="modalTitle">Request & Zeitplan</h2>
|
||||||
|
</div>
|
||||||
|
<button class="ghost-btn" id="modalCloseBtn" type="button">×</button>
|
||||||
|
</div>
|
||||||
|
<form id="automationForm" class="form-grid" novalidate>
|
||||||
|
<div class="field">
|
||||||
|
<label for="typeSelect">Typ</label>
|
||||||
|
<select id="typeSelect">
|
||||||
|
<option value="request">HTTP Request</option>
|
||||||
|
<option value="email">E-Mail</option>
|
||||||
|
<option value="flow">Flow (bis 3 Schritte)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="nameInput">Name *</label>
|
||||||
|
<input id="nameInput" type="text" placeholder="API Ping (stündlich)" required maxlength="160">
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="descriptionInput">Notizen</label>
|
||||||
|
<textarea id="descriptionInput" rows="2" placeholder="Kurzbeschreibung oder Zweck"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="field" data-section="http">
|
||||||
|
<label for="urlInput">URL-Template *</label>
|
||||||
|
<input id="urlInput" type="url" placeholder="https://api.example.com/{{date}}/trigger" required>
|
||||||
|
</div>
|
||||||
|
<div class="field inline" data-section="http">
|
||||||
|
<label for="methodSelect">Methode</label>
|
||||||
|
<select id="methodSelect">
|
||||||
|
<option value="GET">GET</option>
|
||||||
|
<option value="POST">POST</option>
|
||||||
|
<option value="PUT">PUT</option>
|
||||||
|
<option value="PATCH">PATCH</option>
|
||||||
|
<option value="DELETE">DELETE</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="field inline">
|
||||||
|
<label for="activeToggle">Aktiv</label>
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox" id="activeToggle" checked>
|
||||||
|
<span class="switch-slider"></span>
|
||||||
|
<span class="switch-label">Plan aktiv</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="field" data-section="http">
|
||||||
|
<label for="headersInput">Headers (Key: Value pro Zeile oder JSON)</label>
|
||||||
|
<textarea id="headersInput" rows="3" placeholder="Authorization: Bearer {{token}}\nContent-Type: application/json"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="field" data-section="http">
|
||||||
|
<label for="bodyInput">Body (optional, Templates möglich)</label>
|
||||||
|
<textarea id="bodyInput" rows="5" placeholder='{"date":"{{date}}","id":"{{uuid}}"}'></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="field full" data-section="email">
|
||||||
|
<label for="emailToInput">E-Mail Empfänger *</label>
|
||||||
|
<input id="emailToInput" type="text" placeholder="max@example.com, lisa@example.com">
|
||||||
|
</div>
|
||||||
|
<div class="field full" data-section="email">
|
||||||
|
<label for="emailSubjectInput">Betreff *</label>
|
||||||
|
<input id="emailSubjectInput" type="text" placeholder="Status Update {{date}}">
|
||||||
|
</div>
|
||||||
|
<div class="field full" data-section="email">
|
||||||
|
<label for="emailBodyInput">Body *</label>
|
||||||
|
<textarea id="emailBodyInput" rows="6" placeholder="Hallo,\nheutiger Status: {{uuid}}"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="field full" data-section="flow">
|
||||||
|
<div class="template-hint">
|
||||||
|
<p class="template-title">Flow Schritte</p>
|
||||||
|
<p class="template-copy">Max. 3 Schritte, Kontext steht als {{step1_json}}, {{step1_text}}, {{step1_status_code}} usw. im nächsten Schritt zur Verfügung.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field" data-section="flow">
|
||||||
|
<label for="flowStep1Url">Step 1 URL *</label>
|
||||||
|
<input id="flowStep1Url" type="url" placeholder="https://api.example.com/first">
|
||||||
|
</div>
|
||||||
|
<div class="field inline" data-section="flow">
|
||||||
|
<label for="flowStep1Method">Step 1 Methode</label>
|
||||||
|
<select id="flowStep1Method">
|
||||||
|
<option value="GET">GET</option>
|
||||||
|
<option value="POST">POST</option>
|
||||||
|
<option value="PUT">PUT</option>
|
||||||
|
<option value="PATCH">PATCH</option>
|
||||||
|
<option value="DELETE">DELETE</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="field" data-section="flow">
|
||||||
|
<label for="flowStep1Headers">Step 1 Headers</label>
|
||||||
|
<textarea id="flowStep1Headers" rows="2" placeholder="Authorization: Bearer {{token}}"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="field" data-section="flow">
|
||||||
|
<label for="flowStep1Body">Step 1 Body</label>
|
||||||
|
<textarea id="flowStep1Body" rows="3" placeholder='{"id":"{{uuid}}"}'></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="field" data-section="flow">
|
||||||
|
<label for="flowStep2Url">Step 2 URL (optional)</label>
|
||||||
|
<input id="flowStep2Url" type="url" placeholder="https://api.example.com/second">
|
||||||
|
</div>
|
||||||
|
<div class="field inline" data-section="flow">
|
||||||
|
<label for="flowStep2Method">Step 2 Methode</label>
|
||||||
|
<select id="flowStep2Method">
|
||||||
|
<option value="GET">GET</option>
|
||||||
|
<option value="POST">POST</option>
|
||||||
|
<option value="PUT">PUT</option>
|
||||||
|
<option value="PATCH">PATCH</option>
|
||||||
|
<option value="DELETE">DELETE</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="field" data-section="flow">
|
||||||
|
<label for="flowStep2Headers">Step 2 Headers</label>
|
||||||
|
<textarea id="flowStep2Headers" rows="2" placeholder="Authorization: Bearer {{token}}"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="field" data-section="flow">
|
||||||
|
<label for="flowStep2Body">Step 2 Body</label>
|
||||||
|
<textarea id="flowStep2Body" rows="3" placeholder='{"fromStep1":"{{step1_json.id}}"}'></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="field inline">
|
||||||
|
<label for="intervalPreset">Intervall</label>
|
||||||
|
<select id="intervalPreset">
|
||||||
|
<option value="hourly">Jede Stunde</option>
|
||||||
|
<option value="daily">Jeden Tag</option>
|
||||||
|
<option value="custom">Eigene Minuten</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="field inline">
|
||||||
|
<label for="intervalMinutesInput">Intervall (Minuten)</label>
|
||||||
|
<input id="intervalMinutesInput" type="number" min="5" max="20160" step="5" value="60">
|
||||||
|
</div>
|
||||||
|
<div class="field inline">
|
||||||
|
<label for="jitterInput">Varianz (Minuten)</label>
|
||||||
|
<input id="jitterInput" type="number" min="0" max="120" step="5" value="10">
|
||||||
|
<small>Auslösung erfolgt zufällig +0…Varianz Min nach dem Intervall.</small>
|
||||||
|
</div>
|
||||||
|
<div class="field inline">
|
||||||
|
<label for="startAtInput">Start ab</label>
|
||||||
|
<input id="startAtInput" type="datetime-local">
|
||||||
|
</div>
|
||||||
|
<div class="field inline">
|
||||||
|
<label for="runUntilInput">Läuft bis</label>
|
||||||
|
<input id="runUntilInput" type="datetime-local">
|
||||||
|
</div>
|
||||||
|
<div class="field full">
|
||||||
|
<div class="template-hint">
|
||||||
|
<p class="template-title">Platzhalter</p>
|
||||||
|
<table class="placeholder-table">
|
||||||
|
<tbody id="placeholderTableBody"></tbody>
|
||||||
|
</table>
|
||||||
|
<p class="placeholder-hint">Beispiele beziehen sich auf den aktuellen Zeitpunkt.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field full">
|
||||||
|
<div class="preview-panel">
|
||||||
|
<div class="preview-header">
|
||||||
|
<div>
|
||||||
|
<p class="panel-eyebrow">Vorschau</p>
|
||||||
|
<h3>Aufgelöste Werte</h3>
|
||||||
|
</div>
|
||||||
|
<button class="secondary-btn" type="button" id="refreshPreviewBtn">Aktualisieren</button>
|
||||||
|
</div>
|
||||||
|
<div class="preview-grid">
|
||||||
|
<div class="preview-block">
|
||||||
|
<p class="preview-label">URL</p>
|
||||||
|
<pre id="previewUrl" class="preview-value">—</pre>
|
||||||
|
</div>
|
||||||
|
<div class="preview-block">
|
||||||
|
<p class="preview-label">Headers</p>
|
||||||
|
<pre id="previewHeaders" class="preview-value">—</pre>
|
||||||
|
</div>
|
||||||
|
<div class="preview-block">
|
||||||
|
<p class="preview-label">Body</p>
|
||||||
|
<pre id="previewBody" class="preview-value">—</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="preview-hint">Die Vorschau nutzt die aktuellen Formularwerte und füllt Platzhalter ({{date}}, {{uuid}}, …) mit Beispielwerten.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field full modal-actions">
|
||||||
|
<div id="formStatus" class="form-status" aria-live="polite"></div>
|
||||||
|
<div class="panel-actions">
|
||||||
|
<button class="ghost-btn" id="resetFormBtn" type="button">Zurücksetzen</button>
|
||||||
|
<button class="primary-btn" id="saveBtn" type="submit">Speichern</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="importModal" class="modal" hidden>
|
||||||
|
<div class="modal__backdrop" id="importModalBackdrop"></div>
|
||||||
|
<div class="modal__content" role="dialog" aria-modal="true" aria-labelledby="importTitle">
|
||||||
|
<div class="modal__header">
|
||||||
|
<div>
|
||||||
|
<p class="panel-eyebrow">Import</p>
|
||||||
|
<h2 id="importTitle">Vorlage einfügen</h2>
|
||||||
|
</div>
|
||||||
|
<button class="ghost-btn" id="importCloseBtn" type="button">×</button>
|
||||||
|
</div>
|
||||||
|
<p class="import-hint">Füge hier "Copy as cURL", "Copy as fetch" oder Powershell ein. Header, Methode, Body und URL werden übernommen.</p>
|
||||||
|
<textarea id="importInput" rows="7" placeholder="curl https://api.example.com -X POST -H 'Authorization: Bearer token' --data '{\"hello\":\"world\"}'"></textarea>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<div id="importStatus" class="import-status" aria-live="polite"></div>
|
||||||
|
<button class="secondary-btn" id="applyImportBtn" type="button">Vorlage übernehmen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section id="view-daily-bookmarks" class="app-view daily-bookmarks-view" data-view="daily-bookmarks">
|
||||||
|
<div class="container">
|
||||||
|
<div class="daily-shell">
|
||||||
|
<header class="hero">
|
||||||
|
<h1 class="hero__title">Daily Bookmarks</h1>
|
||||||
|
<div class="hero__controls">
|
||||||
|
<div class="day-switch">
|
||||||
|
<button class="ghost-btn" id="dailyPrevDayBtn" aria-label="Vorheriger Tag">◀</button>
|
||||||
|
<div class="day-switch__label">
|
||||||
|
<div id="dailyDayLabel" class="day-switch__day">Heute</div>
|
||||||
|
<div id="dailyDaySubLabel" class="day-switch__sub"></div>
|
||||||
|
</div>
|
||||||
|
<button class="ghost-btn" id="dailyNextDayBtn" aria-label="Nächster Tag">▶</button>
|
||||||
|
<button class="ghost-btn ghost-btn--today" id="dailyTodayBtn">Heute</button>
|
||||||
|
</div>
|
||||||
|
<div class="hero__actions">
|
||||||
|
<div class="bulk-actions">
|
||||||
|
<label for="dailyBulkCountSelect">Anzahl</label>
|
||||||
|
<select id="dailyBulkCountSelect">
|
||||||
|
<option value="1">1</option>
|
||||||
|
<option value="5">5</option>
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="15">15</option>
|
||||||
|
<option value="20">20</option>
|
||||||
|
</select>
|
||||||
|
<label class="auto-open-toggle">
|
||||||
|
<input type="checkbox" id="dailyAutoOpenToggle">
|
||||||
|
<span>Auto öffnen</span>
|
||||||
|
</label>
|
||||||
|
<button class="secondary-btn" id="dailyBulkOpenBtn" type="button">Öffnen & abhaken</button>
|
||||||
|
</div>
|
||||||
|
<button class="primary-btn" id="dailyOpenCreateBtn" type="button">+ Bookmark</button>
|
||||||
|
<button class="secondary-btn" id="dailyOpenImportBtn" type="button">Liste importieren</button>
|
||||||
|
<button class="ghost-btn" id="dailyRefreshBtn" type="button">Aktualisieren</button>
|
||||||
|
<span id="dailyHeroStats" class="hero__stats"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div id="dailyAutoOpenOverlay" class="auto-open-overlay" hidden>
|
||||||
|
<div class="auto-open-overlay__panel" id="dailyAutoOpenOverlayPanel">
|
||||||
|
<div class="auto-open-overlay__badge">Auto-Öffnen startet gleich</div>
|
||||||
|
<div class="auto-open-overlay__timer">
|
||||||
|
<span id="dailyAutoOpenCountdown" class="auto-open-overlay__count">0.0</span>
|
||||||
|
<span class="auto-open-overlay__unit">Sek.</span>
|
||||||
|
</div>
|
||||||
|
<p class="auto-open-overlay__text">
|
||||||
|
Die nächste Runde deiner gefilterten Bookmarks öffnet automatisch. Abbrechen, falls du noch warten willst.
|
||||||
|
</p>
|
||||||
|
<p class="auto-open-overlay__hint">Klicke irgendwo in dieses Panel, um abzubrechen.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main class="panel list-panel">
|
||||||
|
<header class="panel__header panel__header--row">
|
||||||
|
<div>
|
||||||
|
<p class="panel__eyebrow">Tägliche Liste</p>
|
||||||
|
<h2 class="panel__title">Alle Bookmarks</h2>
|
||||||
|
</div>
|
||||||
|
<div class="list-summary" id="dailyListSummary"></div>
|
||||||
|
</header>
|
||||||
|
<div id="dailyListStatus" class="list-status" role="status" aria-live="polite"></div>
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table class="bookmark-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="col-url">
|
||||||
|
<button type="button" class="sort-btn" data-sort-key="url_template">URL (aufgelöst)</button>
|
||||||
|
</th>
|
||||||
|
<th class="col-marker">
|
||||||
|
<button type="button" class="sort-btn" data-sort-key="marker">Marker</button>
|
||||||
|
</th>
|
||||||
|
<th class="col-created">
|
||||||
|
<button type="button" class="sort-btn" data-sort-key="created_at">Erstelldatum</button>
|
||||||
|
</th>
|
||||||
|
<th class="col-last">
|
||||||
|
<button type="button" class="sort-btn" data-sort-key="last_completed_at">Letzte Erledigung</button>
|
||||||
|
</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
<tr class="table-filter-row">
|
||||||
|
<th>
|
||||||
|
<label class="visually-hidden" for="dailyUrlFilter">Nach URL filtern</label>
|
||||||
|
<input id="dailyUrlFilter" type="search" placeholder="URL filtern">
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<label class="visually-hidden" for="dailyMarkerFilter">Nach Marker filtern</label>
|
||||||
|
<select id="dailyMarkerFilter">
|
||||||
|
<option value="">Alle Marker</option>
|
||||||
|
<option value="__none">Ohne Marker</option>
|
||||||
|
</select>
|
||||||
|
</th>
|
||||||
|
<th></th>
|
||||||
|
<th></th>
|
||||||
|
<th class="filter-hint">
|
||||||
|
<button type="button" class="ghost-btn ghost-btn--tiny" id="dailyResetViewBtn" aria-label="Filter und Sortierung zurücksetzen">↺</button>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="dailyTableBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="dailyBookmarkModal" class="modal" hidden>
|
||||||
|
<div class="modal__backdrop" id="dailyModalBackdrop"></div>
|
||||||
|
<div class="modal__content" role="dialog" aria-modal="true" aria-labelledby="dailyModalTitle">
|
||||||
|
<header class="modal__header">
|
||||||
|
<div>
|
||||||
|
<p class="panel__eyebrow" id="dailyFormModeLabel">Neues Bookmark</p>
|
||||||
|
<h2 id="dailyModalTitle" class="panel__title">Bookmark pflegen</h2>
|
||||||
|
<p class="panel__subtitle">Titel optional, URL-Template erforderlich. Platzhalter werden für den gewählten Tag aufgelöst.</p>
|
||||||
|
</div>
|
||||||
|
<button class="ghost-btn modal__close" type="button" id="dailyModalCloseBtn" aria-label="Schließen">×</button>
|
||||||
|
</header>
|
||||||
|
<form id="dailyBookmarkForm" class="bookmark-form" autocomplete="off">
|
||||||
|
<label class="field">
|
||||||
|
<span>Titel (optional)</span>
|
||||||
|
<input id="dailyTitleInput" type="text" name="title" maxlength="160" placeholder="z.B. Daily Gewinnspielrunde">
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>URL-Template *</span>
|
||||||
|
<input id="dailyUrlInput" type="url" name="url_template" maxlength="800" placeholder="https://www.test.de/tag-{{day}}/" required>
|
||||||
|
</label>
|
||||||
|
<div id="dailyUrlSuggestionBox" class="suggestion-box" hidden></div>
|
||||||
|
<label class="field">
|
||||||
|
<span>Notiz (optional)</span>
|
||||||
|
<textarea id="dailyNotesInput" name="notes" maxlength="800" rows="3" placeholder="Kurze Hinweise oder To-do für diesen Link"></textarea>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Marker (optional)</span>
|
||||||
|
<input id="dailyMarkerInput" type="text" name="marker" maxlength="120" placeholder="z.B. März-Import oder Kampagne A">
|
||||||
|
</label>
|
||||||
|
<div class="field field--switch">
|
||||||
|
<span>Status</span>
|
||||||
|
<label class="switch-control">
|
||||||
|
<input id="dailyActiveInput" type="checkbox" name="is_active" checked>
|
||||||
|
<span class="switch-control__label">Bookmark ist aktiv</span>
|
||||||
|
</label>
|
||||||
|
<p class="field__hint">Deaktivierte Bookmarks bleiben erhalten, werden aber beim Auto-Öffnen und Abhaken übersprungen.</p>
|
||||||
|
</div>
|
||||||
|
<div class="form-preview">
|
||||||
|
<div>
|
||||||
|
<p class="form-preview__label">Aufgelöste URL für den gewählten Tag:</p>
|
||||||
|
<a id="dailyPreviewLink" class="form-preview__link" href="#" target="_blank" rel="noopener">–</a>
|
||||||
|
</div>
|
||||||
|
<div class="form-preview__actions">
|
||||||
|
<button class="secondary-btn" type="button" id="dailyResetBtn">Zurücksetzen</button>
|
||||||
|
<button class="primary-btn" type="submit" id="dailySubmitBtn">Speichern</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="placeholder-help">
|
||||||
|
<p class="placeholder-help__title">Dynamische Platzhalter</p>
|
||||||
|
<ul class="placeholder-help__list">
|
||||||
|
<li><code>{{day}}</code> Tag des Monats (1–31), <code>{{dd}}</code> zweistellig</li>
|
||||||
|
<li><code>{{date}}</code> liefert <code>YYYY-MM-DD</code></li>
|
||||||
|
<li><code>{{mm}}</code> Monat zweistellig, <code>{{yyyy}}</code> Jahr</li>
|
||||||
|
<li><code>{{day+1}}</code> oder <code>{{date-2}}</code> verschieben um Tage</li>
|
||||||
|
<li><code>{{counter:477}}</code> Basiswert + aktueller Tag, z.B. <code>https://www.test.de/sweepstakes/{{counter:477}}/</code></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div id="dailyFormStatus" class="form-status" role="status" aria-live="polite"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="dailyImportModal" class="modal" hidden>
|
||||||
|
<div class="modal__backdrop" id="dailyImportBackdrop"></div>
|
||||||
|
<div class="modal__content" role="dialog" aria-modal="true" aria-labelledby="dailyImportModalTitle">
|
||||||
|
<header class="modal__header">
|
||||||
|
<div>
|
||||||
|
<p class="panel__eyebrow">Masseneingabe</p>
|
||||||
|
<h2 id="dailyImportModalTitle" class="panel__title">Viele Bookmarks importieren</h2>
|
||||||
|
<p class="panel__subtitle">Füge hunderte Links gleichzeitig hinzu und vergebe einen gemeinsamen Marker.</p>
|
||||||
|
</div>
|
||||||
|
<button class="ghost-btn modal__close" type="button" id="dailyImportCloseBtn" aria-label="Schließen">×</button>
|
||||||
|
</header>
|
||||||
|
<form id="dailyImportForm" class="bookmark-form" autocomplete="off">
|
||||||
|
<label class="field">
|
||||||
|
<span>Liste der URL-Templates *</span>
|
||||||
|
<textarea id="dailyImportInput" name="import_urls" maxlength="120000" rows="8" placeholder="Jeder Link in eine neue Zeile, z.B. https://example.com/{{date}}/"></textarea>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Marker für alle Einträge (optional)</span>
|
||||||
|
<input id="dailyImportMarkerInput" type="text" name="import_marker" maxlength="120" placeholder="z.B. Batch März 2024">
|
||||||
|
</label>
|
||||||
|
<p class="import-hint">Doppelte oder ungültige Zeilen werden automatisch übersprungen.</p>
|
||||||
|
<div class="import-actions">
|
||||||
|
<button class="ghost-btn" type="button" id="dailyImportResetBtn">Zurücksetzen</button>
|
||||||
|
<button class="primary-btn" type="submit" id="dailyImportSubmitBtn">Importieren</button>
|
||||||
|
</div>
|
||||||
|
<div id="dailyImportStatus" class="import-status" role="status" aria-live="polite"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
<section id="view-dashboard" class="app-view" data-view="dashboard">
|
<section id="view-dashboard" class="app-view" data-view="dashboard">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="page-toolbar page-toolbar--dashboard">
|
<div class="page-toolbar page-toolbar--dashboard">
|
||||||
@@ -183,7 +665,7 @@
|
|||||||
<option value="5">Profil 5</option>
|
<option value="5">Profil 5</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-primary" id="refreshBtn">
|
<button type="button" class="btn btn-primary" id="dailyRefreshBtn">
|
||||||
🔄 Aktualisieren
|
🔄 Aktualisieren
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -813,6 +1295,9 @@
|
|||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
<script src="dashboard.js"></script>
|
<script src="dashboard.js"></script>
|
||||||
<script src="settings.js"></script>
|
<script src="settings.js"></script>
|
||||||
|
<script src="vendor/list.min.js"></script>
|
||||||
|
<script src="automation.js"></script>
|
||||||
|
<script src="daily-bookmarks.js"></script>
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
const buttons = Array.from(document.querySelectorAll('[data-view-target]'));
|
const buttons = Array.from(document.querySelectorAll('[data-view-target]'));
|
||||||
@@ -889,5 +1374,35 @@
|
|||||||
window.history.replaceState({ view: defaultView }, '', initQuery ? `${window.location.pathname}?${initQuery}` : window.location.pathname);
|
window.history.replaceState({ view: defaultView }, '', initQuery ? `${window.location.pathname}?${initQuery}` : window.location.pathname);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
const AUTOMATION_VIEW = 'automation';
|
||||||
|
const DAILY_VIEW = 'daily-bookmarks';
|
||||||
|
function handleViewChange(event) {
|
||||||
|
const view = event?.detail?.view;
|
||||||
|
if (view === AUTOMATION_VIEW) {
|
||||||
|
window.AutomationPage?.activate?.();
|
||||||
|
} else {
|
||||||
|
window.AutomationPage?.deactivate?.();
|
||||||
|
}
|
||||||
|
if (view === DAILY_VIEW) {
|
||||||
|
window.DailyBookmarksPage?.activate?.();
|
||||||
|
} else {
|
||||||
|
window.DailyBookmarksPage?.deactivate?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('app:view-change', handleViewChange);
|
||||||
|
|
||||||
|
const automationSection = document.querySelector('[data-view="automation"]');
|
||||||
|
if (automationSection && automationSection.classList.contains('app-view--active')) {
|
||||||
|
window.AutomationPage?.activate?.();
|
||||||
|
}
|
||||||
|
const dailySection = document.querySelector('[data-view="daily-bookmarks"]');
|
||||||
|
if (dailySection && dailySection.classList.contains('app-view--active')) {
|
||||||
|
window.DailyBookmarksPage?.activate?.();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
/* Settings Page Styles */
|
/* Settings Page Styles */
|
||||||
|
|
||||||
.settings-container {
|
.settings-container {
|
||||||
max-width: 800px;
|
max-width: var(--content-max-width, 1600px);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 24px 0;
|
padding: 0 0 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-section {
|
.settings-section {
|
||||||
|
|||||||
@@ -4,6 +4,11 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--content-max-width: 1300px;
|
||||||
|
--top-gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
background: #f0f2f5;
|
background: #f0f2f5;
|
||||||
@@ -24,7 +29,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 1200px;
|
max-width: var(--content-max-width);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
@@ -35,7 +40,7 @@ header {
|
|||||||
padding: 16px 18px;
|
padding: 16px 18px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
|
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
|
||||||
margin-bottom: 18px;
|
margin-bottom: var(--top-gap);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -1533,13 +1538,13 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bookmark-page {
|
.bookmark-page {
|
||||||
margin-top: 32px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bookmark-page__panel {
|
.bookmark-page__panel {
|
||||||
width: min(960px, 100%);
|
width: 100%;
|
||||||
|
max-width: var(--content-max-width);
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
border: 1px solid #e5e7eb;
|
border: 1px solid #e5e7eb;
|
||||||
|
|||||||
Reference in New Issue
Block a user