Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions PATCHES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Lokale Patches · trypost (Fork)

Privater Fork `Cryptoom/trypost` (origin). Upstream `trypostit/trypost` als Remote `upstream`.
Angelegt 2026-08-25 als Infrastruktur-Vorbereitung (noch KEIN aktiver Patch), Muster identisch
zu `~/voice-clone/whatsapp-mcp/PATCHES.md` und dem telegram-mcp-Fork.

Lizenz-Hinweis (wichtig, anders als bei den anderen beiden Forks): TryPost ist **AGPL-3.0**,
nicht MIT/Apache. AGPL hat Netzwerk-Copyleft: sobald eine modifizierte Version selbst gehostet
UND von Dritten (echten Kunden-Workspaces, nicht nur Digital Mind Agency/Jasmin intern) genutzt
wird, muss diesen Dritten der modifizierte Quellcode zugaenglich gemacht werden (z.B. Link im
Footer/Impressum zum Fork-Repo). Fuer den aktuellen Piloten (nur wir + Jasmin) unkritisch, vor
dem ersten echten Kunden-Workspace mit gepatchtem TryPost aber Pflicht-Punkt.

Update-Flow: `git fetch upstream && git merge upstream/main`, danach jeden Patch unten anhand
seines Marker-Strings gegenpruefen (`grep` reicht meist), erst dann `docker compose up -d --build`
auf web02 (Update-Klasse B/C-Aequivalent, siehe `~/.claude/rules/web02-docker-updates.md`, dort
noch nachtragen sobald der erste echte Patch aktiv ist).

## Aktive Patches

### Patch 1 · update-workspace-tool

Fuegt ein schreibendes MCP-Tool fuer die Workspace-Brand-Settings hinzu. Upstream hat nur
`GetWorkspaceTool` (read-only, `#[IsReadOnly]`), keinen API- oder MCP-Weg um `name`,
`brand_website`, `brand_description`, `brand_voice_traits`, `brand_color`, `background_color`,
`text_color`, `brand_font`, `image_style` oder `content_language` programmatisch zu setzen. Ohne
diesen Patch braucht jedes automatisierte Kunden-Onboarding (neuer Workspace, Brand per Skript
vorbefuellen) einen manuellen Umweg ueber die UI.

- **Dateien**:
- `app/Mcp/Tools/Workspace/UpdateWorkspaceTool.php` (neu). Validiert dieselben Regeln wie
`App\Http\Requests\App\Workspace\UpdateWorkspaceRequest`, aber PATCH-Semantik (`sometimes`
statt `required`, nur uebergebene Felder werden geschrieben). Autorisierung ueber
`AuthorizesMcpTool::authorizeCurrentWorkspace($request, 'update', ...)`, spiegelt
`WorkspaceController::updateSettings()`.
- `app/Mcp/Servers/TryPostServer.php` (Import + Registry-Eintrag unter "Workspace").
- **Marker-String zum Wiedererkennen nach `git merge upstream/main`**: Klassenname
`UpdateWorkspaceTool` (Datei existiert upstream nicht) plus der Registry-Kommentar
`// Workspace` in `TryPostServer.php` (dort pruefen ob `UpdateWorkspaceTool::class` noch
direkt nach `GetWorkspaceTool::class` steht).
- **Test**: `tests/Feature/Mcp/WorkspaceToolTest.php` (4 neue Tests: valides Update, ungueltige
`brand_color`, Cross-Workspace-Isolation, Ability-Check fuer Member ohne Admin/Owner-Rolle).
- Bricht bei einem Merge NUR falls upstream `UpdateWorkspaceRequest`, `WorkspaceResource` oder
die `update`-Ability in `WorkspacePolicy` umbenennt/entfernt, dann Patch-Regeln nachziehen.
- **Deployed** 25.08.2026: `/opt/trypost` auf web02 laeuft seit diesem Patch auf dem Fork-Remote
`Cryptoom/trypost` (vorher `trypostit/trypost` upstream), `docker compose up -d --build`
erfolgreich, live verifiziert (Klasse instanziierbar im Produktions-Container).

### Patch 2 · madevisible-brand-token-reskin

