Einstellungen und App-Konfiguration¶
Coworkee kennt zwei Arten von Konfiguration, die Administratoren zur Laufzeit ändern.
| Einstellungen | App-Konfiguration | |
|---|---|---|
| Für | einzelne Werte pro Installation, Mandant oder Benutzer (SMTP-Host, Theme, Registrierung an/aus) | eine typisierte Options-Klasse der App (Grenzen, Schalter, Integrationen) |
| Definiert durch | ISettingDefinitionContributor |
eine C#-Klasse, optional aus JSON erzeugt |
| Gespeichert in | Einstellungstabelle, Geheimnisse verschlüsselt | cw.ConfigurationEntries, gelesen über IConfiguration |
| Gelesen mit | ISettingProvider |
IOptionsMonitor<T> |
| Admin-Seite | Einstellungen, Tabs System und Organisation | Einstellungen, ein Tab pro Klasse (nur System-Mandant) |
Einstellungen¶
internal sealed class MailSettingDefinitions : ISettingDefinitionContributor
{
public void Define(SettingDefinitionContext context) =>
context.Group("Mail", "Mail")
.Add("Mail.Smtp.Host", "SMTP host", SettingType.String, [SettingScope.Global])
.Add("Mail.Smtp.Port", "SMTP port", SettingType.Int, [SettingScope.Global], defaultValue: "25")
.Add("Mail.Smtp.Password", "SMTP password", SettingType.Secret, [SettingScope.Global]);
}
Typen sind String, Text, Bool, Int, Secret, Choice und Language; Gültigkeitsbereiche Global, Tenant und User. Der spezifischste Wert gewinnt. Geheimnisse werden mit ASP.NET Core Data Protection verschlüsselt und nie an den Client geschickt; Einstellungen mit visibleToClient: true schon.
Standardwerte können auch aus der Konfiguration kommen: Coworkee:Settings:Defaults:Mail.Smtp.Host. Der AppHost nutzt das für Mailpit.
Der Abschnitt Cloud-Dateiauswahl im Reiter System enthält die IDs von Google Drive, OneDrive und Dropbox für jedes Upload-Feld, siehe Dateien.
Eigene Einstellungen auf der Admin-Seite¶
Die Tabs System (Bereich Global) und Organisation (Bereich Tenant) sowie Meine Einstellungen des Benutzers (Bereich User) haben keinen Formularcode pro Einstellung. Sie laden die Definitionen und zeigen sie mit MudExObjectEditForm; eine Einstellung deines Moduls erscheint dort, sobald ihr Contributor registriert ist:
internal sealed class BillingSettingDefinitions : ISettingDefinitionContributor
{
public void Define(SettingDefinitionContext context) =>
context.Group("Billing", "Billing")
.Add("Billing.Currency", "Currency", SettingType.Choice, [SettingScope.Tenant], "EUR", choices: ["EUR", "USD", "CHF"],
description: "The currency of new invoices.")
.Add("Billing.ApiKey", "API key", SettingType.Secret, [SettingScope.Global]);
}
context.Services.AddSingleton<ISettingDefinitionContributor, BillingSettingDefinitions>();
| Definition | Formular |
|---|---|
| Gruppe | ein Abschnitt mit dem Anzeigenamen der Gruppe |
Anzeigename, description |
Beschriftung und Hilfetext (über die Übersetzungen, de.json kann sie also übersetzen) |
String / Int |
Text- oder Zahlenfeld; der Standardwert ist der Platzhalter, ein leeres Feld fällt auf den Standard zurück |
Text |
ein Textfeld mit vier Zeilen |
Bool |
ein Kontrollkästchen mit dem gespeicherten Wert oder dem Standard |
Choice |
eine Auswahl der choices; jeder Wert erscheint übersetzt, wenn die Übersetzungen einen Eintrag dafür haben, sonst unverändert |
Language |
eine Auswahl der Sprachen der App; leer heißt erben, angezeigt als Configured default im Tab System und Inherited in Organisation und Meine Einstellungen (dort beschriftet mit Language) |
Secret |
ein Passwortfeld, das leer bleibt; der Hilfetext sagt, ob ein Wert gespeichert ist, und nur ein eingetippter Wert wird gesendet |
enabledWhen |
das Feld ist deaktiviert, solange der Wert der anderen Einstellung nicht in den Werten der Bedingung liegt |
Gruppe mit page |
erscheint nicht in den Einstellungs-Tabs; siehe unten |
| Bereiche | eine Einstellung erscheint im Tab jedes ihrer Bereiche |
Speichern sendet nur geänderte Werte; der Server prüft sie (Expected a whole number. usw.), die Meldung erscheint in der Snackbar. Wer die Tabs sieht, folgt der Berechtigung Settings.Manage; System erscheint nur in der System-Organisation.
Hinter dem Formular steht eine zur Laufzeit erzeugte Klasse mit einer Eigenschaft je Einstellung (Mail.Smtp.Host wird Mail_Smtp_Host), das Formular verhält sich also wie jedes andere Objektformular. Felder haben kein eigenes Zurücksetzen: Ein geleertes Text- oder Zahlenfeld kehrt zum Standard zurück.
Abhängige Einstellungen¶
enabledWhen bindet ein Feld an eine andere Einstellung: Es ist deaktiviert, solange deren Wert nicht unter den genannten Werten ist. SettingCondition.IsTrue deckt den häufigen Fall eines Schalters ab:
context.Group("FilePickers", "Cloud file pickers")
.Add("FilePickers.GoogleDrive.Enabled", "Google Drive", SettingType.Bool, [SettingScope.Global])
.Add("FilePickers.GoogleDrive.ClientId", "Google Drive client id", SettingType.String, [SettingScope.Global],
enabledWhen: SettingCondition.IsTrue("FilePickers.GoogleDrive.Enabled"));
// mehrere Werte: die Übersetzungsfelder hängen vom Anbieter ab
enabledWhen: new SettingCondition(LocalizationSettings.TranslationProvider, [TranslationProviders.DeepL, TranslationProviders.Azure])
Das ist nur eine Erleichterung im Formular; der Server erzwingt sie nicht. Eine Bedingung auf eine Einstellung, die nicht im Formular steht (etwa in einem anderen Bereich), lässt das Feld aktiv.
Gruppen auf eigener Seite¶
context.Group(name, displayName, page: "/admin/ai-settings") markiert eine Gruppe als auf einer eigenen Admin-Seite gepflegt. Die Einstellungs-Tabs überspringen sie, Meine Einstellungen des Benutzers zeigen weiter jede Gruppe ohne page. So arbeitet die KI-Gruppe: Ihre Einstellungen, auch der MCP-Schalter, liegen auf /admin/ai-settings. Ebenso Mail: Anbieter, Zugangsdaten und der Schalter für den eigenen Server einer Organisation liegen auf /admin/mail-settings, siehe Mailversand.
Spracheinstellungen¶
SettingType.Language passt für Einstellungen, die eine Kultur halten. Localization.Culture (Default language) ist die Sprache, in der die App startet: Eine Organisation überschreibt den Systemwert, ein Benutzer überschreibt beide. Ein leerer Wert erbt.

