Skip to content

Commit eb709e3

Browse files
author
root
committed
fix app detail
1 parent c769cc5 commit eb709e3

105 files changed

Lines changed: 3128 additions & 1291 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/domain/ai/chat/service.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ func (s *Service) ListMessages(ctx context.Context, sessionID, ownerID string) (
8888
return s.repo.ListMessages(ctx, sessionID)
8989
}
9090

91-
9291
func (s *Service) SendMessage(ctx context.Context, sessionID, ownerID, content string, attachments []MessageAttachment, onChunk func(string) error) (*Message, error) {
9392
content = strings.TrimSpace(content)
9493
attachments = normalizeAttachments(attachments)

backend/domain/apptemplates/service.go

Lines changed: 99 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,18 @@ type Manifest struct {
4747
}
4848

4949
type RenderSpec struct {
50-
Env map[string]string `json:"env"`
51-
ComposeValues map[string]any `json:"compose_values"`
52-
Exposure map[string]any `json:"exposure"`
53-
Files []any `json:"files"`
50+
Env map[string]string `json:"env"`
51+
ComposeValues map[string]any `json:"compose_values"`
52+
Exposures []TemplateExposure `json:"exposures"`
53+
Files []any `json:"files"`
54+
}
55+
56+
type TemplateExposure struct {
57+
Label string `json:"label"`
58+
Service string `json:"service"`
59+
Port int `json:"port"`
60+
Protocol string `json:"protocol"`
61+
Default bool `json:"default,omitempty"`
5462
}
5563

5664
type Source struct {
@@ -76,24 +84,25 @@ type RenderRequest struct {
7684
}
7785

7886
type RenderedTemplate struct {
79-
TemplateKey string
80-
ProjectName string
81-
Compose string
82-
ResolvedEnv map[string]any
83-
SecretRefs []string
84-
Metadata map[string]any
85-
ExposureIntent map[string]any
86-
RenderExposure map[string]any
87-
Manifest Manifest
87+
TemplateKey string
88+
ProjectName string
89+
Compose string
90+
ResolvedEnv map[string]any
91+
SecretRefs []string
92+
Metadata map[string]any
93+
ExposureIntent map[string]any
94+
RenderExposures []TemplateExposure
95+
Manifest Manifest
8896
}
8997

9098
type DescribeResponse struct {
91-
TemplateKey string `json:"templateKey"`
92-
Manifest Manifest `json:"manifest"`
93-
Inputs []Field `json:"inputs"`
94-
Source Source `json:"source"`
95-
Exposure map[string]any `json:"exposure,omitempty"`
96-
ComposeValues map[string]any `json:"composeValues,omitempty"`
99+
TemplateKey string `json:"templateKey"`
100+
Manifest Manifest `json:"manifest"`
101+
Inputs []Field `json:"inputs"`
102+
Source Source `json:"source"`
103+
Exposure map[string]any `json:"exposure,omitempty"`
104+
Exposures []TemplateExposure `json:"exposures,omitempty"`
105+
ComposeValues map[string]any `json:"composeValues,omitempty"`
97106
}
98107

99108
type Service struct {
@@ -166,18 +175,14 @@ func (s *Service) Render(app core.App, request RenderRequest) (*RenderedTemplate
166175
return nil, err
167176
}
168177
result := &RenderedTemplate{
169-
TemplateKey: request.TemplateKey,
170-
ProjectName: projectName,
171-
Compose: compose,
172-
ResolvedEnv: resolvedEnv,
173-
SecretRefs: secretRefs,
174-
RenderExposure: cloneAnyMap(tpl.Render.Exposure),
175-
Manifest: tpl.Manifest,
176-
ExposureIntent: map[string]any{
177-
"exposure_type": normalizedExposureType(tpl.Render.Exposure),
178-
"is_primary": true,
179-
"target_port": tpl.Render.Exposure["targetPort"],
180-
},
178+
TemplateKey: request.TemplateKey,
179+
ProjectName: projectName,
180+
Compose: compose,
181+
ResolvedEnv: resolvedEnv,
182+
SecretRefs: secretRefs,
183+
RenderExposures: cloneTemplateExposures(tpl.Render.Exposures),
184+
Manifest: tpl.Manifest,
185+
ExposureIntent: exposureIntentFromTemplateExposures(tpl.Render.Exposures),
181186
Metadata: map[string]any{
182187
"candidate_kind": "store-prefill",
183188
"prefill_context": map[string]any{
@@ -190,6 +195,7 @@ func (s *Service) Render(app core.App, request RenderRequest) (*RenderedTemplate
190195
"template_revision": tpl.Source.TemplateRevision,
191196
"origin_kind": tpl.Source.OriginKind,
192197
"origin_ref": tpl.Source.OriginRef,
198+
"exposures": templateExposuresToMaps(tpl.Render.Exposures),
193199
"input_values": values,
194200
"secret_refs": secretRefs,
195201
},
@@ -211,7 +217,8 @@ func (s *Service) Describe(templateKey string) (*DescribeResponse, error) {
211217
Manifest: tpl.Manifest,
212218
Inputs: append([]Field(nil), tpl.Inputs.Fields...),
213219
Source: tpl.Source,
214-
Exposure: cloneAnyMap(tpl.Render.Exposure),
220+
Exposure: legacyExposureFromTemplateExposures(tpl.Render.Exposures),
221+
Exposures: cloneTemplateExposures(tpl.Render.Exposures),
215222
ComposeValues: cloneAnyMap(tpl.Render.ComposeValues),
216223
}, nil
217224
}
@@ -331,22 +338,6 @@ func renderCompose(base string, resolvedEnv map[string]any) (string, error) {
331338
if err := yaml.Unmarshal([]byte(base), &node); err != nil {
332339
return "", fmt.Errorf("invalid compose yaml: %w", err)
333340
}
334-
visitAndReplace(&node, func(value string) (string, bool, error) {
335-
if !strings.Contains(value, "${") {
336-
return value, false, nil
337-
}
338-
result := placeholderPattern.ReplaceAllStringFunc(value, func(raw string) string {
339-
token := strings.TrimSuffix(strings.TrimPrefix(raw, "${"), "}")
340-
if envValue, ok := resolvedEnv[token]; ok {
341-
return stringify(envValue)
342-
}
343-
return raw
344-
})
345-
if strings.Contains(result, "${") {
346-
return "", false, fmt.Errorf("unresolved compose placeholder in %q", value)
347-
}
348-
return result, true, nil
349-
})
350341
if err := visitAndReplace(&node, func(value string) (string, bool, error) {
351342
if !strings.Contains(value, "${") {
352343
return value, false, nil
@@ -432,16 +423,67 @@ func requirementBytes(requirements map[string]any) int64 {
432423
return 0
433424
}
434425

435-
func normalizedExposureType(exposure map[string]any) string {
436-
kind := strings.TrimSpace(fmt.Sprint(exposure["kind"]))
437-
switch kind {
438-
case "http", "https", "tcp", "port":
439-
return "port"
440-
case "internal_only":
441-
return "internal_only"
442-
default:
443-
return kind
426+
func exposureIntentFromTemplateExposures(exposures []TemplateExposure) map[string]any {
427+
for _, exposure := range exposures {
428+
if exposure.Default || len(exposures) == 1 {
429+
if exposure.Port <= 0 {
430+
return nil
431+
}
432+
return map[string]any{
433+
"exposure_type": "port",
434+
"is_primary": true,
435+
"target_port": exposure.Port,
436+
}
437+
}
444438
}
439+
return nil
440+
}
441+
442+
func cloneTemplateExposures(input []TemplateExposure) []TemplateExposure {
443+
if len(input) == 0 {
444+
return nil
445+
}
446+
return append([]TemplateExposure(nil), input...)
447+
}
448+
449+
func templateExposuresToMaps(input []TemplateExposure) []map[string]any {
450+
if len(input) == 0 {
451+
return nil
452+
}
453+
result := make([]map[string]any, 0, len(input))
454+
for _, exposure := range input {
455+
item := map[string]any{
456+
"label": strings.TrimSpace(exposure.Label),
457+
"service": strings.TrimSpace(exposure.Service),
458+
"port": exposure.Port,
459+
"protocol": strings.TrimSpace(exposure.Protocol),
460+
}
461+
if exposure.Default {
462+
item["default"] = true
463+
}
464+
result = append(result, item)
465+
}
466+
return result
467+
}
468+
469+
func legacyExposureFromTemplateExposures(exposures []TemplateExposure) map[string]any {
470+
for _, exposure := range exposures {
471+
if exposure.Default || len(exposures) == 1 {
472+
if exposure.Port <= 0 {
473+
return nil
474+
}
475+
kind := strings.TrimSpace(exposure.Protocol)
476+
if kind == "" {
477+
kind = "http"
478+
}
479+
return map[string]any{
480+
"kind": kind,
481+
"service": strings.TrimSpace(exposure.Service),
482+
"targetPort": exposure.Port,
483+
}
484+
}
485+
}
486+
return nil
445487
}
446488

447489
func uniqueStrings(values []string) []string {

backend/domain/apptemplates/service_test.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ func TestDescribeFindsRuntimeTemplateUnderApposDataTemplatesApps(t *testing.T) {
1616
}
1717
write := func(path, content string) {
1818
t.Helper()
19-
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
19+
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
2020
t.Fatalf("write %s: %v", path, err)
2121
}
2222
}
2323
write(filepath.Join(templateDir, "manifest.json"), `{"key":"runtime-template-test","name":"Runtime Template Test","trademark":"Runtime Template Test","category":"Test","requirements":{},"serviceRoles":{}}`)
2424
write(filepath.Join(templateDir, "inputs.schema.json"), `{"fields":[{"key":"admin_email","type":"string","label":"Admin Email","required":true,"default":"admin@example.com","visibility":"basic","storage_mode":"plain"}]}`)
25-
write(filepath.Join(templateDir, "render.json"), `{"env":{"ADMIN_EMAIL":"${admin_email}"},"compose_values":{},"exposure":{},"files":[]}`)
25+
write(filepath.Join(templateDir, "render.json"), `{"env":{"ADMIN_EMAIL":"${admin_email}"},"compose_values":{},"exposures":[{"label":"Web","service":"app","port":80,"protocol":"http","default":true}],"files":[]}`)
2626
write(filepath.Join(templateDir, "source.json"), `{"template_revision":"test","origin_kind":"runtime","origin_ref":"unit-test"}`)
2727
write(filepath.Join(composeDir, "base.yml"), "services:\n app:\n image: nginx:alpine\n")
2828
t.Cleanup(func() {
@@ -39,4 +39,7 @@ func TestDescribeFindsRuntimeTemplateUnderApposDataTemplatesApps(t *testing.T) {
3939
if response.Manifest.Trademark != "Runtime Template Test" {
4040
t.Fatalf("expected trademark Runtime Template Test, got %q", response.Manifest.Trademark)
4141
}
42-
}
42+
if len(response.Exposures) != 1 || response.Exposures[0].Service != "app" || response.Exposures[0].Port != 80 {
43+
t.Fatalf("expected compact exposures in describe response, got %+v", response.Exposures)
44+
}
45+
}

backend/domain/catalog/admin.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,4 @@ func sourceFileStatus(dir, name string) AdminSourceFileStatus {
106106
status.SizeBytes = info.Size()
107107
status.ModifiedAt = info.ModTime().UTC().Format(time.RFC3339)
108108
return status
109-
}
109+
}

backend/domain/catalog/contracts.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -186,19 +186,19 @@ type PersonalizationListResponse struct {
186186
}
187187

188188
type CustomAppRecord struct {
189-
ID string `json:"id"`
190-
Key string `json:"key"`
191-
Trademark string `json:"trademark"`
192-
LogoURL *string `json:"logo_url"`
193-
Overview string `json:"overview"`
194-
Description *string `json:"description"`
195-
CategoryKeys []string `json:"category_keys"`
196-
ComposeYAML string `json:"compose_yaml"`
197-
EnvText *string `json:"env_text"`
198-
Visibility string `json:"visibility"`
199-
CreatedBy string `json:"created_by"`
200-
Created string `json:"created"`
201-
Updated string `json:"updated"`
189+
ID string `json:"id"`
190+
Key string `json:"key"`
191+
Trademark string `json:"trademark"`
192+
LogoURL *string `json:"logo_url"`
193+
Overview string `json:"overview"`
194+
Description *string `json:"description"`
195+
CategoryKeys []string `json:"category_keys"`
196+
ComposeYAML string `json:"compose_yaml"`
197+
EnvText *string `json:"env_text"`
198+
Visibility string `json:"visibility"`
199+
CreatedBy string `json:"created_by"`
200+
Created string `json:"created"`
201+
Updated string `json:"updated"`
202202
}
203203

204204
type CustomAppUpsert struct {

backend/domain/catalog/service.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,17 +138,17 @@ func (s *Service) AppDetail(app core.App, auth *core.Record, locale, key string)
138138
if custom, ok, err := loadVisibleCustomAppDetail(app, auth, key, secondaryToPrimaries, secondaryTitles, personalization); err != nil {
139139
return nil, err
140140
} else if ok {
141-
custom.Installed = installedSummary
142-
return custom, nil
141+
custom.Installed = installedSummary
142+
return custom, nil
143143
}
144144

145145
for _, product := range bundle.Products {
146146
if product.Key != key {
147147
continue
148148
}
149-
response := officialDetail(product, personalization[key], bundle.SourceVersion, locale)
150-
response.Installed = installedSummary
151-
return response, nil
149+
response := officialDetail(product, personalization[key], bundle.SourceVersion, locale)
150+
response.Installed = installedSummary
151+
return response, nil
152152
}
153153

154154
return nil, fmt.Errorf("catalog app not found")

backend/domain/catalog/source.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ func ensureCatalogSeedDir(dir string) error {
156156

157157
func writeCatalogSeedFile(path string, data []byte) error {
158158
tmpPath := path + ".tmp"
159-
if err := os.WriteFile(tmpPath, data, 0o644); err != nil {
159+
if err := os.WriteFile(tmpPath, data, 0o600); err != nil {
160160
return err
161161
}
162162
if err := os.Rename(tmpPath, path); err != nil {
@@ -166,11 +166,6 @@ func writeCatalogSeedFile(path string, data []byte) error {
166166
return nil
167167
}
168168

169-
func isReadableDir(path string) bool {
170-
info, err := os.Stat(path)
171-
return err == nil && info.IsDir()
172-
}
173-
174169
func catalogSeedFileExists(path string) bool {
175170
info, err := os.Stat(path)
176171
return err == nil && !info.IsDir()

backend/domain/catalog/source_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,4 @@ func TestLoadBundleBootstrapsEmbeddedSeedIntoConfiguredRuntimeDir(t *testing.T)
3737
t.Fatalf("expected seeded runtime file %s to be non-empty", name)
3838
}
3939
}
40-
}
40+
}

0 commit comments

Comments
 (0)