Ersetzt die komplette upstream Gumroad-Optik (warmes Cream, Ink-Border, Violett-Akzent,
Figtree/Instrument-Serif, harte Offset-Shadows) in der `:root`-Deklaration durch die
madevisible.io-Brand-Tokens (tiefes Teal-Primary, Navy-Text, Gold-Akzent, weiche getoente
Shadows, Geist-Font-Stack). Concept-Only-Reskin, damit App und die madevisible.io-Kunden-Instanz
optisch zusammengehoeren.

- **Datei**: `resources/css/app.css`, die `:root`-Deklaration (aktuell Zeilen ca. 16-80).
- **Marker-Strings zum Wiedererkennen nach `git merge upstream/main`**: die konkreten
Hex-Werte `#0c6e6d` (`--primary`/`--ring`/`--sidebar-primary`), `#0a2540` (`--foreground`),
`#c99a5c` (`--accent`) und `#a67c3f` (`--chart-3`, WCAG-korrigiert von urspruenglich `#c99a5c`
in Review Round 1). Keiner dieser Werte kommt in der upstream-Fassung vor (dort `#7c3aed`
Primary/Violett, `#0a0a0a` Foreground/Ink, `#faf8f5` Background). Ebenso der Kommentar
`madevisible.io brand tokens (Mediterranean Light / Premium)` direkt unter `:root {`.
- **Bruchbedingung**: Upstream aendert dieselbe `:root`-Block-Struktur in `app.css` (neue
Variablen, umbenannte Tokens, andere Reihenfolge) und `git merge upstream/main` laeuft
**konfliktfrei** durch. Ein konfliktfreier Merge ueberschreibt in diesem Fall die
Fork-Farbwerte stillschweigend mit den upstream-Originalwerten, weil beide Seiten dieselben
Zeilen anfassen und Git sonst laut einen Konflikt melden wuerde. Nach jedem Merge darum
gezielt gegen die obigen Hex-Werte greppen (`grep -c '#0c6e6d' resources/css/app.css`), nicht
nur auf einen Merge-Konflikt verlassen.
- Betrifft NUR `:root` (Light-Theme-Tokens). Ein eventueller `.dark`-Block bleibt unangetastet,
falls upstream einen ergaenzt, muesste der Reskin dort nachgezogen werden.

### Patch 3 · sidebar-referral-discord-entfernung

Entfernt die zwei untersten Bottom-Nav-Eintraege der Sidebar ("Earn 30% referral" und "Discord
community"), auf Ollis ausdruecklichen Wunsch (das TryPost-eigene Affiliate-/Community-Programm
soll im Digital-Mind-Agency-Weissabel-Kontext nicht auftauchen). Entfernt zugleich die dadurch
unbenutzten Icon-Imports.

- **Datei**: `resources/js/components/AppSidebar.vue`.
- **Marker-Strings zum Wiedererkennen nach `git merge upstream/main`**: die Imports
`IconBrandDiscord` und `IconGift` (aktuell in der Fork-Fassung NICHT mehr im
`@tabler/icons-vue`-Import-Block von `AppSidebar.vue` vorhanden) sowie die beiden
Objekt-Eintraege mit `href: 'https://affiliates.trypost.it/'` und
`href: 'https://trypost.it/discord'` im `bottomNavItems`-Computed (in der Fork-Fassung
entfernt). Der Docs-Link (`https://docs.trypost.it`) und `IconAffiliate` bleiben unveraendert
bestehen und sind NICHT Teil dieses Patches.
- **Bruchbedingung**: Upstream ergaenzt in `bottomNavItems` neue Eintraege oder aendert die
Struktur des Arrays (z.B. fuegt selbst wieder einen Referral- oder Community-Link hinzu) und
`git merge upstream/main` laeuft konfliktfrei durch, weil der Fork die betroffenen Zeilen
bereits geloescht hat und Git keinen Konflikt sieht. Ein solcher Merge kann den
Referral-/Discord-Link stillschweigend zurueckbringen. Nach jedem Merge pruefen:
`grep -n "trypost.it/discord\|affiliates.trypost.it" resources/js/components/AppSidebar.vue`
muss leer bleiben.

## Geprueft und NICHT gepatcht: is_aigc-Composer-Toggle (25.08.2026)

