Add Putzliga mood import
This commit is contained in:
+108
-1
@@ -40,7 +40,7 @@ final class App
|
|||||||
$this->triggerReminderCheckFromTraffic($method, $path);
|
$this->triggerReminderCheckFromTraffic($method, $path);
|
||||||
$hasUsers = $this->users->hasAnyUsers();
|
$hasUsers = $this->users->hasAnyUsers();
|
||||||
$isAuthenticated = $this->auth->check();
|
$isAuthenticated = $this->auth->check();
|
||||||
$systemPaths = ['/reminders/run', '/api/health'];
|
$systemPaths = ['/reminders/run', '/api/health', '/api/putzliga'];
|
||||||
|
|
||||||
// A failed setup must never leave the app in a half-authenticated redirect loop.
|
// A failed setup must never leave the app in a half-authenticated redirect loop.
|
||||||
if (!$hasUsers && $isAuthenticated) {
|
if (!$hasUsers && $isAuthenticated) {
|
||||||
@@ -157,6 +157,14 @@ final class App
|
|||||||
$this->handleHealthImportStatus();
|
$this->handleHealthImportStatus();
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
case '/api/putzliga':
|
||||||
|
if ($method !== 'POST') {
|
||||||
|
json_response(['ok' => false, 'message' => 'Method Not Allowed'], 405);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->handlePutzligaImport();
|
||||||
|
return;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
View::render('not-found', [
|
View::render('not-found', [
|
||||||
@@ -348,6 +356,81 @@ final class App
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function handlePutzligaImport(): void
|
||||||
|
{
|
||||||
|
$token = $this->healthImportBearerToken();
|
||||||
|
if ($token === '') {
|
||||||
|
json_response(['ok' => false, 'message' => 'Bearer-Token fehlt.'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->users->findByPutzligaImportToken($token);
|
||||||
|
if ($user === null) {
|
||||||
|
json_response(['ok' => false, 'message' => 'Bearer-Token ist ungültig.'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = $this->decodeHealthImportPayload((string) file_get_contents('php://input'));
|
||||||
|
$date = (string) ($payload['date'] ?? '');
|
||||||
|
$tasksPayload = is_array($payload['tasks'] ?? null) ? $payload['tasks'] : [];
|
||||||
|
$tasks = array_values(array_filter(array_map(
|
||||||
|
static fn (mixed $task): string => trim((string) $task),
|
||||||
|
$tasksPayload
|
||||||
|
), static fn (string $task): bool => $task !== ''));
|
||||||
|
|
||||||
|
if (!$this->isValidDate($date)) {
|
||||||
|
json_response(['ok' => false, 'message' => 'Datum fehlt oder ist ungültig.'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($tasks) < 3) {
|
||||||
|
json_response(['ok' => false, 'message' => 'Mindestens 3 erledigte Aufgaben sind nötig.'], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$username = (string) ($user['username'] ?? '');
|
||||||
|
$settings = $this->hydrateSettings($this->settings->forUser($username));
|
||||||
|
$entries = $this->entries->all($username);
|
||||||
|
$entryMap = [];
|
||||||
|
foreach ($entries as $entry) {
|
||||||
|
if (is_array($entry) && $this->isValidDate((string) ($entry['date'] ?? ''))) {
|
||||||
|
$entryMap[(string) $entry['date']] = $entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$entry = $entryMap[$date] ?? $this->scoring->normalize([
|
||||||
|
'date' => $date,
|
||||||
|
'pain_enabled' => !empty($settings['tracking']['pain_enabled']),
|
||||||
|
'walk_mode' => (string) ($settings['walk']['mode'] ?? 'time'),
|
||||||
|
'summary' => ['comment' => '', 'mood' => 0, 'energy' => 0, 'stress' => 0, 'alcohol' => false],
|
||||||
|
'events' => [],
|
||||||
|
'background_image' => '',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$importID = 'putzliga-' . $date;
|
||||||
|
$events = array_values(array_filter(
|
||||||
|
is_array($entry['events'] ?? null) ? $entry['events'] : [],
|
||||||
|
static fn (array $event): bool => (string) ($event['import_id'] ?? '') !== $importID
|
||||||
|
));
|
||||||
|
$events[] = [
|
||||||
|
'id' => $importID,
|
||||||
|
'type' => 'event',
|
||||||
|
'time' => (string) ($payload['time'] ?? ''),
|
||||||
|
'comment' => 'Du warst fleißig',
|
||||||
|
'value' => 0,
|
||||||
|
'unit' => '',
|
||||||
|
'mood' => 0,
|
||||||
|
'energy' => 1,
|
||||||
|
'stress' => 0,
|
||||||
|
'source' => 'putzliga',
|
||||||
|
'import_id' => $importID,
|
||||||
|
'task_titles' => array_slice(array_values(array_unique($tasks)), 0, 20),
|
||||||
|
];
|
||||||
|
|
||||||
|
$entry['events'] = $events;
|
||||||
|
$entryMap[$date] = $entry;
|
||||||
|
$this->persistUserEntries($username, $settings, array_values($entryMap));
|
||||||
|
$this->users->recordPutzligaImport($username, 'ok', count($tasks) . ' Aufgaben synchronisiert.');
|
||||||
|
|
||||||
|
json_response(['ok' => true, 'message' => 'Putzliga-Moment aktualisiert.', 'tasks' => count($tasks)]);
|
||||||
|
}
|
||||||
|
|
||||||
private function healthImportBearerToken(): string
|
private function healthImportBearerToken(): string
|
||||||
{
|
{
|
||||||
$header = trim((string) ($_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? ''));
|
$header = trim((string) ($_SERVER['HTTP_AUTHORIZATION'] ?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ?? ''));
|
||||||
@@ -2394,6 +2477,14 @@ final class App
|
|||||||
$healthImportToken = null;
|
$healthImportToken = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$pendingPutzligaTokens = is_array($_SESSION['_putzliga_import_token'] ?? null) ? $_SESSION['_putzliga_import_token'] : [];
|
||||||
|
$putzligaImportToken = $pendingPutzligaTokens[$user['username']] ?? null;
|
||||||
|
if (is_string($putzligaImportToken)) {
|
||||||
|
unset($_SESSION['_putzliga_import_token'][$user['username']]);
|
||||||
|
} else {
|
||||||
|
$putzligaImportToken = null;
|
||||||
|
}
|
||||||
|
|
||||||
$optionsOpenPanel = trim((string) ($_GET['panel'] ?? ''));
|
$optionsOpenPanel = trim((string) ($_GET['panel'] ?? ''));
|
||||||
if ($optionsOpenPanel === 'score') {
|
if ($optionsOpenPanel === 'score') {
|
||||||
$optionsOpenPanel = '';
|
$optionsOpenPanel = '';
|
||||||
@@ -2414,6 +2505,9 @@ final class App
|
|||||||
'healthImportConfig' => $this->users->healthImportConfig($user['username']),
|
'healthImportConfig' => $this->users->healthImportConfig($user['username']),
|
||||||
'healthImportToken' => $healthImportToken,
|
'healthImportToken' => $healthImportToken,
|
||||||
'healthImportUrl' => app_origin() . '/api/health',
|
'healthImportUrl' => app_origin() . '/api/health',
|
||||||
|
'putzligaImportConfig' => $this->users->putzligaImportConfig($user['username']),
|
||||||
|
'putzligaImportToken' => $putzligaImportToken,
|
||||||
|
'putzligaImportUrl' => app_origin() . '/api/putzliga',
|
||||||
'backupAvailable' => class_exists('ZipArchive'),
|
'backupAvailable' => class_exists('ZipArchive'),
|
||||||
'aiConfig' => $user['is_admin'] ? $this->aiConfig->get() : null,
|
'aiConfig' => $user['is_admin'] ? $this->aiConfig->get() : null,
|
||||||
'aiStatus' => $user['is_admin'] ? $this->openAi->configuration() : null,
|
'aiStatus' => $user['is_admin'] ? $this->openAi->configuration() : null,
|
||||||
@@ -2499,6 +2593,19 @@ final class App
|
|||||||
redirect('/options?panel=health');
|
redirect('/options?panel=health');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($form === 'putzliga_import_token') {
|
||||||
|
$token = $this->users->issuePutzligaImportToken($user['username']);
|
||||||
|
$_SESSION['_putzliga_import_token'][$user['username']] = $token;
|
||||||
|
flash('success', 'Der Putzliga-Token wurde erstellt. Kopiere ihn in Putzliga.');
|
||||||
|
redirect('/options?panel=health');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($form === 'putzliga_import_revoke') {
|
||||||
|
$this->users->revokePutzligaImportToken($user['username']);
|
||||||
|
flash('success', 'Der Putzliga-Token wurde deaktiviert.');
|
||||||
|
redirect('/options?panel=health');
|
||||||
|
}
|
||||||
|
|
||||||
if ($form === 'password') {
|
if ($form === 'password') {
|
||||||
$current = (string) ($_POST['current_password'] ?? '');
|
$current = (string) ($_POST['current_password'] ?? '');
|
||||||
$new = (string) ($_POST['new_password'] ?? '');
|
$new = (string) ($_POST['new_password'] ?? '');
|
||||||
|
|||||||
@@ -221,6 +221,8 @@ final class EntryRepository
|
|||||||
$eventLines[] = '- Tiefschlaf: ' . (string) ($event['sleep_deep'] ?? 0);
|
$eventLines[] = '- Tiefschlaf: ' . (string) ($event['sleep_deep'] ?? 0);
|
||||||
$eventLines[] = '- REM-Schlaf: ' . (string) ($event['sleep_rem'] ?? 0);
|
$eventLines[] = '- REM-Schlaf: ' . (string) ($event['sleep_rem'] ?? 0);
|
||||||
$eventLines[] = '- Kernschlaf: ' . (string) ($event['sleep_core'] ?? 0);
|
$eventLines[] = '- Kernschlaf: ' . (string) ($event['sleep_core'] ?? 0);
|
||||||
|
$taskTitles = is_array($event['task_titles'] ?? null) ? array_values(array_filter($event['task_titles'], 'is_string')) : [];
|
||||||
|
$eventLines[] = '- Aufgaben: ' . ($taskTitles !== [] ? base64_encode((string) json_encode($taskTitles, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) : '');
|
||||||
$route = is_array($event['route'] ?? null) ? $event['route'] : [];
|
$route = is_array($event['route'] ?? null) ? $event['route'] : [];
|
||||||
$eventLines[] = '- Route: ' . ($route !== [] ? base64_encode((string) json_encode($route, JSON_UNESCAPED_SLASHES)) : '');
|
$eventLines[] = '- Route: ' . ($route !== [] ? base64_encode((string) json_encode($route, JSON_UNESCAPED_SLASHES)) : '');
|
||||||
$eventLines[] = '';
|
$eventLines[] = '';
|
||||||
@@ -348,6 +350,7 @@ final class EntryRepository
|
|||||||
'sleep_deep' => (float) ($this->extract('/^- Tiefschlaf:\s*(.*)$/mu', $block) ?? 0),
|
'sleep_deep' => (float) ($this->extract('/^- Tiefschlaf:\s*(.*)$/mu', $block) ?? 0),
|
||||||
'sleep_rem' => (float) ($this->extract('/^- REM-Schlaf:\s*(.*)$/mu', $block) ?? 0),
|
'sleep_rem' => (float) ($this->extract('/^- REM-Schlaf:\s*(.*)$/mu', $block) ?? 0),
|
||||||
'sleep_core' => (float) ($this->extract('/^- Kernschlaf:\s*(.*)$/mu', $block) ?? 0),
|
'sleep_core' => (float) ($this->extract('/^- Kernschlaf:\s*(.*)$/mu', $block) ?? 0),
|
||||||
|
'task_titles' => $this->decodeStringList((string) ($this->extract('/^- Aufgaben:\s*(.*)$/mu', $block) ?? '')),
|
||||||
'route' => $this->decodeRoute((string) ($this->extract('/^- Route:\s*(.*)$/mu', $block) ?? '')),
|
'route' => $this->decodeRoute((string) ($this->extract('/^- Route:\s*(.*)$/mu', $block) ?? '')),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -373,6 +376,29 @@ final class EntryRepository
|
|||||||
return $base;
|
return $base;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function decodeStringList(string $encoded): array
|
||||||
|
{
|
||||||
|
$encoded = trim($encoded);
|
||||||
|
if ($encoded === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = base64_decode($encoded, true);
|
||||||
|
if ($decoded === false) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = json_decode($decoded, true);
|
||||||
|
if (!is_array($items)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_filter(array_map(
|
||||||
|
static fn (mixed $item): string => trim((string) $item),
|
||||||
|
$items
|
||||||
|
), static fn (string $item): bool => $item !== ''));
|
||||||
|
}
|
||||||
|
|
||||||
private function extractSection(string $content, string $startHeading, string $endHeading): ?string
|
private function extractSection(string $content, string $startHeading, string $endHeading): ?string
|
||||||
{
|
{
|
||||||
$pattern = '/' . preg_quote($startHeading, '/') . '\s*(.*?)\s*' . preg_quote($endHeading, '/') . '/su';
|
$pattern = '/' . preg_quote($startHeading, '/') . '\s*(.*?)\s*' . preg_quote($endHeading, '/') . '/su';
|
||||||
|
|||||||
@@ -484,6 +484,10 @@ final class ScoringService
|
|||||||
'sleep_deep' => max(0.0, min(24.0, is_numeric($event['sleep_deep'] ?? null) ? (float) $event['sleep_deep'] : 0.0)),
|
'sleep_deep' => max(0.0, min(24.0, is_numeric($event['sleep_deep'] ?? null) ? (float) $event['sleep_deep'] : 0.0)),
|
||||||
'sleep_rem' => max(0.0, min(24.0, is_numeric($event['sleep_rem'] ?? null) ? (float) $event['sleep_rem'] : 0.0)),
|
'sleep_rem' => max(0.0, min(24.0, is_numeric($event['sleep_rem'] ?? null) ? (float) $event['sleep_rem'] : 0.0)),
|
||||||
'sleep_core' => max(0.0, min(24.0, is_numeric($event['sleep_core'] ?? null) ? (float) $event['sleep_core'] : 0.0)),
|
'sleep_core' => max(0.0, min(24.0, is_numeric($event['sleep_core'] ?? null) ? (float) $event['sleep_core'] : 0.0)),
|
||||||
|
'task_titles' => array_values(array_slice(array_filter(array_map(
|
||||||
|
static fn (mixed $title): string => trim((string) $title),
|
||||||
|
is_array($event['task_titles'] ?? null) ? $event['task_titles'] : []
|
||||||
|
), static fn (string $title): bool => $title !== ''), 0, 20)),
|
||||||
'route' => $this->normalizeRoute($event['route'] ?? []),
|
'route' => $this->normalizeRoute($event['route'] ?? []),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,6 +171,40 @@ final class UserRepository
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function findByPutzligaImportToken(string $token): ?array
|
||||||
|
{
|
||||||
|
$tokenHash = hash('sha256', $token);
|
||||||
|
|
||||||
|
foreach ($this->all() as $user) {
|
||||||
|
$config = $user['putzliga_import'] ?? null;
|
||||||
|
|
||||||
|
if (!is_array($config) || empty($config['enabled'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hash_equals((string) ($config['token_hash'] ?? ''), $tokenHash)) {
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function putzligaImportConfig(string $username): array
|
||||||
|
{
|
||||||
|
$user = $this->find($username);
|
||||||
|
$config = is_array($user['putzliga_import'] ?? null) ? $user['putzliga_import'] : [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'enabled' => !empty($config['enabled']),
|
||||||
|
'token_prefix' => (string) ($config['token_prefix'] ?? ''),
|
||||||
|
'created_at' => (string) ($config['created_at'] ?? ''),
|
||||||
|
'last_import_at' => (string) ($config['last_import_at'] ?? ''),
|
||||||
|
'last_status' => (string) ($config['last_status'] ?? ''),
|
||||||
|
'last_message' => (string) ($config['last_message'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function issueHealthImportToken(string $username): string
|
public function issueHealthImportToken(string $username): string
|
||||||
{
|
{
|
||||||
$token = 'mood_health_' . bin2hex(random_bytes(24));
|
$token = 'mood_health_' . bin2hex(random_bytes(24));
|
||||||
@@ -236,6 +270,89 @@ final class UserRepository
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function issuePutzligaImportToken(string $username): string
|
||||||
|
{
|
||||||
|
$token = 'mood_putzliga_' . bin2hex(random_bytes(24));
|
||||||
|
$normalized = normalize_username($username);
|
||||||
|
$users = $this->all();
|
||||||
|
$updated = false;
|
||||||
|
|
||||||
|
foreach ($users as &$user) {
|
||||||
|
if (($user['username'] ?? '') !== $normalized) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentConfig = is_array($user['putzliga_import'] ?? null) ? $user['putzliga_import'] : [];
|
||||||
|
$user['putzliga_import'] = [
|
||||||
|
'enabled' => true,
|
||||||
|
'token_hash' => hash('sha256', $token),
|
||||||
|
'token_prefix' => substr($token, 0, 20),
|
||||||
|
'created_at' => date(DATE_ATOM),
|
||||||
|
'last_import_at' => (string) ($currentConfig['last_import_at'] ?? ''),
|
||||||
|
'last_status' => (string) ($currentConfig['last_status'] ?? ''),
|
||||||
|
'last_message' => (string) ($currentConfig['last_message'] ?? ''),
|
||||||
|
];
|
||||||
|
$user['updated_at'] = date(DATE_ATOM);
|
||||||
|
$updated = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
unset($user);
|
||||||
|
|
||||||
|
if (!$updated) {
|
||||||
|
throw new RuntimeException('Der Putzliga-Token konnte keinem Benutzer zugeordnet werden.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->write(['users' => $users]);
|
||||||
|
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function revokePutzligaImportToken(string $username): void
|
||||||
|
{
|
||||||
|
$normalized = normalize_username($username);
|
||||||
|
$users = $this->all();
|
||||||
|
$updated = false;
|
||||||
|
|
||||||
|
foreach ($users as &$user) {
|
||||||
|
if (($user['username'] ?? '') !== $normalized || !array_key_exists('putzliga_import', $user)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($user['putzliga_import']);
|
||||||
|
$user['updated_at'] = date(DATE_ATOM);
|
||||||
|
$updated = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
unset($user);
|
||||||
|
|
||||||
|
if ($updated) {
|
||||||
|
$this->write(['users' => $users]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function recordPutzligaImport(string $username, string $status, string $message): void
|
||||||
|
{
|
||||||
|
$normalized = normalize_username($username);
|
||||||
|
$users = $this->all();
|
||||||
|
|
||||||
|
foreach ($users as &$user) {
|
||||||
|
if (($user['username'] ?? '') !== $normalized) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = is_array($user['putzliga_import'] ?? null) ? $user['putzliga_import'] : [];
|
||||||
|
$config['last_import_at'] = date(DATE_ATOM);
|
||||||
|
$config['last_status'] = $status;
|
||||||
|
$config['last_message'] = substr($message, 0, 240);
|
||||||
|
$user['putzliga_import'] = $config;
|
||||||
|
$user['updated_at'] = date(DATE_ATOM);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
unset($user);
|
||||||
|
|
||||||
|
$this->write(['users' => $users]);
|
||||||
|
}
|
||||||
|
|
||||||
public function recordHealthImport(string $username, string $status, string $message): void
|
public function recordHealthImport(string $username, string $status, string $message): void
|
||||||
{
|
{
|
||||||
$normalized = normalize_username($username);
|
$normalized = normalize_username($username);
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ $formatBalanceValue = static function (?array $entry) use ($settings): string {
|
|||||||
<?php $eventComment = trim((string) ($item['comment'] ?? '')); ?>
|
<?php $eventComment = trim((string) ($item['comment'] ?? '')); ?>
|
||||||
<?php if (preg_match('/^\s*-\s*(?:Stimmung|Energie|Stress)\s*:\s*[+-]?\d+\s*$/u', $eventComment) === 1) { $eventComment = ''; } ?>
|
<?php if (preg_match('/^\s*-\s*(?:Stimmung|Energie|Stress)\s*:\s*[+-]?\d+\s*$/u', $eventComment) === 1) { $eventComment = ''; } ?>
|
||||||
<?php $isImportedHealth = (string) ($item['source'] ?? '') === 'health_auto_export'; ?>
|
<?php $isImportedHealth = (string) ($item['source'] ?? '') === 'health_auto_export'; ?>
|
||||||
|
<?php $isPutzliga = (string) ($item['source'] ?? '') === 'putzliga'; ?>
|
||||||
<?php $sportType = $eventType === 'sport' ? find_sport_type($settings, (string) ($item['sport_type_id'] ?? '')) : null; ?>
|
<?php $sportType = $eventType === 'sport' ? find_sport_type($settings, (string) ($item['sport_type_id'] ?? '')) : null; ?>
|
||||||
<?php $isImportedWalkSport = $isImportedHealth && $eventType === 'sport' && str_contains(strtolower((string) ($sportType['label'] ?? $eventComment)), 'spaziergang'); ?>
|
<?php $isImportedWalkSport = $isImportedHealth && $eventType === 'sport' && str_contains(strtolower((string) ($sportType['label'] ?? $eventComment)), 'spaziergang'); ?>
|
||||||
<?php $eventTone = signal_value_class(normalize_signal_value($item['mood'] ?? 0)); ?>
|
<?php $eventTone = signal_value_class(normalize_signal_value($item['mood'] ?? 0)); ?>
|
||||||
@@ -184,6 +185,7 @@ $formatBalanceValue = static function (?array $entry) use ($settings): string {
|
|||||||
'sleep_deep' => (float) ($item['sleep_deep'] ?? 0),
|
'sleep_deep' => (float) ($item['sleep_deep'] ?? 0),
|
||||||
'sleep_rem' => (float) ($item['sleep_rem'] ?? 0),
|
'sleep_rem' => (float) ($item['sleep_rem'] ?? 0),
|
||||||
'sleep_core' => (float) ($item['sleep_core'] ?? 0),
|
'sleep_core' => (float) ($item['sleep_core'] ?? 0),
|
||||||
|
'task_titles' => is_array($item['task_titles'] ?? null) ? $item['task_titles'] : [],
|
||||||
]); ?>
|
]); ?>
|
||||||
<?php $hasEventImage = is_string($item['image_url'] ?? null); ?>
|
<?php $hasEventImage = is_string($item['image_url'] ?? null); ?>
|
||||||
<?php $routeMap = is_array($item['route_map'] ?? null) ? $item['route_map'] : null; ?>
|
<?php $routeMap = is_array($item['route_map'] ?? null) ? $item['route_map'] : null; ?>
|
||||||
@@ -220,6 +222,14 @@ $formatBalanceValue = static function (?array $entry) use ($settings): string {
|
|||||||
<p class="timeline-card__value"><?= e((string) $item['comment']) ?></p>
|
<p class="timeline-card__value"><?= e((string) $item['comment']) ?></p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($isPutzliga && is_array($item['task_titles'] ?? null) && $item['task_titles'] !== []): ?>
|
||||||
|
<div class="timeline-card__stats" aria-label="Erledigte Aufgaben">
|
||||||
|
<?php foreach ($item['task_titles'] as $taskTitle): ?>
|
||||||
|
<span><?= e((string) $taskTitle) ?></span>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if ($eventType === 'sleep' && $sleepActualTotal > 0): ?>
|
<?php if ($eventType === 'sleep' && $sleepActualTotal > 0): ?>
|
||||||
<div class="sleep-phase-bar" aria-label="Schlafdauer" style="--sleep-optimal-left: <?= e((string) $sleepOptimalPercent) ?>%; --sleep-actual-left: <?= e((string) $sleepActualPercent) ?>%">
|
<div class="sleep-phase-bar" aria-label="Schlafdauer" style="--sleep-optimal-left: <?= e((string) $sleepOptimalPercent) ?>%; --sleep-actual-left: <?= e((string) $sleepActualPercent) ?>%">
|
||||||
<svg class="sleep-phase-bar__svg" viewBox="0 0 100 10" preserveAspectRatio="none" aria-hidden="true" focusable="false">
|
<svg class="sleep-phase-bar__svg" viewBox="0 0 100 10" preserveAspectRatio="none" aria-hidden="true" focusable="false">
|
||||||
|
|||||||
@@ -165,6 +165,35 @@
|
|||||||
<p class="helper-text">Lege in Health Auto Export zwei REST-API-Automationen an: Gesundheitsmetriken mit <code>step_count</code> und <code>sleep_analysis</code>, sowie Workouts mit JSON Version 2 und Routendaten.</p>
|
<p class="helper-text">Lege in Health Auto Export zwei REST-API-Automationen an: Gesundheitsmetriken mit <code>step_count</code> und <code>sleep_analysis</code>, sowie Workouts mit JSON Version 2 und Routendaten.</p>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
<article class="detail-card detail-card--overlay">
|
||||||
|
<p class="eyebrow">Putzliga</p>
|
||||||
|
<div class="stack-form">
|
||||||
|
<label><span>URL in Putzliga</span><input type="text" value="<?= e((string) $putzligaImportUrl) ?>" readonly></label>
|
||||||
|
<label><span>HTTP-Header</span><input type="text" value="Authorization: Bearer <?= !empty($putzligaImportConfig['token_prefix']) ? e((string) $putzligaImportConfig['token_prefix']) . '…' : 'TOKEN' ?>" readonly></label>
|
||||||
|
</div>
|
||||||
|
<p class="helper-text">Putzliga erstellt ab 3 erledigten Aufgaben pro Tag einen Moment „Du warst fleißig“ und aktualisiert ihn, wenn weitere Aufgaben dazukommen.</p>
|
||||||
|
<?php if (!empty($putzligaImportToken)): ?>
|
||||||
|
<label><span>Neuer Token, nur jetzt sichtbar</span><input type="text" value="<?= e((string) $putzligaImportToken) ?>" readonly></label>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="user-list">
|
||||||
|
<div class="user-row"><strong>Token</strong><span><?= !empty($putzligaImportConfig['enabled']) ? 'aktiv' : 'nicht eingerichtet' ?></span></div>
|
||||||
|
<div class="user-row"><strong>Letzter Sync</strong><span><?= !empty($putzligaImportConfig['last_import_at']) ? e(format_display_datetime((string) $putzligaImportConfig['last_import_at'])) : '-' ?></span></div>
|
||||||
|
<div class="user-row"><strong>Statusmeldung</strong><span><?= !empty($putzligaImportConfig['last_message']) ? e((string) $putzligaImportConfig['last_message']) : '-' ?></span></div>
|
||||||
|
</div>
|
||||||
|
<form method="post" action="/options" class="stack-form">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="form_name" value="putzliga_import_token">
|
||||||
|
<button class="primary-button" type="submit"><?= !empty($putzligaImportConfig['enabled']) ? 'Putzliga-Token neu erstellen' : 'Putzliga-Token erstellen' ?></button>
|
||||||
|
</form>
|
||||||
|
<?php if (!empty($putzligaImportConfig['enabled'])): ?>
|
||||||
|
<form method="post" action="/options" class="stack-form">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="form_name" value="putzliga_import_revoke">
|
||||||
|
<button class="ghost-button" type="submit">Putzliga-Token deaktivieren</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
</article>
|
||||||
|
|
||||||
<?php if (!empty($healthImportToken)): ?>
|
<?php if (!empty($healthImportToken)): ?>
|
||||||
<article class="detail-card detail-card--overlay health-token-card">
|
<article class="detail-card detail-card--overlay health-token-card">
|
||||||
<p class="eyebrow">Neuer Token</p>
|
<p class="eyebrow">Neuer Token</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user