Typisierte App-Einstellungen¶
Die App beschreibt ihre Einstellungen als Klassen. Werte kommen aus appsettings und der Umgebung; was Administratoren ändern, liegt in cw.ConfigurationEntries und gilt darüber für jeden Dienst der App. Die Einstellungsseite zeigt jede Klasse mit MudExObjectEditForm, wie die klassische Einstellungsseite, und Dienste lesen IOptionsMonitor<T> und sehen Änderungen nach wenigen Sekunden. Werte, die nur beim Start gelesen werden, gelten nach einem Neustart.
Jeder Dienst, der die Änderungen sehen soll, nimmt die Datenbank-Schicht in seine Konfiguration auf:
builder.Configuration.AddCoworkeeDatabaseConfiguration("myapp"); // Name des Connection Strings der App-Datenbank
Eingebaute App-Einstellungen¶
Ohne weiteren Code zeigt die Seite App-Einstellungen: CoworkeeAppSettings, gebunden an den Abschnitt Coworkee, mit Selbstregistrierung (Registration), Benachrichtigungs-Zusammenfassungen (Notifications) und Hintergrundjobs (Jobs). Verdrahtungswerte, die der AppHost üblicherweise setzt (Jobs:ConnectionStringName, Jobs:RunServer, Jobs:DashboardPath, Notifications:PublicAppUrl), sind geschützt: Sie ändern sich erst, nachdem du das Feld mit seinem Schloss-Button entsperrt hast, und gelten dann nach einem Neustart; Listen, an die der Configuration Binder nur anhängen könnte (Jobs:Queues, Jobs:RetryDelaysInSeconds), sind ausgeblendet.
App-Einstellungen erweitern¶
Leite von CoworkeeAppSettings ab und registriere den Typ auf beiden Seiten. Die Regeln der eingebauten Abschnitte bleiben; eigene kommen hinzu:
public sealed class MyAppSettings : CoworkeeAppSettings
{
public BillingSettings Billing { get; set; } = new();
}
public sealed class BillingSettings
{
public string Currency { get; set; } = "EUR";
public string? ApiKey { get; set; }
public string? WebhookUrl { get; set; }
public string? WebhookSecret { get; set; }
}
context.Services.AddCoworkeeSettings<MyAppSettings>(context.Configuration, rules => rules
.Lock(s => s.Billing.Currency) // deaktiviert sichtbar, nie geändert
.Protect(s => s.Billing.WebhookUrl) // nach dem Entsperren des Feldes änderbar
.Hide(s => s.Billing.WebhookSecret)); // verlässt den Server nie
builder.Services.AddCoworkeeSettings<MyAppSettings>(meta =>
{
meta.Property(s => s.Billing.ApiKey).WithGroup("Billing");
meta.Property(s => s.Billing.Currency).WithGroup("Billing");
});
Dienste lesen IOptionsMonitor<MyAppSettings>; die neuen Eigenschaften liegen unter Coworkee:Billing.
App-Einstellungen ersetzen¶
Jede andere Klasse ersetzt die eingebaute vollständig, meist mit eigenem Abschnitt:
context.Services.AddCoworkeeSettings<ShopSettings>(context.Configuration, section: "Shop", title: "Shop"); // Server
builder.Services.AddCoworkeeSettings<ShopSettings>(section: "Shop", title: "Shop"); // Client
Regeln¶
Lock(s => s.A.B) |
der Wert erscheint deaktiviert mit Schloss-Symbol („In der Konfiguration festgelegt“); der Server behält, was gilt, egal was kommt. Für Werte, die nur der Host setzt. Ein gesperrtes Objekt sperrt alle seine Eigenschaften |
Protect(s => s.A.B) |
der Wert ist änderbar, aber riskant: Das Feld bleibt deaktiviert, bis du seinen Schloss-Button drückst, und ist nach dem Speichern oder Neuladen wieder geschützt. Nur das Formular schützt, der Server speichert, was ankommt |
Hide(s => s.A.B) |
der Wert wird weder an den Browser geschickt noch geändert |
| Geheimnisse | Eigenschaften, die wie Passwort, Secret, API-Key, Token oder Connection String heißen, kommen maskiert; die Maske zurückzuschicken behält den gespeicherten Wert |
| Meta | das meta des Clients gruppiert, beschriftet, sortiert oder rendert Eigenschaften wie in jedem MudExObjectEditForm; Sperren und Ausblendungen des Servers gelten zusätzlich. Das Formular zeigt jeden Abschnitt unter seiner Überschrift in zwei Spalten (Listen über die volle Breite); WrapInMudItem(i => i.md = 12) verbreitert ein einzelnes Feld. Berechnete Eigenschaften (ohne Setter) entfallen, Felder haben kein eigenes Zurücksetzen; ein Listeneintrag in seinem Dialog (etwa ein Registrierungsdokument) folgt denselben Regeln |
| Gültigkeit | typisierte Einstellungen gelten für die ganze Installation und werden nur in der System-Organisation bearbeitet; Werte pro Mandant und Benutzer sind Einstellungen (oben) |
Standardwerte wiederherstellen verwirft die in der App geänderten Werte; appsettings und Umgebung gelten wieder.
Editoren für Dateitypen, Größen, Zeitpläne und Übersetzungen¶
Vier Attribute aus Coworkee.Contracts.Configuration wählen für eine Eigenschaft einen passenderen Editor, auf der Einstellungsseite und in jedem anderen Objektformular der App:
| Attribut | Eigenschaft | Editor |
|---|---|---|
[ContentTypes] |
List<string> oder string[] mit MIME-Typen |
Chips mit lesbaren Namen und Symbolen sowie eine Suche über alle MIME-Typen, die MimeType von Nextended kennt, nach Name, Dateiendung (.epub) oder MIME-Typ, jeder Treffer mit Symbol und Art (Bilder, Dokumente, Videos, Audio, Archive, Text, Schriftarten, 3D-Modelle, Sonstige). Platzhalter (image/*, video/*, audio/*, text/*) und gängige Typen stehen vorn; das Menü daneben fügt ganze Gruppen hinzu (alle Bilder, Office-Dokumente, ...). Sie zeigt höchstens 50 Treffer, damit der Editor schnell bleibt. Leer erlaubt jeden Typ |
[FileSize] |
long? oder long in Bytes |
eine Zahl in KB, MB oder GB; leer heißt keine Begrenzung |
[Cron] |
string mit einem Cron-Ausdruck |
Vorlagen (alle paar Minuten, stündlich, täglich, wöchentlich, monatlich) und der Ausdruck selbst, in Worten beschrieben (UTC); ein ungültiger Ausdruck wird nicht übernommen |
[Translations] |
Dictionary<string, string> von Kultur zu Text |
eine Zeile pro Sprache mit Name und Code; die Sprachen der App zur Auswahl, andere Kulturcodes wie fr-CA werden eingetippt, Zeilen per Button entfernt |
public sealed class ImportSettings
{
[Cron]
public string Schedule { get; set; } = "0 2 * * *";
[ContentTypes]
public List<string> AcceptedFiles { get; set; } = ["text/csv"];
[FileSize]
public long? MaxFileSize { get; set; } = 10 * 1024 * 1024;
[Translations]
public Dictionary<string, string> Titles { get; set; } = [];
}
Die eingebauten Einstellungen nutzen sie für die Registrierungsdokumente (Registration:Documents, mit Namen und Beschreibungen pro Sprache) und die Uhrzeit der Zusammenfassung (Notifications:DigestCron). Ein RenderWith in der Client-meta hat weiterhin Vorrang vor dem Attribut.
Beschriftungen, Hilfetexte und Gruppen aus [Display]¶
[Display] aus System.ComponentModel.DataAnnotations beschreibt eine Eigenschaft dort, wo sie deklariert ist, ohne Client-Code: Name ist die Beschriftung, Description der Hilfetext unter dem Feld und GroupName der Abschnitt, in dem sie steht. Wie jeder Text des Formulars laufen sie über die Übersetzungen; der deutsche Text gehört also in de.json.
public sealed class BillingSettings
{
[Display(Name = "Currency", GroupName = "Invoices", Description = "The currency of new invoices.")]
public string Currency { get; set; } = "EUR";
}
Ein Registrierungsdokument zeigt, wie das aussieht: Sein Dialog hat die Abschnitte Für den Benutzer sichtbar (Name und Hinweis, gültig in jeder Sprache), Übersetzungen (optional) (Name und Hinweis für einzelne Sprachen, jede Zeile beschriftet mit Übersetzung für Deutsch usw.) und Upload (Pflicht, erlaubte Dateitypen, größte Datei).
Weitere Abschnitte¶
Weitere Klassen erscheinen als weitere Tabs neben den App-Einstellungen:
builder.Configuration
.AddCoworkeeAppConfigurationDefaults(SharemeConfiguration.Defaults(), SharemeConfiguration.Section)
.AddCoworkeeDatabaseConfiguration("shareme");
context.Services.AddCoworkeeAppConfiguration<SharemeConfiguration>(context.Configuration, SharemeConfiguration.Section, SharemeConfiguration.Title,
rules => rules.Lock(c => c.Storage));
builder.Services.AddCoworkeeAppConfiguration<SharemeConfiguration>(SharemeConfiguration.Section, SharemeConfiguration.Title);
AddCoworkeeAppConfigurationDefaults legt eine JSON-Datei des Abschnitts als unterste Schicht an. Sharemee erzeugt SharemeConfiguration mit Nextended.CodeGen aus appconfig.json; JSON-Standardwerte und Klasse können so nicht auseinanderlaufen:
{ "JsonConfigs": [ { "SourceFile": "Configuration/appconfig.json", "RootClassName": "SharemeConfiguration", "Namespace": "Shareme.Contracts.Configuration" } ] }
Die alte Adresse /admin/configuration führt zur Einstellungsseite.
Dienste¶
CoworkeeServicesOptions (Abschnitt Coworkee:Services) benennt die Dienste der App: API, Auth-Server und Web-App, das Aspire-Dashboard, das Hangfire-Dashboard, Mailpit, pgAdmin und jede weitere Ressource mit Web-Endpunkt. Der AppHost füllt sie in der Entwicklung; Deployments tragen ihre öffentlichen Adressen in appsettings ein:
{ "Coworkee": { "Services": {
"myapp-api": { "Url": "https://api.example.com", "HealthPath": "/health" },
"grafana": { "Url": "https://grafana.example.com", "Title": "Grafana" } } } }
public sealed class Links(IOptionsMonitor<CoworkeeServicesOptions> services)
{
public string? Mailpit => services.CurrentValue.GetValueOrDefault("mail")?.Url;
}
Administration > System > Dienste zeigt sie als Kacheln mit Symbol, Adresse und aktuellem Zustand, gruppiert nach Quelle (App für Coworkee:Services, AspireUI für die Apps eines AspireUI); ein Klick öffnet den Dienst in einem neuen Tab, Dienste mit mehreren Adressen listen alle als Links. Eine Quelle, die nicht erreichbar war, zeigt einen Hinweis, die anderen Quellen erscheinen trotzdem. Der Browser fragt die API (GET /api/v1/services, Einstellungs-Berechtigung, System-Organisation, Antwort { services, unavailableSources }), die jeden Dienst alle zehn Sekunden prüft: Mit HealthPath zählt ein Erfolgsstatus als läuft, ohne jede Antwort unter 500.
Dienstverzeichnis¶
IServiceDirectory verbindet die konfigurierten Dienste (Coworkee:Services) mit denen weiterer Quellen, sodass Backend-Code einen Dienst über den Namen findet, ohne zu wissen, wo er eingetragen ist. Es liegt im Namespace Coworkee.Settings und wird vom Settings-Modul registriert: Jeder Host, der das Modul einbindet, kann es injizieren. Es ist scoped, wie die anderen Settings-Dienste; ein Singleton oder ein Hosted Service legt zuerst einen Scope an. Es gibt keine Benutzerprüfung, es ist Backend-Code.
using Coworkee.Contracts.Settings;
using Coworkee.Settings;
public sealed class BillingClient(IServiceDirectory directory, IHttpClientFactory clients)
{
public async Task<string?> StatusAsync(CancellationToken cancellationToken)
{
if (await directory.ResolveAsync("billing", cancellationToken) is not { } billing)
{
return null; // unbekannter Name, oder die Quelle, die ihn kennt, ist nicht erreichbar
}
var address = billing.Urls?.FirstOrDefault(u => u.StartsWith("https:", StringComparison.OrdinalIgnoreCase)) ?? billing.Url;
if (address.Length == 0)
{
return null; // aufgelistet, aber ohne Adresse
}
using var client = clients.CreateClient();
client.BaseAddress = new Uri(address);
return await client.GetStringAsync("status", cancellationToken);
}
}
In einem Singleton oder Hosted Service legst du den Scope selbst an:
public sealed class BillingPoller(IServiceScopeFactory scopes) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var scope = scopes.CreateScope();
var directory = scope.ServiceProvider.GetRequiredService<IServiceDirectory>();
var billing = await directory.ResolveAsync("billing", stoppingToken);
// ...
}
}
ResolveAsync(name)findet einen Dienst über den Namen, ohne Beachtung der Groß-/Kleinschreibung. Die konfigurierten Dienste kommen zuerst, ein konfigurierter Dienst gewinnt also gegen eine gleichnamige AspireUI-App. Der Aufruf ist schnell: Der Zustand eines konfigurierten Dienstes wird nicht geprüft und bleibtUnknown.ResolveAsync("aspireui:billing")fragt nur die Quelle vor dem Doppelpunkt (appfürCoworkee:Services,aspireuioder den Namen deiner eigenen Quelle). Nutz es, wenn zwei Quellen denselben Namen kennen und du eine bestimmte brauchst. Ein Präfix, das kein Quellenname ist, gehört zum Namen.GetAllAsync()listet jeden Dienst jeder Quelle, ausgefallene Quellen stehen inUnavailableSources. Das zeigen die Admin-Seite undGET /api/v1/services.
Wann ResolveAsync null liefert: Keine Quelle kennt den Namen, oder die einzige Quelle, die ihn kennt, ist nicht erreichbar (sie ist ausgefallen oder hat nicht innerhalb von drei Sekunden geantwortet). Der Aufrufer kann beide Fälle nicht unterscheiden; GetAllAsync kann es über UnavailableSources. Behandle null immer: Ein Dienst kann jederzeit fehlen.
Url oder Urls: Url ist die erste Adresse, Urls listet alle Adressen, wenn eine Quelle mehrere kennt. Konfigurierte Dienste haben genau eine Url und keine Urls. Eine AspireUI-App listet alle ihre Endpunkte in Urls, http-Endpunkte können vorn stehen, und eine App ohne Endpunkte hat eine leere Url. Wähl aus Urls, wenn dir https wichtig ist (siehe Beispiel), und prüf auf ein leeres Ergebnis.
IServiceDirectory, IOptionsMonitor<CoworkeeServicesOptions> oder Aspire Service Discovery?
| Bedarf | Verwenden |
|---|---|
| Die Adresse eines Dienstes, den du selbst konfigurierst (ein Link, eine Weiterleitung), synchron, ohne I/O | IOptionsMonitor<CoworkeeServicesOptions> |
| Ein Dienst, der aus einer AspireUI-App oder deiner eigenen Quelle stammen kann, oder eine Liste aller Dienste | IServiceDirectory |
| Aufrufe zwischen den Ressourcen eines AppHosts (API zum Auth-Server, Worker zur API) | Aspire Service Discovery: WithReference im AppHost und https+http://name als Basisadresse des HttpClient |
Das Verzeichnis dient dem Finden und Anzeigen von Diensten, Discovery dem Aufrufen von Ressourcen, die der AppHost verdrahtet.
Eine Quelle, die fehlschlägt oder länger als drei Sekunden braucht, wird ausgelassen, als nicht erreichbar gemeldet und stört die anderen Quellen nicht. Einen Fehler, den die Quelle schon geloggt hat, loggt das Verzeichnis nicht noch einmal.
AspireUI listet die Apps und Stacks, die du selbst mit einem AspireUI hostest (Source = aspireui, alle Adressen in Urls). Der AppHost verbindet es mit WithRetrieveApi(), mehr ist nicht nötig:
Ohne Aspire oder für eine veröffentlichte App trägst du Adresse und Schlüssel der Retrieve-API selbst ein:
Ohne Url bleibt die Quelle inaktiv. Die Liste wird 15 Sekunden zwischengespeichert; der Schlüssel geht nur als X-Api-Key an das AspireUI und wird weder geloggt noch von der API zurückgegeben. Benutzerinformationen in der Adresse werden vor dem Loggen und Zwischenspeichern entfernt.
Eigene Quelle: Implementiere IServiceSource und registriere sie neben den eingebauten. Diese liest eine JSON-Liste [{ "name": "...", "url": "..." }] aus einer Registry:
using System.Net.Http.Json;
using Coworkee.Contracts.Settings;
using Coworkee.Settings;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
public sealed class RegistryServiceSource(IHttpClientFactory clients) : IServiceSource
{
public string Name => "registry";
public async Task<IReadOnlyList<ServiceDto>> GetAsync(CancellationToken cancellationToken)
{
var entries = await clients.CreateClient().GetFromJsonAsync<List<RegistryEntry>>("https://registry.example.com/services.json", cancellationToken) ?? [];
return [.. entries.Select(e => new ServiceDto(e.Name, e.Name, e.Url, ServiceHealth.Unknown, Name))];
}
private sealed record RegistryEntry(string Name, string Url);
}
// in ConfigureServices
services.TryAddEnumerable(ServiceDescriptor.Singleton<IServiceSource, RegistryServiceSource>());
Wirf eine Ausnahme, wenn die Quelle nicht gefragt werden kann; das Verzeichnis meldet sie dann als nicht erreichbar. Überschreib FindAsync, wenn sich ein einzelner Dienst günstiger beantworten lässt als durch Auflisten aller. Das Verzeichnis setzt Source jedes Eintrags auf den Name seiner Quelle.