Der urspruenglich fuer diesen Fork geplante Patch (TikTok-`is_aigc`-Toggle im Post-Composer,
fuer die KI-Kennzeichnungs-Pflicht) ist **obsolet**: das Feature existiert bereits vollstaendig
upstream (`resources/js/components/posts/editor/TikTokSettings.vue`, Checkbox `isAigc`;
`app/Support/PostPlatformMetaRules.php`, `platforms.*.meta.is_aigc`; `TikTokPublisher.php`,
setzt `$postInfo['is_aigc']`; auch im MCP `CreatePostTool.php`). Kein Patch noetig. Meta/
Instagram und YouTube haben kein aequivalentes API-Feld (nur Checklisten-Eintrag), das bleibt
eine offene Luecke, aber kein Fork-Patch-Kandidat solange die Plattformen selbst kein API-Feld
anbieten.

## Wie ein neuer Patch hier reinkommt

1. Aenderung im Fork machen, committen, Marker-String im Commit-Message + hier dokumentieren
(Datei, Zeile/Funktion, WARUM, welcher Marker-String das Wiedererkennen nach einem Merge
erlaubt).
2. `web02`-Deploy: `/opt/trypost` laeuft als lokaler Build aus Git-Checkout (Update-Klasse C,
siehe `~/.claude/CLAUDE.md` Docker-Tabelle), NICHT das published Image. Seit Patch 1
(25.08.2026) laeuft der Server-Checkout gegen `Cryptoom/trypost` (Fork), nicht mehr gegen
upstream.
3. Nach jedem `git merge upstream/main`: alle Marker-Strings unten gegenpruefen, dieser
Abschnitt fasst dann "Stand nach Merge <datum>" analog zum whatsapp-mcp-Muster.
2 changes: 2 additions & 0 deletions app/Mcp/Servers/TryPostServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
use App\Mcp\Tools\SocialAccount\ListSocialAccountsTool;
use App\Mcp\Tools\SocialAccount\ToggleSocialAccountTool;
use App\Mcp\Tools\Workspace\GetWorkspaceTool;
use App\Mcp\Tools\Workspace\UpdateWorkspaceTool;
use Laravel\Mcp\Server;
use Laravel\Mcp\Server\Attributes\Icon;
use Laravel\Mcp\Server\Attributes\Instructions;
Expand Down Expand Up @@ -91,6 +92,7 @@ class TryPostServer extends Server

// Workspace
GetWorkspaceTool::class,
UpdateWorkspaceTool::class,

// API Keys
ListApiKeysTool::class,
Expand Down
88 changes: 88 additions & 0 deletions app/Mcp/Tools/Workspace/UpdateWorkspaceTool.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

declare(strict_types=1);

namespace App\Mcp\Tools\Workspace;

use App\Enums\Workspace\BrandFont;
use App\Enums\Workspace\BrandVoiceTrait;
use App\Enums\Workspace\ContentLanguage;
use App\Enums\Workspace\ImageStyle;
use App\Http\Resources\Api\WorkspaceResource;
use App\Mcp\Concerns\AuthorizesMcpTool;
use App\Models\Workspace;
use Illuminate\Contracts\JsonSchema\JsonSchema;
use Illuminate\Validation\Rule;
use Laravel\Mcp\Request;
use Laravel\Mcp\Response;
use Laravel\Mcp\ResponseFactory;
use Laravel\Mcp\Server\Attributes\Description;
use Laravel\Mcp\Server\Tool;

