Internal Documentation internal
TalkIDE internal documentation

Implementovatelná UC pro fázi F3 — uživatel si vytváří a spravuje prostředí (kind=USER_CREATED, resource_mode=SHARED). Staví na Foundation F1 (UC-10010) a Billing read-path F2 (UC-10012).

  • Každé nové prostředí obdrží vlastní K8s namespace {tenant}-{env-slug} provisionovaný přes nový EnvironmentProvisioner wrapper (interface + K8s impl + Noop; vzor ADR-014/ADR-015).
  • Prostředí DEFAULT „TalkIDE” (deletable=false) nelze smazat — hardcoded invariant BE i FE.
  • PROJECT.environment_id (nový nullable FK) váže projekt na cílové prostředí nasazení. F1/F2 projekty bez volby = NULL → implicitně fallback na DEFAULT „TalkIDE”.
  • OD-3 (LOCKED): nové prostředí vždy resource_mode=SHARED. DEDICATED je gated post-alfa F5.
  • OD-2 (LOCKED): F3 se nedotýká žádného enforcement, suspendu ani scale-to-zero. To je F4.
  • todo-list.talkide.app (popelkam) a DEFAULT namespace tenant-popelkam jsou nedotčeny.
  • Related: ADR-026, UC-10010 F1, UC-10011 F3 scope, UC-10012 F2

Odchylky implementace / rozhodnutí

FE umístění Environments — přesun z ProfileScreen na Studio screen + /environments (ADR-026 rovnocennost).

Původní návrh v UC-10013 (první verze) umísťoval správu prostředí do ProfileScreen.vue jako záložku ?section=environments v nastavení profilu. Toto umístění nesedí s principem ADR-026, který definuje Environment jako first-class entitu rovnocennou Projectu, nikoliv jako nastavení uživatele.

Nové umístění (tato verze UC):

  • Studio screen (/studio) — blok „Environments” pod sekcí projektů, vizuálně menší kompaktní karty (EnvironmentCard), odkaz „Zobrazit vše (N)” → /environments.
  • Samostatná stránka /environments (EnvironmentsScreen.vue) — plnohodnotná správa (seznam, Create, Delete). Přebírá funkcionalitu původní EnvironmentsSection.vue.
  • ProfileScreen.vue — záložka environments a položka v sidebar nav se odstraňuje.

Odchylka je záměrná, zdůvodněná ADR-026 a FE architekturou (vzor: Projects má vlastní /projects route i blok na Studio screen — Environments kopíruje stejný vzor).

Lazy-vs-eager provisioning tenze (ADR-026 §4 vs. UC-10011 F3 scope).

ADR-026 §4 (ř. 197–199) říká: „Provisioning je lazy (vzniká při prvním nasazení projektu do prostředí, NE při Create Environment)”.

UC-10011 F3 scope (acceptance kritérium 1) říká opačně: „User vytvoří prostředí TEST → vznikne namespace {tenant}-test — tedy eager provisioning při Create Environment.

Tato UC se přiklání k eager provisioning (provision AT Create) z následujících důvodů:

  • Acceptance kritérium F3 je explicitně formulováno jako eager a slouží jako testovací cíl před F4.
  • Eager provisioning dává uživateli okamžitou zpětnou vazbu — namespace buď vznikne a Create uspěje, nebo celá operace selže atomicky (@Transactional rollback).
  • Lazy provisioning v F3 by zkomplikoval chybové scénáře F4 (namespace-cut-over), kde je nutné mít namespace garantovaně připravený před nasazením projektu.
  • ADR-026 §4 byl pravděpodobně napsán s výhledem na DEDICATED prostředí (těžký provisioning) — pro SHARED namespace je eager řešením s nulovými náklady.

Odchylka od ADR-026 §4 je záměrná a omezená na SHARED USER_CREATED prostředí v F3. Pro DEDICATED (F5) platí původní lazy/async vzor z ADR-026 §4. ADR-026 může být aktualizován PR autorem F3 po implementaci.


Přehled deliverables F3

DeliverableEndpoint / komponentaStav
Create EnvironmentPOST /api/v1/environmentsNové
Get Environment detailGET /api/v1/environments/{id}Nové
List EnvironmentsGET /api/v1/environmentsRozšíření F1 (již existuje)
Delete EnvironmentDELETE /api/v1/environments/{id}Nové
EnvironmentProvisioner wrapperInterface + K8s impl + NoopNová infrakomponenta
projects.environment_id FKLiquibase 0040DB migrace
FE Environment managementStudio blok + /environments (EnvironmentsScreen)Nová FE stránka + Studio widget
Create Project — env selectorVolitelný environmentId v requestRozšíření

Datový model a changesety

Mermaid ER (delta F3 — dotčené entity a vazby)

