Add reMarkable backup system with web UI
- Docker container with rmapi cloud backup, PDF conversion pipeline - Web UI: file browser, multi-select, delete/move, thumbnail preview - Sync: backup from reMarkable cloud, rmdoc→PDF conversion via rmc+Inkscape - Excluded-files mechanism to prevent deleted items from returning after sync Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
118
remarkable/web/setup.php
Executable file
118
remarkable/web/setup.php
Executable file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require __DIR__ . '/auth.php';
|
||||
|
||||
$RMAPI = '/usr/local/bin/rmapi';
|
||||
$flash = '';
|
||||
$flashOk = true;
|
||||
|
||||
function h(string $v): string {
|
||||
return htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
}
|
||||
|
||||
// rmapi-Status prüfen
|
||||
function rmapiStatus(string $bin): array {
|
||||
exec($bin . ' ls / 2>&1', $out, $code);
|
||||
return ['ok' => $code === 0, 'out' => implode("\n", $out)];
|
||||
}
|
||||
|
||||
// Token einrichten
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['code'])) {
|
||||
$code = trim($_POST['code']);
|
||||
$code = preg_replace('/[^a-zA-Z0-9]/', '', $code);
|
||||
if (strlen($code) === 8) {
|
||||
$descriptors = [
|
||||
0 => ['pipe', 'r'],
|
||||
1 => ['pipe', 'w'],
|
||||
2 => ['pipe', 'w'],
|
||||
];
|
||||
$proc = proc_open(escapeshellarg($RMAPI) . ' ls /', $descriptors, $pipes);
|
||||
if (is_resource($proc)) {
|
||||
fwrite($pipes[0], $code . "\n");
|
||||
fclose($pipes[0]);
|
||||
$out = stream_get_contents($pipes[1]);
|
||||
fclose($pipes[1]);
|
||||
fclose($pipes[2]);
|
||||
$exitCode = proc_close($proc);
|
||||
$flashOk = $exitCode === 0;
|
||||
$flash = $flashOk ? 'Token erfolgreich eingerichtet!' : 'Fehler: Ungültiger Code oder Verbindungsproblem.';
|
||||
}
|
||||
} else {
|
||||
$flash = 'Code muss genau 8 Zeichen lang sein.';
|
||||
$flashOk = false;
|
||||
}
|
||||
}
|
||||
|
||||
$status = rmapiStatus($RMAPI);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>reMarkable Setup</title>
|
||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='18' fill='%23000'/%3E%3Ctext y='.9em' font-size='72' font-family='Georgia,serif' font-weight='bold' fill='white' x='50%25' text-anchor='middle'%3Erm%3C/text%3E%3C/svg%3E">
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#f4f4f4;color:#222;min-height:100vh;display:flex;align-items:center;justify-content:center}
|
||||
.card{background:#fff;border-radius:12px;padding:32px;width:440px;max-width:95vw;box-shadow:0 4px 24px rgba(0,0,0,.1)}
|
||||
h1{font-size:20px;font-weight:700;margin-bottom:8px}
|
||||
.subtitle{font-size:14px;color:#666;margin-bottom:24px}
|
||||
.status{padding:12px 16px;border-radius:8px;font-size:14px;margin-bottom:20px;display:flex;align-items:center;gap:10px}
|
||||
.status.ok{background:#dcfce7;border:1px solid #86efac;color:#166534}
|
||||
.status.err{background:#fee2e2;border:1px solid #fca5a5;color:#991b1b}
|
||||
.flash{padding:10px 16px;border-radius:8px;font-size:14px;margin-bottom:16px}
|
||||
.flash.ok{background:#dcfce7;border:1px solid #86efac;color:#166534}
|
||||
.flash.err{background:#fee2e2;border:1px solid #fca5a5;color:#991b1b}
|
||||
label{display:block;font-size:13px;font-weight:500;margin-bottom:6px;color:#444}
|
||||
input[type=text]{width:100%;padding:10px 12px;border:1px solid #ccc;border-radius:8px;font-size:16px;letter-spacing:4px;text-align:center;margin-bottom:12px}
|
||||
.btn{width:100%;padding:10px;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;border:none;background:#2563eb;color:#fff}
|
||||
.btn:hover{background:#1d4ed8}
|
||||
.hint{font-size:13px;color:#666;margin-top:16px;line-height:1.6}
|
||||
.hint a{color:#2563eb}
|
||||
.back{display:block;text-align:center;margin-top:20px;font-size:13px;color:#2563eb;text-decoration:none}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>📄 reMarkable Setup</h1>
|
||||
<p class="subtitle">Verbindung zur reMarkable Cloud einrichten</p>
|
||||
|
||||
<div class="status <?= $status['ok'] ? 'ok' : 'err' ?>">
|
||||
<?= $status['ok'] ? '✓ Verbunden mit reMarkable Cloud' : '✗ Nicht verbunden — Token fehlt oder abgelaufen' ?>
|
||||
</div>
|
||||
|
||||
<?php if ($flash !== ''): ?>
|
||||
<div class="flash <?= $flashOk ? 'ok' : 'err' ?>"><?= h($flash) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!$status['ok']): ?>
|
||||
<form method="post">
|
||||
<label for="code">One-Time-Code</label>
|
||||
<input type="text" id="code" name="code" maxlength="8" placeholder="xxxxxxxx" autocomplete="off" autofocus>
|
||||
<button type="submit" class="btn">Verbinden</button>
|
||||
</form>
|
||||
<p class="hint">
|
||||
Code abrufen unter:<br>
|
||||
<a href="https://my.remarkable.com/device/desktop/connect" target="_blank">
|
||||
my.remarkable.com/device/desktop/connect
|
||||
</a>
|
||||
</p>
|
||||
<?php else: ?>
|
||||
<form method="post">
|
||||
<label for="code">Token erneuern (neuer One-Time-Code)</label>
|
||||
<input type="text" id="code" name="code" maxlength="8" placeholder="xxxxxxxx" autocomplete="off">
|
||||
<button type="submit" class="btn btn-secondary" style="background:#6b7280">Token erneuern</button>
|
||||
</form>
|
||||
<p class="hint">
|
||||
Neuen Code abrufen unter:<br>
|
||||
<a href="https://my.remarkable.com/device/desktop/connect" target="_blank">
|
||||
my.remarkable.com/device/desktop/connect
|
||||
</a>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<a href="/" class="back">← Zurück zur Übersicht</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user