#[Description('Update the current workspace brand settings (name, website, description, voice traits, colors, font, image style, content language). All fields are optional, only the fields you pass are changed.')]
class UpdateWorkspaceTool extends Tool
{
use AuthorizesMcpTool;

public function handle(Request $request): Response|ResponseFactory
{
$workspace = $this->authorizeCurrentWorkspace(
$request,
'update',
'Not authorized to update this workspace.',
);

if (! $workspace instanceof Workspace) {
return $workspace;
}

$hex = ['nullable', 'string', 'regex:/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/'];

$validated = $request->validate([
'name' => ['sometimes', 'required', 'string', 'max:255'],
'brand_website' => ['sometimes', 'nullable', 'url', 'max:255'],
'brand_description' => ['sometimes', 'nullable', 'string', 'max:2000'],
'brand_voice_traits' => ['sometimes', 'nullable', 'array'],
'brand_voice_traits.*' => ['string', Rule::enum(BrandVoiceTrait::class)],
'brand_color' => ['sometimes', ...$hex],
'background_color' => ['sometimes', ...$hex],
'text_color' => ['sometimes', ...$hex],
'brand_font' => ['sometimes', 'required', 'string', Rule::in(BrandFont::values())],
'image_style' => ['sometimes', 'required', 'string', Rule::in(ImageStyle::values())],
'content_language' => ['sometimes', 'string', Rule::in(ContentLanguage::values())],
]);

if (array_key_exists('brand_voice_traits', $validated) && $validated['brand_voice_traits'] !== null) {
$validated['brand_voice_traits'] = BrandVoiceTrait::coerce($validated['brand_voice_traits']);
}

// PATCH semantics: only the fillable fields the caller actually passed
// are written. Mirrors UpdateWorkspaceRequest's field set minus
// logo_url (logo upload has its own dedicated flow, not brand settings).
$payload = array_intersect_key($validated, array_flip($workspace->getFillable()));

if ($payload !== []) {
$workspace->update($payload);
}

return Response::structured((new WorkspaceResource($workspace))->resolve());
}

public function schema(JsonSchema $schema): array
{
return [
'name' => $schema->string()->description('Workspace display name.'),
'brand_website' => $schema->string()->description('Brand website URL.'),
'brand_description' => $schema->string()->description('Brand description, up to 2000 characters.'),
'brand_voice_traits' => $schema->array()
->items($schema->string()->enum(BrandVoiceTrait::values()))
->description('Brand voice traits (replaces the existing set). See the grouped trait list for single-select spectrum groups vs additive style traits.'),
'brand_color' => $schema->string()->description('Hex color code (e.g. #FF5733).'),
'background_color' => $schema->string()->description('Hex color code (e.g. #FFFFFF).'),
'text_color' => $schema->string()->description('Hex color code (e.g. #111111).'),
'brand_font' => $schema->string()->enum(BrandFont::values())->description('Brand font family name.'),
'image_style' => $schema->string()->enum(ImageStyle::values())->description('AI image generation style.'),
'content_language' => $schema->string()->enum(ContentLanguage::values())->description('Content language code (e.g. "en", "de").'),
];
}
}
96 changes: 48 additions & 48 deletions resources/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,65 +16,65 @@