erDiagram
    TENANT {
        bigint id
        string name
        string slug
        bigint owner_id
    }
    ENVIRONMENT {
        bigint id
        bigint tenant_id
        string kind
        string name
        string slug
        string resource_mode
        string status
        boolean deletable
        string namespace_ref
        jsonb config
        timestamp created_at
        timestamp updated_at
    }
    PROJECT {
        bigint id
        bigint tenant_id
        bigint environment_id
        string name
        string status
        string slug
    }
    TENANT ||--o{ ENVIRONMENT : has
    TENANT ||--o{ PROJECT : has
    PROJECT }o--o| ENVIRONMENT : deployed_to

projects.environment_id je nullableNULL znamená „nasadit do DEFAULT prostředí tenanta” (F1/F2 projekty bez explicitní volby). Explicitní FK na konkrétní environment(id) se nastavuje při Create Project (pokud user vybere prostředí) nebo může být NULL po smazání prostředí (fallback na DEFAULT).

Nové Liquibase changesety (F3)

SouborObsahRollback
0040-add-environment-id-to-projects.xmlALTER TABLE projects ADD COLUMN environment_id BIGINT NULL REFERENCES environment(id) ON DELETE SET NULL + indexALTER TABLE projects DROP COLUMN environment_id

Changeset 0040 je jediný DB changeset pro F3. ON DELETE SET NULL zajistí fallback na DEFAULT při smazání prostředí (viz Delete Environment flow).

<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
    xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
        http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.9.xsd">

    <changeSet id="0040-add-environment-id-to-projects" author="talkide">
        <addColumn tableName="projects">
            <column name="environment_id" type="BIGINT">
                <constraints nullable="true"
                    foreignKeyName="fk_projects_environment"
                    references="environment(id)"
                    deleteCascade="false"/>
            </column>
        </addColumn>

        <!-- ON DELETE SET NULL: při smazání prostředí se projekt fallbackuje na DEFAULT -->
        <sql>
            ALTER TABLE projects
            DROP CONSTRAINT IF EXISTS fk_projects_environment;

            ALTER TABLE projects
            ADD CONSTRAINT fk_projects_environment
            FOREIGN KEY (environment_id)
            REFERENCES environment(id)
            ON DELETE SET NULL;
        </sql>

        <!-- Index pro rychlé vyhledávání projektů v daném prostředí -->
        <createIndex tableName="projects" indexName="idx_projects_environment_id">
            <column name="environment_id"/>
        </createIndex>

        <rollback>
            <dropIndex tableName="projects" indexName="idx_projects_environment_id"/>
            <dropColumn tableName="projects" columnName="environment_id"/>
        </rollback>
    </changeSet>

</databaseChangeLog>

Registrace v db.changelog-master.xml (přidat za 0039-add-hosting-spending-limit-to-user-budget.xml):

<include file="changes/0040-add-environment-id-to-projects.xml" relativeToChangelogFile="true"/>

EnvironmentProvisioner — nový wrapper (ADR-014/ADR-015 vzor)

Existující NamespaceProvisioner je hardcoded na tenant-{slug} (metoda provisionTenantNamespace(tenantId: Long)). Pro F3 je potřeba provisionovat libovolný namespace {tenant}-{env-slug}. Proto vzniká nový wrapper EnvironmentProvisioner s jiným podpisem.

Interface

package com.mddsummer.talkide.features.environment.infra

/**
 * ADR-026 F3 — provisioning K8s namespace pro user-created prostředí.
 *
 * Nový wrapper separátní od [NamespaceProvisioner] (ten je hardcoded na
 * `tenant-{slug}` a slouží Create Project). EnvironmentProvisioner provisionuje
 * libovolný `namespaceName` předaný volajícím.
 *
 * Implementace:
 * - [K8sEnvironmentProvisioner] — produkční impl (talkide.k8s.enabled=true)
 * - [NoopEnvironmentProvisioner] — dev/test fallback
 *
 * Vzor (ADR-014/ADR-015): idempotentní get-or-create, failure → výjimka →
 * @Transactional rollback na use-case úrovni.
 */
interface EnvironmentProvisioner {

    /**
     * Provisionuje K8s namespace [namespaceName] pro prostředí. Idempotentní.
     * Vytvoří namespace + ResourceQuota + LimitRange (sdílené kvóty).
     * Failure → výjimka → rollback DB insertů v @Transactional.
     *
     * @param namespaceName Cílový namespace, např. `popelkam-stage1`
     * @param tenantId Tenant ID pro labels
     * @param environmentId Environment ID pro labels
     * @throws io.fabric8.kubernetes.client.KubernetesClientException při K8s API chybě
     */
    fun provisionEnvironmentNamespace(namespaceName: String, tenantId: Long, environmentId: Long)

    /**
     * Deprovisionuje namespace — smaže namespace a všechny K8s objekty v něm.
     * Volá se z DeleteEnvironmentUseCase. Failure → TeardownFailedException (catch v use-case).
     *
     * @param namespaceName Namespace ke smazání
     */
    fun deprovisionEnvironmentNamespace(namespaceName: String)
}

Noop implementace

@Component
@ConditionalOnProperty(name = ["talkide.k8s.enabled"], havingValue = "false", matchIfMissing = true)
class NoopEnvironmentProvisioner : EnvironmentProvisioner {
    private val log = LoggerFactory.getLogger(javaClass)

    override fun provisionEnvironmentNamespace(namespaceName: String, tenantId: Long, environmentId: Long) {
        log.info("NoopEnvironmentProvisioner.provisionEnvironmentNamespace({}) — K8s disabled", namespaceName)
    }

    override fun deprovisionEnvironmentNamespace(namespaceName: String) {
        log.info("NoopEnvironmentProvisioner.deprovisionEnvironmentNamespace({}) — K8s disabled", namespaceName)
    }
}

K8s implementace (klíčové body)

@Component
@ConditionalOnProperty(name = ["talkide.k8s.enabled"], havingValue = "true")
class K8sEnvironmentProvisioner(
    private val client: KubernetesClient,
    private val properties: TalkideProperties,
) : EnvironmentProvisioner {

    override fun provisionEnvironmentNamespace(namespaceName: String, tenantId: Long, environmentId: Long) {
        // 1. Namespace (idempotentní get-or-create)
        ensureNamespace(namespaceName, tenantId, environmentId)
        // 2. ResourceQuota
        ensureResourceQuota(namespaceName)
        // 3. LimitRange
        ensureLimitRange(namespaceName)
        // Poznámka: registry secrets, TLS secret a workspace PVC
        // se provisionují lazy při prvním Publish do tohoto prostředí (F4 scope).
        // Pro F3 stačí namespace + kvóty = prostředí je použitelné pro billing attribution.
    }

    override fun deprovisionEnvironmentNamespace(namespaceName: String) {
        val existing = client.namespaces().withName(namespaceName).get()
        if (existing == null) {
            log.warn("deprovisionEnvironmentNamespace: namespace '{}' not found — treating as already gone", namespaceName)
            return
        }
        client.namespaces().withName(namespaceName).delete()
        log.info("Deleted K8s namespace '{}'", namespaceName)
    }

    // ensureNamespace, ensureResourceQuota, ensureLimitRange — stejný vzor
    // jako K8sNamespaceProvisioner (viz reálný kód), ale s label
    // talkide.app/environment-id = environmentId.toString()
}

API kontrakty

1. Create Environment — POST /api/v1/environments

sequenceDiagram
    actor User
    participant FE
    participant BE as BE (CreateEnvironmentUseCase)
    participant EP as EnvironmentProvisioner
    participant DB

    User->>+FE: vyplní formulář (name, slug)
    FE->>FE: validace slug RFC-1123, délka, reserved list
    alt form nevalidní
        FE-->>User: zobraz chybové hlášky
    end
    User->>+FE: odešle formulář

    FE->>+BE: POST /api/v1/environments <br> CreateEnvironmentRequest

    BE->>BE: validate request (slug RFC-1123, ≤20 znaků, not reserved)
    alt slug neplatný nebo reserved
        BE-->>FE: 400 Bad Request <br> ErrorResponse (VALIDATION_ERROR)
    end

    BE->>+DB: SELECT 1 FROM environment WHERE tenant_id=? AND slug=?
    alt slug již existuje v rámci tenanta
        DB-->>BE: exists
        BE-->>FE: 409 Conflict <br> ErrorResponse (CONFLICT_ENVIRONMENT_SLUG)
    end
    DB-->>-BE: not found

    BE->>+DB: INSERT INTO environment (tenant_id, kind, name, slug, resource_mode, status, deletable, namespace_ref) <br> VALUES (?, 'USER_CREATED', ?, ?, 'SHARED', 'ACTIVE', true, null)
    DB-->>-BE: environment (id)

    BE->>+EP: provisionEnvironmentNamespace("{tenant}-{env-slug}", tenantId, envId)
    alt provisioning selže (K8s API error)
        EP-->>BE: KubernetesClientException
        BE->>DB: @Transactional ROLLBACK (environment INSERT zrušen)
        BE-->>FE: 503 Service Unavailable <br> ErrorResponse (ENVIRONMENT_PROVISIONING_FAILED)
    end
    EP-->>-BE: OK

    BE->>+DB: UPDATE environment SET namespace_ref='{tenant}-{env-slug}', updated_at=NOW() WHERE id=?
    DB-->>-BE: OK

    BE-->>-FE: 201 Created <br> EnvironmentDto
    FE-->>-User: prostředí vytvořeno, přesměrování na detail

POST /api/v1/environments CreateEnvironmentRequest:

{
  "name": "Stage 1",
  "slug": "stage1"
}

201 Created EnvironmentDto:

{
  "id": 2,
  "tenantId": 42,
  "kind": "USER_CREATED",
  "name": "Stage 1",
  "slug": "stage1",
  "resourceMode": "SHARED",
  "status": "ACTIVE",
  "deletable": true,
  "namespaceRef": "popelkam-stage1",
  "createdAt": "2026-05-19T10:00:00",
  "updatedAt": "2026-05-19T10:00:00"
}

400 Bad Request (validace slug) ErrorResponse:

{
  "code": "VALIDATION_ERROR",
  "message": "Bad request"
}

409 Conflict (slug již existuje v rámci tenanta) ErrorResponse:

{
  "code": "CONFLICT_ENVIRONMENT_SLUG",
  "message": "Environment with this slug already exists"
}

401 Unauthorized ErrorResponse:

{
  "code": "AUTHENTICATION_FAILED",
  "message": "Authentication required"
}

503 Service Unavailable (provisioning K8s namespace selhal) ErrorResponse:

{
  "code": "ENVIRONMENT_PROVISIONING_FAILED",
  "message": "Failed to provision environment namespace"
}

2. Get Environment detail — GET /api/v1/environments/{id}

sequenceDiagram
    actor User
    participant FE
    participant BE as BE (GetEnvironmentUseCase)
    participant DB

    User->>+FE: zobrazí detail prostředí

    FE->>+BE: GET /api/v1/environments/{id}

    BE->>BE: getCurrentTenantId() z JWT
    BE->>+DB: SELECT * FROM environment WHERE id=?
    alt prostředí nenalezeno
        DB-->>BE: 0 rows
        BE-->>FE: 404 Not Found <br> ErrorResponse (NOT_FOUND)
    end
    DB-->>-BE: environment row

    BE->>BE: ověří environment.tenantId == currentTenantId
    alt jiný tenant
        BE-->>FE: 403 Forbidden <br> ErrorResponse (FORBIDDEN)
    end

    BE-->>-FE: 200 OK <br> EnvironmentDto
    FE-->>-User: zobrazí detail

GET /api/v1/environments/{id}

200 OK EnvironmentDto:

{
  "id": 2,
  "tenantId": 42,
  "kind": "USER_CREATED",
  "name": "Stage 1",
  "slug": "stage1",
  "resourceMode": "SHARED",
  "status": "ACTIVE",
  "deletable": true,
  "namespaceRef": "popelkam-stage1",
  "createdAt": "2026-05-19T10:00:00",
  "updatedAt": "2026-05-19T10:00:00"
}

404 Not Found ErrorResponse:

{
  "code": "NOT_FOUND",
  "message": "Environment not found"
}

403 Forbidden ErrorResponse:

{
  "code": "FORBIDDEN",
  "message": "Access denied"
}

401 Unauthorized ErrorResponse:

{
  "code": "AUTHENTICATION_FAILED",
  "message": "Authentication required"
}

3. List Environments — GET /api/v1/environments

Rozšíření existujícího F1 endpointu — v F3 může vrátit N prostředí (DEFAULT + user-created).

GET /api/v1/environments

200 OK EnvironmentListResponse:

{
  "environments": [
    {
      "id": 1,
      "tenantId": 42,
      "kind": "DEFAULT",
      "name": "TalkIDE",
      "slug": "talkide",
      "resourceMode": "SHARED",
      "status": "ACTIVE",
      "deletable": false,
      "namespaceRef": "tenant-popelkam",
      "createdAt": "2026-05-19T09:00:00",
      "updatedAt": "2026-05-19T09:00:00"
    },
    {
      "id": 2,
      "tenantId": 42,
      "kind": "USER_CREATED",
      "name": "Stage 1",
      "slug": "stage1",
      "resourceMode": "SHARED",
      "status": "ACTIVE",
      "deletable": true,
      "namespaceRef": "popelkam-stage1",
      "createdAt": "2026-05-19T10:00:00",
      "updatedAt": "2026-05-19T10:00:00"
    }
  ]
}

401 Unauthorized ErrorResponse:

{
  "code": "AUTHENTICATION_FAILED",
  "message": "Authentication required"
}

4. Delete Environment — DELETE /api/v1/environments/{id}

sequenceDiagram
    actor User
    participant FE
    participant BE as BE (DeleteEnvironmentUseCase)
    participant EP as EnvironmentProvisioner
    participant DB

    User->>+FE: klikne Delete na prostředí

    FE->>FE: ověří deletable==true (guard na FE)
    alt deletable==false
        FE-->>User: Delete button disabled / chybová hláška
    end

    FE->>+BE: DELETE /api/v1/environments/{id}

    BE->>BE: getCurrentTenantId() z JWT
    BE->>+DB: SELECT * FROM environment WHERE id=?
    alt nenalezeno
        DB-->>BE: 0 rows
        BE-->>FE: 404 Not Found <br> ErrorResponse (NOT_FOUND)
    end
    DB-->>-BE: environment row

    BE->>BE: ověří tenantId ownership
    alt jiný tenant
        BE-->>FE: 403 Forbidden
    end

    BE->>BE: ověří environment.deletable
    alt deletable==false (DEFAULT „TalkIDE")
        BE-->>FE: 409 Conflict <br> ErrorResponse (CONFLICT_ENVIRONMENT_NOT_DELETABLE)
    end

    BE->>+DB: UPDATE environment SET status='DEPROVISIONING', updated_at=NOW() WHERE id=?
    DB-->>-BE: OK

    BE->>+EP: deprovisionEnvironmentNamespace(environment.namespaceRef)
    alt deprovision selže
        EP-->>BE: exception
        Note over BE: TeardownFailedException — namespace možná ve špatném stavu
        BE->>DB: UPDATE environment SET status='ACTIVE' (rollback status)
        BE-->>FE: 503 Service Unavailable <br> ErrorResponse (ENVIRONMENT_DEPROVISION_FAILED)
    end
    EP-->>-BE: OK

    Note over DB: ON DELETE SET NULL na fk_projects_environment<br/>= projects.environment_id → NULL (automaticky DB)
    BE->>+DB: DELETE FROM environment WHERE id=?
    DB-->>-BE: OK

    BE-->>-FE: 204 No Content
    FE-->>-User: prostředí smazáno, seznam refreshnutý

DELETE /api/v1/environments/{id}

204 No Content (úspěch)

409 Conflict (DEFAULT prostředí — nelze smazat) ErrorResponse:

{
  "code": "CONFLICT_ENVIRONMENT_NOT_DELETABLE",
  "message": "This environment cannot be deleted"
}

404 Not Found ErrorResponse:

{
  "code": "NOT_FOUND",
  "message": "Environment not found"
}

403 Forbidden ErrorResponse:

{
  "code": "FORBIDDEN",
  "message": "Access denied"
}

401 Unauthorized ErrorResponse:

{
  "code": "AUTHENTICATION_FAILED",
  "message": "Authentication required"
}

503 Service Unavailable (deprovision namespace selhal) ErrorResponse:

{
  "code": "ENVIRONMENT_DEPROVISION_FAILED",
  "message": "Failed to deprovision environment namespace"
}

5. Create Project — volitelný env selector

Existující POST /api/v1/projects rozšiřuje request o volitelný environmentId. Pokud není uveden nebo je null, projekt se váže na DEFAULT prostředí tenanta (get-or-create, stejný vzor jako F1).

POST /api/v1/projects CreateProjectRequest (delta F3):

{
  "name": "My App",
  "description": "Optional description",
  "environmentId": 2
}

Pole environmentId je volitelné (null = DEFAULT). Pokud je uvedeno, BE ověří:

  1. Prostředí existuje.
  2. Prostředí patří stejnému tenantovi.
  3. Prostředí má status=ACTIVE.

Při nesplnění bodů 1–2 (prostředí neexistuje nebo patří jinému tenantovi) → 400 VALIDATION_ERROR. Při nesplnění bodu 3 (prostředí existuje, patří tenantovi, ale není ACTIVE) → 409 CONFLICT_ENVIRONMENT_NOT_ACTIVE.


Frontend

Umístění Environments v UI (ADR-026 first-class vzor)

Environments kopírují stejný FE vzor jako Projects: blok na Studio screen + samostatná stránka. Toto odpovídá reálné struktuře FE (src/screens/).

1. Studio screen — blok „Environments” pod sekcí projektů

Komponenta/soubor: src/screens/studio/StudioScreen.vue

Pod stávající sekcí „Your Projects” (grid ProjectCard komponent) přibude nový blok se stejnou vizuální strukturou jako sekce Issues widget nebo Projects header:

// Vzor převzatý z existujícího StudioScreen.vue:
<div class="flex items-baseline justify-between mb-4">
  <h2 class="text-[13px] font-mono uppercase font-medium text-[var(--fg-3)] m-0" style="letter-spacing: 0.08em;">
    Environments
  </h2>
  <button ... @click="router.push({ name: 'environments' })">
    Zobrazit vše (N)  <ArrowRight :size="13" />
  </button>
</div>
  • Zobrazuje max. 3 prostředí (stejný limit jako ProjectCard grid).
  • Karty prostředí jsou vizuálně kompaktnější než ProjectCard — bez thumbnail, menší padding (vzor px-4 py-3 jako řádky v EnvironmentsSection.vue), stejný design jazyk (rounded-[var(--r-lg)] bg-[var(--bg-2)] border border-[var(--line-1)]).
  • Doporučená komponenta: src/screens/studio/components/EnvironmentCard.vue (nová, lehká varianta pro Studio blok).
  • Odkaz „Zobrazit vše (N)” používá router.push({ name: 'environments' }) a zobrazuje celkový count (environmentStore.total). Renderuje se vždy (i pro 0 prostředí, kde N = 0 — user vždy vidí cestu do správy).
  • onMounted v StudioScreen.vue přidá environmentStore.fetchEnvironments() (fire-and-forget, stejný vzor jako projectStore.fetchProjects).

2. Samostatná stránka /environmentsEnvironmentsScreen.vue

Route: /environments, name: environments, meta: { requiresAuth: true }
Soubor: src/screens/environments/EnvironmentsScreen.vue (nový)

Přebírá a obaluje existující EnvironmentsSection.vue (src/screens/environments/components/EnvironmentsSection.vue) — ta obsahuje kompletní logiku (list, Create formulář via CreateEnvironmentForm.vue, Delete dialog, store volání). EnvironmentsScreen.vue přidá jen layout wrapper s TTopBar (stejný vzor jako ProjectsScreen.vue).

Router entry (přidat do src/common/router/index.ts):

{
  path: '/environments',
  name: 'environments',
  component: () => import('@/screens/environments/EnvironmentsScreen.vue'),
  meta: { requiresAuth: true },
},

3. Odstranění z ProfileScreen — zrušení ?section=environments

Soubor: src/screens/profile/ProfileScreen.vue

  • Ze seznamu VALID_SECTIONS se odstraní 'environments'.
  • Z pole SECTIONS (computed) se odstraní položka { id: 'environments', label: ..., icon: Layers } a import Layers z lucide-vue-next.
  • Import EnvironmentsSection z @/screens/environments/components/EnvironmentsSection.vue se odstraní.
  • <EnvironmentsSection v-if="activeSection === 'environments'" /> se odstraní z template.
  • Environments již není nastavení — je to first-class entita (ADR-026).

Create Environment formulář

Komponenta: src/screens/environments/components/CreateEnvironmentForm.vue (existuje, beze změny)

PoleLabelTypPlaceholderPopis
nameNametext„Stage 1”, „QA”Zobrazovaný název prostředí
slugSlugtext„stage1”URL-friendly identifikátor (RFC-1123, nesmí být reserved — test/prod/talkide apod. jsou zakázané)

FE auto-generuje slug z name (lowercase, mezery → pomlčky). User může slug editovat manuálně.

FE Validations

FieldConstraintsSizePatternNote
namenot_blank1–100Zobrazovaný název
slugnot_blank1–20^[a-z0-9][a-z0-9-]*[a-z0-9]$ nebo jednoznakový [a-z0-9]RFC-1123 lowercase
slugnot reservedZobraz chybu pokud uživatel zadá talkide, www, api, prod, atd. (viz reserved list níže)

Reserved slug list (FE whitelist check — klient-side pre-validation): talkide, www, api, app, auth, admin, console, dashboard, mail, smtp, imap, mx, mx1, mx2, ftp, ssh, vpn, static, cdn, assets, media, images, files, downloads, docs, help, support, status, blog, news, about, terms, privacy, legal, test, dev, staging, prod, production, internal, beta, alpha, login, signup, register, logout


Backend

Validations — Create Environment

FieldConstraintsSizePatternNote
namenot_blank1–100
slugnot_blank1–20^[a-z0-9][a-z0-9-]*[a-z0-9]$\&#124;^[a-z0-9]$RFC-1123, lowercase, no trailing/leading dash
slugnot reservedViz EnvironmentSlugValidator.RESERVED_SLUGS
slugunique in tenantexistsByTenantIdAndSlug check; conflict → 409
{tenant}-{slug} délka≤63K8s namespace limit; viz invariant níže

Invariant délky namespace: {tenantSlug}-{envSlug}.length ≤ 63. Tenant slug má typicky 5–30 znaků; env slug max 20. Celková délka {tenant}-{env-slug} = len(tenantSlug) + 1 + len(envSlug). Pokud tenantSlug je dlouhý, BE musí odmítnout s validační chybou VALIDATION_ERROR (pole slug, zpráva: „Combined namespace name would exceed 63 characters”).

Kompletní reserved list (BE EnvironmentSlugValidator): talkide, www, api, app, auth, admin, console, dashboard, mail, smtp, imap, mx, mx1, mx2, ftp, ssh, vpn, static, cdn, assets, media, images, files, downloads, docs, help, support, status, blog, news, about, terms, privacy, legal, test, dev, staging, prod, production, internal, beta, alpha, login, signup, register, logout

Validations — Delete Environment

CheckZdrojChyba
Environment existujeDB lookup by id404 NOT_FOUND
Tenant ownershipenv.tenantId == currentTenantId403 FORBIDDEN
deletable == trueenv.deletable409 CONFLICT_ENVIRONMENT_NOT_DELETABLE

Klíčové backend komponenty F3

KomponentaPopis
EnvironmentProvisioner (interface)Nový wrapper — viz sekce výše
K8sEnvironmentProvisionerProd implementace (fabric8, @ConditionalOnProperty talkide.k8s.enabled=true)
NoopEnvironmentProvisionerDev/test fallback (matchIfMissing = true)
CreateEnvironmentUseCase@Transactional — INSERT + provisionEnvironmentNamespace + UPDATE namespace_ref; rollback při KubernetesClientException
GetEnvironmentUseCaseReadonly — lookup by id + tenant ownership check
DeleteEnvironmentUseCaseUPDATE status=DEPROVISIONING + deprovisionEnvironmentNamespace + DELETE; rollback status při deprovision failure
EnvironmentSlugValidatorCentralizovaný validátor — RFC-1123, reserved list, délkový limit
ProjectEntity.environmentIdNový nullable Long? sloupec (changeset 0040)

Nové ErrorCode enum hodnoty (implementátor MUSÍ přidat do com.mddsummer.talkide.common.exception.model.ErrorCode): reálný ErrorCode.kt tyto kódy zatím nemá — F3 je zavádí:

  • CONFLICT_ENVIRONMENT_SLUG (“Environment with this slug already exists”)
  • CONFLICT_ENVIRONMENT_NOT_DELETABLE (“This environment cannot be deleted”)
  • ENVIRONMENT_PROVISIONING_FAILED (“Failed to provision environment namespace”)
  • ENVIRONMENT_DEPROVISION_FAILED (“Failed to deprovision environment namespace”)
  • CONFLICT_ENVIRONMENT_NOT_ACTIVE (“Target environment is not active”)

Validační chyby env-slug (RFC-1123, reserved, délka, combined ≤63) používají existující VALIDATION_ERROR (ne nový kód).

Implementační poznámka — EnvironmentDto.updatedAt (kód má LocalDateTime?): EnvironmentEntity companion create() / Create flow nastavuje updated_at = created_at už při insertu (stejný vzor jako F1 createDefault()). Proto updatedAt v JSON kontraktech níže je vždy non-null (= createdAt při vytvoření, aktualizováno při každém UPDATE např. status change). JSON příklady tedy zůstávají platné a kontrakt čistý — updatedAt se nikdy nevrací null.

Slug validátor — klíčový kód

object EnvironmentSlugValidator {

    val RESERVED_SLUGS = setOf(
        "talkide", "www", "api", "app", "auth", "admin", "console", "dashboard",
        "mail", "smtp", "imap", "mx", "mx1", "mx2", "ftp", "ssh", "vpn",
        "static", "cdn", "assets", "media", "images", "files", "downloads",
        "docs", "help", "support", "status", "blog", "news", "about", "terms",
        "privacy", "legal", "test", "dev", "staging", "prod", "production",
        "internal", "beta", "alpha", "login", "signup", "register", "logout"
    )

    private val RFC1123_PATTERN = Regex("^[a-z0-9]([a-z0-9-]*[a-z0-9])?$")

    /**
     * Validates env-slug: RFC-1123, ≤20 chars, not reserved, combined ns ≤63.
     * @throws BadRequestException on validation failure
     */
    fun validate(slug: String, tenantSlug: String) {
        require(slug.length in 1..20) { "slug must be 1–20 characters" }
        require(RFC1123_PATTERN.matches(slug)) { "slug must match RFC-1123 (lowercase, alphanumeric, dashes)" }
        require(slug !in RESERVED_SLUGS) { "slug '$slug' is reserved" }
        val nsName = "$tenantSlug-$slug"
        require(nsName.length <= 63) { "Combined namespace name '$nsName' exceeds 63 characters (K8s limit)" }
    }
}

Test Cases

GIVENWHENTHEN
Autentizovaný tenant, slug „stage1” dostupnýPOST /api/v1/environments { name: "Stage 1", slug: "stage1" }201 Created; environment row v DB s kind=USER_CREATED, status=ACTIVE, namespace_ref=popelkam-stage1; namespace popelkam-stage1 provisionován
Autentizovaný tenant, slug „stage1” již existuje u téhož tenantaPOST /api/v1/environments { name: "Stage 1 dup", slug: "stage1" }409 CONFLICT_ENVIRONMENT_SLUG
Autentizovaný tenant, slug prod (reserved)POST /api/v1/environments { name: "PROD", slug: "prod" }400 VALIDATION_ERROR (slug reserved)
Autentizovaný tenant, slug talkide (reserved — default)POST /api/v1/environments { name: "X", slug: "talkide" }400 VALIDATION_ERROR (slug reserved)
Autentizovaný tenant, slug test-env-very-long-name-that-exceeds-limit (>20 znaků)POST /api/v1/environments { slug: "..." }400 VALIDATION_ERROR (slug too long)
Tenant se slugem 40+ znaků, env-slug 20 znaků (combined >63)POST /api/v1/environments { slug: "envslug20charslength" }400 VALIDATION_ERROR (combined ns name too long)
Slug s velkými písmeny TESTPOST /api/v1/environments { slug: "TEST" }400 VALIDATION_ERROR (not RFC-1123 lowercase)
Slug se začínající pomlčkou -testPOST /api/v1/environments { slug: "-test" }400 VALIDATION_ERROR (RFC-1123 violation)
K8s provisioner vrátí KubernetesClientExceptionPOST /api/v1/environments (K8s nedostupný)503 ENVIRONMENT_PROVISIONING_FAILED; žádný environment řádek v DB (rollback)
Autentizovaný tenantGET /api/v1/environments (má DEFAULT + 1 USER_CREATED)200 OK; seznam 2 prostředí
Autentizovaný tenantGET /api/v1/environments/{existingId}200 OK; EnvironmentDto
Autentizovaný tenant, id neexistujeGET /api/v1/environments/{nonExistentId}404 NOT_FOUND
Autentizovaný tenant, id patří jinému tenantoviGET /api/v1/environments/{otherTenantEnvId}403 FORBIDDEN
USER_CREATED prostředí s deletable=true, 2 projekty na tohoto prostředíDELETE /api/v1/environments/{id}204 No Content; namespace deprovisionován; environment smazán z DB; projects.environment_id na NULL (ON DELETE SET NULL)
DEFAULT prostředí „TalkIDE” (deletable=false)DELETE /api/v1/environments/{defaultId}409 CONFLICT_ENVIRONMENT_NOT_DELETABLE
USER_CREATED prostředí, deprovision selžeDELETE /api/v1/environments/{id} (K8s error)503 ENVIRONMENT_DEPROVISION_FAILED; environment status vrácen na ACTIVE; namespace stav nedefinovaný (ops cleanup potřeba)
Neautentizovaný požadavekJakýkoli GET/POST/DELETE /api/v1/environments401 AUTHENTICATION_FAILED
Create Project s platným environmentId (USER_CREATED, ACTIVE, same tenant)POST /api/v1/projects { environmentId: 2, ... }201 Created; projects.environment_id = 2
Create Project bez environmentId (null)POST /api/v1/projects { ... }201 Created; projects.environment_id = NULL (fallback DEFAULT)
Create Project s environmentId prostředí jiného tenantaPOST /api/v1/projects { environmentId: 99, ... }400 VALIDATION_ERROR (env-id neexistuje v tenant scope = validační chyba requestu)
GET /api/v1/environments — Noop provisioner (lokální dev)Create + List201 + 200 OK; namespace_ref nastaven; žádný K8s API call

Acceptance kritéria F3 (mapování na Test Cases)

Acceptance kritérium (z UC-10011)Pokryto test cases
1. User vytvoří prostředí „TEST” (SHARED) → vznikne namespace {tenant}-test s ResourceQuota; environment záznam s namespace_refTC: Create happy path, K8s provisioner
2. Nasazení projektu do „TEST” funguje (Publish target volba v UI)TC: Create Project s environmentId; FE env selector
3. Delete „TEST” → namespace deprovisionován; environment DEPROVISIONING → smazán; projekt bez deployment-targetu fallback na DEFAULTTC: Delete USER_CREATED + ON DELETE SET NULL
4. Delete DEFAULT „TalkIDE” → 409 CONFLICT_ENVIRONMENT_NOT_DELETABLETC: Delete DEFAULT
5. env-slug validace: rezervované slugy → 400/409TC: reserved slug, velikost písma, délka, unikátnost
6. todo-list.talkide.app a DEFAULT environment nedotčenyChangeset 0040 je additivní nullable FK; DEFAULT env a tenant-popelkam namespace beze změny
7. Faktura (F2) ukáže 2 řádky při aktivním TEST prostředíNení přímý test case v F3; F2 billing automaticky zachytí nový namespace (OpenCost aggregate=namespace)


FEEDBACK

Chyběla mi znalost reálného EnvironmentDto.createdAt/updatedAt typu — kód používá LocalDateTime (ne Instant), což jsem zjistil až z DTOs, ne z dokumentace; bylo by užitečné mít tento detail v F1 UC. Dále bych uvítal předpřipravený “change catalog” existujících changeset čísel přímo v zadání (nemusel bych ls changesety), nebo aspoň potvrzení nejvyššího čísla. Celkově by implementaci pomohla šablona EnvironmentProvisioner interface s jasným podpisem — tato design tenze (nový wrapper vs. rozšíření stávajícího) stála nejvíc analytické práce.

FE kontrakt update (tato revize): Reálná FE struktura odpovídala popisu — vzor Studio blok + samostatná route je přesně vzor, který FE již používá pro Projects i Issues. EnvironmentsSection.vue jako znovupoužitelná komponenta je čistý design (stačí ji obalit do EnvironmentsScreen.vue). Jediná věc, která chyběla v zadání: existence src/screens/environments/ adresáře s EnvironmentsSection.vue (bylo třeba přečíst FE kód, aby bylo jasné, že komponenta existuje a není třeba ji navrhovat od nuly).

Was this page helpful?

Thanks for the feedback.