3 Commits

Author SHA1 Message Date
H1K0 b25085bcd7 feat(backend): set application_name on database connections
deploy / deploy (push) Successful in 1m38s
Tag pooled connections with application_name="tanabata-backend" via the
parsed pgxpool config, so the backend's sessions are identifiable in
pg_stat_activity and server logs. An application_name supplied in the
DSN (or PGAPPNAME) still takes precedence.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 10:51:37 +03:00
H1K0 cf6c312e04 fix(frontend): don't treat empty 2xx bodies as request failures
The API client only skipped JSON parsing for 204, so a success with an
empty body (e.g. 201 from POST /pools/:id/files) hit res.json() on an
empty stream, threw, and surfaced as "Failed to add to pool" even though
the add had committed. Read the body as text and parse only when present.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 10:43:23 +03:00
H1K0 da4ce37aff fix(frontend): use opaque background icons for PWA install
The manifest's largest, maskable icon (ms-icon-310x310) was transparent,
so installing the PWA produced a transparent app icon. Generate opaque
192/512 "any" + maskable icons from favicon-bg.png (solid #524B6B
background, maskable variants padded into the inner 80% safe zone) and
point the manifest at them instead of the transparent ms-icon entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 10:39:41 +03:00
7 changed files with 40 additions and 10 deletions
+20 -2
View File
@@ -11,12 +11,30 @@ import (
"tanabata/backend/internal/db"
)
// appName tags every connection as application_name, so the backend's sessions
// are identifiable in pg_stat_activity and server logs (and distinguishable from
// e.g. goose migrations or a psql shell).
const appName = "tanabata-backend"
// NewPool creates and validates a *pgxpool.Pool from the given connection URL.
// The pool is ready to use; the caller is responsible for closing it.
func NewPool(ctx context.Context, url string) (*pgxpool.Pool, error) {
pool, err := pgxpool.New(ctx, url)
cfg, err := pgxpool.ParseConfig(url)
if err != nil {
return nil, fmt.Errorf("pgxpool.New: %w", err)
return nil, fmt.Errorf("pgxpool.ParseConfig: %w", err)
}
// Set application_name unless the operator already specified one in the DSN
// (or via PGAPPNAME), so an explicit override still wins.
if cfg.ConnConfig.RuntimeParams == nil {
cfg.ConnConfig.RuntimeParams = map[string]string{}
}
if cfg.ConnConfig.RuntimeParams["application_name"] == "" {
cfg.ConnConfig.RuntimeParams["application_name"] = appName
}
pool, err := pgxpool.NewWithConfig(ctx, cfg)
if err != nil {
return nil, fmt.Errorf("pgxpool.NewWithConfig: %w", err)
}
if err := pool.Ping(ctx); err != nil {
pool.Close()
+6 -1
View File
@@ -128,8 +128,13 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
invalidateSectionCache(path, (init?.method ?? 'GET').toUpperCase());
// A success doesn't guarantee a JSON body: 204 never has one, and some 200/201
// responses (e.g. POST /pools/:id/files) complete with an empty body. Parsing
// those as JSON throws, so read the text first and only parse when present —
// otherwise an empty 201 would surface as a spurious "failed" error.
if (res.status === 204) return undefined as T;
return res.json();
const text = await res.text();
return (text ? JSON.parse(text) : undefined) as T;
}
/** Upload with XHR so we can track progress via onProgress(0100). */
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+14 -7
View File
@@ -35,21 +35,28 @@
"type": "image/png"
},
{
"src": "/images/android-icon-192x192.png",
"src": "/images/pwa-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "/images/apple-icon-180x180.png",
"sizes": "180x180",
"type": "image/png"
"src": "/images/pwa-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "/images/ms-icon-310x310.png",
"sizes": "310x310",
"src": "/images/pwa-maskable-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
"purpose": "maskable"
},
{
"src": "/images/pwa-maskable-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}