:root {
/* TryPost design system — warm cream + ink + signature violet.
Mirrors the marketing site (~/Herd/trypost-site) so the app and the
site share one identity. All structural borders are ink-black. */
--background: #faf8f5;
--foreground: #0a0a0a;
/* madevisible.io brand tokens (Mediterranean Light / Premium), see
digital-mind-agency/DESIGN.md. Replaces the upstream Gumroad-style
warm cream + violet identity for this concept branch. */
--background: #fbf7f1;
--foreground: #0a2540;
--card: #ffffff;
--card-foreground: #0a0a0a;
--card-foreground: #0a2540;
--popover: #ffffff;
--popover-foreground: #0a0a0a;
--primary: #7c3aed;
--popover-foreground: #0a2540;
--primary: #0c6e6d;
--primary-foreground: #ffffff;
--secondary: #f4f4f0;
--secondary-foreground: #0a0a0a;
--muted: #f4f4f0;
--muted-foreground: #52525b;
--accent: #ede9fe;
--accent-foreground: #5b21b6;
--destructive: #e54b4f;
--secondary: #f4ece0;
--secondary-foreground: #0a2540;
--muted: #f4ece0;
--muted-foreground: #475c73;
--accent: #c99a5c;
--accent-foreground: #0a2540;
--destructive: #b8462f;
--destructive-foreground: #ffffff;
--success: #16a34a;
--error: #e54b4f;
--warning: #d97706;
--info: #2563eb;
--border: #0a0a0a;
--input: #0a0a0a;
--ring: #7c3aed;
--chart-1: #7c3aed;
--chart-2: #8b5cf6;
--chart-3: #a78bfa;
--chart-4: #5b21b6;
--chart-5: #4c1d95;
--success: #0f8a88;
--error: #b8462f;
--warning: #c99a5c;
--info: #0c6e6d;
--border: #0a2540;
--input: #0a2540;
--ring: #0c6e6d;
--chart-1: #0c6e6d;
--chart-2: #0f8a88;
--chart-3: #a67c3f;
--chart-4: #084e4d;
--chart-5: #806026;
--sidebar: #ffffff;
--sidebar-foreground: #0a0a0a;
--sidebar-primary: #7c3aed;
--sidebar-foreground: #0a2540;
--sidebar-primary: #0c6e6d;
--sidebar-primary-foreground: #ffffff;
--sidebar-accent: #ede9fe;
--sidebar-accent-foreground: #5b21b6;
--sidebar-border: #0a0a0a;
--sidebar-ring: #7c3aed;
--font-sans: Figtree, ui-sans-serif, system-ui, -apple-system,
--sidebar-accent: #c99a5c;
--sidebar-accent-foreground: #0a2540;
--sidebar-border: #0a2540;
--sidebar-ring: #0c6e6d;
--font-sans: Geist, ui-sans-serif, system-ui, -apple-system,
BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
--font-serif: 'Instrument Serif', ui-serif, Georgia, Cambria,
'Times New Roman', Times, serif;
--font-display: 'Instrument Serif', ui-serif, Georgia, Cambria,
'Times New Roman', Times, serif;
--font-serif: Geist, ui-sans-serif, system-ui, -apple-system,
'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-display: Geist, ui-sans-serif, system-ui, -apple-system,
'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco,
Consolas, 'Liberation Mono', 'Courier New', monospace;
--radius: 0.75rem;
/* Gumroad-style offset shadows: solid ink, no blur. */
--shadow-2xs: 1px 1px 0 0 #0a0a0a;
--shadow-xs: 2px 2px 0 0 #0a0a0a;
--shadow-sm: 3px 3px 0 0 #0a0a0a;
--shadow: 4px 4px 0 0 #0a0a0a;
--shadow-md: 5px 5px 0 0 #0a0a0a;
--shadow-lg: 6px 6px 0 0 #0a0a0a;
--shadow-xl: 8px 8px 0 0 #0a0a0a;
--shadow-2xl: 10px 10px 0 0 #0a0a0a;
--radius: 10px;
/* Soft tinted shadows instead of the upstream hard-ink offset shadows. */
--shadow-2xs: 0px 1px 2px 0 rgba(10, 37, 64, 0.05);
--shadow-xs: 0px 1px 3px 0 rgba(10, 37, 64, 0.06);
--shadow-sm: 0px 2px 4px 0 rgba(10, 37, 64, 0.07);
--shadow: 0px 4px 8px 0 rgba(10, 37, 64, 0.08);
--shadow-md: 0px 6px 12px 0 rgba(10, 37, 64, 0.08);
--shadow-lg: 0px 10px 20px 0 rgba(10, 37, 64, 0.09);
--shadow-xl: 0px 16px 32px 0 rgba(10, 37, 64, 0.1);
--shadow-2xl: 0px 24px 48px 0 rgba(10, 37, 64, 0.12);
--tracking-normal: 0em;
--spacing: 0.25rem;
}
Expand Down
12 changes: 0 additions & 12 deletions resources/js/components/AppSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@ import {
IconAffiliate,
IconAlertTriangle,
IconBolt,
IconBrandDiscord,
IconCalendar,
IconChartBar,
IconChevronRight,
IconClock,
IconFileCheck,
IconFileText,
IconGift,
IconHash,
IconLifebuoy,
IconPencil,
Expand Down Expand Up @@ -173,16 +171,6 @@ const workspaceNavItems = computed<NavItem[]>(() => [
]);

const bottomNavItems = computed(() => [
{
title: trans('sidebar.support.referral'),
href: 'https://affiliates.trypost.it/',
icon: IconGift,
},
{
title: trans('sidebar.support.discord'),
href: 'https://trypost.it/discord',
icon: IconBrandDiscord,
},
{
title: trans('sidebar.support.docs'),
href: 'https://docs.trypost.it',
Expand Down
Loading