Add Rails backend: accounts via magic link, server documents, sharing

The app becomes a Rails 8 application; the Vue frontend moves to
app/frontend and is served through vite_rails.

- Without an account documents stay in the browser (IndexedDB). Sign-in
  is by magic link or the 6-digit code from the same mail; afterwards
  local documents can be uploaded into the account.
- Server documents (/d/<id>) with versioned saves and a single-editor
  lock per browser tab; others see them read-only and follow by polling.
- Sharing via link (works without an account) and via e-mail invitation,
  roles viewer/editor; the owner manages links and people.
- Store gains a read-only mode that blocks all content changes.
- Solid Queue, Cache and Cable share the one PostgreSQL database.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
wkh
2026-09-28 20:09:06 +02:00
co-authored by Claude Opus 5.5
parent 5a0f1fcd78
commit e14bada114
190 changed files with 6077 additions and 722 deletions
+93
View File
@@ -0,0 +1,93 @@
<script setup lang="ts">
/**
* Shown once after signing in when this browser still has local documents:
* which of them should move into the account (server)? Uploaded ones are
* removed from the browser. "Später" leaves everything as is -- uploading
* stays possible from the document list.
*/
import { ref, watch } from 'vue';
import { useLibrary } from '../store/library';
import BaseModal from './BaseModal.vue';
const props = defineProps<{ open: boolean }>();
const emit = defineEmits<{ (e: 'close'): void }>();
const library = useLibrary();
const selected = ref<Set<string>>(new Set());
const busy = ref(false);
watch(
() => props.open,
(o) => {
if (o) selected.value = new Set(library.state.docs.map((d) => d.id));
},
);
function toggle(id: string) {
const s = new Set(selected.value);
if (s.has(id)) s.delete(id);
else s.add(id);
selected.value = s;
}
async function run() {
busy.value = true;
try {
await library.upload([...selected.value]);
emit('close');
} finally {
busy.value = false;
}
}
</script>
<template>
<BaseModal :open="open" title="Dokumente in dein Konto übernehmen?" @close="emit('close')">
<p class="lead">
In diesem Browser liegen noch Dokumente. Ausgewählte werden in dein Konto hochgeladen – dann sind sie
auf allen Geräten da und lassen sich teilen – und hier aus dem Browser entfernt.
</p>
<ul class="list">
<li v-for="d in library.state.docs" :key="d.id">
<label>
<input type="checkbox" :checked="selected.has(d.id)" @change="toggle(d.id)" />
{{ d.title }}
</label>
</li>
</ul>
<template #footer>
<button @click="emit('close')">Später</button>
<button class="primary" :disabled="busy || !selected.size" @click="run">
{{ busy ? 'Lade hoch …' : `${selected.size} hochladen` }}
</button>
</template>
</BaseModal>
</template>
<style scoped>
.lead {
margin: 0 0 12px;
font-size: 13px;
line-height: 1.5;
}
.list {
list-style: none;
margin: 0;
padding: 0;
max-height: 40vh;
overflow-y: auto;
}
.list li + li {
margin-top: 4px;
}
.list label {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: 6px;
background: var(--surface-2);
font-size: 13px;
cursor: pointer;
}
</style>