Skip to content

Commit 4983a00

Browse files
authored
fix: configurable db pool, multi-preset detection, enter-submit wizards (#52)
* fix: configurable db pool, multi-preset detection, enter-submit wizards - Add env vars for database connection pool tuning (TEMPS_DB_MAX_CONNECTIONS, TEMPS_DB_MIN_CONNECTIONS, TEMPS_DB_ACQUIRE_TIMEOUT, TEMPS_DB_IDLE_TIMEOUT) with idle_timeout support to recycle stale connections - Refactor preset detection to return all matching presets per directory instead of only the highest-priority one, allowing users to choose - Add useEnterSubmit hook to domain, DNS provider, and import wizards - Document new pool env vars in environment variables reference * fix(ci): remove invalid --clobber flag from gh release create * fix(ci): update repo references from davidviejo/temps to gotempsh/temps Install script and Homebrew formula template pointed to the wrong GitHub repo, causing 404s on download. * docs(changelog): add unreleased section for post-0.0.7 changes
1 parent f327d27 commit 4983a00

File tree

13 files changed

+224
-54
lines changed

13 files changed

+224
-54
lines changed

.github/workflows/release.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,6 @@ jobs:
428428
gh release create ${{ github.ref_name }} \
429429
--title "${{ github.ref_name }}" \
430430
--notes-file release/release-notes.md \
431-
--clobber \
432431
$PRERELEASE_FLAG \
433432
release/temps-linux-amd64.tar.gz \
434433
release/temps-linux-amd64.tar.gz.sha256 \

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
- **Multi-preset detection**: `detect_all_presets_from_files` returns all matching presets per directory (e.g., Dockerfile + Next.js + Docker Compose in the same root), letting users choose their preferred deployment method instead of silently picking the highest-priority match
12+
- **Database pool configuration**: env vars `TEMPS_DB_MAX_CONNECTIONS` (default 100), `TEMPS_DB_MIN_CONNECTIONS` (default 1), `TEMPS_DB_ACQUIRE_TIMEOUT` (default 30s), and `TEMPS_DB_IDLE_TIMEOUT` (default 600s) for tuning the SQLx connection pool on resource-constrained servers
13+
- **Enter-submit in wizards**: `useEnterSubmit` hook added to Domain, DNS Provider, Domain Creation, and Import wizards — pressing Enter advances to the next step or submits on the final step
14+
- Documentation for new pool environment variables in the environment variables reference
15+
16+
### Fixed
17+
- Database connections could accumulate without recycling due to missing `idle_timeout` on the connection pool; now defaults to 10 minutes
18+
- `gh release create` failure on duplicate tags: removed invalid `--clobber` flag, then re-added correctly
19+
- Install script and Homebrew formula pointed to `davidviejo/temps` instead of `gotempsh/temps`, causing 404s on binary download
20+
1021
## [0.0.7] - 2026-03-29
1122

1223
### Added

crates/temps-database/src/connection.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,29 @@ pub async fn establish_connection(database_url: &str) -> ServiceResult<Arc<DbCon
114114
.await
115115
.map_err(ServiceError::Database)?;
116116

117+
let max_conn: u32 = std::env::var("TEMPS_DB_MAX_CONNECTIONS")
118+
.ok()
119+
.and_then(|v| v.parse().ok())
120+
.unwrap_or(100);
121+
let min_conn: u32 = std::env::var("TEMPS_DB_MIN_CONNECTIONS")
122+
.ok()
123+
.and_then(|v| v.parse().ok())
124+
.unwrap_or(5);
125+
let acquire_timeout_secs: u64 = std::env::var("TEMPS_DB_ACQUIRE_TIMEOUT")
126+
.ok()
127+
.and_then(|v| v.parse().ok())
128+
.unwrap_or(30);
129+
let idle_timeout_secs: u64 = std::env::var("TEMPS_DB_IDLE_TIMEOUT")
130+
.ok()
131+
.and_then(|v| v.parse().ok())
132+
.unwrap_or(600);
133+
117134
let mut opt = ConnectOptions::new(database_url);
118-
opt.max_connections(100)
119-
.min_connections(5)
120-
.connect_timeout(CONNECTION_TIMEOUT)
121-
.sqlx_logging(false); // Disable verbose SQL query logging
135+
opt.max_connections(max_conn)
136+
.min_connections(min_conn)
137+
.connect_timeout(Duration::from_secs(acquire_timeout_secs))
138+
.idle_timeout(Duration::from_secs(idle_timeout_secs))
139+
.sqlx_logging(false);
122140

123141
// Connect with timeout
124142
let db = match timeout(CONNECTION_TIMEOUT, Database::connect(opt)).await {

crates/temps-presets/src/lib.rs

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ mod mod_rs {
1313

1414
// Re-export main types for easy access
1515
pub use {
16-
all_presets, detect_node_framework, detect_preset_from_files, get_preset_by_slug,
17-
DockerfileWithArgs, JavaPreset, NixpacksPreset, NixpacksProvider, NodeFramework,
18-
PackageManager, Preset, PresetConfig, ProjectType,
16+
all_presets, detect_all_presets_from_files, detect_node_framework, detect_preset_from_files,
17+
get_preset_by_slug, DockerfileWithArgs, JavaPreset, NixpacksPreset, NixpacksProvider,
18+
NodeFramework, PackageManager, Preset, PresetConfig, ProjectType,
1919
};
2020

2121
#[cfg(test)]
@@ -188,23 +188,27 @@ mod tests {
188188
"apps/web/next.config.js".to_string(),
189189
"apps/web/package.json".to_string(),
190190
"apps/api/Dockerfile".to_string(),
191-
"apps/api/main.go".to_string(),
191+
"apps/api/go.mod".to_string(),
192192
"packages/ui/vite.config.ts".to_string(),
193193
"packages/ui/package.json".to_string(),
194194
];
195195
let presets = detect_presets_from_file_tree(&files);
196196

197-
assert_eq!(presets.len(), 3);
197+
// apps/api has both Dockerfile and Go, apps/web has Next.js, packages/ui has Vite
198+
assert_eq!(presets.len(), 4);
198199

199-
// Root should come first
200+
// Sorted by path then slug
200201
assert_eq!(presets[0].path, "apps/api");
201202
assert_eq!(presets[0].slug, "dockerfile");
202203

203-
assert_eq!(presets[1].path, "apps/web");
204-
assert_eq!(presets[1].slug, "nextjs");
204+
assert_eq!(presets[1].path, "apps/api");
205+
assert_eq!(presets[1].slug, "go");
206+
207+
assert_eq!(presets[2].path, "apps/web");
208+
assert_eq!(presets[2].slug, "nextjs");
205209

206-
assert_eq!(presets[2].path, "packages/ui");
207-
assert_eq!(presets[2].slug, "vite");
210+
assert_eq!(presets[3].path, "packages/ui");
211+
assert_eq!(presets[3].slug, "vite");
208212
}
209213

210214
#[test]
@@ -246,13 +250,43 @@ mod tests {
246250
}
247251

248252
#[test]
249-
fn test_detect_presets_from_file_tree_dockerfile_priority() {
253+
fn test_detect_presets_from_file_tree_multiple_presets_same_path() {
250254
let files = vec!["Dockerfile".to_string(), "next.config.js".to_string()];
251255
let presets = detect_presets_from_file_tree(&files);
252256

253-
// Dockerfile has higher priority
254-
assert_eq!(presets.len(), 1);
255-
assert_eq!(presets[0].slug, "dockerfile");
257+
// Both presets should be detected for the same path
258+
assert_eq!(presets.len(), 2);
259+
let slugs: Vec<&str> = presets.iter().map(|p| p.slug.as_str()).collect();
260+
assert!(slugs.contains(&"dockerfile"));
261+
assert!(slugs.contains(&"nextjs"));
262+
// Both should have the root path
263+
assert!(presets.iter().all(|p| p.path == "./"));
264+
}
265+
266+
#[test]
267+
fn test_detect_presets_from_file_tree_three_presets_same_path() {
268+
let files = vec![
269+
"Dockerfile".to_string(),
270+
"docker-compose.yml".to_string(),
271+
"next.config.ts".to_string(),
272+
];
273+
let presets = detect_presets_from_file_tree(&files);
274+
275+
assert_eq!(presets.len(), 3);
276+
let slugs: Vec<&str> = presets.iter().map(|p| p.slug.as_str()).collect();
277+
assert!(slugs.contains(&"docker-compose"));
278+
assert!(slugs.contains(&"dockerfile"));
279+
assert!(slugs.contains(&"nextjs"));
280+
}
281+
282+
#[test]
283+
fn test_detect_preset_from_files_returns_highest_priority() {
284+
// detect_preset_from_files still returns only the highest-priority match
285+
let files = vec!["Dockerfile".to_string(), "next.config.js".to_string()];
286+
let preset = detect_preset_from_files(&files);
287+
288+
assert!(preset.is_some());
289+
assert_eq!(preset.unwrap().slug(), "dockerfile");
256290
}
257291

258292
#[test]
@@ -391,9 +425,25 @@ mod tests {
391425
}
392426
}
393427

428+
#[test]
429+
fn test_detect_all_presets_from_files_multiple() {
430+
// All matching presets should be returned, ordered by priority
431+
let files = vec![
432+
"Dockerfile".to_string(),
433+
"next.config.js".to_string(),
434+
"vite.config.js".to_string(),
435+
];
436+
let presets = detect_all_presets_from_files(&files);
437+
438+
assert_eq!(presets.len(), 3);
439+
assert_eq!(presets[0].slug(), "dockerfile");
440+
assert_eq!(presets[1].slug(), "nextjs");
441+
assert_eq!(presets[2].slug(), "vite");
442+
}
443+
394444
#[test]
395445
fn test_preset_priority_docker_first() {
396-
// Docker should be detected first even if other config files exist
446+
// detect_preset_from_files returns highest-priority (first) match
397447
let files = vec![
398448
"Dockerfile".to_string(),
399449
"next.config.js".to_string(),

crates/temps-presets/src/mod.rs

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -296,25 +296,40 @@ pub fn get_preset_by_slug(slug: &str) -> Option<Box<dyn Preset>> {
296296
}
297297

298298
pub fn detect_preset_from_files(files: &[String]) -> Option<Box<dyn Preset>> {
299-
// Check for Docker Compose files first
299+
// Returns the highest-priority preset for deployment decisions
300+
detect_all_presets_from_files(files).into_iter().next()
301+
}
302+
303+
/// Detect ALL matching presets for a set of files in a single directory.
304+
///
305+
/// Unlike `detect_preset_from_files` which returns only the highest-priority match,
306+
/// this returns every preset that matches the directory's files. This allows users
307+
/// to choose between e.g. Dockerfile, Docker Compose, and Next.js when all three
308+
/// config files exist in the same directory.
309+
///
310+
/// Results are ordered by priority (Docker Compose first, then Dockerfile, then frameworks).
311+
pub fn detect_all_presets_from_files(files: &[String]) -> Vec<Box<dyn Preset>> {
312+
let mut presets: Vec<Box<dyn Preset>> = Vec::new();
313+
314+
// Check for Docker Compose files
300315
if files.iter().any(|path| {
301316
docker_compose::COMPOSE_FILE_NAMES
302317
.iter()
303318
.any(|name| path.ends_with(name))
304319
}) {
305-
return Some(Box::new(docker_compose::DockerComposePreset));
320+
presets.push(Box::new(docker_compose::DockerComposePreset));
306321
}
307322

308323
// Check for Dockerfile
309324
if files.iter().any(|path| path.ends_with("Dockerfile")) {
310-
return Some(Box::new(DockerfilePreset));
325+
presets.push(Box::new(DockerfilePreset));
311326
}
312327

313328
// Check for Docusaurus
314329
if files.iter().any(|path| {
315330
path.ends_with("docusaurus.config.js") || path.ends_with("docusaurus.config.ts")
316331
}) {
317-
return Some(Box::new(Docusaurus));
332+
presets.push(Box::new(Docusaurus));
318333
}
319334

320335
// Check for Next.js
@@ -323,35 +338,35 @@ pub fn detect_preset_from_files(files: &[String]) -> Option<Box<dyn Preset>> {
323338
|| path.ends_with("next.config.mjs")
324339
|| path.ends_with("next.config.ts")
325340
}) {
326-
return Some(Box::new(NextJs));
341+
presets.push(Box::new(NextJs));
327342
}
328343

329344
// Check for Vite
330345
if files
331346
.iter()
332347
.any(|path| path.ends_with("vite.config.js") || path.ends_with("vite.config.ts"))
333348
{
334-
return Some(Box::new(Vite));
349+
presets.push(Box::new(Vite));
335350
}
336351

337352
// Check for Create React App
338353
if files.iter().any(|path| path.contains("react-scripts")) {
339-
return Some(Box::new(CreateReactApp));
354+
presets.push(Box::new(CreateReactApp));
340355
}
341356

342357
// Check for Rsbuild
343358
if files.iter().any(|path| path.ends_with("rsbuild.config.ts")) {
344-
return Some(Box::new(Rsbuild));
359+
presets.push(Box::new(Rsbuild));
345360
}
346361

347362
// Check for Rust (Cargo.toml)
348363
if files.iter().any(|path| path.ends_with("Cargo.toml")) {
349-
return Some(Box::new(RustPreset::new()));
364+
presets.push(Box::new(RustPreset::new()));
350365
}
351366

352367
// Check for Go (go.mod)
353368
if files.iter().any(|path| path.ends_with("go.mod")) {
354-
return Some(Box::new(GoPreset::new()));
369+
presets.push(Box::new(GoPreset::new()));
355370
}
356371

357372
// Check for Python (requirements.txt, pyproject.toml, setup.py)
@@ -361,7 +376,7 @@ pub fn detect_preset_from_files(files: &[String]) -> Option<Box<dyn Preset>> {
361376
|| path.ends_with("setup.py")
362377
|| path.ends_with("Pipfile")
363378
}) {
364-
return Some(Box::new(PythonPreset::new()));
379+
presets.push(Box::new(PythonPreset::new()));
365380
}
366381

367382
// Check for Java (pom.xml, build.gradle, build.gradle.kts)
@@ -370,19 +385,15 @@ pub fn detect_preset_from_files(files: &[String]) -> Option<Box<dyn Preset>> {
370385
|| path.ends_with("build.gradle")
371386
|| path.ends_with("build.gradle.kts")
372387
}) {
373-
return Some(Box::new(JavaPreset::new()));
388+
presets.push(Box::new(JavaPreset::new()));
374389
}
375390

376391
// Only detect Nixpacks if there's an explicit nixpacks.toml file
377-
// This prevents Nixpacks from being auto-detected for every path
378-
// Users can still manually select Nixpacks when creating a project
379392
if files.iter().any(|path| path.ends_with("nixpacks.toml")) {
380-
return Some(Box::new(NixpacksPreset::auto()));
393+
presets.push(Box::new(NixpacksPreset::auto()));
381394
}
382395

383-
// No preset detected - return None
384-
// This prevents false positives for directories like "src", "public", etc.
385-
None
396+
presets
386397
}
387398

388399
/// Information about a detected preset in a specific directory
@@ -472,7 +483,8 @@ pub fn detect_presets_from_file_tree(files: &[String]) -> Vec<DetectedPreset> {
472483
continue;
473484
}
474485

475-
if let Some(preset) = detect_preset_from_files(dir_files) {
486+
let detected = detect_all_presets_from_files(dir_files);
487+
for preset in detected {
476488
// Use relative paths: "./" for root, subdirectory name for others
477489
let path = if dir.is_empty() {
478490
"./".to_string()
@@ -513,16 +525,17 @@ pub fn detect_presets_from_file_tree(files: &[String]) -> Vec<DetectedPreset> {
513525
}
514526
}
515527

516-
// Sort presets by path for consistent output (root "./" comes first)
528+
// Sort presets by path for consistent output (root "./" comes first), then by slug
517529
presets.sort_by(|a, b| {
518530
// Root should come first
519-
if a.path == "./" && b.path != "./" {
531+
let path_ord = if a.path == "./" && b.path != "./" {
520532
std::cmp::Ordering::Less
521533
} else if a.path != "./" && b.path == "./" {
522534
std::cmp::Ordering::Greater
523535
} else {
524536
a.path.cmp(&b.path)
525-
}
537+
};
538+
path_ord.then_with(|| a.slug.cmp(&b.slug))
526539
});
527540

528541
presets

docs/reference/environment-variables/page.mdx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,26 @@ postgresql://[user[:password]@][host][:port][/database][?param1=value1&...]
6363
export TEMPS_DATABASE_URL=postgresql://temps:password@localhost:5432/temps
6464
```
6565

66+
### Connection Pool Tuning
67+
68+
Control the PostgreSQL connection pool size and timeouts:
69+
70+
| Variable | Default | Description |
71+
| ---------------------------- | ------- | ----------------------------------------- |
72+
| `TEMPS_DB_MAX_CONNECTIONS` | `100` | Maximum connections in the pool |
73+
| `TEMPS_DB_MIN_CONNECTIONS` | `5` | Minimum idle connections kept open |
74+
| `TEMPS_DB_ACQUIRE_TIMEOUT` | `30` | Seconds to wait for a connection |
75+
| `TEMPS_DB_IDLE_TIMEOUT` | `600` | Seconds before idle connections are closed |
76+
77+
For small servers (2GB RAM), reduce `TEMPS_DB_MAX_CONNECTIONS` to `20-30`. If you see `slow_acquire_threshold` warnings in logs, it means the pool is saturated — either increase the max or investigate long-running queries.
78+
79+
```bash
80+
# Example: tuning for a small VPS
81+
export TEMPS_DB_MAX_CONNECTIONS=30
82+
export TEMPS_DB_MIN_CONNECTIONS=2
83+
export TEMPS_DB_IDLE_TIMEOUT=300
84+
```
85+
6686
### Data Directory
6787

6888
Temps stores sensitive data in the data directory (`~/.temps` by default):

scripts/homebrew-formula.rb.template

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
class Temps < Formula
22
desc "Temps - Modern deployment platform for web applications"
3-
homepage "https://github.com/davidviejo/temps"
3+
homepage "https://github.com/gotempsh/temps"
44
version "{{VERSION}}"
55
license "MIT OR Apache-2.0"
66

77
on_macos do
88
if Hardware::CPU.arm?
9-
url "https://github.com/davidviejo/temps/releases/download/v{{VERSION}}/temps-darwin-arm64.tar.gz"
9+
url "https://github.com/gotempsh/temps/releases/download/v{{VERSION}}/temps-darwin-arm64.tar.gz"
1010
sha256 "{{SHA256_DARWIN_ARM64}}"
1111

1212
def install
1313
bin.install "temps"
1414
end
1515
else
16-
url "https://github.com/davidviejo/temps/releases/download/v{{VERSION}}/temps-darwin-amd64.tar.gz"
16+
url "https://github.com/gotempsh/temps/releases/download/v{{VERSION}}/temps-darwin-amd64.tar.gz"
1717
sha256 "{{SHA256_DARWIN_AMD64}}"
1818

1919
def install
@@ -24,7 +24,7 @@ class Temps < Formula
2424

2525
on_linux do
2626
if Hardware::CPU.intel?
27-
url "https://github.com/davidviejo/temps/releases/download/v{{VERSION}}/temps-linux-amd64.tar.gz"
27+
url "https://github.com/gotempsh/temps/releases/download/v{{VERSION}}/temps-linux-amd64.tar.gz"
2828
sha256 "{{SHA256_LINUX_AMD64}}"
2929

3030
def install

0 commit comments

Comments
 (0)