Interne Entwicklungswerkzeuge entfernen
Night-Agent (ops/night-agent.mjs, Leitplanken, Doku) und das Skript zur Lizenzsystem-Erweiterung werden für den Betrieb einer Installation nicht gebraucht. README-Hinweis entfernt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
fca2b9d746
commit
fddaa46d7d
8 changed files with 2 additions and 413 deletions
|
|
@ -1,50 +0,0 @@
|
|||
# Lizenzsystem-Erweiterung: Edition (Schlüssel-Präfix) und festes Ablaufdatum
|
||||
|
||||
**Zweck:** Das Kundencenter soll Familytool-Stufen verkaufen (Premium, Unlimited, Lifetime, Trial). Das Familytool erkennt die Stufe am **Präfix des Lizenzschlüssels** (`PREMIUM-…`, `TRIAL-…`, `LIFETIME…`, sonst Unlimited). Das Lizenzsystem konnte bisher nur Schlüssel ohne Präfix erzeugen, jede Lizenz wäre also automatisch Unlimited gewesen.
|
||||
|
||||
**Wo einspielen:** auf der Instanz, gegen die das Kundencenter und das Familytool laufen, also **licensing.flessinglabs.com** (nicht auf dem lokalen Dienst dieses Servers). Ohne den Patch verweigert das Kundencenter Bestellungen mit Edition (das Produkt bleibt Entwurf, eine Bestellung würde nie eine falsche Lizenz anlegen).
|
||||
|
||||
Getestet an einer Kopie des Codes mit eigener Testdatenbank auf einem separaten Port: Präfix für alle drei Editionen, ungültiges Präfix → 422, festes Ablaufdatum, unveränderte Anlage ohne Präfix, `validate` mit Präfix-Schlüssel, Erkennung über `GET /`.
|
||||
|
||||
## Ablauf (ca. 10 Minuten, kurze Unterbrechung nur beim Neustart)
|
||||
|
||||
1. **Sichern** (DB und Code des Lizenzsystems auf dem Zielserver):
|
||||
```bash
|
||||
mysqldump --single-transaction <DBNAME> > licensing-vor-erweiterung.sql
|
||||
cp -a <pfad>/backend/app app-vor-erweiterung
|
||||
```
|
||||
2. **Vorab prüfen: startet der Dienst nach einem Neustart korrekt?** (Wir hatten am 26.09. auf dem lokalen Dienst einen Ausfall, weil das venv neuer war als der laufende Prozess. Siehe LICENSING-11.)
|
||||
```bash
|
||||
<pfad>/backend/venv/bin/pip list | grep -iE "bcrypt|passlib|slowapi|starlette" # bcrypt muss < 5 sein (z. B. 4.0.1)
|
||||
grep -n "def validate_license" -A3 <pfad>/backend/app/routers/licenses.py # Parameter muss "request: Request" heißen
|
||||
```
|
||||
Wenn `bcrypt` ≥ 5 ist oder die Schema-Parameter noch `request` heißen: **erst das beheben**, sonst fallen Login und `validate` beim Neustart aus.
|
||||
3. **Datenbank erweitern** (Schlüssel werden mit Präfix länger als 36 Zeichen):
|
||||
```sql
|
||||
ALTER TABLE licenses MODIFY license_key VARCHAR(64) NOT NULL;
|
||||
```
|
||||
4. **Code anpassen** (erst Trockenlauf, dann anwenden):
|
||||
```bash
|
||||
python3 apply_erweiterung.py <pfad>/backend # zeigt nur, was geändert würde
|
||||
python3 apply_erweiterung.py <pfad>/backend --apply # legt Sicherungen *.bak-<zeit> an
|
||||
```
|
||||
Weicht der Code ab, bricht das Skript ohne Änderung ab und nennt die Stelle.
|
||||
5. **Dienst neu starten** (z. B. `systemctl restart licensing-backend`) und prüfen:
|
||||
```bash
|
||||
curl -s https://licensing.flessinglabs.com/api/ # muss "features":["key_prefix","explicit_expiry"] enthalten
|
||||
```
|
||||
Danach kurz testen: Login im Admin-Panel, eine Lizenz prüfen (`validate`) und im Familytool die Lizenzprüfung.
|
||||
6. **Im Kundencenter**: Verbindungen → „Jetzt abgleichen“. Unter „Fähigkeiten“ erscheinen `license.key_prefix` und `license.expiry`. Danach lassen sich die Familytool-Produkte aktivieren.
|
||||
|
||||
## Zurückrollen
|
||||
Dienst stoppen, `*.bak-<zeit>` zurückkopieren, Dienst starten. Die Spalte darf auf 64 Zeichen bleiben. Bereits mit Präfix angelegte Lizenzen bleiben gültig.
|
||||
|
||||
## API-Änderungen im Detail
|
||||
- `POST /licenses/` Body: neues optionales Feld `key_prefix` (`PREMIUM` | `TRIAL` | `LIFETIME`). Antwort enthält den Schlüssel `<PRAEFIX>-<uuid>`.
|
||||
- `POST /licenses/`: ein angegebenes `expires_at` (Ortszeit wie bisher) wird verwendet; ohne Angabe gilt die bisherige Berechnung aus `duration_type`.
|
||||
- `GET /`: Feld `features` (Liste).
|
||||
- Nicht geändert: `validate`, `register`, Shop, Login, alle bestehenden Felder.
|
||||
|
||||
## Bekannte Grenzen
|
||||
- Ein Upgrade von Premium auf Unlimited bedeutet einen **neuen Schlüssel** (das Präfix steckt im Schlüssel). Das Kundencenter legt dann eine neue Lizenz an; die alte wird gesperrt. Eine spätere Lösung mit einem Feld `edition` im Lizenzsystem (statt Präfix) ist möglich.
|
||||
- Das Ablaufdatum wird (wie im Lizenzsystem üblich) als Ortszeit gespeichert.
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Erweiterung des Lizenzsystems um "Edition" (Schlüssel-Präfix) und festes Ablaufdatum – für das Kundencenter.
|
||||
|
||||
Was der Patch tut (alles abwärtskompatibel, bestehende Clients ändern sich nicht):
|
||||
1. POST /licenses/ akzeptiert optional key_prefix: "PREMIUM" | "TRIAL" | "LIFETIME"
|
||||
-> Schlüssel lautet dann "<PRAEFIX>-<uuid>" (das Familytool erkennt daran die Stufe; ohne Präfix = Unlimited).
|
||||
2. POST /licenses/ beachtet ein ausdrücklich angegebenes expires_at (bisher wurde es ignoriert).
|
||||
3. GET / meldet "features": ["key_prefix", "explicit_expiry"] (daran erkennt das Kundencenter die Erweiterung).
|
||||
4. models.License.license_key wird von 36 auf 64 Zeichen erweitert (DB-Änderung: siehe README, VORHER ausführen).
|
||||
|
||||
Nutzung (Standard = nur prüfen, nichts ändern):
|
||||
python3 apply_erweiterung.py /pfad/zu/licensing-system/backend # Trockenlauf
|
||||
python3 apply_erweiterung.py /pfad/zu/licensing-system/backend --apply # anwenden (mit Sicherungskopien *.bak-<zeit>)
|
||||
Das Skript ist idempotent: bereits vorhandene Änderungen werden erkannt und übersprungen.
|
||||
"""
|
||||
import re, sys, time, py_compile, pathlib
|
||||
|
||||
def main() -> int:
|
||||
args = [a for a in sys.argv[1:] if not a.startswith('--')]
|
||||
apply = '--apply' in sys.argv
|
||||
if len(args) != 1:
|
||||
print(__doc__); return 2
|
||||
root = pathlib.Path(args[0]) / 'app'
|
||||
files = {n: root / p for n, p in {'models': 'models.py', 'schemas': 'schemas.py', 'licenses': 'routers/licenses.py', 'main': 'main.py'}.items()}
|
||||
for n, f in files.items():
|
||||
if not f.exists():
|
||||
print(f'FEHLER: {f} nicht gefunden – falscher Pfad? (erwartet: .../licensing-system/backend)'); return 1
|
||||
src = {n: f.read_text() for n, f in files.items()}
|
||||
new = dict(src); report = []
|
||||
|
||||
def step(name, key, old, repl, marker):
|
||||
if marker in new[key]:
|
||||
report.append(f' = {name}: bereits vorhanden'); return True
|
||||
if old not in new[key]:
|
||||
report.append(f' ! {name}: erwartete Stelle NICHT gefunden – bitte manuell prüfen (siehe README)'); return False
|
||||
new[key] = new[key].replace(old, repl, 1); report.append(f' + {name}'); return True
|
||||
|
||||
ok = True
|
||||
ok &= step('models: license_key 64 Zeichen', 'models',
|
||||
'license_key = Column(String(36), unique=True, index=True, default=lambda: str(uuid.uuid4()))',
|
||||
'license_key = Column(String(64), unique=True, index=True, default=lambda: str(uuid.uuid4()))', 'String(64), unique=True')
|
||||
ok &= step('schemas: key_prefix in LicenseCreate', 'schemas',
|
||||
'class LicenseCreate(LicenseBase):\n pass',
|
||||
'class LicenseCreate(LicenseBase):\n # Optionales Schlüssel-Präfix (Edition): Schlüssel lautet "<PRAEFIX>-<uuid>". Ohne Präfix gilt die Lizenz im Familytool als UNLIMITED.\n key_prefix: Optional[Literal["PREMIUM", "TRIAL", "LIFETIME"]] = None',
|
||||
'key_prefix: Optional[Literal')
|
||||
if 'Literal' not in re.split(r'\nclass ', new['schemas'], maxsplit=1)[0]:
|
||||
m = re.search(r'^from typing import (.+)$', new['schemas'], re.M)
|
||||
new['schemas'] = new['schemas'].replace(m.group(0), f'from typing import Literal, {m.group(1)}', 1) if m else 'from typing import Literal\n' + new['schemas']
|
||||
report.append(' + schemas: Literal importiert')
|
||||
ok &= step('licenses: festes Ablaufdatum hat Vorrang', 'licenses',
|
||||
' # Calculate expires_at based on duration_type\n expires_at = None\n if license.duration_type != schemas.LicenseDurationType.UNLIMITED:',
|
||||
' # Ein ausdrücklich angegebenes Ablaufdatum hat Vorrang; sonst wird es aus duration_type berechnet\n expires_at = license.expires_at\n if expires_at is None and license.duration_type != schemas.LicenseDurationType.UNLIMITED:',
|
||||
'expires_at = license.expires_at')
|
||||
ok &= step('licenses: Schlüssel mit Präfix', 'licenses',
|
||||
' db_license = models.License(\n program_id=license.program_id,',
|
||||
' extra = {"license_key": f"{license.key_prefix}-{uuid.uuid4()}"} if license.key_prefix else {}\n db_license = models.License(\n **extra,\n program_id=license.program_id,',
|
||||
'f"{license.key_prefix}-')
|
||||
if not re.search(r'^import uuid$', new['licenses'], re.M):
|
||||
new['licenses'] = re.sub(r'^import datetime$', 'import datetime\nimport uuid', new['licenses'], count=1, flags=re.M); report.append(' + licenses: uuid importiert')
|
||||
ok &= step('main: features in GET /', 'main',
|
||||
'return {"message": "Licensing System Backend is running!"}',
|
||||
'return {"message": "Licensing System Backend is running!", "features": ["key_prefix", "explicit_expiry"]}', '"features"')
|
||||
|
||||
print('\n'.join(report))
|
||||
if not ok:
|
||||
print('\nABBRUCH: Nicht alle Stellen passen (Code weicht ab). Es wurde NICHTS geändert.'); return 1
|
||||
changed = [n for n in files if new[n] != src[n]]
|
||||
if not changed:
|
||||
print('\nNichts zu tun – die Erweiterung ist bereits vollständig eingespielt.'); return 0
|
||||
if not apply:
|
||||
print(f'\nTROCKENLAUF: würde {", ".join(files[n].name for n in changed)} ändern. Mit --apply ausführen.'); return 0
|
||||
ts = time.strftime('%Y%m%d-%H%M%S')
|
||||
for n in changed:
|
||||
files[n].with_name(files[n].name + f'.bak-{ts}').write_text(src[n])
|
||||
files[n].write_text(new[n])
|
||||
py_compile.compile(str(files[n]), doraise=True)
|
||||
print(f'\nANGEWENDET: {", ".join(files[n].name for n in changed)} (Sicherungen: *.bak-{ts}). Jetzt Dienst neu starten (siehe README).')
|
||||
return 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
# Nacht-Agent
|
||||
|
||||
Arbeitet einmal pro Nacht (01:00 Uhr, `kc-night-agent.timer`) offene Plane-Tickets mit dem Label
|
||||
**„agent“** ab, in den Status Backlog oder Todo. Ergebnis ist **immer** ein Branch auf der Forge
|
||||
(`https://forge.flessinghome.de/flessinglabs/kundencenter`), bei Erfolg zusätzlich ein Pull
|
||||
Request und Ticket-Status „In Review“. **Nichts wird automatisch gemergt oder live geschaltet** –
|
||||
das bleibt bei der morgendlichen Durchsicht durch den Assistenten bzw. den Nutzer.
|
||||
|
||||
## Absicherung
|
||||
Der Agent läuft als Betriebssystembenutzer `kundencenter` (systemd-Dienst `kc-night-agent.service`,
|
||||
zusätzlich `ProtectSystem=strict`/`ProtectHome=true`, nur `/var/lib/kundencenter/night-agent`
|
||||
beschreibbar). Dieser Benutzer:
|
||||
- hat kein `sudo`, kann also kein `systemctl restart` und keine Dienste anfassen,
|
||||
- kann `/etc/kundencenter/*` nicht lesen (root:root, chmod 600) – keine Geheimnisse erreichbar,
|
||||
- arbeitet in einer **eigenen frischen Arbeitskopie** (`/var/lib/kundencenter/night-agent/work`),
|
||||
niemals in `/srv/kundencenter` selbst.
|
||||
|
||||
Zusätzlich bekommt jeder Lauf feste Leitplanken als System-Prompt
|
||||
(`ops/night-agent-guardrails.md`): ein Ticket = ein Branch, Tests müssen grün sein, keine
|
||||
Vermutungen bei fehlenden Zugangsdaten, kein Aufräumen außerhalb des Tickets.
|
||||
|
||||
## Ablauf je Ticket
|
||||
1. Plane-Status → „In Progress“, Kommentar mit Branch-Namen.
|
||||
2. `claude -p` mit Ticket-Text + Leitplanken, Budget-Deckel (`--max-budget-usd`, Standard 3 $),
|
||||
Zeit-Deckel (Standard 45 Min./Ticket, insgesamt max. 150 Min./Nacht).
|
||||
3. Unabhängig vom Agenten prüft das Skript selbst: `pnpm install && pnpm build && pnpm typecheck
|
||||
&& pnpm test` (Tests laufen automatisch gegen eine frische `kundencenter_test`, nie gegen die
|
||||
produktive Datenbank).
|
||||
4. Grün + Agent meldet `AGENT_STATUS: ok` → Branch pushen, Pull Request eröffnen, Plane-Status
|
||||
„In Review“, Kommentar mit PR-Link.
|
||||
5. Rot, oder Agent meldet `AGENT_STATUS: blocked`, oder keine Änderungen → Branch wird trotzdem
|
||||
gepusht (nichts geht verloren), aber **kein** Pull Request; Kommentar in Plane beschreibt das
|
||||
Problem. Status bleibt unverändert (kein automatisches „Wartet auf Input“, das entscheidet der
|
||||
Mensch nach Lektüre des Kommentars).
|
||||
|
||||
## Einrichtung
|
||||
- `/etc/kundencenter/night-agent.env` (chmod 600, root): `ANTHROPIC_API_KEY` (eigener, vom
|
||||
interaktiven Zugang getrennter Schlüssel – siehe unten) sowie optionale Stellschrauben
|
||||
(`NIGHT_AGENT_MAX_TICKETS`, `NIGHT_AGENT_BUDGET_USD`, `NIGHT_AGENT_DEADLINE_MIN`,
|
||||
`NIGHT_AGENT_TICKET_TIMEOUT_MIN`).
|
||||
- `/etc/kundencenter/forge.env`: `FORGE_URL`, `FORGE_REPO` (`flessinglabs/kundencenter`),
|
||||
`FORGE_TOKEN`.
|
||||
- Plane-Label „agent“ muss auf ein Ticket gesetzt sein, damit der Agent es aufgreifen darf.
|
||||
- Aktivieren: `systemctl enable --now kc-night-agent.timer`. Einmalig von Hand testen:
|
||||
`systemctl start kc-night-agent.service` (Logs: `journalctl -u kc-night-agent` sowie
|
||||
`/var/lib/kundencenter/night-agent/logs/`).
|
||||
|
||||
## Bekannte Grenzen
|
||||
- `kundencenter_test` wird bei jedem `pnpm test` neu angelegt (siehe `apps/api/test/global-setup.ts`)
|
||||
– Agent-Lauf und ein manueller Testlauf am Tag sollten sich nicht überschneiden.
|
||||
- Der Agent öffnet nie mehr als `NIGHT_AGENT_MAX_TICKETS` Pull Requests pro Nacht.
|
||||
- Kein Zugriff auf Anbieter-Testzugänge (z. B. KeyHelp, KCS) – bei Tickets, die das brauchen,
|
||||
meldet der Agent `blocked`.
|
||||
|
|
@ -34,6 +34,6 @@ Jede Produktversion hat eine Preisbasis. **Brutto**: der Betrag ist der Endkunde
|
|||
Pakete beschreiben mehrere Produkte einer Software (Editionen, Preise, Laufzeiten, Provisionierung). Auf der Übernahmeseite wählt man ein Programm der Verbindung und importiert Paketprodukte als **Entwürfe**. Vorhanden: `familytool.json` (Premium/Unlimited monatlich+jährlich, Lifetime, Trial 14 Tage; Preise brutto laut LICENSE_TIERS.md).
|
||||
|
||||
## Edition (Familytool) und Laufzeitpflege
|
||||
- Die Stufe steckt im Lizenzschlüssel-Präfix (`PREMIUM-`, `TRIAL-`, `LIFETIME`, sonst Unlimited). Der Lizenz-Connector sendet `key_prefix`/`expires_at` nur, wenn das Lizenzsystem die Erweiterung meldet (`GET /` → `features`), und prüft die Antwort. Sonst bleibt das Produkt Entwurf bzw. die Bestellung schlägt sicher fehl. Patch: `docs/lizenzsystem-erweiterung/`.
|
||||
- Die Stufe steckt im Lizenzschlüssel-Präfix (`PREMIUM-`, `TRIAL-`, `LIFETIME`, sonst Unlimited). Der Lizenz-Connector sendet `key_prefix`/`expires_at` nur, wenn das Lizenzsystem die Erweiterung meldet (`GET /` → `features`), und prüft die Antwort. Sonst bleibt das Produkt Entwurf bzw. die Bestellung schlägt sicher fehl.
|
||||
- Nach der Bereitstellung wird das Ablaufdatum der Lizenz an das Vertragsende angeglichen; bei jeder **Vertragsverlängerung** wird die Lizenz mit verlängert (Auftrag `extend`, idempotent). Bis zur Rechnungs-/Zahlungsanbindung geschieht das **ohne Zahlungsprüfung**.
|
||||
- Rollierende Monatsverträge (Mindestlaufzeit 0, automatische Verlängerung) erhalten einen monatlichen Verlängerungstakt. Upgrade Premium → Unlimited bedeutet einen neuen Schlüssel.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue