Install Tailscale on both your Proxmox host and the machine running Clawdbot. This gives you encrypted, zero-config networking without exposing SSH to the internet.
# On Proxmox
curl -fsSL https://tailscale.com/install.sh | sh
tailscale up
Manchmal möchte man Dateien von Git ignorieren lassen, die aber nicht in der .gitignore auftauchen sollen. Vielleicht handelt es sich um persönliche Konfigurationsdateien, lokale Notizen oder IDE-spezifische Ordner, die nur dich betreffen und nicht das gesamte Team.
In diesem Beitrag zeige ich dir das Recordable Pattern, das 37signals (Basecamp, HEY) einsetzt, und erkläre es Schritt für Schritt anhand von lauffähigen Rails-Code-Beispielen. Zielgruppe sind Anfänger bis Fortgeschrittene Ruby on Rails Entwickler, die polymorphe Datenmodelle sauber, skalierbar und gut paginierbar bauen möchten.
Warum das Ganze? Klassische Ansätze wie Single Table Inheritance (STI) oder „nackte“ polymorphe Assoziationen stoßen schnell an Grenzen: STI bläht Tabellen auf, polymorphes CRUD bleibt oft mühsam zu paginieren. Das Recordable Pattern nutzt Rails’ Delegated Types, um eine dünne, performante „Superklasse“-Tabelle (Recording) mit schlanken „Subklasse“-Tabellen (Recordables wie Message, Document, Comment) zu kombinieren.
Das Grundprinzip
Eine zentrale Tabelle „recordings“ hält alle gemeinsamen Metadaten (z. B. Bucket, Creator, Timestamps) und verweist polymorph auf den „konkreten Inhalt“ (recordable).
Jede konkrete Recordable (Message, Document, Comment) hat eine eigene Tabelle mit genau ihren Feldern.
Abfragen, Pagination und gemeinsame Logik passieren über Recording; konkretes Verhalten (z. B. export_html) lebt bei den Recordables.
So bleibst du:
flexibel (neue Typen ohne Recording-Migration),
performant (einheitliche Timeline auf der dünnen Recording-Tabelle),
module Recordable
extend ActiveSupport::Concern
included do
has_many :recordings, as: :recordable, inverse_of: :recordable
end
def display_title
respond_to?(:title) ? title : self.class.name
end
def export_html
"#{display_title}"
end
end
Konkrete Typen:
class Message < ApplicationRecord
include Recordable
validates :title, :content, presence: true
def export_html
"#{title}#{content}"
end
end
class Document < ApplicationRecord
include Recordable
validates :title, presence: true
def export_html
if external_url.present?
"#{title}External: #{external_url}"
else
"#{title}#{body}"
end
end
end
class Comment < ApplicationRecord
include Recordable
validates :body, presence: true
def display_title
body.truncate(40)
end
def export_html
"#{body}"
end
end
Erstellen und Paginieren einer Timeline
Bucket kapselt die Aufnahme neuer Inhalte („record“) und die Timeline. Bucket ist hier eher abstrakt und könnte genausogut auch ein Projekt sein, oder ein Kunde:
class Bucket < ApplicationRecord
has_many :recordings, dependent: :destroy
def timeline(limit: 50)
recordings.order(created_at: :desc).limit(limit).includes(:recordable)
end
def record(recordable, creator:, parent: nil, color: nil)
recordings.create!(recordable: recordable, creator: creator, parent: parent, color: color)
end
end
Typische Abfragen:
bucket = Bucket.find(1)
# Gesamte Timeline effizient paginieren
bucket.timeline(limit: 50).each do |rec|
puts "[#{rec.recordable_type}] #{rec.recordable.display_title}"
end
# Nur bestimmte Typen (Scopes via delegated_type)
Recording.messages.where(bucket: bucket).limit(20)
Recording.documents.where(bucket: bucket).order(created_at: :desc)
Versionierung per Repointing (immutable Recordables)
Statt Inhalte zu „updaten“, erzeugst du neue Recordables und verweist die Recording-Zeile auf die neue Version. Das hält Historie sauber und macht Kopieren effizient.
class Recording < ApplicationRecord
# ...
def repoint_to!(new_recordable)
update!(recordable: new_recordable)
end
end
# Beispiel: Message ? neue Document-Version
msg_rec = bucket.record(Message.create!(title: "Kickoff", content: "Welcome!"), creator: user)
new_doc = Document.create!(title: "Specs v2", body: "Updated requirements")
msg_rec.repoint_to!(new_doc) # Recording zeigt nun auf Document
Wenn du eine echte Ereignis-Historie brauchst, ergänze ein Event-Model und schreibe beim record/repoint_to! Einträge (z. B. „created“, „repointed“, inkl. vorher/nachher recordable_type/_id). Das hält Audit-Trails und erlaubt Timeline-Features (siehe Video/37signals-Artikel).
Copy & Move: Subtrees kopieren
Ein Vorteil des Patterns: Kopieren und Verschieben ganzer Teilbäume wird planbar und schnell, weil Recordings leichtgewichtig sind und Recordables immutable dupliziert werden. Eine einfache Kopier-Serviceklasse:
class Copier
def self.copy!(source_recording:, destination_bucket:, creator:)
new(source_recording, destination_bucket, creator).copy!
end
def initialize(source_recording, destination_bucket, creator)
@source_recording = source_recording
@destination_bucket = destination_bucket
@creator = creator
end
def copy!
ActiveRecord::Base.transaction do
copied_root = copy_recording(@source_recording, parent: nil)
@source_recording.children.each { |child| copy_branch(child, parent: copied_root) }
copied_root
end
end
private
def copy_branch(node, parent:)
copied = copy_recording(node, parent: parent)
node.children.each { |child| copy_branch(child, parent: copied) }
copied
end
def copy_recording(original, parent:)
new_recordable = duplicate_recordable(original.recordable)
@destination_bucket.record(new_recordable, creator: @creator, parent: parent, color: original.color)
end
def duplicate_recordable(recordable)
case recordable
when Message
Message.create!(title: recordable.title, content: recordable.content)
when Document
Document.create!(title: recordable.title, body: recordable.body, external_url: recordable.external_url)
when Comment
Comment.create!(body: recordable.body)
else
raise ArgumentError, "Unsupported recordable: #{recordable.class.name}"
end
end
end
Vergleich mit STI und „plain“ Polymorphismus
STI: Einfache Fälle ok. Bei divergenten Typen entsteht Tabellen-Bloat, viele NULL-Spalten, Migrationen werden groß, Erweiterbarkeit leidet.
Polymorphismus ohne delegated_type: Funktioniert, aber es fehlen bequeme Scopes/Convenience-Methoden; unified Pagination ist oft hakelig.
Recordables + delegated_type: Einheitliche Timeline über eine dünne Tabelle, klare Trennung von Meta vs. Inhalt, leichte Erweiterbarkeit (neuen Typ hinzufügen statt zentrale Tabelle migrieren), gute Performance bei Abfragen.
Wann solltest du das Pattern nutzen?
Du brauchst eine gemischte, paginierbare Timeline (z. B. Aktivitätsfeed über Messages, Documents, Comments).
Deine Typen unterscheiden sich stark in ihren Attributen.
Du willst Polymorphismus am Parent (Recording) und saubere Delegation an die Inhalte.
Wann eher nicht?
Deine Subtypen sind fast identisch (STI könnte reichen).
Du hast keine gemeinsamen Abfragen über Typgrenzen hinweg.
Best Practices
Halte Recording schlank: nur Metadaten und die polymorphe Referenz. Keine großen Textfelder.
Packe typenübergreifende Logik in Recording (oder Concerns), typenspezifische Logik in die Recordables.
Nutze delegate am Recording für gemeinsame Schnittstellen, z. B. delegate :export_html, to: :recordable.
Wenn Auditing/History wichtig ist, ergänze Events und schreibe Änderungen mit.
Autorisierung: Recording bündelt viele Aktionen – prüfe Berechtigungen konsequent.
Fazit
Das Recordable Pattern mit delegated_type ist eine elegante, praxiserprobte Lösung für polymorphe Inhalte in Rails. Es bringt dir:
eine einheitliche, performante Timeline,
klare Verantwortlichkeiten zwischen Metadaten und Inhaltsobjekten,
einfache Erweiterbarkeit ohne große Migrationen,
saubere APIs durch Delegation.
Wenn du Feed-ähnliche Strukturen, Versionierung und Kopier-Features brauchst, wirst du mit Recordings/Recordables sehr schnell produktiv.
Viel Spaß beim Ausprobieren – und schreib mir gern, wenn du Fragen hast oder Beispiele aus deinem Projekt teilen willst!
Ich liebe es, neue Musik zu entdecken und zu hören. In der letzten Zeit bin ich dafür hauptsächlich auf Bandcamp unterwegs - einer Plattform, auf der viele, vor allem unbekanntere Künstler, ihr Schaffen teilweise sogar kostenlos anbieten. Wenn mir etwas gefällt, zögere ich nicht, es zu kaufen. Meine Liste an gekauften Alben wird bald die magische 200er-Grenze überschreiten. https://bandcamp.com/webmatze
Wir alle wissen aber auch, dass es nicht leicht ist, als kreativer Kopf Aufmerksamkeit zu bekommen, geschweige denn davon leben zu können.
Und nun gibt es auch noch Konkurrenz aus einer ganz anderen Ecke für alle Kreativen. Die KI ist auf dem Weg, uns Menschen in kreativer Hinsicht den Rang abzulaufen.
Bilder erstellen? Für die KI kein Problem. Oder vielleicht gleich ganze Videos? Schafft die KI auch. Aber was ist mit Musik? Die kann doch nicht einfach generiert werden, oder?
Ich habe die letzen Tage einige Tools ausprobiert, die es uns erlauben mittels KI ganze Songs zu generieren. Vor allem SUNO hat mich überzeugt.
Es benötigt nur einen einfachen, kurzen Prompt, und schon wird ein kompletter Song innerhalb weniger Sekunden erzeugt. Und das Erschreckende ist, diese künstlich erzeugten Songs sind nicht einmal schlecht.
Seit Version 4 von SUNO kann man sie praktisch kaum noch von 'echten' Liedern unterscheiden.
Hier sind ein paar Beispiele, die ich zusammen mit meinen Kindern erstellt habe. Sie haben mir die Grundidee des Songs genannt, ich habe den Prompt geschrieben, und SUNO macht den Rest.
Ich schwebe:
Nacht der Schatten:
Mein Leben ist schön:
Oder auch auf englisch:
Was haltet ihr von KI-Song-Generatoren? Könnt ihr euch vorstellen, solche Musik zu hören? Würdet ihr überhaupt merken, dass ein Song künstlich erzeugt wurde?
Heute möchte ich euch ein cooles Tool vorstellen, das mir und meinem Team das Leben deutlich einfacher macht: Das Git Dashboard!
Was ist das Git Dashboard?
Stellt euch vor, ihr kommt morgens ins Büro und wollt schnell wissen:
Was habt ihr gestern alles committed?
Was haben eure Teammitglieder gemacht?
Wer war besonders aktiv?
Genau dafür haben wir das Git Dashboard entwickelt! Mit einem einzigen Befehl bekommt ihr einen übersichtlichen Report über alle Aktivitäten in eurem Repository.
Wie funktioniert's?
Super einfach! Nach der Installation könnt ihr zum Beispiel eingeben:
git dashboard # Zeigt den Report von gestern
git dashboard -d today # Zeigt den heutigen Report
git dashboard --date 2024-12-18 # Zeigt den Report für ein bestimmtes Datum
Was macht es so besonders?
Zeigt eure persönlichen Commits übersichtlich an
Listet die Commits eurer Teammitglieder auf
Erstellt eine Rangliste mit Commit-Zahlen und geänderten Zeilen
Berücksichtigt alle Branches
Flexibel im Datum - heute, gestern oder jedes andere Datum
Perfekt für Daily Standups!
Keine Panik mehr vor der Frage "Was hast du gestern gemacht?". Ein schnelles git dashboard und ihr habt alle Infos parat!
Probiert es aus!
Das Tool ist Open Source und super einfach zu installieren. Schaut euch das Projekt auf GitHub an und gebt ihm einen Stern, wenn es euch gefällt!
Als Unix/Linux-Nutzer kennen wir alle den beliebten 'cat' Befehl, um Dateiinhalte anzuzeigen. Aber was, wenn wir den gleichen, vertrauten Befehl auch für URLs verwenden möchten? Hier ist eine elegante Lösung in Form einer ZSH-Funktion.
Die folgende Funktion erweitert den klassischen 'cat' Befehl so, dass er sowohl mit lokalen Dateien als auch mit URLs umgehen kann:
Die Funktion prüft mittels eines regulären Ausdrucks, ob das übergebene Argument eine URL ist. Falls ja, wird curl verwendet, um den Inhalt herunterzuladen und anzuzeigen. Bei normalen Dateien wird der Standard-cat-Befehl verwendet.
Beispiele für die Verwendung:
cat datei.txt -> Zeigt den Inhalt einer lokalen Datei
cat https://github.com/beispiel -> Zeigt den HTML-Inhalt der Website
Die Funktion könnt ihr einfach in eure ~/.zshrc einfügen und nach dem Neuladen der Shell sofort nutzen.
Ein kleiner, aber feiner Hack, der den Workflow beim Arbeiten mit Dateien und Webinhalten vereinfacht. Das ist Unix-Philosophie at its best: Bestehende Werkzeuge erweitern, ohne ihre ursprüngliche Funktionalität zu beeinträchtigen.